Devoxx France 2012 – Manipulation de bytecode

Durant Devoxx France 2012, j'ai assisté à la conférence intitulée Manipulation de bytecode : démocratisons la magie noire, présentée par Julien Ponge (son compte github) et Frédéric Le Mouel.

Parmi les librairies qui manipulent le bytecode, il y a les conteneurs d'EJBs, terracota pour distribuer les calculs et hibernate. Le plugin eclipse gérant la mise en page et l'affichage des écrans Android manipule aussi le bytecode afin d'instrumenter la vraie librairie utilisée dans Android.

Ensuite, Julien et Frédéric nous présentent rapidement le bytecode java : chaque type primitif est représenté en interne par une lettre, les packages des classes sont représentées avec des slash au lieu des points (exemple : java/lang/String)... La machine virtuelle destinée à utiliser le bytecode est une machine à pile avec une zone contenant les constantes (constant pool) et une autre contenant le code à exécuter (opcode). Pour illustrer cela, ils nous font la démonstration d’exécution du bytecode généré par la compilation d'un simple System.out.println pour afficher le résultat d'une addition. Nous pouvons ainsi voir l'évolution de la pile et des variables (les registres du processeur).

ASM est une des librairies permettant la manipulation du bytecode. Elle dispose de 2 APIs :

  • Une API basée sur un modèle événementiel, structurée autour du patron de conception visiteur. Cela peut servir pour faire le profilage de code java, en implémentant l'interface ClassFileTransformer.
  • Une API basée sur un modèle objet, chargeant tout le bytecode en mémoire sous la forme d'un arbre syntaxique du code (AST).

AspectJ est une autre librairie permettant de faire de la manipulation de bytecode. Celle-ci permet de faire de la programmation orientée aspect (AOP) pour, par exemple, rajouter des traces (appels de méthodes...), de la sécurité (vérification d'accès à certains services) ou de la validation (vérification des champs d'un formulaire) ...
Pour illustrer cela, Julien et Frédéric font 2 démonstrations autour du calcul de la suite de Fibonacci, de manière récursive : la première est très lente et fait certains calculs plusieurs fois, la seconde est beaucoup plus rapide car elle utilisent AspectJ pour implémenter un cache des calculs déjà effectués.
Une autre utilisation possible est la modification d'une classe pour lui rajouter l'implémentation d'une interface qu'elle n'implémentait pas auparavant.

Byteman, une autre librairie permettant la manipulation du bytecode, implémente un agent qui modifie les classes d'une JVM en cours de fonctionnement, pour débugger par exemple. Byteman fournit également un injecteur de bytecode, à utiliser dans le setup de JUnit. Il devient ainsi possible, par exemple, de forcer File.mkdirs() à retourner false, ce qui permet d'améliorer la couverture des tests.
Pour accomplir sa tache, Byteman définit un langage proche du bytecode, qui permet de spécifier quelles sont les modifications à faire et l'endroit où il faut les placer.

