Tutoriel de classe Java avancé : un guide pour le rechargement de classe
Publié: 2022-03-11Dans les projets de développement Java, un flux de travail typique consiste à redémarrer le serveur à chaque changement de classe, et personne ne s'en plaint. C'est un fait sur le développement Java. Nous avons travaillé comme ça depuis notre premier jour avec Java. Mais le rechargement des classes Java est-il si difficile à réaliser ? Et ce problème pourrait-il être à la fois difficile et passionnant à résoudre pour les développeurs Java qualifiés ? Dans ce didacticiel de classe Java, je vais essayer de résoudre le problème, de vous aider à bénéficier de tous les avantages du rechargement de classe à la volée et d'augmenter considérablement votre productivité.
Le rechargement des classes Java n'est pas souvent discuté et il existe très peu de documentation explorant ce processus. Je suis ici pour changer cela. Ce didacticiel sur les classes Java fournira une explication étape par étape de ce processus et vous aidera à maîtriser cette technique incroyable. Gardez à l'esprit que la mise en œuvre du rechargement de classe Java nécessite beaucoup de soin, mais apprendre à le faire vous placera dans la cour des grands, à la fois en tant que développeur Java et en tant qu'architecte logiciel. Cela ne fera pas de mal non plus de comprendre comment éviter les 10 erreurs Java les plus courantes.
Configuration de l'espace de travail
Tout le code source de ce tutoriel est téléchargé sur GitHub ici.
Pour exécuter le code pendant que vous suivez ce didacticiel, vous aurez besoin de Maven, Git et d'Eclipse ou d'IntelliJ IDEA.
Si vous utilisez Eclipse :
- Exécutez la commande
mvn eclipse:eclipse
pour générer les fichiers de projet d'Eclipse. - Chargez le projet généré.
- Définissez le chemin de sortie sur
target/classes
.
Si vous utilisez IntelliJ :
- Importez le fichier
pom
du projet. - IntelliJ ne compilera pas automatiquement lorsque vous exécutez un exemple, vous devez donc soit :
- Exécutez les exemples dans IntelliJ, puis chaque fois que vous voulez compiler, vous devrez appuyer sur
Alt+BE
- Exécutez les exemples en dehors d'IntelliJ avec
run_example*.bat
. Définissez la compilation automatique du compilateur IntelliJ sur true. Ensuite, chaque fois que vous modifiez un fichier Java, IntelliJ le compilera automatiquement.
Exemple 1 : recharger une classe avec Java Class Loader
Le premier exemple vous donnera une compréhension générale du chargeur de classe Java. Voici le code source.
Étant donné la définition de classe User
suivante :
public static class User { public static int age = 10; }
Nous pouvons faire ce qui suit :
public static void main(String[] args) { Class<?> userClass1 = User.class; Class<?> userClass2 = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example1.StaticInt$User"); ...
Dans cet exemple de didacticiel, deux classes User
seront chargées dans la mémoire. userClass1
sera chargé par le chargeur de classe par défaut de la JVM, et userClass2
à l'aide de DynamicClassLoader
, un chargeur de classe personnalisé dont le code source est également fourni dans le projet GitHub, et que je décrirai en détail ci-dessous.
Voici le reste de la méthode main
:
out.println("Seems to be the same class:"); out.println(userClass1.getName()); out.println(userClass2.getName()); out.println(); out.println("But why there are 2 different class loaders:"); out.println(userClass1.getClassLoader()); out.println(userClass2.getClassLoader()); out.println(); User.age = 11; out.println("And different age values:"); out.println((int) ReflectUtil.getStaticFieldValue("age", userClass1)); out.println((int) ReflectUtil.getStaticFieldValue("age", userClass2)); }
Et la sortie :
Seems to be the same class: qj.blog.classreloading.example1.StaticInt$User qj.blog.classreloading.example1.StaticInt$User But why there are 2 different class loaders: qj.util.lang.DynamicClassLoader@3941a79c sun.misc.Launcher$AppClassLoader@1f32e575 And different age values: 11 10
Comme vous pouvez le voir ici, bien que les classes User
aient le même nom, ce sont en fait deux classes différentes, et elles peuvent être gérées et manipulées indépendamment. La valeur d'âge, bien que déclarée comme statique, existe en deux versions, attachées séparément à chaque classe, et peut également être modifiée indépendamment.
Dans un programme Java normal, ClassLoader
est le portail apportant des classes dans la JVM. Lorsqu'une classe nécessite le chargement d'une autre classe, c'est la tâche du ClassLoader
d'effectuer le chargement.
Cependant, dans cet exemple de classe Java, le ClassLoader
personnalisé nommé DynamicClassLoader
est utilisé pour charger la deuxième version de la classe User
. Si au lieu de DynamicClassLoader
, nous devions utiliser à nouveau le chargeur de classe par défaut ( avec la commande StaticInt.class.getClassLoader()
), alors la même classe User
sera utilisée, car toutes les classes chargées sont mises en cache.
Le DynamicClassLoader
Il peut y avoir plusieurs classloaders dans un programme Java normal. Celui qui charge votre classe principale, ClassLoader
, est celui par défaut, et à partir de votre code, vous pouvez créer et utiliser autant de classloaders que vous le souhaitez. C'est donc la clé du rechargement de classe en Java. Le DynamicClassLoader
est peut-être la partie la plus importante de tout ce didacticiel, nous devons donc comprendre comment fonctionne le chargement dynamique des classes avant de pouvoir atteindre notre objectif.
Contrairement au comportement par défaut de ClassLoader
, notre DynamicClassLoader
hérite d'une stratégie plus agressive. Un chargeur de classe normal donnerait la priorité à son parent ClassLoader
et ne chargerait que les classes que son parent ne peut pas charger. Cela convient aux circonstances normales, mais pas dans notre cas. Au lieu de cela, DynamicClassLoader
essaiera de parcourir tous ses chemins de classe et de résoudre la classe cible avant d'abandonner le droit à son parent.
Dans notre exemple ci-dessus, le DynamicClassLoader
est créé avec un seul chemin de classe : "target/classes"
(dans notre répertoire actuel), il est donc capable de charger toutes les classes qui résident à cet emplacement. Pour toutes les classes qui ne sont pas là, il devra se référer au chargeur de classe parent. Par exemple, nous devons charger la classe String
dans notre classe StaticInt
, et notre chargeur de classe n'a pas accès au rt.jar
dans notre dossier JRE, donc la classe String
du chargeur de classe parent sera utilisée.
Le code suivant provient de AggressiveClassLoader
, la classe parente de DynamicClassLoader
, et montre où ce comportement est défini.
byte[] newClassData = loadNewClass(name); if (newClassData != null) { loadedClasses.add(name); return loadClass(newClassData, name); } else { unavaiClasses.add(name); return parent.loadClass(name); }
Prenez note des propriétés suivantes de DynamicClassLoader
:
- Les classes chargées ont les mêmes performances et autres attributs que les autres classes chargées par le chargeur de classe par défaut.
- Le
DynamicClassLoader
peut être ramassé avec toutes ses classes et objets chargés.
Avec la possibilité de charger et d'utiliser deux versions de la même classe, nous pensons maintenant à vider l'ancienne version et à charger la nouvelle pour la remplacer. Dans l'exemple suivant, nous ferons exactement cela… en continu.
Exemple 2 : recharger une classe en continu
Ce prochain exemple Java vous montrera que le JRE peut charger et recharger des classes indéfiniment, avec les anciennes classes vidées et récupérées, et une toute nouvelle classe chargée à partir du disque dur et mise à profit. Voici le code source.
Voici la boucle principale :
public static void main(String[] args) { for (;;) { Class<?> userClass = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example2.ReloadingContinuously$User"); ReflectUtil.invokeStatic("hobby", userClass); ThreadUtil.sleep(2000); } }
Toutes les deux secondes, l'ancienne classe User
sera vidée, une nouvelle sera chargée et sa méthode hobby
invoquée.
Voici la définition de la classe User
:
@SuppressWarnings("UnusedDeclaration") public static class User { public static void hobby() { playFootball(); // will comment during runtime // playBasketball(); // will uncomment during runtime } // will comment during runtime public static void playFootball() { System.out.println("Play Football"); } // will uncomment during runtime // public static void playBasketball() { // System.out.println("Play Basketball"); // } }
Lors de l'exécution de cette application, vous devez essayer de commenter et de décommenter le code indiqué code dans la classe User
. Vous verrez que la définition la plus récente sera toujours utilisée.
Voici quelques exemples de sortie :
... Play Football Play Football Play Football Play Basketball Play Basketball Play Basketball
Chaque fois qu'une nouvelle instance de DynamicClassLoader
est créée, elle charge la classe User
à partir du dossier target/classes
, où nous avons défini Eclipse ou IntelliJ pour générer le dernier fichier de classe. Tous les anciens DynamicClassLoader
et les anciennes classes User
seront dissociés et soumis au ramasse-miettes.
Si vous êtes familier avec JVM HotSpot, notez ici que la structure de classe peut également être modifiée et rechargée : la méthode playFootball
doit être supprimée et la méthode playBasketball
ajoutée. Ceci est différent de HotSpot, qui permet uniquement de modifier le contenu de la méthode, ou la classe ne peut pas être rechargée.
Maintenant que nous sommes capables de recharger une classe, il est temps d'essayer de recharger plusieurs classes à la fois. Essayons-le dans l'exemple suivant.
Exemple 3 : recharger plusieurs classes
La sortie de cet exemple sera la même que celle de l'exemple 2, mais montrera comment implémenter ce comportement dans une structure plus semblable à une application avec des objets de contexte, de service et de modèle. Le code source de cet exemple est assez volumineux, je n'en ai donc montré que des parties ici. Le code source complet est ici.
Voici la méthode main
:
public static void main(String[] args) { for (;;) { Object context = createContext(); invokeHobbyService(context); ThreadUtil.sleep(2000); } }
Et la méthode createContext
:
private static Object createContext() { Class<?> contextClass = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example3.ContextReloading$Context"); Object context = newInstance(contextClass); invoke("init", context); return context; }
La méthode invokeHobbyService
:
private static void invokeHobbyService(Object context) { Object hobbyService = getFieldValue("hobbyService", context); invoke("hobby", hobbyService); }
Et voici la classe Context
:
public static class Context { public HobbyService hobbyService = new HobbyService(); public void init() { // Init your services here hobbyService.user = new User(); } }
Et la classe HobbyService
:

public static class HobbyService { public User user; public void hobby() { user.hobby(); } }
La classe Context
dans cet exemple est beaucoup plus compliquée que la classe User
dans les exemples précédents : elle a des liens vers d'autres classes, et elle a la méthode init
à appeler chaque fois qu'elle est instanciée. Fondamentalement, il est très similaire aux classes de contexte d'application du monde réel (qui gardent une trace des modules de l'application et effectuent l'injection de dépendance). Ainsi, être capable de recharger cette classe Context
avec toutes ses classes liées est un grand pas vers l'application de cette technique à la vie réelle.
Au fur et à mesure que le nombre de classes et d'objets augmente, notre étape de "déposer les anciennes versions" deviendra également plus compliquée. C'est aussi la principale raison pour laquelle le rechargement de classe est si difficile. Pour supprimer éventuellement les anciennes versions, nous devrons nous assurer qu'une fois le nouveau contexte créé, toutes les références aux anciennes classes et objets sont supprimées. Comment gérer cela avec élégance ?
La méthode main
ici aura une emprise sur l'objet de contexte, et c'est le seul lien vers toutes les choses qui doivent être supprimées. Si nous rompons ce lien, l'objet de contexte et la classe de contexte, et l'objet de service… seront tous soumis au ramasse-miettes.
Une petite explication sur la raison pour laquelle les classes sont normalement si persistantes et ne sont pas ramassées :
- Normalement, nous chargeons toutes nos classes dans le chargeur de classe Java par défaut.
- La relation classe-chargeur de classe est une relation bidirectionnelle, le chargeur de classe mettant également en cache toutes les classes qu'il a chargées.
- Ainsi, tant que le chargeur de classe est toujours connecté à un thread actif, tout (toutes les classes chargées) sera immunisé contre le ramasse-miettes.
- Cela dit, à moins que nous puissions séparer le code que nous voulons recharger du code déjà chargé par le chargeur de classe par défaut, nos nouvelles modifications de code ne seront jamais appliquées pendant l'exécution.
Avec cet exemple, nous voyons que le rechargement de toutes les classes de l'application est en fait assez facile. L'objectif est simplement de conserver une connexion fine et amovible entre le thread actif et le chargeur de classe dynamique utilisé. Mais que se passe-t-il si nous voulons que certains objets (et leurs classes) ne soient pas rechargés et soient réutilisés entre les cycles de rechargement ? Regardons l'exemple suivant.
Exemple 4 : Séparer les espaces de classe persistants et rechargés
Voici le code source..
La méthode main
:
public static void main(String[] args) { ConnectionPool pool = new ConnectionPool(); for (;;) { Object context = createContext(pool); invokeService(context); ThreadUtil.sleep(2000); } }
Vous pouvez donc voir que l'astuce ici consiste à charger la classe ConnectionPool
et à l'instancier en dehors du cycle de rechargement, à la conserver dans l'espace persistant et à transmettre la référence aux objets Context
La méthode createContext
est également un peu différente :
private static Object createContext(ConnectionPool pool) { ExceptingClassLoader classLoader = new ExceptingClassLoader( (className) -> className.contains(".crossing."), "target/classes"); Class<?> contextClass = classLoader.load("qj.blog.classreloading.example4.reloadable.Context"); Object context = newInstance(contextClass); setFieldValue(pool, "pool", context); invoke("init", context); return context; }
Désormais, on appellera les objets et classes qui sont rechargés à chaque cycle « l'espace rechargeable » et les autres - les objets et classes non recyclés et non renouvelés lors des cycles de rechargement - « l'espace persistant ». Nous devrons être très clairs sur quels objets ou classes restent dans quel espace, traçant ainsi une ligne de séparation entre ces deux espaces.
Comme le montre l'image, non seulement l'objet Context
et l'objet UserService
référence à l'objet ConnectionPool
, mais les classes Context
et UserService
font également référence à la classe ConnectionPool
. C'est une situation très dangereuse qui mène souvent à la confusion et à l'échec. La classe ConnectionPool
ne doit pas être chargée par notre DynamicClassLoader
, il ne doit y avoir qu'une seule classe ConnectionPool
en mémoire, qui est celle chargée par le ClassLoader
par défaut. C'est un exemple de la raison pour laquelle il est si important d'être prudent lors de la conception d'une architecture de rechargement de classe en Java.
Que se passe-t-il si notre DynamicClassLoader
charge accidentellement la classe ConnectionPool
? Ensuite, l'objet ConnectionPool
de l'espace persistant ne peut pas être transmis à l'objet Context
, car l'objet Context
attend un objet d'une classe différente, également nommée ConnectionPool
, mais qui est en fait une classe différente !
Alors, comment empêcher notre DynamicClassLoader
de charger la classe ConnectionPool
? Au lieu d'utiliser DynamicClassLoader
, cet exemple en utilise une sous-classe nommée : ExceptingClassLoader
, qui transmettra le chargement au super classloader en fonction d'une fonction de condition :
(className) -> className.contains("$Connection")
Si nous n'utilisons pas ExceptingClassLoader
ici, le DynamicClassLoader
chargerait la classe ConnectionPool
car cette classe réside dans le dossier « target/classes
». Une autre façon d'empêcher la classe ConnectionPool
d'être récupérée par notre DynamicClassLoader
est de compiler la classe ConnectionPool
dans un dossier différent, peut-être dans un module différent, et elle sera compilée séparément.
Règles pour choisir l'espace
Maintenant, le travail de chargement de la classe Java devient vraiment déroutant. Comment déterminons-nous quelles classes doivent être dans l'espace persistant et quelles classes dans l'espace rechargeable ? Voici les règles :
- Une classe dans l'espace rechargeable peut référencer une classe dans l'espace persistant, mais une classe dans l'espace persistant ne peut jamais référencer une classe dans l'espace rechargeable. Dans l'exemple précédent, la classe
Context
rechargeable fait référence à la classeConnectionPool
persistante, maisConnectionPool
n'a aucune référence àContext
- Une classe peut exister dans l'un ou l'autre espace si elle ne référence aucune classe dans l'autre espace. Par exemple, une classe utilitaire avec toutes les méthodes statiques telles que
StringUtils
peut être chargée une fois dans l'espace persistant et chargée séparément dans l'espace rechargeable.
Vous voyez donc que les règles ne sont pas très contraignantes. À l'exception des classes de croisement qui ont des objets référencés dans les deux espaces, toutes les autres classes peuvent être utilisées librement dans l'espace persistant ou l'espace rechargeable ou les deux. Bien sûr, seules les classes de l'espace rechargeable apprécieront d'être rechargées avec des cycles de rechargement.
Ainsi, le problème le plus difficile avec le rechargement de classe est traité. Dans l'exemple suivant, nous essaierons d'appliquer cette technique à une application Web simple, et profiterons du rechargement des classes Java comme n'importe quel langage de script.
Exemple 5 : Petit annuaire téléphonique
Voici le code source..
Cet exemple sera très similaire à ce à quoi devrait ressembler une application Web normale. Il s'agit d'une application à page unique avec AngularJS, SQLite, Maven et Jetty Embedded Web Server.
Voici l'espace rechargeable dans la structure du serveur Web :
Le serveur web ne contiendra pas de références aux vraies servlets, qui doivent rester dans l'espace rechargeable, afin d'être rechargées. Ce qu'il contient, ce sont des servlets stub, qui, à chaque appel à sa méthode de service, résoudront le servlet réel dans le contexte réel à exécuter.
Cet exemple introduit également un nouvel objet ReloadingWebContext
, qui fournit au serveur Web toutes les valeurs comme un Context normal, mais contient en interne des références à un objet de contexte réel qui peut être rechargé par un DynamicClassLoader
. C'est ce ReloadingWebContext
qui fournit les servlets stub au serveur web.
Le ReloadingWebContext
sera le wrapper du contexte réel, et :
- Rechargera le contexte réel lorsqu'un HTTP GET vers "/" est appelé.
- Fournit des servlets stub au serveur Web.
- Définit des valeurs et invoque des méthodes chaque fois que le contexte réel est initialisé ou détruit.
- Peut être configuré pour recharger le contexte ou non, et quel classloader est utilisé pour le rechargement. Cela aidera lors de l'exécution de l'application en production.
Parce qu'il est très important de comprendre comment on isole l'espace persistant et l'espace rechargeable, voici les deux classes qui se croisent entre les deux espaces :
Classe qj.util.funct.F0
pour l'objet public F0<Connection> connF
in Context
- Objet de fonction, renverra une connexion chaque fois que la fonction est invoquée. Cette classe réside dans le package qj.util, qui est exclu de
DynamicClassLoader
.
Classe java.sql.Connection
pour l'objet public F0<Connection> connF
en Context
- Objet de connexion SQL normal. Cette classe ne réside pas dans le chemin de classe de notre
DynamicClassLoader
, elle ne sera donc pas récupérée.
Sommaire
Dans ce didacticiel sur les classes Java, nous avons vu comment recharger une seule classe, recharger une seule classe en continu, recharger un espace entier de plusieurs classes et recharger plusieurs classes séparément des classes qui doivent être persistantes. Avec ces outils, le facteur clé pour obtenir un rechargement de classe fiable est d'avoir une conception super propre. Ensuite, vous pouvez manipuler librement vos classes et l'ensemble de la JVM.
L'implémentation du rechargement de classe Java n'est pas la chose la plus simple au monde. Mais si vous tentez votre chance et que, à un moment donné, vous constatez que vos cours sont chargés à la volée, vous y êtes presque déjà. Il vous restera très peu de choses à faire avant de pouvoir obtenir une conception propre et totalement superbe pour votre système.
Bonne chance mes amis et profitez de votre nouveau super pouvoir !