Posts Tagged ‘fuite’
Sujet intéressant, les problèmes en production sont quand même pour beaucoup des casse-têtes. Effectivement c’est bien vrai, les problèmes en production sont difficiles à résoudre. Il y a bien une partie technique souvent absconse, mais ce n’est pas un travail en solitaire, c’est un travail d’équipe. L’organisation dans la boite favorise ou freine cette opportunité de résoudre un problème à travers les différentes équipe impliquées. Dans ce billet je vous fait un petit retour d’expérience sur un problème de lenteur sur une application web genre “CRUD” sur une JVM IBM.
Le contexte
“The context is King” disait Andy Hunt dans son livre Pragmatic Learning and Thinking. Alors brève introduction au contexte du problème.
- Application assez simple qui permet de rechercher, consulter, créer, modifier des données différentes issues du domaine métier.
- L’application tourne sur un WebSphere.
- WebSphere tourne sur une JVM IBM. Ah là ça sent la pêche aux informations, n’ayant jamais travaillé sur cette JVM, et pas de bol non plus la JVM IBM sur Windows n’est pas accessible gratuitement depuis le site de IBM.
- L’ensemble tourne sur une machine AIX.
- L’entreprise utilise l’outils Introscope qui permet d’avoir plein de métriques.
Le problème
Les utilisateurs ressentent des lenteurs, voire des freezes. L’outil INSC identifie ces threads en Stalled, et permet de donner des métriques sur les temps d’attentes et de réponses de certains éléments du systèmes, les ingénieurs systèmes utilisent abondamment cet outils. Bref, les temps de réponses vont de quelques dizaines de secondes à plusieurs minutes.
Il y a définitivement un problème. Forcément quand on a un super outils comme INSC qui a plein de métriques, on se balade dedans pour essayer de trouver le problème. Malheureusement un seul thread dump pour voir ce qu’il se passe dans les threads ne suffit pas. Le thread dump révèle que la plupart des threads sont à l’état “Waiting On Condition“. Le super outils INSC indique un usage de la heap, il indique aussi que certaines requêtes SQL sont très très longues, il indique les sessions web ouvertes une petite trentaine, pas de quoi fouetter un chat. Et pourtant il y aurait du CPU assez fortement utilisé, vu depuis Introscope.
Mais voilà l’analyse tourne un peu autour du pot. Pourquoi ces threads sont-elles bloquées? Pourquoi les requêtes SQL sont-elles aussi longues?!
Le problème dans l’analyse
Récapitulons, dans cette situation les métriques de Introscope ont été regardées, et on a un seul thread dump.
Introscope malgré ces métriques ne dit pas ou est le problème, il ne dit même pas quel est le type de problème. Les métriques affichées sont pour certaines intéressantes, je pense à l’identification des threads figées, les temps de réponses de certains composants, la consommation mémoire, et l’utilisation du CPU.
Mais pourquoi cet outils n’a pas aidé à trouver le problème, parce qu’il ne mesure pas les bonnes choses. Et il faut en particulier comprendre que quand un système fonctionne mal il y a un effet de corrélation qui s’applique sur un ensemble de variable. Et ce n’est pas avec Introscope qu’on va pouvoir identifier la cause du ralentissement généralisé, ni trouver une relation de cause-à-effet.
Pour résoudre un problème, il faut s’équiper avec les bons outils! Il faut aussi regarder les bonnes données, au bon endroit, et au bon moment!
A la poursuite du vrai problème (partie 1)
Bon hop, déjà pour commencer j’écarte pour le moment Introscope. Et j’ai un thread dump … de la JVM de IBM, il va falloir essayer les outils IBM qui permettent de traiter ces informations. Je vais me satisfaire ça pour l’instant.
L’analyse du thread dump
Avec : IBM Thread and Monitor Dump Analyzer for Java
Évidement le format ne correspond pas à celui de Sun, heureusement IBM nous fournit des outils pour analyser ces informations. Direction http://www.alphaworks.ibm.com/tech/jca.
A l’ouverture un rapport apparait, il commence par ça en rouge :
***WARNING*** Java heap is almost exhausted : 0% free Java heap Please enable verbosegc trace and use IBM Pattern Modeling and Analysis Tool(http://www.alphaworks.ibm.com/tech/pmat) to analyze garbage collection activities. If heapdumps are generated at the same time, please use IBM HeapAnalyzer(http://www.alphaworks.ibm.com/tech/heapanalyzer) to analyze Java heap.
Ok, là c’est assez facile de savoir ou ça va! Mais allons plus loin!
Il y a également :
Number of Processors : 4
Java version : J2RE 5.0 IBM J9 2.3 AIX ppc64-64 build j9vmap6423-20090707Java Heap Information
Maximum Java heap size : 384m
Initial Java heap size : 384m
OK, j’en apprends un peu plus sur la JVM et la machine.
Free Java heap size: 0 bytes
Allocated Java heap size: 402 653 184 bytes
Ok évidement tout s’explique il ne reste plus rien pour allouer dans la Heap.
Tiens dans la ligne de commande je voit que Introscope est un agent de le JVM:
-Xshareclasses:name=webspherev61_%g,groupAccess,nonFatal -Dibm.websphere.internalClassAccessMode=allow -Dcom.wily.introscope.agentProfile=/opt/wily/wilyAgent/AvtAgent.profile -javaagent:/opt/wily/wilyAgent/Agent.jar
C’est intéressant, si la JVM est lente, ca peut vouloir dire que les mesures Introscope sont aussi soumises aux lenteurs de la JVM.
Le rapport est sympa, il donne la répartition de la mémoire de la JVM :
Memory Segment Analysis
Memory Type # of Segments Used Memory(bytes) Used Memory(%) Free Memory(bytes) Free Memory(%) Total Memory(bytes) Internal 13 1 191 740 88,4 156 452 11,6 1 348 192 Object (reversed) 1 402 653 184 100 0 0 402 653 184 Class 9 735 228 637 344 90,96 22 724 116 9,04 251 361 460 JIT Code Cache 5 41 943 040 100 0 0 41 943 040 JIT Data Cache 3 17 018 496 67,63 8 147 328 32,37 25 165 824 Overall 9 757 691 443 804 95,71 31 027 896 4,29 722 471 700
Ok, c’était un tableau intéressant, on voit clairement que dans une JVM il n’y a pas que de la Heap (pour ceux qui ne le savait pas), en effet on voit donc les sections suivantes (les passages surlignés viennent de moi, malheureusement l’outil d’IBM ne nous aide pas là dessus) :
- la mémoire interne de la JVM (les objets internes, les structure de thread, et autres objets natifs) : bonne utilisation
- les objets, en une seule section de mémoire, c’est la Heap, et là ben effectivement elle utilisée à 100%.
- les sections des classes, c’est la ou le byte code de vos classes est stocké par la JVM, mais ce n’est pas dans la Heap (chez la JVM de Sun ça correspondrait à la PermGen area), bref là aussi 90% d’utilisation c’est plutôt pas mal.
- JIT Code Cache et JIT Data Cache, c’est là ou la JVM va stocker le code natif qu’elle aura compilée depuis le bytecode, là aussi c’est rempli à100% mais c’est peut-être normal, après tout la taille totale est plus petite.
On voit aussi que la mémoire accessible dans la Heap est quand même supérieure a ce qui est indiqué dans la ligne de commande, à savoir les 384 MB. Ne connaissant pas la JVM IBM, je ne suis pas certains des raisons induisant ce phénomène.
Mais à 100% d’utilisation, ça sent le GC qui s’excite pour garder ses petits. Mais le rapport est long et n’est pas terminé, il reste des choses à lire.
Thread Status Analysis
Status Number of Threads : 170 Percentage Deadlock 0 0 % Runnable 12 7 % Waiting on Condition 158 93 % Waiting On Monitor 0 0 % Suspended 0 0 % Object.wait() 0 0 % Blocked 0 0 % Parked 0 0 %
Vous vous souvenez des threads vues en Wait on Condition au tout début, on les retrouve donc ici dans les stats du thread dump. Il y a environ 158 thread qui ne font rien et 12 threads qui travaillent. Alors petite parenthèse, qu’est ce que ça veut dire ce Waiting on Condition. Les raisons peuvent être les suivantes :
- Thread.sleep(), en gros on indique simplement à la thread de ne rien faire, mais c’est quand même la JVM qui gère ce sleep()
- Object.wait(), en gros quelque part dans le code un thread est en attente pour qu’une condition se réalise, voire le code en question pour en savoir plus sur la condition. Cette condition peut aussi être une condition interne à la JVM.
- La thread est en train de se synchroniser avec une autre, elle doit donc attendre que l’autre thread finisse son job, on verra probablement dans la stack un appel à un Thread.join().
- Unsafe.park, et autres support pour les lock
- La thread est blockée par des opérations d’I/O.
Déjà ce n’est pas forcément un problème pour toutes les threads, typiquement on peut s’attendre à voir des threads relatives aux systèmes de cache (ehcache et consorts) qui sont dans ces états. Ensuite il faut comprendre que ce mécanisme implique des conditions internes à la JVM. L’ordonnanceur (scheduler) de la JVM, qui en réalité fait appel au scheduler de l’OS, donne la main à d’autres traitements (java ou pas).
Le prochain tableau du rapport nous indique ou sont (toutes) nos threads, mais pas d’analyse par type de statut. On observe bien certaines des raisons citées plus haut.
Thread Method Analysis
Method Name Number of Threads : 170 Percentage java/lang/Object.wait(Native Method) 88 52 % java/lang/Thread.sleep(Native Method) 63 37% NO JAVA STACK 6 4 % java/net/PlainSocketImpl.socketAccept(Native Method) 5 3 % java/net/SocketInputStream.socketRead0(Native Method) 3 2 % com/ibm/misc/SignalDispatcher.waitForSignal(Native Method) 1 1 % d’autres ligne à 1%…
Pas mal de thread sont en attente, et beaucoup d’autres dorment. Quelques threads sans stack Java, ce sont des threads qui appartiennent à la JVM.
J’arrête là pour le moment sur le rapport de cet outils IBM, mais ce qu’il faut retenir c’est que le thread dump est utile pour étudier des threads, mais c’est utile sur un laps de temps, avec un seul cliché on ne se rends pas compte réellement du comportement. Et en plus ces données ne sont pas intéressantes pour savoir ce qu’il se passe coté gestion mémoire. Heureusement que J9 (la JVM de Mr IBM) fournit quelques infos, sinon je ne vois pas comment diagnostiquer le problème sans jeter un œil sur les données adéquates, c’est à dire le log du Garbage Collector.
L’analyse du log GC, enfin!
Après l’obtention du fameux log, il faut encore trouver un outil IBM pour analyser le fichier. Les choses deviennent intéressantes. Comme je l’ai déjà dit, les logs sont au format IBM, ça ressemble à du XML. Et pour analyser ces logs rien de mieux que les outils de IBM, non? Google me dit rapidement qu’il me faut donc ce truc Pattern Modeling and Analysis Tool for Java Garbage Collector, direction http://www.alphaworks.ibm.com/tech/pmat.
Avec : Pattern Modeling and Analysis Tool for Java Garbage Collector
Un petit graphique pour regarder ce qu’il se passe.
En rouge l’usage de la Heap, en bleu le marquage des objets à virer, et en vert les temps de compression de la mémoire. Effectivement le GC à l’air de bien s’amuser dans la mémoire, et d’être appelé assez souvent.
Bon celà dit je ne suis pas convaincu par l’outil IBM, il manque des informations que j’avais vu en texte dans le log GC ; un évènement GC à cette tête là :
<af type="tenured" id="388571" timestamp="Jun 21 03:01:11 2010" intervalms="228.858"></pre>
<minimum requested_bytes="168" />
<time exclusiveaccessms="0.489" />
<tenured freebytes="0" totalbytes="402653184" percent="0" >
<soa freebytes="0" totalbytes="402653184" percent="0" />
<loa freebytes="0" totalbytes="0" percent="0" />
</tenured>
<gc type="global" id="388576" totalid="388576" intervalms="230.720">
<refs_cleared soft="0" threshold="32" weak="76" phantom="0" />
<finalization objectsqueued="0" />
<timesms mark="408.282" sweep="5.061" compact="0.000" total="415.341" />
<tenured freebytes="209018360" totalbytes="402653184" percent="51" >
<soa freebytes="209018360" totalbytes="402653184" percent="51" />
<loa freebytes="0" totalbytes="0" percent="0" />
</tenured>
</gc>
<tenured freebytes="209013328" totalbytes="402653184" percent="51" >
<soa freebytes="209013328" totalbytes="402653184" percent="51" />
<loa freebytes="0" totalbytes="0" percent="0" />
</tenured>
<time totalms="417.431" />
</af>
Vu l’allure du log, on a pas l’impression d’être sur un GC de type generationnel, mais je ne suis pas encore sûr, c’est une JVM IBM. Bon revenons à nos moutons:
- il y a eu 230 ms d’écoulées avant le dernier GC.
- la tenured indique directement qu’il n’y a plus de place dans la mémoire,
- on voit que le GC est de type global, ce qui veut dire que c’est toute la zone mémoire qui est affectée par le GC, c’est long
- la tenured libère environ 200 MB, soit 51%!
- le temps total mis par ce GC est de 420 ms, c’est long.
Et il y a plein d’entrées comme ça, ça fait beaucoup de GC globaux de 1 demi-secondes, tous les 5ème de secondes. Le GC prends du temps CPU pour nettoyer la mémoire un peu trop souvent. Et ce ci pourrait bien être la cause des ralentissements observés. En gros soit il n’y a simplement pas assez de mémoire, soit il y a une fuite mémoire.
Avec : IBM Support Assistant et Garbage Collection and Memory Visualizer
En me renseignant, je voulais jeter un œil aux outils IBM plus récents, tel que celui mentionné par Holly Cummins au ParisJug (le 8 Juin 2010). Pour commencer il faut un compte IBM et télécharger IBM Support Assistant, direction http://www-01.ibm.com/software/support/isa/.
Après une fois qu’on a l’outil, il faut à nouveau télécharger des plugins (nommé additif sur l’interface en français). Bon en fait le Health Center en me sert à rien puisqu’il il faut se connecter à une JVM IBM, ayant une JVM Sun sur mon poste je ne vais quand même pas aller taper sur la prod si tenté que ce soit possible. Finalement j’opte pour le plugin : Garbage Collection and Memory Visualizer.
Donc finalement j’essaye cet outils d’analyse, et j’ai un rapport bien plus sympa et complet avec plein de graphiques qui m’intéressent.
Déjà le rapport débute par :
Your application appears to be leaking memory. This is indicated by the used heap increasing at a greater rate than the application workload (measured by the amount of data freed). To investigate further see Guided debugging for Java
Ok, je m’en doutais déjà mais c’est quand même mieux que de dire que la mémoire est quasiment entièrement utilisée. Et on retrouve les alertes suivantes dans le rapport :
The application seems to be using some quite large objects. The largest request which triggered an allocation failure (and was recorded in the verbose gc log) was for 5242904 bytes.
5MB quand même! Cela dit ça n’arrive pas souvent, c’est peut-être un cache qui charge des données depuis le disque. Le graphe suivant (choisir Object Size dans les templates de graphique sur la droite) montre la taille des allocations demandées.
Mais la concomitance de ses demandes d’allocation avec l’utilisation de la heap fait sourciller.
On continue
Garbage collection is causing some large pauses. The largest pause was 7362 ms. This may affect application responsiveness. If responsiveness is a concern then a switch of policy or reduction in heap size may be helpful.
Effectivement le temps passé dans l’application et le temps passé dans le GC indique manifestement qu’il y a une suractivité anormale du GC.
En fait on voit même que le GC est en train de compacter la mémoire au moment de l’incident, c’est la courbe rouge clair (entre ~0.5s et 1s), ajouté à cela le temps de marquage des objets à virer (bon en fait dans le graphique que j’ai fait, le temps de pause est principalement du au temps de marquage), l’ensemble donnant un temps de pause pour laisser le GC travailler allant de 1 à 7s (par GC bien évidement).
Et là effectivement expliquer les ralentissements de l’application devient plus facile, merci à ces beaux graphiques explicites.
On peut regarder vraiment beaucoup de chose avec cet outils, même s’il y a des défauts manifestes dans l’interface utilisateur. C’est quand même plutôt pas mal.
Je termine sur un petit résumé des valeurs intéressantes que nous donne cet outils.
Allocation failure count 59971 Forced collection count 3 GC Mode optthruput Largest memory request (bytes) 5242904 Mean garbage collection pause (ms) 491 Mean heap unusable due to fragmentation (MB) 0.2 Mean interval between collections (minutes) 0.01 Number of collections 59974 Proportion of time spent in garbage collection pauses (%) 58.24 Proportion of time spent unpaused (%) 41.76 Rate of garbage collection (MB/minutes) 13250
Tiens le mode GC est optthruput, en fait c’est une des polices du comportement du GC, et probablement de la manière de segmenter la mémoire (Nursery (Young), Old (Tenured)).
En effet dans les logs GC, je n’ai pratiquement vu que des GC globaux et uniquement sur la section de la tenured, à priori pas de zone nursery, c’est probablement du à ce comportement du GC.
En me renseignant donc, il y a 4 polices de GC dans la JVM J9 de IBM :
- optthruput : Optimisé pour throughput (le débit), flat heap <= Bingo
- optavgpause : Optimisé pour les temps de pause (Stop-The-World), le CMS est configuré pour prendre le moins de temps, flat heap
- subpool : Un police optimisé pour les machine multi-processeur, flat heap
- gencon : C’est le GC générationnel, qui est divisé en zone
- nursery : qui permet la collection rapide et efficace des objets de vie courte, pas de pause
- tenured : zone des vieux objets, mais un GC dans cette zone est global et demande à pauser l’application
A la poursuite du vrai problème (partie 2)
La pèche aux informations
Après avoir passé le GC au mode générationnel, il y a toujours ces problèmes de lenteurs mais ce n’est plus généralisé à toute l’appli, pas de log GC pour vérifier mais Introscope indique une utilisation relativement correcte de la mémoire, bizarre. Back to basics!
Le thread dump de la JVM IBM me dit toujours que la Heap est utilisée à 100%, mais je vois quand même
Free Java heap size: 72 041 864 bytes
Allocated Java heap size: 402 653 184 bytes
Et plus loin :
Last Garbage Collection Detail
Nursery Area Free : 59 307 392 bytes Total : 60 397 568 bytes 98 % free
Tenured Area Free : 17 058 368 bytes Total : 335 544 320 bytes 5% free
Global Garbage Collector Counter : 148
La tenured est bien remplie et utilise un très grosse partie de la heap ; memory leak ou beaucoup d’objet à mettre en cache. Ou encore autre chose, sans mesures claires pour écarter les hypothèses ces dur.
Pour quoi ne pas activer dans tous les cas le log GC, la JVM IBM offre des option pour gérer la rotation des logs GC, comme ça l’argument de saturation du disque tombe à l’eau. Mais bon il faut lire la documentation; donc petit passage chez IBM grâce à Google, et hop :
-Xverbosegclog[:<file>[,<X>,<Y>]]
Et voilà : file étant le couple chemin + fichier, X le nombre de fichier maximum (ça tourne et écrase les fichiers), Y le nombre de cycle GC. Ce que ne dis pas par contre la doc IBM c’est la taille approximative d’un GC, donc 700 cycles de GC ≃ 1 MB. Il est même possible d’utiliser des tokens utilisés pour les dumps dans WAS 7 (voir ici).
Recoupement des informations
Le log du Garbage Collector ne venant pas, il faut chercher autrement. Je demande au moment ou le problème se reproduit , de faire plusieurs thread dump d’affilé séparé de quelques secondes (~20s) et de faire également un listing des sous-processus java.
En effet le thread dump est bien sympa, mais il ne donne pas la consommation CPU des threads.
Dans un environnement il faut entre dans le terminal (Dans Linux l’identifiant des thread est dans la colonne LWD.) :
ps -fLp <processid> -L
Evidement il s’agit d’un AIX et les commendes sont différents, pas de soucis un petit tour dans la doc IBM et il faut entrer la commande suivante, et là l’identifiant de la thread est dans la colonne TID :
ps -mp <processid> -o THREAD
On a alors un listing énorme, que j’ai tronqué ici.
USER PID PPID TID S CP PRI SC WCHAN F TT BND COMMAND wasadmin 393262 401580 - A 188 60 203 * 202001 - - /opt/was61/java/bin/java ... - - - 700535 S 0 82 1 f100070f1000ab40 8410400 - - - - - - 741581 S 0 82 1 f100070f1000b540 8410400 - - - - - - 802997 S 0 82 1 f100070f1000c440 8410400 - - - - - - 884895 S 0 82 1 f100070f1000d840 8410400 - - - - - - 1183791 Z 0 82 1 - c00001 - - - - - - 1667157 R 60 122 0 - 400000 - - - - - - 1708269 S 0 82 1 f100070f1001a140 8410400 - - - - - - 1736831 S 0 82 1 f100070f1001a840 8410400 - - -
La colonne CP me dit que manifestement la thread 1667157 utilise plutôt pas mal le CPU, qu’est-ce que donne cette thread du coté du thread dump ?! Au fait on repère 3 threads dans le même cas.
Il faut savoir que dans le thread dump il y a l’identifiant de la thread en Java, mais qu’il y a aussi et surtout de mentionné l’identifiant natif de la thread, par exemple NID.
Je google “1667157 in hex” ce qui me renvoie 0×197055. En utilisant l’outils IBM mentionné plus haut, on voit clairement que la thread en cause correspond à du code métier, développé ici.
Chacune des 3 threads passent dans le même bout de code. Autant au début j’ai des doutes, après toute la présomption d’innocence compte aussi pour le code, d’autant plus qu’il s’agit d’un code lent qui utilise beaucoup de reflection. Mais faut prendre en compte aussi le fait que la pile descend à chaque fois dans la couche Hibernate, ça vaut le coup d’aller voir. Les développeurs qui connaissent un peu mieux le code poussent dans cette direction.
Entre temps les DBA confirme que la base de données réponds très bien, mais qu’elle enregistre un très fort nombre d’un certain type de requête SQL.
Bingo, il y a une race condition dans une des boucles, et celle-ci part en boucle infinie. Ceci explique la très forte utilisation de la mémoire et les lenteurs remarquées.
Bilan
Un problème peut en cacher un autre, ou plus exactement un problème peut en provoquer d’autres. Il faut juste avoir des moyens de mesurer les changements qu’on apporte si on veut isoler / écarter des catégorie de problèmes.
L’outillage on l’a vu est essentiel, Introscope apporte des choses, mais il ne permet pas tout. Qui plus est, on ne sait pas précisément ce qu’il mesure et ou! Les temps de réponses SQL, n’étaient par exemple pas crédible, car Introscope mesurait également les GC.
Dans notre cas ici, j’aurais aussi bien aimé avoir une JVM IBM sur mon poste histoire de jouer plus facilement avec. C’est dommage que IBM ne fournisse pas gratuitement sa JVM au moins pour le développement.
Accessoirement ce serait bien un jour d’avoir des format de log normalisé entre les JVM, ainsi que certaines des options afférentes.
Finalement ce qui a pris le plus de temps était d’obtenir les bonnes données, pour prendre les meilleurs choix. L’impression de travailler les mains dans le noir n’était pas l’idéal pour résoudre le problème, mais c’est au moins formateur. Je peux dire que j’ai bien apprécié certains retours et recommandations des développeurs. L’équipe système étant surchargée n’a pas pu nous donné un support optimal, et cette carence s’est ressentie notamment pour avoir les données à temps. Mais leur vu du problème a permis d’orienter la recherche sur les parties qui pouvait poser problème.
Références & Documentation
- http://websphere.sys-con.com/node/921279?page=0,1
- http://sites.google.com/site/threaddumps/java-thread-dumps
- http://java.sun.com/developer/technicalArticles/Programming/Stacktrace/
- http://geekexplains.blogspot.com/2008/07/threadstate-in-java-blocked-vs-waiting.html
- http://www.ibm.com/developerworks/java/library/j-nativememory-aix/
- http://www.ibm.com/developerworks/ibm/library/i-garbage1/
- http://www-01.ibm.com/support/docview.wss?rs=180&context=SSEQTP&dc=DB560&dc=DB520&uid=swg21384096&loc=en_US&cs=UTF-8&lang=en&rss=ct180websphere
- http://publib.boulder.ibm.com/infocenter/javasdk/v6r0/index.jsp?topic=/com.ibm.java.doc.diagnostics.60/diag/appendixes/cmdline/cmdline_gc.htm
Prologue
A l’école nos professeurs nous apprenaient ce qu’était la programmation orientée objet; en particulier l’encapsulation. En effet avoir un accès public aux variables internes d’un objet n’est pas particulièrement recommandé, pourtant nous avons connaissons tous la convention JavaBean :
class Bean {
/** constructeur sans argument, optionnel si c'est le seul constructeur de la classe */
public Bean() { }
public void setBeanName(String name) {
beanName = name;
}
public String getBeanName() {
return beanName;
}
}
Manque de bol, cette convention qui a pourtant son utilité -voire sa nécéssité- peut dans certains contextes briser l’encapsulation, et plus dangereux pour votre code, elle permet à vos objets d’être mutable, c’est à dire de pouvoir modifier l’état d’un objet après sa création. Bien que dans certains cas le design ou le rôle de la classe demande cette caractéristique, dans beaucoup d’autres situations la mutabilité peut poser problème.
D’ailleurs historiquement les JavaBeans ont été pensé pour être utilisé par des applications graphiques afin d’être construit itérativement et finalement pour être facilement dé/sérialisés [1]. Mais ces objets exposent publiquement leurs états, du coup :
- Il y a de l’adhérence à des propriétés internes d’un objet, s’il y a beaucoup de code qui utilise ces propriétés internes, l’évolutivité et la maintenance de ce code peut très vite devenir difficile et donc couteuse.
- Ce n’est plus vraiment de la programmation orientée objet. C’est en quelque sorte des variables globales, ça fait plus de 30 ans qu’on sait que les variables globales c’est mal! Demandez à Barbara Liskov [2].
- Avec cette possibilité de muter les objets, il peut y avoir des problèmes au runtime, et croyez moi avec l’arrivée de la parallélisation en plus dans vos applications il va y avoir des surprises.
Bon revenons au design, et aux problèmes rencontrés.
Illustration des problèmes de design du code
hashCode et equals
Donc pour commencer, on va juste faire quelques tests sur un objet sans les méthodes hashCode() et equals(). Prenons les test suivants, je créé 4 instances de beans, obj1 et obj3 puis obj2 et obj4 ont les mêmes propriétés.
Ce test montre les problèmes quand on oublie les méthodes equals et hashCode.
public class MutabilityCanBeBadTest {
private AJavaBean obj1;
private AJavaBean obj2;
private AJavaBean obj3;
private AJavaBean obj4;
@Before
public void initTheBeans() {
obj1 = new AJavaBean();
obj1.setName("paraboot");
obj1.setSellingDate(new GregorianCalendar(2010, 03, 30).getTime());
obj2 = new AJavaBean();
obj2.setName("ethnies");
obj2.setSellingDate(new GregorianCalendar(2010, 10, 30).getTime());
obj3 = new AJavaBean();
obj3.setName("paraboot");
obj3.setSellingDate(new GregorianCalendar(2010, 03, 30).getTime());
obj4 = new AJavaBean();
obj4.setName("ethnies");
obj4.setSellingDate(new GregorianCalendar(2010, 10, 30).getTime());
}
@Test
public void objectsShouldBeEquals() throws Exception {
assertEquals(obj2, obj4); // fail
assertEquals(obj1, obj3); // fail
}
@Test
public void hashCodeShouldBeEquals() throws Exception {
assertEquals(obj1.hashCode(), obj3.hashCode()); // fail
assertEquals(obj2.hashCode(), obj4.hashCode()); // fail
}
@Test
public void addAndRemoveToHashBasedCollection() throws Exception {
Set<AJavaBean> set = new HashSet<AJavaBean>();
assertTrue(set.add(obj1));
assertTrue(set.add(obj2));
assertFalse(set.add(obj3)); // fail
assertFalse(set.add(obj4)); // fail
assertEquals(2, set.size()); // fail
assertTrue(set.remove(obj1));
assertTrue(set.remove(obj2));
assertFalse(set.remove(obj3)); // fail
assertFalse(set.remove(obj4)); // fail
}
}
Si l’implémentation de AJavaBean oublie donc le hashCode et le equals, la plus part des assertions ne marchent plus.
public class AJavaBean {
private String name;
private Date sellingDate;
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public Date getSellingDate() {
return sellingDate;
}
public void setSellingDate(final Date uid) {
this.sellingDate = uid;
}
@Override
public String toString() {
return "AJavaBean [name=" + name + ", sellingDate=" + sellingDate + "]";
}
}
Que s’est-il passé? S’il n’y a pas de hashCode et de equals, ce sont les méthodes de la super classe qui sont utilisées, dans le code listé plus haut ce sont les méthodes de Object qui seront utilisées pour tester l’égalité et le hashCode.
- Donc pour l’égalité Object.equals(Object) vérifie uniquement si l’instance est la même. Ce qui explique que les tests d’égalité échouent plus haut.
- Pour le hashCode, c’est la JVM qui le génère, bref autant dire que le hashcode est différent pour chaque instance. Ceci explique que les instances obj1 et obj2 sont ajoutées au HashSet, si le hashcode avait été le même alors les opérations d’ajout et de suppression auraient renvoyé false (n’oublions pas qu’il s’agit d’un HashSet).
Et donc pour le code mutable
Ok, bon maintenant qu’on a vu ça, notre bean implémente les méthodes equals et hashCode de manière idoine, c’est à dire dans notre cas que le code se base sur les attributs name et sellingDate. Pas de mystère, on peut utiliser l’outils de génération de l’IDE.
Eclipse génère ça:
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((sellingDate == null) ? 0 : sellingDate.hashCode());
return result;
}
@Override</div>
public boolean equals(final Object obj) {
if (this == obj) { return true; }
if (obj == null) { return false; }
if (getClass() != obj.getClass()) { return false; }
AJavaBean other = (AJavaBean) obj;
if (name == null) {
if (other.name != null) { return false; }
} else if (!name.equals(other.name)) { return false; }
if (sellingDate == null) {
if (other.sellingDate != null) { return false; }
} else if (!sellingDate.equals(other.sellingDate)) { return false; }
return true;
}
Bon à priori on se dit que notre code est safe puisqu’on a nos méthodes equals et hashcode, mais on se fourvoie ; notre objet est mutable!
Exemple :
@Test
public void playWithMutabilityWithABeanInHashBasedCollection() throws Exception {
Set<AJavaBean> set = new HashSet<AJavaBean>();
assertTrue(set.add(obj1));
assertTrue(set.add(obj2));
obj2.setSellingDate(new GregorianCalendar(2010, 05, 30).getTime()); // valeur précédente : 2010-10-30
assertTrue(set.remove(obj2)); // owned
assertEquals(2, set.size()); // owned
assertFalse(set.add(obj2)); // owned
}
Alors on sait que les méthodes equals et hashCode utilisent les deux propriétés name et sellingDate, donc quand on ajoute un objet dans le HashSet le hashCode correspondra au calcul fait partir des valeurs des ces attributs. Mais voilà le hashcode de l’objet n’est calculé qu’une fois, au moment de l’interaction dans la Map (ajout, suppression, contains, etc…).
Donc ce qu’il se passe c’est qu’on a fait muter l’état de notre objet, du coup le hashcode est différent, mais la collection conserve la référence de l’objet qu’elle contiens et ne recalcule pas son hashcode! C’est aussi avec avec ce genre de code que vous pouvez avoir des fuites mémoires. Et on est même pas dans un contexte multithreadé, alors imaginez si la collection est partagée entre plusieurs thread!
Attention aux collections ou aux dates du JDK
Par ignorance puis par laxisme, j’avoue que j’ai écris du code qui ressemble à ça (et j’ai honte de le dire) :
public class AnotherSupposedImmutableClass {
private final String name;
private final Date aDate;
private final Map<String, Integer> aMap;
public AnotherSupposedImmutableClass(final String name, final Date aDate, final Map<String, Integer> aMap) {
super();
this.name = name;
this.aDate = aDate;
this.aMap = aMap;
}
public String getName() {
return name;
}
public Date getADate() {
return aDate;
}
public Map<String, Integer> getAMap() {
return aMap;
}
}
Et forcement il y a des hics! A priori notre classe n’est pas mutable. Mais cela ne vous aura pas échappé, les propriétés aDate et aMap sont mutable!
@Test
public void playWithInternalMutability() throws Exception {
Map<String, Integer> map = new HashMap<String, Integer>();
AnotherSupposedImmutableClass supposedImmutableClass = new AnotherSupposedImmutableClass(
"name",
new GregorianCalendar(2010, 05, 30).getTime(),
map
);
supposedImmutableBean.getADate().setTime(123456789l); // oups
supposedImmutableBean.getAMap().put("trente quatre", Integer.valueOf(34)); // oups
supposedImmutableBean.getAMap().clear(); // oups, again
}
Et là, vous vous retrouverez les mêmes surprises que celles vu plus haut, ou évidement pire si vous êtes dans une application multithreadée. A ce sujet j’ai vu des ConcurrentModificationException parceque levé par du code à priori immutable, une optimisation d’un vieux code multithreadé avait déplacé une section qui modifiait une Map.
Je vous conseille vivement d’utiliser des objets immutables pour vos property, les librairies Joda-Time [3] et Google-Collections [4] fournissent des objets immutables.
Le pattern Builder de Joshua Bloch
Pour Joshua Bloch, c’est un peu une référence en Java, je pense qu’on peut lui faire confiance. Il est l’auteur du fameux livre Effective Java [5].
Alors pourquoi le pattern Builder de Joshua Bloch et non le pattern Builder du GoF ? En fait ce design vient d’une constatation au sujet de la construction d’objet complexes et pour s’affranchir des inconvénients des accesseurs.
En gros un objet du genre agrégat pourrait être construit avec un constructeur avec un paquet d’argument ou itérativement avec une foule de setter. Mais, un les gros constructeur ce n’est pas très pratique, puis deux les setters ça peux vite être lourd et ça rends votre objet mutable (ce qui n’est donc pas souhaité dans tous les cas).
Cette déclinaison du builder permet de construire un objet itérativement sans forcer la mutabilité.
Exemple les collections google :
public abstract class ImmutableMap<K, V> implements Map<K, V>, Serializable {
// ...
public static <K, V> Builder<K, V> builder() {
return new Builder<K, V>();
}
// ...
public static class Builder<K, V> {
final List<Entry<K, V>> entries = Lists.newArrayList();
public Builder() {}
public Builder<K, V> put(K key, V value) {
entries.add(entryOf(key, value));
return this;
}
// ...
public ImmutableMap<K, V> build() {
return fromEntryList(entries);
}
}
private static <K, V> ImmutableMap<K, V> fromEntryList(List<Entry<K, V>> entries) {
// ...
}
// ...
}
Ou encore avec une classe de notre domaine :
public class ACoolImmutableClass {
private final String name;
private final DateTime sometime;
private final List<String> listOfStuff;
// many other fields
public String getName() {
return name;
}
public DateTime getSometime() {
return sometime;
}
public static class Builder {
private String name;
private DateTime sometime;
private List<String> listOfStuff = new ArrayList<String>();
public Builder withName(String name) {
this.name = name;
return this;
}
public Builder at(DateTime moment) {
this.sometime = moment;
return this;
}
public Builder addThisThing(String thing) {
this.listOfStuff.add(thing);
return this;
}
public ACoolImmutableClass build() {
return new ACoolImmutableClass(this);
}
}
private ACoolImmutableClass(Builder builder) {
this.name = builder.name;
this.sometime = builder.sometime;
this.listOfStuff = ImmutableList.copyOf(builder.listOfStuff);
}
}
A noter que cette classe utilise des objets immutables pour ces attributs (DateTime, et ImmutableList).
Un des avantages, c’est qu’il est possible de valider les propriétés avant la création effective de l’objet. Avec les setters c’est faisable mais ça peut être délicat dans certaines situations.
Il y a un plugin Eclipse, qui permet de générer ces Builder, celà dit il est loin d’être super user friendly.
http://code.google.com/p/bpep/
Quoiqu’il en soit en aucun cas ce pattern n’est un remplacement du pattern Builder du GoF, il s’agit plus d’un pattern à appliquer dans un contexte ou il faut des objets immutable. Et encore ce n’est pas la seule solution, JodaTime typiquement n’utilise pas de builders.
Comment gérer la modification de l’objet
Si un comportement qui fait partit du domaine de l’objet et doit modifier l’état, alors il faut peut-être créer une nouvelle instance. La bibliothèque Joda-Time fait typiquement ça lorsqu’il y a modification d’un champs.
DateTime instance1 = new DateTime("2009-04-01");
DateTime instance2 = instance1.withYear(2010);
Je ne m’étends pas sur le sujet, mais ce genre de choses dépends de votre contexte, du rôle et du besoin. Un objet devrait être par défaut immutable, sauf si vraiment votre domaine identifie un cas ou l’état doit bouger et alors vous aurez des méthodes documentées qui appliqueront cette modification.
Conclusion
Mieux vaut des objets bien pensés et immutables que d’introduire la possibilité de changer l’état d’un objet et avoir des surprises. Et puis aussi :
- Il y a un risque fort d’avoir des problèmes au runtime, d’autant plus 10 ans après lorsqu’il y a une évolution à apporter et que plus personne ne sait qu’à tel endroit dans le code il y a le truc qui fout tout en l’air. Et les problèmes au runtime ca peut vite couter cher à analyser.
- Si vos objets ne peuvent pas être modifié alors vous n’aurez pas à vous soucier des problèmes de concurrences, c’est manifestement un gain de temps au développement et en maintenance. (Et donc un gain d’argent sur le long terme.)
- Bon ces objets sont bien cool, mais voilà il y a encore plein de framework (à tord ou à raison) qui se basent sur la convention JavaBean, je pense notamment aux objets marshallés en XML et consort.
- Ce code basé sur les builders est propre, mais il faut passer un petit peut plus de temps pour le faire. Il y a bien un plugin pour Eclipse, mais quid des autres IDE.
Quoi qu’il en soit, ces solutions sont toujours à appliquer avec du recul et toujours en fonction du contexte de votre domaine.
D’ailleurs cette entrée parle des problèmes rencontrés avec les collections du JDK, mais le problème pourrait se manifester différemment si une collection ou un de vos objets fonctionne autrement.
Encore une fois les remarques sont les bienvenues, ça fait plus de 40 ans que l’Homme fait du logiciel, et mafois on se plante encore assez souvent.
Références
- http://www.javaworld.com/javaworld/jw-09-2003/jw-0905-toolbox.html
- http://www.infoq.com/presentations/liskov-power-of-abstraction
- http://dutheil.brice.online.fr/blog/index.php/2010/02/09/a-propos-de-joda-time/
- http://dutheil.brice.online.fr/blog/index.php/2010/02/16/les-collections-par-google-comment-sy-retrouver/
- http://www.amazon.fr/Effective-Java-Joshua-Bloch/dp/0321356683/ref=sr_1_1?ie=UTF8&s=english-books&qid=1269958692&sr=8-1
- http://rwhansen.blogspot.com/2007/07/theres-builder-pattern-that-joshua.html









Follow