Blog de David Masclet - Java / J2EE2020-02-11T15:05:04+00:00urn:md5:bd7c47a89c3b735a7167e4bd4cde9285DotclearLes génériques java et EasyMockurn:md5:f19caa2fcd8f4d7d4c8728c658af5c8b2010-10-21T21:42:00+02:002015-06-07T11:22:48+02:00MD3804-GANDIJava / J2EE <p>Parfois nous devons exécuter un traitement et si le résultat est
<code>null</code> nous voulons renvoyer un résultat par défaut. par exemple une
liste vide. cela permet d'éviter les <code>NullPointerException</code>s si nous
parcourons la liste. Exemple :</p>
<pre>
List<Contact> contacts = getContacts();
for (Contact contact : contacts){
contact.setActive(true);
}
</pre>
<p>Si <code>getContacts()</code> renvoie <code>null</code> le code renverra une
<code>NullPointerException</code>. il est possible avec les
<code>génériques</code> Java de le faire d'une manière élégante :</p>
<pre>
public static <T> T notNull(T test, T defaultvalue){
return t == null ? test : defaultValue;
}
</pre>
<p>On peut alors écrire :</p>
<pre>
List<Contact> contacts = notNull(getContacts(),new ArrayList<Contact>);
</pre>
<p>Ce code n'est pas révolutionnaire mais il montre à quel point les génériques
de de Java peuvent être utiles. C'est d'ailleurs de cette manière que <a href="http://easymock.org/" hreflang="en">EasyMock</a> fonctionne. Lorsque vous
écrivez</p>
<pre>
Myclass mock = EasyMock.createMock(MyClass.class);
EasyMock.expect(mock.function()).andReturn(value);
</pre>
<p>Le type de <code>value</code> est déterminé en générique Java par le retour
de <code>function()</code> :</p>
<pre>
public <O> O expect(O t) {
return t;
}
</pre>Détecter et corriger les fuites mémoire javaurn:md5:d93407da3c19da44efb39ee7a0264fcf2010-03-01T09:16:00+01:002015-06-07T11:23:53+02:00MD3804-GANDIJava / J2EEFuites mémoirejava<p>Ceux qui ont déjà développé en C, puis en Java auront probablement apprécié
la gestion de la mémoire par la JVM. En effet, Java masque la complexité de la
gestion de la mémoire aux développeurs. Cette tache est déléguée au
<code>garbage Collector</code>. Cependant même si cette gestion est dite
'automatique', cela ne permet pas de faire n'importe quoi. Que ce soit en C,
Java, ou un autre langage, la mémoire est une ressource limitée ! En java
lorsque la JVM n'a plus assez de mémoire pour s'exécuter, elle génère une
Erreur <code>java.lang.OutOfMemoryError</code>.</p>
<p>Plusieurs cas sont alors possible :</p>
<ul>
<li>Soit vous n'avez pas alloué assez de mémoire à la JVM</li>
<li>Soit votre code a ce qu'on appelle un fuite mémoire</li>
</ul>
<p>Si vous êtes dans le premier cas, les choses se résolvent assez
facilement : il vous suffit d'allouer plus de mémoire grâce aux paramètres
<code>-Xmx</code>, <code>-Xms</code> et <code>-XX:PermSize</code> (ou de
modifier le code pour qu'il gaspille moins de mémoire). Dans le deuxième cas
vous allez devoir trouver où se situe la fuite mémoire. Si vous avez utilisé
des logiciels pour vous y aider, vous avez peut être été perdu par toutes les
possibilités qu'ils offrent et par la profusion d'informations qu'il
fournissent.</p>
<p>Le but de ce billet est d'expliquer comment détecter et corriger les fuites
mémoires avec Eclipse Memory Analyser Tool un outil gratuit et très utile qui
aide vraiment à trouver les fuites mémoires. Nous allons voir comment extraire
les informations pertinentes, les comprendre, et trouver les responsables des
fuites mémoire en ayant une approche pragmatique. Je vous propose de nous
appuyer sur du code simple (disponible en fichier attaché à ce billet) générant
une fuite mémoire.</p> <h3>La gestion de la mémoire en Java</h3>
<p>La JVM dispose de plusieurs zones mémoire distinctes :</p>
<ul>
<li>La pile (ou heap), est l'espace mémoire où tous les objets et tableaux sont
stockés. Lorsque la JVM démarre, la pile est initialisée à la valeur spécifiée
par <code>-Xms</code> et elle grandira dynamiquement quand les objets seront
crées, jusqu'à ce qu'elle atteigne sa taille maximale spécifiée par le
paramètre <code>-Xmx</code>. Si la taille maximale est atteinte, et que plus de
mémoire est nécessaire, la JVM générera une <code>java.lang.OutOfMemoryError:
Java heap space</code>.</li>
<li>Permgen space : il s'agit de l'espace mémoire qui est alloué pour le
chargement des classes (pas les objets). A la différence de la pile, la taille
de cette région est fixe et ne varie plus après le démarrage de la JVM. Pour la
JVM de Sun, la taille par défaut du permGen space est de 64M. Vous pouvez
changer cette valeur grâce au paramètre <code>-XX:PermSize</code>. Rappelons
tout de même que les options commençant par <code>XX</code> ne sont pas des
paramètres HotSpot stables et que leurs compatibilités dépendent du vendeur et
de la version de la JVM. Si la mémoire du permgen est insuffisante une
<code>java.lang.OutOfMemoryError: PermGen space</code> sera renvoyée</li>
<li>il existe un troisième type d'erreurs : les erreurs natives. Elles
apparaissent lorsque le système hôte ne peut plus fournir de mémoire (plus
assez de place en mémoire vive, ou plus assez de swap). Dans ce cas la JVM
génère une Erreur <code>java.lang.OutOfMemoryError: request <size> bytes
for <reason></code>.</li>
</ul>
<p>Cas des champs statiques : Les champs statiques ne peuvent être garbage
collectés tant que la classe qui les référence est chargée. il ne peuvent être
garbage collectés que lorsque le class loader qui est responsable du chargement
de la classe est lui même garbage collecté.</p>
<p>Chaque instance de classe a une taille en mémoire, cette taille appelée
"Shallow Heap" est constituée de types primitifs (int, char, ...). Une instance
peut avoir également des références sur d'autres objets qui ont eux même une
taille. Les objets ainsi liés entre eux constituent un graphe.</p>
<p>Tous les graphes disposent d'une racine d'attache, ces racines peuvent être,
par exemple, des threads ou des class loaders qui tiennent des références sur
les attributs statiques. Ces racines se nomment en anglais des "GC root".
Toutes les instances en mémoire finissent par être attachées à une racine, si
ce n'est pas le cas, le Garbage Collector peut supprimer ces instances du
graphe, et libérer de la mémoire.</p>
<p>Les références peuvent être de plusieurs types. Il y avait les références
fortes "StrongReference" et depuis java 5 il existe des références dites douces
"SoftReference" : L'objet est référencé, mais si la JVM a besoin de
mémoire, elle peut libérer cet objet. On trouve également les références
légères "WeakReference": L'objet restera en mémoire, tant qu'il existe une
référence forte ou douce sur lui. enfin il existe des
<code>PhantomReference</code> : les objets peuvent être supprimés de la
mémoire si aucune référence forte, douce, ou légère existe sur cette objet.</p>
<h3>Empreinte de la mémoire (Heap dump)</h3>
<p>Afin de permettre des diagnostiques et de voir ce que contient la mémoire de
la JVM, il est possible de générer une "empreinte de la mémoire" qui correspond
à tous les objets chargés à un instant précis. Ces empreintes sont plus connus
sous le nom de "Heap dump".</p>
<p>Ces dumps contiennent :</p>
<ul>
<li>Tous les objets : champs, références, et objet primitifs.</li>
<li>Toutes les classes : Class loader, super Class, champs statiques</li>
</ul>
<p>En revanche il ne contient pas les informations sur qui et où sont crées les
objets, ni quels sont les objets qui ont déjà été garbage collectés.</p>
<p>Il existe différentes méthodes pour générer ces dumps. Soit en spécifiant
les options <code>-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/dump.hprof</code> au lancement de la JVM afin qu'un dump
soit généré lorsqu'une <code>java.lang.OutOfMemoryError</code> survient. Notez
toutefois que comme toutes les options commençant par <code>XX</code>, elles
sont dépendantes du vendeur de JVM. La deuxième solution est de faire un dump à
la demande, à l'aide du <code>JDK 6</code> et de <code>JConsole</code>. les
dumps à la demande sont utiles, par exemples, après des benchs.</p>
<p>Voici la procédure :</p>
<ul>
<li>Lancez <code>JConsole</code> situé dans le répertoire "bin" de la JVM</li>
<li>Sélectionnez le processus Java pour lequel vous voulez faire le dump.
Vérifiez sur l'onglet "VM summary" que c'est bien le bon processus.</li>
<li>Rendez vous sur l'onglet Mbeans</li>
<li>Sélectionnez l'item com.sun.management / hotspot diagnostic / operation /
dumpheap</li>
<li>Spécifiez le chemin complet du fichier où vous voulez stocker le dump dans
le premier champs de saisie (laissez le deuxième champs à true). Les dumps ont
souvent l'extension hprof, et MAT ne verra pas le fichier s'il n'a pas la bonne
extension.</li>
<li>Validez en cliquant sur le bouton "dumpHeap". Cela figera l'exécution de la
JVM (donc à éviter en production), exécutera un garbage collector, et générera
un fichier binaire avec le contenu de la mémoire.</li>
</ul>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.jconsole_m.jpg" alt="jconsole" style="display:block; margin:0 auto;" title="jconsole, fév. 2010" /></p>
<p>Si vous ne possedez pas JDK 6.0, il est possible d'utiliser <a href="http://java.sun.com/javase/6/docs/technotes/tools/share/jmap.html" hreflang="en">jmap</a> :</p>
<pre>
jmap -heap:format=b <PID_DU_PROCESS_JAVA>
</pre>
<h3>Qu'est ce qu'une fuite mémoire</h3>
<p>Une fuite mémoire est une dérive non contrôlée de l'utilisation de la
mémoire. elle est souvent due à un objet qui garde des références à une
multitude d'autres, ce qui empêche le garbage collector de les enlever de la
mémoire. Cette dernière grandit alors de plus en plus et finit par saturer.</p>
<p>Pour comprendre une fuite mémoire il faut connaitre quelques termes (en
anglais):</p>
<ul>
<li>Dominator tree : Liste des plus gros objets et de qui les
référencent.</li>
<li>Leak suspect : Objet ayant une grosse taille dans le dominator
tree.</li>
<li>Accumulation point : Gros objet référençant beaucoup d'autres petits
objets</li>
</ul>
<p>Voici un représentation graphique des termes évoqués ci dessus :</p>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.dominator_tree_m.jpg" alt="IMAGE" style="display:block; margin:0 auto;" title="dominator tree, fév. 2010" /></p>
<p>Shallow Heap et retained Heap : il s'agit de la taille que prend un
objet sur la pile. "shallow heap" représente la taille des éléments de l'objet
sans inclure les objets qu'il référence, tandis que "retained Heap" les prend
en compte : elle représente la taille qui sera libéré si cet objet est
garbage collecté. Voici un exemple pour mieux comprendre :</p>
<p>Ci dessous ce trouve la représentation interne d'une string que j'ai pu
obtenir avec le mode debug d'Eclipse :</p>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/retained_size.jpg" alt="retained size" style="display:block; margin:0 auto;" title="retained size, fév. 2010" /></p>
<p>On voit qu'une string est représentée par trois entiers : count, hash,
et offset, qui représente la shallow heap, ainsi qu'une référence "value" à un
tableau de char<a href="https://davidmasclet.gisgraphy.com/index.php?post/2010/02/05/"></a> . count, hash, offset et value représente la
retained heap car le char<a href="https://davidmasclet.gisgraphy.com/index.php?post/2010/02/05/"></a> sera également garbage collecté si la
string l'est aussi.</p>
<h3>Eclipse Memory Analyser Tool</h3>
<p>Il existe déjà des outils permettant d'analyser les dumps, mais ils sont
soit difficiles d'approche comme <a href="http://java.sun.com/javase/6/docs/technotes/tools/share/jhat.html" hreflang="en">jhat</a> (Java Heap Analysis Tool) et jmap soit ils sont payants comme
Jprofiler.</p>
<p>Eclipse a développé un outil permettant d'analyser les heap dumps. Cela
permet d'aider à trouver où se situent les fuites mémoires et de détecter les
gaspillages et améliorations possibles de l'utilisation de la mémoire. L'outil
ne se contente pas de vous analyser la mémoire, il cherche également où peuvent
se situer les fuites ainsi que les optimisations. c'est là le point fort de cet
outil.</p>
<p>MAT peut être installé en tant que plugin dans Eclipse, ce qui a l'avantage
de pouvoir regarder le code en même temps que de regarder le dump, ou en tant
que client lourd (RCP) à part entière, des scripts sont alors disponibles en
plus du plugins. Peu importe votre choix, vous pouvez vous rendre sur <a href="http://www.eclipse.org/mat/downloads.php" hreflang="en">cette page</a> pour le
télécharger.</p>
<h3>Utilisation de MAT avec un code d'exemple</h3>
<p>Je vous propose de tester MAT avec du code simple (disponible en fichier
attaché à ce billet) générant une fuite mémoire. Le code représentera plusieurs
objets métiers : une entreprise, des salariés, et des adresses, ainsi
qu'une classe permettant de générer quelques instances de ces objets et une
<code>HashMap</code> statique servant de cache pour les objets
<code>Entreprise</code>. Enfin une fois tous ces objets chargés, je provoquerai
une fuite mémoire en remplissant un tableau avec des <code>Double</code>. Je
dis bien "provoquerai", car à la différence des Exceptions, les Errors sont
générées par la JVM et non par le code : si je fais</p>
<pre>
throw new OutOfMemoryError();
</pre>
<p>et que je spécifie les options <code>HeapDumpOnOutOfMemoryError</code> le
dump ne sera pas généré.</p>
<p>je lance donc la classe Application avec les options
<code>-XX:+HeapDumpOnOutOfMemoryError -Xms5m -Xmx10m
-XX:MaxPermSize=256m</code>.</p>
<p>En l'absence de <code>HeapDumpPath</code>, c'est un fichier avec le PID qui
sera généré, l'avantage étant qu'un nouveau fichier de dump sera généré à
chaque <code>java.lang.OutOfMemoryError</code> (il ne sera pas écrasé puisque
le nom sera différent à chaque fois)</p>
<p>Le lancement du programme me donne la sortie suivante :</p>
<pre>
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid2331.hprof ...
Heap dump file created [13486084 bytes in 0.401 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.ArrayList.ensureCapacity(ArrayList.java:169)
at java.util.ArrayList.add(ArrayList.java:351)
at com.demo.mat.OutOfMemoryHelper.addDouble(OutOfMemoryHelper.java:18)
at com.demo.mat.OutOfMemoryHelper.throwOutOfMemoryError(OutOfMemoryHelper.java:13)
at com.demo.mat.Application.main(Application.java:74)
</pre>
<p>Il ne reste plus qu'a analyser le dump. vous pouvez l'ouvrir directement en
lançant MAT, puis en allant dans <code>File->Open Heap Dump</code> et en
sélectionnant le fichier java_pid2331.hprof. (seuls les fichiers .hprof
apparaitrons). MAT générera alors plusieurs indexes et un rapport HTML. Les
indexes permettent de naviguer plus rapidement car le format hprof n'est pas
très exploitable en l'état. Cette génération n'aura lieu qu'une seul fois et
lorsque vous rouvrirez ce dump, Mat utilisera les fichier déjà générés.</p>
<p>Dans notre cas le fichier sera de 10 méga octets (environ taille du Xmx),
mais si vous voulez analyser de gros fichiers je vous conseille d'utiliser la
ligne de commande:</p>
<pre>
./MemoryAnalyzer -consolelog -application org.eclipse.mat.api.parse java_pid2331.hprof
</pre>
<p>car MAT a des fois du mal à les digérer. L'ouverture par MAT sera alors plus
rapide et facile.</p>
<p>vous pouvez aussi utiliser le scipt ParseHeapDump.sh, il vous faudra par
contre remplacer les ^M, pour cela ouvrez le fichier en mod binaire avec
VIM :</p>
<pre>
vim - b ParseHeapDump.sh
puis :%s/<CTRL+V><CTRL+M>//g pour remplacer le ^M par chaine vide
</pre>
<h3>Prise en main de l'outil</h3>
<p>Le rapport donne une première vue avec</p>
<ul>
<li>les informations sur la taille du dump (8.7M) et le nombre de classe
chargées (360)</li>
<li>un camembert représentant les plus gros objets en mémoire. En passant votre
souris sur chaque partie du camembert, vous pouvez avoir des informations sur
les attributs (références et champs natifs) de l'objet dans la colonne de
gauche 'attributes'</li>
</ul>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.mat_report_m.jpg" alt="mat report" style="display:block; margin:0 auto;" title="mat report, fév. 2010" /></p>
<p>Différentes actions menant vers différentes vues du dump :</p>
<ul>
<li>Un histogramme : qui affiche une représentation de l'utilisation
mémoire par classes (nombre d'objets instanciés par classe, ainsi que leurs
tailles 'shallow' et 'retained'). Vous pouvez d'ailleurs voir que le nombre de
<code>char<a href="https://davidmasclet.gisgraphy.com/index.php?post/2010/02/05/"></a></code> est sensiblement le même que le nombre de
<code>String</code> ;)</li>
<li>Un dominator tree (voir définition ci dessus) : présente non plus une
vue par classes mais par objets</li>
<li>Une vue graphique des objets du dominator tree groupés selon différents
critères : class loaders, packages, classes. Le dominator tree travaille,
si on ne spécifie pas de groupage, au niveau objet</li>
<li>Une liste des classes chargées plusieurs fois par différents class
loader.</li>
</ul>
<p>Les parties les plus intéressantes du rapport et les plus exploitables
directement sont sans aucun doute les deux rapports suivants:</p>
<ul>
<li>Liste des 'leak suspects', c'est à dire que l'outil vous suggère où peuvent
se situer les responsables de fuites mémoires.</li>
<li>Une liste des objets gaspillant la mémoire (e.g : ArrayList
surdimensionnées ayant beaucoup d'éléments vides) avec des suggestions
d'optimisations de l'utilisation mémoire (à ne pas confondre avec le rapports
des fuites mémoires.)</li>
</ul>
<p>Voyons vue par vue, les choses que nous pouvons faire : Dans toutes les
vues vous avez la possibilité de voir ce que contient l'objet sélectionné dans
l'onglet "attributes".</p>
<p>Il est possible de calculer la taille de la 'retained heap' via l'icône
représentant un calculatrice.</p>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.calculate_retained_heap_size_m.jpg" alt="calculate retained size" style="display:block; margin:0 auto;" title="calculate retained size, fév. 2010" /></p>
<p>Vous pouvez ainsi trier les objets par 'retained heap' pour identifier les
'leak supect'. Vous pouvez aussi les filtrés en rentrant des expressions
régulières juste en dessous des libellés de colonnes (e.g : "regex" en
dessous de 'Class Name'). Attention pour les regexp, elles sont sensibles à la
casse.</p>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.histograme_filtres_m.jpg" alt="histograme filtres" style="display:block; margin:0 auto;" title="histograme filtres, fév. 2010" /></p>
<p>Une action qui révèle souvent des informations intéressantes, consiste à
grouper les objets. Si vous les groupez par classe, vous pouvez par exemple
obtenir le nombre d'objets et le pourcentage de la mémoire totale qu'occupe les
objets de cette classe. Cela permet d'identifier les points d'accumulation plus
facilement lorsque la fuite mémoire ne concerne pas un seul gros objet (ce qui
n'est pas notre cas) mais une multitude d'objets de la même classe. Par défaut,
la distribution de l'occupation de la mémoire ce fait par objet, en les
groupant, les shallow / retained heap sont alors additionnés, laissant souvent
voir une répartition inégale de l'utilisation de la mémoire selon les classes
(<a href="http://dev.eclipse.org/blogs/memoryanalyzer/2008/05/08/the-power-of-aggregation-making-sense-of-the-objects-in-a-heap-dump/" hreflang="en">en savoir plus</a>). Vous pouvez aussi les grouper par class
loader, ce qui peut se révéler intéressant pour des webapps, Tomcat, par
exemple, utilise des class loader differents : webappClassLoader et un
SystemClassLoader (<a href="http://opensource.atlassian.com/confluence/spring/pages/viewpage.action?pageId=2669" hreflang="en">en savoir plus</a>). OSGI (<a href="https://davidmasclet.gisgraphy.com/post/2009/11/26/10-minutes-pour...comprendre-OSGI" hreflang="en">voir
l'article publié précédemment</a>) utilise aussi beaucoup de class loader
différents</p>
<h3>Explication des menus</h3>
<p>Lorsque vous cliquez droit sur les objets du dominator tree ou sur les
classes de l'histogramme, vous obtenez un menu vous permettant d'avoir des
informations sur l'élément sélectionné :</p>
<ul>
<li>List objects=>with outgoing references: objets référencés par l'élément
sélectionné. Si on additionne la taille de ces objets, on obtient la retained
size.</li>
<li>List objects=>with incomming references : objets qui référencent
l'élément sélectionné.</li>
</ul>
<p><br />
La liste des objets est contextuelle à l'élément sélectionné : si les
objets sont groupés par classe, ou que l'élément est une classe, une ligne par
objet de cette classe sera affichée, si l'élément est un objet, alors seuls les
objets concernés par cet objet (référencé par / qui référence) seront
affichés<br /></p>
<ul>
<li>List class => by outgoing references : les classes des objets
référencés par l'élément.</li>
<li>List class => by Incoming references : Les classes des objets qui
ont une référence sur l'élément</li>
</ul>
<p><br />
La liste des classes affichée est également contextuelle. Exemple pour outgoing
référence : si l'élément sélectionné est une classe, alors la liste de
toutes les classes référencées par tous les objets de cette classe sera listée,
si l'élément est un objet, seule la liste des classes référencées par cet objet
sera affichée.<br /></p>
<ul>
<li>Path to GC root => Affiche les chemins des objets qui référence cet
objet jusqu'à leur élément racine (un peu comme les incomming objects mais
moins en profondeur car s'arrête lorsque l'on arrive au sommet de la pile du
thread auquel appartient l'élément). Il est possible via les sous menus de
filtrer selon les weak / soft / hard références. ce qui peut être utile lorsque
vous utilisez des caches d'objets avec des références soft, car vous pouvez
uniquement inspecter l'élément en tenant compte ou pas du cache. Ce menu n'est
applicable que pour un objet pas pour classe.</li>
</ul>
<p><br /></p>
<ul>
<li>Merge Path to GC root : affiche le chemin <strong>le plus
court</strong> parmi les références racines pour chaque instance de la classe
ou de l'objet sélectionné</li>
</ul>
<p><br />
Java basics permet d'avoir des renseignements divers sur l'objet (e.g :
chercher une <code>String</code>). Depuis la version 0.8, il est possible
d'analyser les stacktraces. Ces informations sont disponibles via le menu java
basics=>threads stacks</p>
<ul>
<li>Java collections sert plus à détecter les gaspillages de mémoire. il permet
par exemple d'avoir le ratio de remplissage d'un tableau.</li>
</ul>
<ul>
<li>Leak identification=> Component Report : permet d'avoir des
informations sur la mémoire occupée par cette objet : taille que l'objet
ou que les objets d'une classe occupent dans la pile, leurs <code>soft</code>
et leurs <code>hard</code> références,</li>
</ul>
<ul>
<li>Leak identification=> Top consumer : les plus gros éléments
regroupés et triés selon différents critères : Objet, Classe, Class
loader, Package. il permet d'avoir une répartition des objets par classe
(e.g : trouver quels sont les plus gros objets de la classe Salariés). ce
menu est utile lorsque vous l'appliquez sur une classe, car il montre quels
sont les plus gros objets de cette classe et leurs répartitions</li>
</ul>
<ul>
<li>Immediate dominator : Donne les classes des objets qui font que
l'élément est gardé en mémoire (e.g : les immediate dominators de
char<a href="https://davidmasclet.gisgraphy.com/index.php?post/2010/02/05/"></a> sont bien souvent des <code>String</code>). Il est souvent
utile de filtrer les classes des package <code>com.sun.*</code> et
<code>java.*</code> car on peut supposer que la fuite ne vienne pas de là. Dans
l'exemple ci dessous, pour les immediate dominator de char<a href="https://davidmasclet.gisgraphy.com/index.php?post/2010/02/05/"></a>, si
on considère la ligne concernant <code>Entreprise</code> : La première
colonne donne le nombre d'objets de la classe listée (1), la deuxième donne le
nombre d'objets référencés par cette classe (3) (un objet Entreprise référence
3 char<a href="https://davidmasclet.gisgraphy.com/index.php?post/2010/02/05/"></a> correspondant aux trois commentaires).</li>
</ul>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.imediate_dominator_m.jpg" alt="immediate dominator" style="display:block; margin:0 auto;" title="immediate dominator, fév. 2010" /></p>
<ul>
<li>Show retained set : donne le nombre d'objets que référence l'élément,
groupés par classe. le retained set représente les objets qui seront libérés si
l'élément est garbage collecté. Pour un objet Entreprise, il contient 3
char<a href="https://davidmasclet.gisgraphy.com/index.php?post/2010/02/05/"></a>, 3 Strings et une Entreprise (les trois commentaires+ la
class ). vous pouvez filtrer le retained set par le ou les noms des champs. On
peut alors se demander "Pourquoi les objets "raison social", "salariés" ne sont
pas dans la retained set alors qu'on les voit si on liste les outgoing objects
de Entreprise. La réponse est que ces objets ne seront pas libérés si
Entreprise disparait. Pour le prouver faites un incomming référence sur les
objet de Entreprise : les commentaires ne sont uniquement référencés par
Entreprise, alors que raison social par exemple est également référencé comme
clé pour la <code>Hashmap</code> de <code>cacheEntreprise</code>. De même pour
"raison social" qui est une constante car déclarée dans le code, alors que les
3 commentaires sont générés au runtime.</li>
</ul>
<h3>Savoir lire les graphes</h3>
<p>Lorsque l'on est devant un graphe, il est important de savoir le lire.
prenons l'exemple pour des 'outgoing références' :</p>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.outgoing_m.jpg" alt="outgoing" style="display:block; margin:0 auto;" title="outgoing, fév. 2010" /></p>
<p>La classe Entreprise à des référence vers 4 objets de type String qui se
nomme 'commentaire', 'commentaire2', commentaire3, et 'raison social'. et une
ArrayList qui se nome salarié. si l'un des objet avait une valeur null, il
n'aurait pas été listé.</p>
<p>Voyons maintenant comment lire des 'incoming références' :</p>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.incoming_m.jpg" alt="incoming" style="display:block; margin:0 auto;" title="incoming, fév. 2010" /></p>
<p>L'objet entreprise (je dis bien objet et pas classe) est référencé
par :</p>
<ul>
<li>Le champs 'entreprise' de 3 objets de type Salarié,</li>
<li>Le thread lui même , correspondant à la déclaration de la variable
(Entreprise entreprise = new Entreprise() )</li>
<li>Le champs 'value' d'une HashMap, correspondant à la <code>HashMap</code> de
cache nommée entreprise et appartenant à la classe CacheEntreprise .</li>
</ul>
<p>Lorsque l'on analyse des dumps mémoire, il est fortement conseillé de
connaitre la représentation interne des objets <code>Map</code>,
<code>Collections</code>, <code>String</code>, etc. Cela permet de mieux
naviguer. Une <code>HashMap</code> est par exemple constitué d'un tableau de
hashMapEntry, lui même constitué de champs key, value, et hash. Si vous voulez
explorer un objet, allez sur l'onglet 'attributes', cliquez droit, puis
sélectionnez 'Go into' :</p>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.attribut_m.jpg" alt="attribut" style="display:block; margin:0 auto;" title="attribut, fév. 2010" /></p>
<h3>Les rapports de fuites mémoires</h3>
<p>Entrons maintenant dans le vif du sujet. Le rapport proposant des
responsables des fuites mémoires. Comment cela fonctionne ? En fait
l'outil analyse le dump et recherche les 'Leak suspects', Il commence par
chercher les points d'accumulation en analysant les écarts significatifs dans
la retained size entre deux objets du graphe, puis remonte les 'incomming
references', prend le plus court chemin, et en déduit le suspect.</p>
<p>Un rapport HTML comprenant plusieurs sections est alors généré :</p>
<ul>
<li>les points d'accumulation : donnent les informations sur la taille et
le type d'objets, le thread incriminé, et le nombre d'objets contenus.</li>
</ul>
<p>Le rapport génère aussi une liste de mots clés permettant de le googliser ou
de les taper dans un bug tracker. L'idée est à mon sens excellente :
Lorsque vous créez le bug, vous y ajoutez ces mots clés, et une personne qui
désire retrouver ce bug ou voir si une résolution a déjà été trouvée, tape ces
mots clés, retrouve le bug et une potentielle résolution éprouvée.</p>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.leak_report_m.jpg" alt="leak report" style="display:block; margin:0 auto;" title="leak report, fév. 2010" /></p>
<p>Deux questions se posent alors :</p>
<ul>
<li>Qui garde une référence à cet objet ?</li>
<li>Pourquoi l'objet est si gros et que contient il ?</li>
</ul>
<p>Pour la première question on va chercher quel est le plus court chemin du GC
root jusqu'à l'objet du point d'accumulation :</p>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.leak_report_shortest_path_m.jpg" alt="leak report shortest path" style="display:block; margin:0 auto;" title="leak report shortest path, fév. 2010" /></p>
<p>Le graphe ce lit 'ligne du haut' est contenu par 'ligne du dessous'. Dans
l'exemple ci dessus : le tableau d'objet est référencé par le thread
(Chemin le plus court) et par une <code>ArrayList</code></p>
<p>Pour la deuxième question MAT nous donne un aperçu des objets référencés par
le point d'accumulation. Cela permet en général d'avoir une idée plus précise
de la cause de la fuite.</p>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.leak_report_accumulated_object_m.jpg" alt="leak report accumulated object" style="display:block; margin:0 auto;" title="leak report accumulated object, fév. 2010" /></p>
<p>Le rapport fournit également une répartition des objets référencé par le
point d'accumulation par classes. Dans notre cas, le tableau d'objet ne
contient que des <code>Double</code>, ce qui laisse à penser qu'une méthode
remplit le tableau.</p>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.leak_suspect_byclass_m.jpg" alt="leak suspect by class" style="display:block; margin:0 auto;" title="leak suspect by class, fév. 2010" /></p>
<p>Une autre information intéressante est l'environnement d'exécution. Dans
quel contexte a eu lieu le dump. Cela à une importance quand vous utilisez
<code>-XX:HeapDumpOnOutOfMemoryError</code> . Les informations importantes sont
entre autres :</p>
<ul>
<li>Les propriétés de la JVM (Xms, Xmx). Peut être un mauvais dimensionnement
de la mémoire ?</li>
<li>Le nombre de threads et leurs tailles permettent de voir si un seul thread
est incriminé, et dans ce cas, selon la nature de traitement qu'à le thread,
vous pouvez plus cibler la fuite (exemple : un thread de purge d'un
cache)</li>
<li>L'histogramme des classes peut également donner des informations sur la
cause. S'il existe un grand nombre d'objets appartenant à un classe d'un de vos
package, il est probable (pas à 100%, mais vous disposez déjà d'une piste) que
la fuite est causée par du code vous appartenant.</li>
</ul>
<h3>Informations sur les threads</h3>
<p>MAT fournit des informations sur le context d'exécution des threads. il
inspecte les variables <code>threadLocal</code> et selon ce qu'il y trouve,
nous donne des informations supplémentaires. exemple :</p>
<ul>
<li>s'il trouve des objets sur des requêtes HTTP il inspectera l'objet et en
extraira les headers, URL, status code, etc.</li>
<li>s'il trouve des connections JDBC, il extraira les resultsets, query,...on
peut par exemple voir qu'il manque un clause <code>where</code>, ce qui fait
que beaucoup trop d'objets sont rapatriés, et cause une trop grande
consommation mémoire</li>
<li>s'il trouve des contexts OSGI, il explorera leurs contenus.</li>
</ul>
<p>Voici deux exemples : le premier pour un thread exécutant une requête
SQL :</p>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.thread_sql_m.jpg" alt="thread sql" style="display:block; margin:0 auto;" title="thread sql, fév. 2010" /></p>
<p><br />
le deuxième pour un thread exécutant une requête soap :</p>
<p><img src="https://davidmasclet.gisgraphy.com/public/memoryleak/.thread_soap_m.jpg" alt="thread soap" style="display:block; margin:0 auto;" title="thread soap, fév. 2010" /></p>
<p>On peut légitimement se poser la question "comment sont déterminer ces
informations ? " en fait si le suspect est un thread, il explore son nom
(exemple "HTTP-processor" pour un thread Tomcat, "RMI TCP
Connection(8)-127.0.0.1 pour un connecteur jmx, "Timer-3" pour un
<code>timerThread</code>), il inspecte les variables <code>threadLocal</code>
et selon ce qu'il y trouve nous donne des informations supplémentaires. Ce
système d'exploration est basé sur un système de plugins, vous pouvez donc en
créer vous même selon vos besoins</p>
<h3>OQL ou Object Query Language</h3>
<p>Pour permetre de rechercher des objets facilement, vous pouvez les
rechercher avec une syntaxe SQL like. Exemple :</p>
<pre>
select e.nom from com.demo.mat.Entreprise e
</pre>
<p>Vous pouvez utiliser des clauses WHERE, des UNION, des projections (avg,
min, max,...). vraiment très pratique pour trouver, compter, avoir des
statistiques sur des objets en particulier. (<a href="http://en.wikipedia.org/wiki/Object_Query_Language" hreflang="en">en savoir
plus</a>)</p>
<p>Voilà pour la théorie :) j'espère que vous maitrisez les différents concepts
de l'outil et que vous savez désormais aller chercher les informations
pertinentes.</p>
<p>Le prochain post sera basé sur un cas concret et expliquera comment j'ai
résolu une fuite mémoire sur Gisgraphy, en 15 minutes à l'aide de MAT.</p>Untar en javaurn:md5:35bc135c7eab6c2dec71beddf7d200132009-12-07T13:44:00+01:002015-06-07T11:25:43+02:00MD3804-GANDIJava / J2EEbz2bzip2gzgzipjavatar<p>Autant trouver un bout de code pour de-zipper un fichier est assez facile,
autant en trouver un pour de-tarrer est déjà un peu plus complexe. Ayant eu à
le coder, Je vous propose (ça évitera peut être des galères à certains ;) ) une
classe qui permet de le faire. Elle permet de décompresser les fichiers tar,
tar.gzip, tar.gz, tar.bz2, et tar.bzip2, le choix du traitement se fait selon
l'extension.</p> <pre class="brush:java">
/*******************************************************************************
* Gisgraphy Project
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA
*
* Copyright 2008 Gisgraphy project
* David Masclet
*
*
*******************************************************************************/
package com.gisgraphy.helper;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.GZIPInputStream;
import org.apache.tools.bzip2.CBZip2InputStream;
import org.apache.tools.tar.TarEntry;
import org.apache.tools.tar.TarInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility class to untar files, files can be zipped in multi format (extension
* tar, tar.gzip,tar.gz, tar.bz2, tar.bzip2 are supported).
*
* @author <a href="mailto:david.masclet@gisgraphy.com">David Masclet</a>
*
*/
public class Untar {
private String tarFileName;
private File dest;
/**
* The logger
*/
private static final Logger logger = LoggerFactory.getLogger(Untar.class);
/**
* (note : constructor that takes two files as parameter could probably be a better design)
* @param tarFileName
* the path to the file we want to untar
* @param dest
* the directory where the file should be untar
*/
public Untar(String tarFileName, File dest) {
this.tarFileName = tarFileName;
this.dest = dest;
}
private InputStream getDecompressedInputStream(final String name, final InputStream istream) throws IOException {
logger.info("untar: decompress " + name + " to " + dest);
if (name == null) {
throw new RuntimeException("fileName to decompress can not be null");
}
if (name.toLowerCase().endsWith("gzip") || name.toLowerCase().endsWith("gz")) {
return new BufferedInputStream(new GZIPInputStream(istream));
} else if (name.toLowerCase().endsWith("bz2") || name.toLowerCase().endsWith("bzip2")) {
final char[] magic = new char[] { 'B', 'Z' };
for (int i = 0; i < magic.length; i++) {
if (istream.read() != magic[i]) {
throw new RuntimeException("Invalid bz2 file." + name);
}
}
return new BufferedInputStream(new CBZip2InputStream(istream));
} else if (name.toLowerCase().endsWith("tar")) {
return istream;
}
throw new RuntimeException("can only detect compression for extension tar, gzip, gz, bz2, or bzip2");
}
/**
* process the untar operation
*
* @throws IOException
*/
public void untar() throws IOException {
logger.info("untar: untar " + tarFileName + " to " + dest);
TarInputStream tin = null;
try {
if (!dest.exists()) {
dest.mkdir();
}
tin = new TarInputStream(getDecompressedInputStream(tarFileName, new FileInputStream(new File(tarFileName))));
TarEntry tarEntry = tin.getNextEntry();
while (tarEntry != null) {
File destPath = new File(dest.toString() + File.separatorChar + tarEntry.getName());
if (tarEntry.isDirectory()) {
destPath.mkdir();
} else {
if (!destPath.getParentFile().exists()) {
destPath.getParentFile().mkdirs();
}
logger.info("untar: untar " + tarEntry.getName() + " to " + destPath);
FileOutputStream fout = new FileOutputStream(destPath);
try {
tin.copyEntryContents(fout);
} finally {
fout.flush();
fout.close();
}
}
tarEntry = tin.getNextEntry();
}
} finally {
if (tin != null) {
tin.close();
}
}
}
}
</pre>Retenter l'execution d'une fonction plusieurs fois en Javaurn:md5:2a182478713fa26110b0afd97e076e812009-11-23T11:19:00+01:002015-06-07T11:25:58+02:00MD3804-GANDIJava / J2EEjava<p>Certains d'entre vous connaissent peut être la fonction javascript <a href="http://www.prototypejs.org/api/utility/try-these" hreflang="en">Try.these</a>
de Prototype.</p>
<p>Elle permet d'exécuter plusieurs fonctions et de retourner la valeur de la
première fonction qui ne génère pas d'exception. Cela permet d'essayer
plusieurs façons de faire, mais aussi de retenter l'exécution d'une fonction,
si on lui passe plusieurs fois la même fonction.</p>
<p>Je vous propose un code qui permet de faire l'équivalent en java. Pour cela
J'ai utilisé le patron de conception 'template' et les génériques Java 5. J'ai
eu besoin de coder cela afin d'être plus tolérant aux erreurs (retenter
plusieurs fois une requête HTTP qui pouvait aboutir à un timeout serveur). Je
vous conseille de restreindre le catch de la fonction <code>retry()</code> afin
de de ne retenter l'exécution, uniquement pour une exception donnée et de
propager les autres exceptions le cas échéant.</p> <p>Voci le code :</p>
<pre class="brush: java">
package com.gisgraphy;
public abstract class RetryOnErrorTemplate {
private int alreadyTry = 0;
private int numberOfTimesToRetry = 1;
private T returnvalue = null;
private RetryOnErrorTemplate retry() throws Exception {
try {
returnvalue = tryThat();
alreadyTry++;
return this;
} catch (Exception e) {
numberOfTimesToRetry--;
alreadyTry++;
if (numberOfTimesToRetry == 0) {
throw e;
} else {
return retry();
}
}
}
public abstract T tryThat() throws Exception;
public T times(int numberOfTry) throws Exception {
this.numberOfTimesToRetry = numberOfTry;
retry();
return returnvalue;
}
/**
* @return The number of times the code has already been try
*/
public int getAlreadyTry() {
return alreadyTry;
}
/**
* @return The number of times the code should be try
*/
public int getNumberOfTimesToRetry() {
return numberOfTimesToRetry;
}
}
</pre>
<p>Voci le test JUnit 4:</p>
<pre class="brush : java">
package com.gisgraphy;
import junit.framework.Assert;
import org.junit.Test;
import com.gisgraphy.domain.geoloc.service.geoloc.GisgraphyCommunicationException;
public class RetryOnErrorTemplateTest {
private static final String THE_RETURNED_VALUE = "The returned value";
@Test
public void testRetryWhenCodeThrows() throws Exception {
RetryOnErrorTemplate<String> retryOnError = new RetryOnErrorTemplate<String>() {
@Override
public String tryThat() throws Exception {
if (true) {
throw new GisgraphyCommunicationException();
}
return "will never be return";
}
};
try {
retryOnError.times(3);
Assert.fail("the code should have throws");
} catch (GisgraphyCommunicationException ignore) {
}
Assert.assertEquals("The number of retry is not the expected one",3, retryOnError.getAlreadyTry());
}
@Test
public void testRetryWhenCodeDoesnTThrows() throws Exception {
RetryOnErrorTemplate<String> retryOnError = new RetryOnErrorTemplate<String>() {
@Override
public String tryThat() throws Exception {
return THE_RETURNED_VALUE;
}
};
Assert.assertEquals("The returned value is not the expected one",THE_RETURNED_VALUE, retryOnError.times(3));
Assert.assertEquals("Only one try should have been done",1, retryOnError.getAlreadyTry());
}
}
</pre>Faire des tests combinatoires avec les theories JUnit (partie2)urn:md5:883e510bfcecc9cfe6a7a32edc03c50c2009-11-19T09:50:00+01:002015-06-07T11:26:10+02:00MD3804-GANDIJava / J2EEjavajunittheory<p>Lors du <a href="https://davidmasclet.gisgraphy.com/post/2009/11/17/faire-des-test-combinatoires-avec-les-theory-JUnit">précédent
post</a> je vous parlais des Theories. Aujourd'hui je vous propose d'aller un
peu plus loin dans la définition du jeu de données en faisant de la
combinatoire et en définissant un jeu de données dynamiquement.</p> <p>Disons que l'on veuille tester une méthode avec un jeu de données spécifié
sous forme de tableau. Nous allons utiliser l'annotation
<code>@ParametersSuppliedBy</code> :</p>
<p>Définissons une annotation <code>IntegerArrayParameters</code> où l'on
pourra spécifier une liste de <code>int</code> :</p>
<pre>
package com.gisgraphy.samples;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.junit.experimental.theories.ParametersSuppliedBy;
@Retention(RetentionPolicy.RUNTIME)
@ParametersSuppliedBy(IntegerArayParametersSupplier.class)
public @interface IntegerArrayParameters {
int[] values();
}
</pre>
<p>Il nous faut aussi définir une autre annotation qui créera le jeu de données
à partir des valeurs rentrées, (cela peut paraître un peu obscure et redondant
mais cela deviendra sans doute plus clair avec le deuxième exemple) :</p>
<pre>
package com.gisgraphy.samples;
import java.util.ArrayList;
import java.util.List;
import org.junit.experimental.theories.ParameterSignature;
import org.junit.experimental.theories.ParameterSupplier;
import org.junit.experimental.theories.PotentialParameterValue;
public class IntegerArayParametersSupplier extends ParameterSupplier {
@Override
public List<PotentialParameterValue> getValueSources(Object test, ParameterSignature sig) {
IntegerArrayParameters annotation = (IntegerArrayParameters) sig.getSupplierAnnotation();
List<PotentialParameterValue> list = new ArrayList<PotentialParameterValue>();
for (int value : annotation.values()) {
list.add(PotentialParameterValue.forValue(value));
}
return list;
}
}
</pre>
<p>On peut alors définir un jeu de données de <code>int</code> et faire de la
combinatoire :</p>
<pre>
package com.gisgraphy.samples;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
@RunWith(Theories.class)
public class AbstractHomeControllerTest {
@Theory
public void testHandleRequestInternal(@IntegerArrayParameters(values = {1,2,3}) int param1,
@IntegerArrayParameters(values = {4,5,6}) int param2) throws Exception {
System.out.println("param1=" + param1 + " param2=" + param2);
//do the stuff
}
}
</pre>
<p>Afin de montrer la combinatoire, voici la sortie de la console lorsqu'on
joue le test :</p>
<pre>
param1=1 param2=4
param1=1 param2=5
param1=1 param2=6
param1=2 param2=4
param1=2 param2=5
param1=2 param2=6
param1=3 param2=4
param1=3 param2=5
param1=3 param2=6
</pre>
<p>Deuxième exemple : Reprenons la classe de la <a href="https://davidmasclet.gisgraphy.com/post/2009/11/17/faire-des-test-combinatoires-avec-les-theory-JUnit">partie 1
sur les théories</a>, avec la classe qui calculait la racine carrée. Nous
avions un assumeTrue pour différencier les valeurs positives des valeurs
négatives qui n'était pas très élégant du tout. Procédons différemment et
définissons un jeu de données qui balaye les nombres compris entre deux valeurs
spécifiées :</p>
<p>Définissons notre <code>RangeParameter</code> :</p>
<pre>
package com.gisgraphy.samples;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.junit.experimental.theories.ParametersSuppliedBy;
@Retention(RetentionPolicy.RUNTIME)
@ParametersSuppliedBy(RangeParametersSupplier.class)
public @interface RangeParameters {
int from();
int to();
}
</pre>
<p>Puis un <code>RangeParametersSupplier</code> :</p>
<pre>
package com.gisgraphy.samples;
import java.util.ArrayList;
import java.util.List;
import org.junit.experimental.theories.ParameterSignature;
import org.junit.experimental.theories.ParameterSupplier;
import org.junit.experimental.theories.PotentialParameterValue;
public class RangeParametersSupplier extends ParameterSupplier {
@Override
public List<PotentialParameterValue> getValueSources(Object test, ParameterSignature sig) {
RangeParameters annotation = (RangeParameters) sig.getSupplierAnnotation();
List<PotentialParameterValue> list = new ArrayList<PotentialParameterValue>();
for (int i=annotation.from(); i<=annotation.to(); i++) {
list.add(PotentialParameterValue.forValue(i));
}
return list;
}
}
</pre>
<p>Enfin reprenons la classe initiale et modifions la pour tester toutes les
valeurs de 1 à 10 :</p>
<pre>
import junit.framework.TestCase;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
@RunWith(Theories.class)
public class SqrtTest extends TestCase {
public static int count = 0;
//La classe à tester
public static class MySqrt {
public static Double GetSqrt(int param) {
if (param < 0){
throw new IllegalArgumentException(
"Param should not be negative");
}
return Math.sqrt(param);
}
}
@Theory
public void testSqrtShouldReturnTheCorrectValue(
@RangeParameters(from=1,to=10) int value) {
System.out.println("value="+value);
assertEquals(Math.sqrt(value), MySqrt.GetSqrt(value));
}
}
</pre>
<p>Pour preuve, voici la sortie standard :</p>
<pre>
value=1
value=2
value=3
value=4
value=5
value=6
value=7
value=8
value=9
value=10
</pre>
<p>On peut alors définir des jeux de données très facilement.</p>Faire des tests combinatoires avec les theories JUniturn:md5:f041947df44c3de719f38fdf5d07b16e2009-11-17T23:32:00+01:002015-06-07T11:26:15+02:00MD3804-GANDIJava / J2EEjavatests<p>Il arrive parfois que l'on ait à faire des tests avec des jeux de données
combinatoires. Par exemple, on a un jeu de données A : disons {A1,A2}, et
un jeu de données B : {B1,B2}, et on voudrait faire un test où toutes les
combinaisons A * B soit testées :</p>
<ul>
<li>testMethode(A1,B1)</li>
<li>testMethode(A1,B2)</li>
<li>testMethode(A2,B1)</li>
<li>testMethode(A2,B2)</li>
</ul>
<p>Il est possible dans ce cas d'utiliser les <q>theories</q>. Il s'agit d'une
technique, pour l'instant expérimentale (cf : nom du package), disponible
à partir de JUnit 4.4.</p> <p>Commençons pas un exemple simple où il nous faut tester une méthode avec un
jeu de données mais sans la combinatoire :</p>
<ul>
<li>testMethode(A1)</li>
<li>testMethode(A2)</li>
</ul>
<pre>
package com.gisgraphy.samples;
import static org.junit.Assume.assumeTrue;
import junit.framework.TestCase;
import org.junit.experimental.theories.DataPoint;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
@RunWith(Theories.class)
public class SqrtTest extends TestCase {
public static int count = 0;
//La classe a tester
public static class MySqrt {
public static Double GetSqrt(Double param) {
if (param < 0){
throw new IllegalArgumentException("Param should not be negative");
}
return Math.sqrt(param);
}
}
@DataPoint
public static final Double value1 = 25D;
@DataPoint
public static final Double value2 = 16D;
@DataPoint
public static final Double value3 = -1D;
@Theory
public void testSqrtShouldReturnTheCorrectValue(Double value) {
assumeTrue(value.doubleValue() > 0);
assertEquals(Math.sqrt(value), MySqrt.GetSqrt(value));
}
@Theory
public void testSqrtShouldThrowForNegativeParam(Double value) {
try {
assumeTrue(value.doubleValue() < 0);
Double result = MySqrt.GetSqrt(value);
fail("negative parameter Should Throw");
} catch (IllegalArgumentException e) {
//OK
}
}
}
</pre>
<p>Tout d'abord, il faut annoter la classe de test avec
<code>@RunWith(Theories.class)</code> pour lui spécifier le type de test.</p>
<p>L'annotation <em>@DataPoint</em> permet de définir le jeu de données qui
sera appliqué aux méthodes annotées par <em>@Theory</em>.</p>
<p>On remarquera que la syntaxe 'public void testXXX(){...} qui, normalement,
ne prend pas de paramètres, se retrouve avec un paramètre qui est du même type
que les champs annotés par <em>@DataPoint</em></p>
<p>Dans l'exemple ci-dessus, les méthodes testSqrtShouldThrowForNegativeParam()
et testSqrtShouldReturnTheCorrectValue() se verront appliquer
successivement : value1,value2, et value3.</p>
<p>Afin de ne pas exécuter les tests avec toutes les valeurs pour toutes les
méthodes, il est utile d'utiliser <em>assumeTrue</em> (en import static) pour
spécifier une condition afin que le test soit exécuté ou pas. Dans notre
exemple testSqrtShouldReturnTheCorrectValue() ne prend que les valeurs
positives tandis que 'testSqrtShouldThrowForNegativeParam' ne prend que les
valeurs négatives. Toutes les valeurs seront appliquées mais le test les
utilisera ou pas. Ceci est donné à titre d'exemple mais ne fait pas partie des
théories en soit.</p>
<p>Une utilisation classique des théories est de faire un jeu de données avec
des valeurs aux limites, null, des valeurs correctes, et des valeurs
incorrectes afin de rendre un code plus robuste et éviter les trous dans la
raquette. Il ne faut pas les utiliser à tort et à travers car les assertions
peuvent vite devenir compliquées :</p>
<p><code>assert que Truc si valeur = 1 sinon assert machin</code></p>
<p>Si vous codez ce genre de choses dans un test : c'est un 'smell' que
les théories ne sont pas adéquates dans votre cas.</p>
<p>Le prochain <a href="https://davidmasclet.gisgraphy.com/post/2009/11/19/Faire-des-tests-combinatoires-avec-les-theories-JUnit-%28partie2%29">
post</a> sera sur l'aspect combinatoire des théories. A demain ! :)</p>