Pour terminer la présentation, Julien et Frédéric nous présentent la version béta du projet de recherche JooFlux (celui-ci sera bientôt mis en open source). Ce projet utilise le travail de Rémi Forax sur le nouveau bytecode invokedynamic (introduit dans Java 7). Byteman modifie toute une classe, même si une seule de ses méthodes a changé (ce qui force le compilateur JIT à optimiser de nouveau l'ensemble des méthodes). A l'opposé, JooFlux ne modifie seulement ce qui est nécessaire, ce qui optimise l'utilisation du compilateur JIT. JooFlux dispose également d'une interface Swing/JMX pour gérer l'injection de bytecode. Comme exemple d'utilisation, on peut remplacer tous les appels invoke* par un appel invokedynamic.

Pour plus d’informations sur le bytecode, vous pouvez consulter les slides de la présentation JVM Bytecode for Dummies faite par Charles Nutter pour JavaOne 2011.

Devoxx France 2012 – CRaSH un shell pour étendre la plateforme Java

Durant Devoxx France 2012, j'ai assisté à la conférence intitulée CRaSH un shell pour étendre la plate-forme Java, présentée par Julien Viet (son compte github).

CRaSH (Common ReusAble SHell) est un shell sous la forme d'un interpréteur de commandes qui permet l'accès à la machine virtuelle Java sur laquelle il tourne. Ce shell, sous licence open source LGPL, vient de sortir en version 1.0 et peut être récupéré sous la forme d'un fichier zip ou d'un installateur. Il peut fonctionner en mode autonome (comme le shell Bash) ou en mode attachement (un agent logiciel s'attache à une machine virtuelle Java en cours de fonctionnement).

Pour bénéficier du déploiement à chaud de nouvelles commandes CRaSH, il faut développer les commandes en groovy en étendant la classe CRaSHCommand. Il est aussi possible d'écrire des commandes en Java en implémentant l'interface ShellCommand, mais sans pouvoir bénéficier du déploiement à chaud. Une commande CRaSH peut aussi être développée sous la forme d'un script groovy. En plus de l'ajout de nouvelles commandes, un mécanisme de plugins permet de rajouter des nouvelles fonctionnalités en étendant la classe CRaSHPlugin.

Parmi les commandes disponibles en standard, la commande jdbc permet de se connecter à une base de données en passant par un DataSource définie dans un annuaire JNDI ou directement en utilisant une chaîne de connexion JDBC. Il devient alors possible d’exécuter directement des requêtes SQL. Parmi les autres commandes disponibles, on trouve la commande thread pour contrôler les threads et la commande log pour gérer les logs.

Par défaut, vous pouvez vous connecter à une instance de CRaSH par SSH ou par Telnet mais c'est désactivable. Pour plus d'informations, veuillez consulter la documentation.

Grâce à CRaSH, vous pouvez par exemple écrire des scripts pour tester une application Swing en utilisant la classe java.awt.Robot.

Devoxx France 2012 – Cache Java avec Guava

A l'occasion de Devoxx France 2012, j'ai assisté à la conférence intitulée Cache Java avec Guava présentée par Charles Fry.

La librairie Guava Cache fonctionne un niveau au dessus de ConcurrentHashmap mais n'offre pas autant de fonctionnalités que Ehcache. Les caches de Guava ne sont pas pré-chargés par défaut et sont tous thread safe. Le chargement d'un cache se fait avec une implémentation de la classe abstraite CacheLoader.

La construction d'un cache se fait progressivement, en commençant par un appel à la méthode statique CacheBuilder.newBuilder(), puis en chaînant les appels aux méthodes de la classe CacheBuilder. Cela permet de spécifier sa configuration et notamment sa stratégie d'éviction des valeurs : valeur utilisée le moins récemment avec expireAfterAccess(long, TimeUnit), valeur modifiée le moins récemment avec expireAfterWrite(long, TimeUnit) ... En plus du temps qui s'écoule, l'éviction de valeurs peut aussi être déclenchée en spécifiant le poids total maximum du cache par un appel à maximumWeight(long), qu'il faut combiner avec un appel à weigher(Weighter) pour indiquer comment peser une entrée du cache.
La construction du cache se termine en appelant build() ou build(CacheLoader), qui renvoient une nouvelle instance de Cache.

En plus des fonctionnalités classiques, les statistiques du cache peuvent être activées en appelant recordStats(). L'utilisation de la méthode removalListener(RemovalListener) permet de savoir quand une valeur vient d'être supprimée du cache. Par défaut, cette notification est synchrone, ce qui peut ralentir les opérations sur le cache. Pour traiter les notifications de manière asynchrone, vous pouvez encapsuler votre RemovalListener en appelant RemovalListeners.asynchronous(RemovalListener, Executor). Lors de la construction du cache, en appelant refreshAfterWrite(long, TimeUnit), on programme le rafraîchissement automatique d'une valeur un certain temps après sa création ou sa dernière modification. Dans certains cas, il peut être plus intéressant de charger toutes les valeurs du cache en une seule fois en surchargeant la méthode CacheLoader.loadAll(Iterable).

Parmi les autres fonctionnalités, on trouve Cache.get(K, Callable) pour récupérer une valeur dans le cache en spécifiant comment l'y ajouter si elle est absente. Dans ce cas, un unique appel au Callable est fait par le cache, même si plusieurs threads demandent la valeur pour cette clé. Enfin, il est possible d'obtenir une vue du cache sous la forme d'une ConcurrentMap en appelant asMap().

DevOps @ Devoxx

J'ai eu la chance de pouvoir assister à la présentation "Les 5 mercenaires du DevOps", qui s'est tenue à l'occasion de Devoxx 2012.

Dans une très bonne ambiance, les animateurs de la session nous ont dressé un tableau très pertinent de ce qui se cache derrière le terme DevOps, agrémenté d'illustrations très pratiques. Pour ceux qui ont raté cet événement, voici ce que j'en ai retenu.

1. DevOps, kézaco?

Le terme DevOps mérite une petite explication. Dans le développement d'une application ou d'un logiciel, trois acteurs principaux sont impliqués :

- le business (le biz) : c'est le client (ou les utilisateurs)

- les développeurs (le dev) : les réalisateurs du projet ou du produit

- les opérations (les ops) : les personnes qui assurent son fonctionnement opérationnel.

Le terme DevOps désigne le point de rencontre entre le monde des développeurs et celui des opérations.

2. C'est quoi le problème?

Si vous faites partie du monde des Dev ou du monde des Ops, cette question ne vous surprend pas... Disons qu'il est difficile pour les uns et les autres de se comprendre, parce que leurs besoins sont presque antinomiques :

- les développeurs cherchent à délivrer des fonctionnalités nouvelles toujours plus rapidement

- les opérationnels cherchent la stabilité et la performance.

De plus, le dialogue est souvent perturbé par le fait que les développeurs se contentent de livrer des éléments de l'application, parfois avec une documentation (in)complète - et complexe - que les opérationnels peinent à installer. Et dans l'autre sens, les opérationnels ne savent pas quelles informations fournir aux développeurs pour les aider à reproduire ou à comprendre des dysfonctionnements de l'application.

3. Et l' intégration continue, c'est pas fait pour ça?

L'intégration continue c'est bien. Mais c'est surtout fait pour améliorer le travail des développeurs et la qualité de leurs livrables. Mais ça ne résout pas le problème de "qu'est-ce qu'un livrable"? Pour le développeur, c'est souvent un jar, ou un war, accompagné d'une bonne documentation d'installation. L'opérationnel, lui, préfèrerait un rpm, incluant les écrans de paramétrage spécifique et les procédures de migration et de rollback, au cas où...

Donc, si une intégration continue est nécessaire pour adresser les problématiques DevOps, elle n'est pas suffisante. Il faut combler l'espace entre ce que le développeur considère comme étant un livrable (le war), et ce que l'opérationnel considère comme étant un input (le rpm).

4. DevOps, c'est des outils?

Oui. Mais pas que.

DevOps c'est avant tout une philosophie, une volonté. Un peu comme l'agilité qui, elle, a permis d'améliorer le dialogue et la collaboration entre les deux premiers maillons de la chaine, le biz et le dev (mais bon... agile, ça sonne quand même mieux que BizDev!).

Mais, comme il est très difficile de faire du SCRUM sans outil, DevOps a aussi besoin d'un outillage adapté.

5. OK, je suis convaincu. De quels outils ai-je besoin?

On peut considérer que l'outillage utilisé pour l'intégration continue se prête bien à la mise en place de DevOps. Notamment :

- une repository d'artefacts qui permettra d'échanger les packages de livraison entre l'équipe de développement et celle d'exploitation.

- un moteur d'intégration continue qui permettra d'automatiser les jobs de déploiement pour une mise en production ou une livraison aux équipes de tests sans soucis.

L'outillage à adopter dépend de votre budget. Mais parmi les solutions les plus populaires on peut citer :

- SVN ou git pour la gestion de configuration

- Nexus, Archiva ou Artifactory pour la repository d'artefacts

- Bamboo, Teamcity ou Jenkins pour le moteur (de l'avis des animateurs, Bamboo et Teamcity sont plus avancés que Jenkins, mais ils ne sont pas gratuits...). On peut imaginer un moteur pour le développement, un pour la QA et un pour les Ops.

- Fisheye, Crucible pour la collaboration autour des sources et des packages

- Confluence pour la collaboration au sens large

DevOps se justifiant également pour le dialogue entre le développement et les équipes qualité, on peut ajouter à cette liste Sonar pour la validation du code.

Devoxx France 2012 – 2ème Partie

Restlet

Open API

Dans sa présentation de Restlet, Jérôme Louvel commence par une explication des avantages de l’Open API.

L’Open API c’est fournir des API publiques, libres. Cela permet d’enrichir un site (affichage de tweets, de blogs, etc.) ou encore de créer des API composites.

Cela permet aussi d’exposer son service sous différents formes : mobile connecté, mobile natif, HTML5/Js, API partenaire, etc.

Jérôme Louvel explique par la suite qu’il s’agit d’une évolution logique de l’open source. Après les OS, les serveurs d’application, les API aussi deviennent Open.

Prenons l’exemple de twitter. Il fournit des API publiques pour afficher ses tweets, pour faire des recherches ou encore faire du streaming. Ces Open API permettent à d’autre société de fournir un meilleur service à leurs utilisateurs.

Un autre exemple bien connu sont les Open API de Google Map qui permettent à beaucoup de société de fournir à leurs utilisateurs un service de localisation ou de navigation à moindre coût.

Il est important de comprendre que ces Open API sont en fait des services rendus et ont aussi un cycle de vie et des montées de versions. Le contrat peut évoluer. Il faut donc bien gérer ses API avec sa documentation, maintenir des anciennes versions actives.

Devoxx France 2012 – Deadlock Victim

Lors de la conférence intitulée Deadlock Victim faite à Devoxx France 2012, Heinz Kabutz et Olivier Croisier ont parlé du sujet des DeadLock.

En prenant le problème du dîner des philosophes comme exemple, ils nous montrent qu'une stratégie inappropriée peut conduire à un DeadLock. Dans le cas d'une base de données, le moteur choisit la victime du DeadLock en retournant une exception SQL à l'un des clients impliqués. Par contre, dans le cas d'un programme Java faisant intervenir au minimum 2 threads qui veulent chacun accéder à 2 ressources (après les avoir verrouillées), il n'y a aucune exception pour indiquer qu'un thread est impliqué dans un DeadLock (dans ce cas, il serait bien qu'une Error soit lancée par le thread).

La solution à ce problème (appelé Left-Right DeadLock) consiste à appliquer la stratégie suivante pour tous les philosophes :

  1. prendre (verrouiller) le couvert à sa droite
  2. prendre (verrouiller) le couvert à sa gauche
  3. poser (déverrouiller) le couvert à sa gauche
  4. poser (déverrouiller) le couvert à sa droite

Remarques :

  • Il est bien sûr possible de prendre la stratégie opposée : le couvert de gauche, puis celui de droite. L'essentiel est que tous les philosophes appliquent la même stratégie.
  • Il est important qu'un philosophe pose ses couverts dans l'ordre inverse où il les a pris

Au lieu d'utiliser la notion droite-gauche, il est possible de numéroter chacun des couverts avec un numéro unique. Dans ce cas, il y a 2 stratégies possibles : prendre d'abord le couvert avec le plus petit numéro (parmi les 2 situés près d'un philosophe), prendre d'abord celui avec le plus grand numéro. Encore une fois, l'important est que les philosophes appliquent tous la même stratégie. Cela permet de trouver une solution générique à tous les problèmes de DeadLock : il faut définir un ordre strict parmi les ressources (en leur associant par exemple un identifiant numérique unique) et appliquer la stratégie du "plus petit d'abord".

Parmi les cas concrets, il y a celui de la méthode privée writeObject(ObjectOutputStream) de la classe Vector, synchronisée sur l'instance. Un DeadLock risque de se produire lors de la sérialisation de 2 vecteurs contenant chacun une référence sur l'autre.

Pour la partie test unitaire, comment vérifier que le DeadLock est corrigé ? Combien de fois faut-il faire le test pour prouver que la correction est efficace ? En effet, un DeadLock peut se produire de temps en temps et pas forcément systématiquement. Un bon test doit passer ou ne pas passer, indépendamment des conditions extérieures (charge du processeur ...). Heinz et Olivier proposent de rajouter une méthode vide nommée sleep dans le code en production et de l'appeler entre les 2 tentatives de verrouillage de 2 ressources différentes. En surchargeant la méthode sleep dans le code des tests pour appeler Thread.sleep(long), il devient possible de révéler la présence ou l'absence de DeadLock de manière déterministe.

Depuis Java 6, il existe de nouvelles fonctionnalités permettant de faciliter la détection et la correction d'un DeadLock :

  • Visualiser l'état des threads d'une JVM en fonctionnement :
    • Sous Windows : Il faut que la JVM ait été lancée dans une console et taper Ctrl-Break
    • Sous Linux : Si la JVM a été lancée dans une console, taper Ctrl-. Sinon, envoyer le signal QUIT au processus avec la commande kill -QUIT process_id, où process_id est l'identifiant du processus (que l'on peut retrouver avec la commande ps axf | grep java)

    Ensuite, il faut utiliser la commande jstack process_id.

  • Lister les threads impliqués dans un DeadLock en utilisant JMX. Cela ne fonctionne que pour les locks du même type que celui utilisé par ReentrantLock et pas pour les locks utilisant un moniteur (mot clé synchronized).

Pour illustrer cela, Heinz et Olivier ont créé une classe appelée DeadlockArbitrator qui tue un thread au hasard parmi ceux qui sont impliqués dans un DeadLock (dont la liste est obtenue par JMX). Ensuite, ils font la démonstration d'une application Swing avec un bouton pour provoquer un DeadLock et un autre pour tuer un thread provoquant un DeadLock (en utilisant la classe DeadlockArbitrator).
Remarque : pour tuer un thread, la classe DeadlockArbitrator utilise la méthode dépréciée (mais bien utile dans ce cas) Thread.stop(Throwable). Il ne faut utiliser cette technique que pour du code que l'on ne peut pas corriger (librairies tierces). Dans les autres cas, il faut évidemment corriger le DeadLock.

Pour plus d'informations sur des sujets techniques comme celui-ci, Heinz nous invite à consulter son site Javaspecialists.eu