I. Les agents Java▲
Les agents Java permettent d'instrumenter des programmes qui sont lancés sur la JVM. Au démarrage de celle-ci, ils vont pouvoir intercepter le chargement des classes et ainsi en modifier le bytecode associé.
I-A. Utilisation▲
Les utilisations possibles des agents Java sont :
- le rechargement des classes au runtime (exemple : avec Spring Loaded) ;
- visualisation de l'utilisation de la mémoire : https://devcenter.heroku.com/articles/java-memory-issues#memory-logging-agent ;
- exemple sur GitHub : https://github.com/heroku/heroku-javaagent ;
- surveiller les performances d'une application Java dans Heroku (exemple : avec l'add-on NewRelic).
I-B. Développer un agent▲
Trois choses sont nécessaires à la conception d'un agent Java :
- un fichier manifest, qui doit préciser la classe à charger pour lancer l'agent ;
- une classe qui charge l'agent, avec une méthode « premain(String args, Instrumentation inst) » ;
- l'agent doit être déployé sous forme de jar.
Ci-dessous, un exemple d'agent Java :
Le fichier MANIFEST.MF :
2.
Manifest-Version: 1.0
Premain-Class: fr.ippon.agent.LoggingAgent
La classe de l'agent Java :
2.
3.
4.
5.
6.
7.
public
class
LoggingAgent {
public
static
void
premain
(
String agentArgs, Instrumentation instrumentation) {
System.out.println
(
"Entering premain method"
);
instrumentation.addTransformer
(
new
LoggingClassFileTransformer
(
));
}
}
La méthode premain() est quelque peu similaire à la méthode main().
Le premier paramètre permet de récupérer les arguments passés en ligne de commande. Le second, quant à lui, contient plusieurs méthodes permettant de modifier les classes chargées.
Voici un exemple basique de “ClassFileTransformer” :
2.
3.
4.
5.
6.
7.
8.
public
class
LoggingClassFileTransformer implements
ClassFileTransformer {
public
byte
[] transform
(
ClassLoader loader, String className, Class<
?>
classBeingRedefined,
ProtectionDomain protectionDomain, byte
[] classfileBuffer) throws
IllegalClassFormatException {
System.out.println
(
"Transforming class "
+
className);
return
classfileBuffer;
}
}
Pour aller plus loin dans la manipulation de bytecode, il est possible d'utiliser Javassist.
I-C. Chargement d'un agent▲
Pour charger un agent, il suffit de rajouter l'option “-javaagent” à la commande Java :
-
javaagent:{
javaagent}
.jar ...
Pour en charger plusieurs, il suffit de rajouter les mêmes paramètres, ils seront lancés dans l'ordre dans lequel ils ont été déclarés.
II. Utilisation de Spring Loaded▲
Pour utiliser Spring Loaded, il faut d'abord télécharger l'agent Java à cette adresse : https://github.com/spring-projects/spring-loaded/releases.
II-A. Spring Tool Suite▲
Le rechargement d'une application Spring Boot avec Spring Tool Suite est aisé, il suffit de spécifier les arguments de la VM suivants :
L'option “-noverify” permet de désactiver la vérification du bytecode généré, et ainsi d'éviter des exceptions liées à cette vérification.
II-B. IntelliJ Idea▲
La configuration avec IntelliJ Idea est tout aussi simple, là encore, il faut ajouter les arguments de la VM suivants :
La prise en compte des modifications est quelque peu différente de celle avec Spring Tool Suite : IntelliJ Idea ne compilant pas les classes automatiquement, il est nécessaire de recompiler manuellement les classes modifiées (Build -> Compile `*.java' ou CTRL + F9), afin de bénéficier du rechargement.
II-C. Maven▲
Pour recharger une application Spring Boot avec Maven, il suffit d'ajouter la dépendance suivante au plugin spring-boot-maven-plugin :
2.
3.
4.
5.
<dependency>
<groupid>
org.springframework</groupid>
<artifactid>
springloaded</artifactid>
<version>
1.2.3.RELEASE</version>
</dependency>
Le lancement de l'application Spring Boot avec Maven est fait grâce à la commande suivante : mvn spring-boot:run.
II-D. Spring Loaded in action▲
Pour démontrer la prise en compte des changements apportés dans une application, testons Spring Loaded à l'aide d'une simple application Spring Boot qui comporte un contrôleur REST Spring MVC. Celui-ci renvoie une simple chaîne de caractères :
2.
3.
4.
5.
6.
7.
@RestController
public
class
ReloadableController {
@RequestMapping
(
value =
"/home"
, method =
RequestMethod.GET)
public
String home
(
) {
return
"home"
;
}
}
Après avoir lancé l'application, la page à l'adresse « localhost:8080/home » affiche la chaîne de caractères « home ».
Pour vérifier la prise en compte des changements dans un contrôleur de l'application, modifions la chaîne de caractères retournée par celui-ci, pour la remplacer par “reloaded-home” :
2.
3.
4.
5.
6.
7.
@RestController
public
class
ReloadableController {
@RequestMapping
(
value =
"/home"
, method =
RequestMethod.GET)
public
String home
(
) {
return
"reloaded-home"
;
}
}
Après modification de la classe “ReloadableController” (et compilation manuelle avec IntelliJ Idea), nous pouvons voir dans les logs que Spring Loaded a détecté la classe modifiée (ici notre contrôleur accessible à l'adresse “/home”) et l'a bien rechargée :
En rafraîchissant la page “/home”, le contrôleur retourne bien la nouvelle valeur, sans avoir redémarré l'application :
Spring Loaded prend aussi en compte la modification des adresses des contrôleurs ou encore l'ajout de paramètres dans leurs méthodes.
TIP : il est possible de développer un plugin pour Spring Loaded qui sera notifié de chaque rechargement, et ainsi pourra effectuer différentes actions lors de celui-ci. Il suffit pour cela de créer une classe implémentant l'interface ReloadEventProcessorPluginet et de l'activer grâce à la méthode registerGlobalPlugin de la classe SpringLoadedPreProcessor. Cette classe sera alors appelée lorsque Spring Loaded effectuera un rechargement.
II-E. Limitations▲
Spring Loaded comporte les limitations suivantes :
- il n'est pas possible de modifier une hiérarchie de classes ;
- il ne recharge pas la configuration de Spring Security ;
- il ne supporte pas l'ajout d'un nouveau contrôleur ;
- on ne peut pas changer le niveau de log ;
-
le rechargement des classes n'est pas disponible pour les applications utilisant les préfixes de package suivants :
- org/springframework/,
- org/springsource/loaded/,
- et bien d'autres :https://github.com/spring-projects/spring-loaded/wiki/Basic-usage-information.
Pour des problématiques d'utilisations particulières, n'hésitez pas à consulter les « issues » sur GitHub : https://github.com/spring-projects/spring-loaded/issues?page=2&q=is:issue+is:open.
III. En résumé▲
Spring Loaded, aussi utilisé par Grails2, permet de gagner un temps de développement précieux pour des applications simples, notamment grâce à la prise en compte de changements dans les contrôleurs Spring MVC.
Spring Loaded constitue une alternative gratuite à JRebel, dans une certaine limite. Pour recharger des applications plus complexes (notamment avec l'utilisation de Spring Security), il est nécessaire de se tourner vers la solution JRebel qui supporte les serveurs d'applications ainsi que de nombreux frameworks : http://manuals.zeroturnaround.com/jrebel/misc/integrations.html. Pour permettre toutes ces fonctionnalités, JRebel se base sur les classloaders pour accepter le rechargement des classes/frameworks.
Une autre alternative intéressante à étudier est la solution de JHipster. Basée sur Spring Loaded, jhipster-loaded va beaucoup plus loin dans le rechargement des classes et est capable de prendre en compte des changements apportés à une configuration Liquibase, Jackson, JPA ou encore Mongo. Le projet est disponible ici : https://github.com/jhipster/jhipster-loaded.
Le projet d'exemple sur les agents Java ainsi que l'application Spring Boot sont disponibles à ces adresses :
IV. Conclusion et remerciements▲
Cet article a été publié avec l'aimable autorisation de Florian Lopes. L'article original peut être vu sur le blog/site de Ippon.
Nous tenons à remercier Jacques Jean pour sa relecture orthographique et Malick SECK pour la mise au gabarit.