Dans Projets de développement Java , un flux de travail typique implique le redémarrage du 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 développeurs Java qualifiés ? Dans ce didacticiel de classe Java, j'essaierai de résoudre le problème, de vous aider à profiter 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 abordé et il existe très peu de documentation explorant ce processus. Je suis ici pour changer cela. Ce tutoriel 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 .
Tout le code source de ce didacticiel est téléchargé sur GitHub Ici .
Pour exécuter le code pendant que vous suivez ce tutoriel, vous aurez besoin Maven , Aller et soit Éclipse ou IDÉE IntelliJ .
mvn eclipse:eclipse
pour générer les fichiers de projet d'Eclipse.target/classes
.pom
fichier.Alt+B E
run_example*.bat
. Définissez la fonction de compilation automatique du compilateur IntelliJ sur true. Ensuite, chaque fois que vous modifiez un fichier java, IntelliJ le compilera automatiquement.Le premier exemple vous donnera une compréhension générale du chargeur de classe Java. Voici le code source.
Compte tenu de ce qui suit User
définition de classe:
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, il y aura deux User
classes chargées dans la mémoire. userClass1
sera chargé par le chargeur de classe par défaut de la JVM, et userClass2
en utilisant le 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 des main
méthode:
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: [email protected] [email protected] And different age values: 11 10
Comme vous pouvez le voir ici, bien que le User
les classes ont 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 qui introduit les classes dans la JVM. Lorsqu'une classe nécessite le chargement d'une autre classe, c'est la tâche de ClassLoader
de faire le chargement.
Cependant, dans cet exemple de classe Java, le paramètre personnalisé ClassLoader
nommé DynamicClassLoader
est utilisé pour charger la deuxième version de User
classe. Si au lieu de DynamicClassLoader
, nous devions utiliser à nouveau le chargeur de classe par défaut (avec la commande StaticInt.class.getClassLoader()
), alors le même User
class sera utilisée, car toutes les classes chargées sont mises en cache.
DynamicClassLoader
Il peut y avoir plusieurs chargeurs de classe 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 chargeurs de classe que vous le souhaitez. C'est donc la clé du rechargement de classe en Java. Le DynamicClassLoader
est probablement la partie la plus importante de tout ce didacticiel, nous devons donc comprendre le fonctionnement du 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 à son parent ClassLoader
la priorité et ne charge que les classes que son parent ne peut pas charger. Cela convient aux circonstances normales, mais pas dans notre cas. Au lieu de cela, le DynamicClassLoader
essaiera de parcourir tous ses chemins de classe et de résoudre la classe cible avant qu'elle n'abandonne 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 absentes, il devra faire référence au chargeur de classe parent. Par exemple, nous devons charger le String
classe dans notre StaticInt
classe, et notre chargeur de classe n'a pas accès à rt.jar
dans notre dossier JRE, donc le String
la classe 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
:
DynamicClassLoader
peuvent être récupérés 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.
Cet exemple Java suivant vous montrera que le JRE peut charger et recharger des classes pour toujours, avec d'anciennes classes vidées et récupérées, et une toute nouvelle classe chargée depuis le disque dur et utilisée. 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'ancien User
classe sera sauvegardée, une nouvelle sera chargée et sa méthode hobby
invoqué.
Voici le User
définition de classe:
@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é dans le champ User
classe. Vous verrez que la définition la plus récente sera toujours utilisée.
faut-il apprendre le c avant le c++
Voici un exemple de sortie:
... Play Football Play Football Play Football Play Basketball Play Basketball Play Basketball
Chaque fois qu'une nouvelle instance de DynamicClassLoader
est créé, il chargera le User
classe de target/classes
dossier, où nous avons défini Eclipse ou IntelliJ pour générer le dernier fichier de classe. Tous les anciens DynamicClassLoader
s et les anciens User
les classes seront dissociées et soumises au ramasse-miettes.
Si vous êtes familier avec JVM HotSpot, il convient de noter ici que la structure de classe peut également être modifiée et rechargée: le playFootball
doit être supprimée et le playBasketball
méthode 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.
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 le main
méthode:
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 le Context
classe:
public static class Context { public HobbyService hobbyService = new HobbyService(); public void init() { // Init your services here hobbyService.user = new User(); } }
Et le HobbyService
classe:
public static class HobbyService { public User user; public void hobby() { user.hobby(); } }
Le Context
classe dans cet exemple est beaucoup plus compliquée que la classe User
classe dans les exemples précédents: elle a des liens vers d'autres classes, et elle a le init
méthode à 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 garde la trace des modules de l'application et effectue l'injection de dépendances). Donc pouvoir recharger ceci Context
la classe 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 «supprimer les anciennes versions» deviendra également plus compliquée. C'est aussi la principale raison pour laquelle le rechargement de classe est si difficile. Pour éventuellement supprimer d'anciennes versions, nous devrons nous assurer qu'une fois le nouveau contexte créé, tout les références aux anciennes classes et objets sont supprimées. Comment gérer cela avec élégance?
Le main
ici aura une prise sur l'objet de contexte, et c'est le seul lien à toutes les choses qui doivent être abandonné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 normalement les classes sont si persistantes et ne sont pas ramassées:
Avec cet exemple, nous voyons que recharger toutes les classes de l’application est en fait assez simple. Le but est simplement de conserver une connexion mince et supprimable entre le thread en direct et le chargeur de classe dynamique utilisé. Mais que se passe-t-il si nous voulons que certains objets (et leurs classes) ne pas être rechargée et réutilisée entre les cycles de rechargement? Regardons l'exemple suivant.
Le main
méthode:
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 le truc ici est de charger le ConnectionPool
classe et l'instanciant en dehors du cycle de rechargement, en le gardant dans l'espace persistant et en passant la référence à Context
objets
Le createContext
La méthode 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, nous appellerons les objets et les classes rechargés à chaque cycle «l'espace rechargeable» et 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, dessinant ainsi une ligne de séparation entre ces deux espaces.
Comme on le voit sur l'image, non seulement les Context
objet et le UserService
objet faisant référence au ConnectionPool
objet, mais le Context
et UserService
Les classes font également référence à ConnectionPool
classe. Il s'agit d'une situation très dangereuse qui conduit souvent à la confusion et à l'échec. Le ConnectionPool
classe ne doit pas être chargée par notre DynamicClassLoader
, il ne doit y en avoir qu'un seul ConnectionPool
classe dans la mémoire, qui est celle chargée par défaut ClassLoader
. 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.
Et si notre DynamicClassLoader
charge accidentellement le ConnectionPool
classe? Puis le ConnectionPool
objet de l'espace persistant ne peut pas être passé à Context
objet, car le Context
object attend un objet d'une classe différente, qui est également nommé ConnectionPool
, mais est en fait une classe différente!
Alors, comment pouvons-nous empêcher notre DynamicClassLoader
depuis le chargement du ConnectionPool
classe? Au lieu d'utiliser DynamicClassLoader
, cet exemple utilise une sous-classe de celui-ci nommée: ExceptingClassLoader
, qui transmettra le chargement au super chargeur de classe en fonction d'une fonction de condition:
(className) -> className.contains('$Connection')
Si nous n’utilisons pas ExceptingClassLoader
ici, puis le DynamicClassLoader
chargerait le ConnectionPool
classe car cette classe réside dans le 'target/classes
' dossier. Une autre façon d'éviter les ConnectionPool
cours repris par notre DynamicClassLoader
est de compiler le ConnectionPool
class dans un dossier différent, peut-être dans un module différent, et il sera compilé séparément.
Maintenant, le travail de chargement de classe Java devient vraiment déroutant. Comment déterminer quelles classes doivent être dans l'espace persistant et quelles classes dans l'espace rechargeable? Voici les règles:
Context
classe fait référence à la persistance ConnectionPool
classe, mais ConnectionPool
n'a pas de référence à Context
StringUtils
peut être chargé une fois dans l'espace persistant et chargé séparément dans l'espace rechargeable.Vous voyez donc que les règles ne sont pas très restrictives. À l'exception des classes de croisement qui ont des objets référencés dans les deux espaces, toutes les autres classes peuvent être librement utilisées dans l'espace persistant ou dans 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.
Le problème le plus difficile du rechargement de classe est donc résolu. Dans l'exemple suivant, nous essaierons d'appliquer cette technique à une simple application Web et apprécierons de recharger les classes Java comme n'importe quel langage de script.
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 Serveur Web intégré Jetty .
Voici l’espace rechargeable dans la structure du serveur Web:
Le serveur Web ne contiendra pas de références aux véritables 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 présente également un nouvel objet ReloadingWebContext
, qui fournit au serveur Web toutes les valeurs comme un contexte normal, mais contient en interne des références à un objet de contexte réel qui peut être rechargé par un DynamicClassLoader
. C'est ça ReloadingWebContext
qui fournissent des servlets de stub au serveur Web.
Le ReloadingWebContext
sera le wrapper du contexte réel, et:
Comme il est très important de comprendre comment nous isolons l'espace persistant et l'espace rechargeable, voici les deux classes qui se croisent entre les deux espaces:
comment effectuer un test d'utilisabilité
Classe qj.util.funct.F0
pour objet public F0 connF
dans Context
DynamicClassLoader
. Classe java.sql.Connection
pour objet public F0 connF
dans Context
DynamicClassLoader
, elle ne sera donc pas récupérée.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 un design 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 essayez, et à un moment donné, vous trouvez que vos classes sont chargées à la volée, alors vous y êtes presque déjà. Il vous restera très peu à faire avant de pouvoir réaliser une conception parfaitement propre pour votre système.
Bonne chance mes amis et profitez de votre nouvelle superpuissance!