Tutorial avansat de clasă Java: un ghid pentru reîncărcarea clasei
Publicat: 2022-03-11În proiectele de dezvoltare Java, un flux de lucru tipic implică repornirea serverului cu fiecare schimbare de clasă și nimeni nu se plânge de asta. Acesta este un fapt despre dezvoltarea Java. Am lucrat așa din prima noastră zi cu Java. Dar reîncărcarea clasei Java este atât de dificil de realizat? Și ar putea acea problemă să fie atât provocatoare, cât și interesantă de rezolvat pentru dezvoltatorii Java calificați? În acest tutorial de clasă Java, voi încerca să rezolv problema, să vă ajut să obțineți toate beneficiile reîncărcării cursului din mers și să vă sporesc enorm productivitatea.
Reîncărcarea clasei Java nu este adesea discutată și există foarte puțină documentație care explorează acest proces. Sunt aici să schimb asta. Acest tutorial de clase Java va oferi o explicație pas cu pas a acestui proces și vă va ajuta să stăpâniți această tehnică incredibilă. Rețineți că implementarea reîncărcării clasei Java necesită multă grijă, dar învățarea cum să o faceți vă va pune în ligile mari, atât ca dezvoltator Java, cât și ca arhitect software. De asemenea, nu va strica să înțelegeți cum să evitați cele mai frecvente 10 greșeli Java.
Configurarea spațiului de lucru
Tot codul sursă pentru acest tutorial este încărcat pe GitHub aici.
Pentru a rula codul în timp ce urmați acest tutorial, veți avea nevoie de Maven, Git și fie Eclipse, fie IntelliJ IDEA.
Dacă utilizați Eclipse:
- Rulați comanda
mvn eclipse:eclipse
pentru a genera fișierele de proiect ale Eclipse. - Încărcați proiectul generat.
- Setați calea de ieșire la
target/classes
.
Dacă utilizați IntelliJ:
- Importați fișierul
pom
al proiectului. - IntelliJ nu se va compila automat atunci când rulați vreun exemplu, așa că trebuie să fie:
- Rulați exemplele în IntelliJ, apoi de fiecare dată când doriți să compilați, va trebui să apăsați
Alt+BE
- Rulați exemplele în afara IntelliJ cu
run_example*.bat
. Setați compilatorul automat al compilatorului IntelliJ la true. Apoi, de fiecare dată când modificați orice fișier java, IntelliJ îl va compila automat.
Exemplul 1: Reîncărcarea unei clase cu Java Class Loader
Primul exemplu vă va oferi o înțelegere generală a încărcării claselor Java. Aici este codul sursă.
Având în vedere următoarea definiție a clasei de User
:
public static class User { public static int age = 10; }
Putem face următoarele:
public static void main(String[] args) { Class<?> userClass1 = User.class; Class<?> userClass2 = new DynamicClassLoader("target/classes") .load("qj.blog.classreloading.example1.StaticInt$User"); ...
În acest exemplu tutorial, vor exista două clase de User
încărcate în memorie. userClass1
va fi încărcat de încărcătorul de clasă implicit al JVM, iar userClass2
folosind DynamicClassLoader
, un încărcător de clasă personalizat al cărui cod sursă este furnizat și în proiectul GitHub și pe care îl voi descrie în detaliu mai jos.
Iată restul metodei 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)); }
Și ieșirea:
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
După cum puteți vedea aici, deși clasele de User
au același nume, sunt de fapt două clase diferite și pot fi gestionate și manipulate independent. Valoarea vârstei, deși este declarată ca fiind statică, există în două versiuni, atașându-se separat fiecărei clase și poate fi modificată și independent.
Într-un program Java normal, ClassLoader
este portalul care aduce clase în JVM. Când o clasă necesită încărcarea unei alte clase, sarcina ClassLoader
este să facă încărcarea.
Cu toate acestea, în acest exemplu de clasă Java, ClassLoader
personalizat numit DynamicClassLoader
este utilizat pentru a încărca a doua versiune a clasei User
. Dacă în loc de DynamicClassLoader
, ar trebui să folosim din nou încărcătorul de clasă implicit (cu comanda StaticInt.class.getClassLoader()
), atunci aceeași clasă de User
va fi folosită, deoarece toate clasele încărcate sunt stocate în cache.
DynamicClassLoader
Pot exista mai multe classloader într-un program Java normal. Cea care vă încarcă clasa principală, ClassLoader
, este cea implicită și, din codul dvs., puteți crea și utiliza câte classloadere doriți. Aceasta este, deci, cheia reîncărcării clasei în Java. DynamicClassLoader
este probabil cea mai importantă parte a acestui întreg tutorial, așa că trebuie să înțelegem cum funcționează încărcarea dinamică a clasei înainte de a ne putea îndeplini scopul.
Spre deosebire de comportamentul implicit al ClassLoader
, DynamicClassLoader
moștenește o strategie mai agresivă. Un classloader obișnuit ar acorda ClassLoader
-ului părintelui său prioritatea și ar încărca numai clasele pe care părintele său nu le poate încărca. Este potrivit pentru circumstanțe normale, dar nu și în cazul nostru. În schimb, DynamicClassLoader
va încerca să cerceteze toate căile sale de clasă și să rezolve clasa țintă înainte de a renunța la dreptul părintelui său.
În exemplul nostru de mai sus, DynamicClassLoader
este creat cu o singură cale de clasă: "target/classes"
(în directorul nostru curent), deci este capabil să încarce toate clasele care rezidă în acea locație. Pentru toate clasele care nu sunt acolo, va trebui să se refere la încărcătorul de clasă părinte. De exemplu, trebuie să încărcăm clasa String
în clasa noastră StaticInt
, iar încărcătorul nostru de clasă nu are acces la rt.jar
din folderul nostru JRE, așa că va fi utilizată clasa String
a încărcării clasei părinte.
Următorul cod este de la AggressiveClassLoader
, clasa părinte a DynamicClassLoader
și arată unde este definit acest comportament.
byte[] newClassData = loadNewClass(name); if (newClassData != null) { loadedClasses.add(name); return loadClass(newClassData, name); } else { unavaiClasses.add(name); return parent.loadClass(name); }
Luați notă de următoarele proprietăți ale DynamicClassLoader
:
- Clasele încărcate au aceleași performanțe și alte atribute ca și alte clase încărcate de încărcătorul de clasă implicit.
-
DynamicClassLoader
poate fi colectat de gunoi împreună cu toate clasele și obiectele încărcate.
Cu capacitatea de a încărca și de a folosi două versiuni ale aceleiași clase, acum ne gândim să aruncăm versiunea veche și să o încărcăm pe cea nouă pentru a o înlocui. În exemplul următor, vom face exact asta... continuu.
Exemplul 2: Reîncărcarea continuă a unei clase
Acest următor exemplu Java vă va arăta că JRE poate încărca și reîncărca clase pentru totdeauna, cu clasele vechi aruncate și gunoiul colectat și o clasă nouă încărcată de pe hard disk și pusă în funcțiune. Aici este codul sursă.
Iată bucla principală:
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); } }
La fiecare două secunde, vechea clasă User
va fi descărcată, va fi încărcată una nouă și va fi invocată metoda hobby
-ului acesteia.
Iată definiția clasei de 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"); // } }
Când rulați această aplicație, ar trebui să încercați să comentați și să decomentați codul indicat în clasa User
. Veți vedea că cea mai nouă definiție va fi întotdeauna folosită.
Iată câteva exemple de rezultate:
... Play Football Play Football Play Football Play Basketball Play Basketball Play Basketball
De fiecare dată când este creată o nouă instanță a DynamicClassLoader
, aceasta va încărca clasa User
din folderul target/classes
, unde am setat Eclipse sau IntelliJ să scoată cel mai recent fișier de clasă. Toate vechile clase DynamicClassLoader
și vechile clase de User
vor fi deconectate și supuse colectorului de gunoi.
Dacă sunteți familiarizat cu JVM HotSpot, atunci este de remarcat aici că structura clasei poate fi, de asemenea, modificată și reîncărcată: metoda playFootball
urmează să fie eliminată și metoda playBasketball
adăugată. Acest lucru este diferit de HotSpot, care permite doar modificarea conținutului metodei sau clasa nu poate fi reîncărcată.
Acum că suntem capabili să reîncărcăm o clasă, este timpul să încercăm să reîncărcăm mai multe clase simultan. Să încercăm în exemplul următor.
Exemplul 3: Reîncărcarea mai multor clase
Rezultatul acestui exemplu va fi același cu Exemplul 2, dar va arăta cum să implementați acest comportament într-o structură mai asemănătoare aplicației cu obiecte context, serviciu și model. Codul sursă al acestui exemplu este destul de mare, așa că am arătat doar părți din el aici. Codul sursă complet este aici.
Iată metoda main
:
public static void main(String[] args) { for (;;) { Object context = createContext(); invokeHobbyService(context); ThreadUtil.sleep(2000); } }
Și metoda 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; }
Metoda invokeHobbyService
:
private static void invokeHobbyService(Object context) { Object hobbyService = getFieldValue("hobbyService", context); invoke("hobby", hobbyService); }
Și aici este clasa Context
:

public static class Context { public HobbyService hobbyService = new HobbyService(); public void init() { // Init your services here hobbyService.user = new User(); } }
Și clasa HobbyService
:
public static class HobbyService { public User user; public void hobby() { user.hobby(); } }
Clasa Context
din acest exemplu este mult mai complicată decât clasa User
din exemplele anterioare: are legături către alte clase și are metoda init
pentru a fi apelată de fiecare dată când este instanțiată. Practic, este foarte asemănător cu clasele de context ale aplicației din lumea reală (care ține evidența modulelor aplicației și face injecția de dependență). Deci, posibilitatea de a reîncărca această clasă Context
împreună cu toate clasele ei legate este un pas grozav către aplicarea acestei tehnici în viața reală.
Pe măsură ce numărul de clase și obiecte crește, pasul nostru de a „elimina versiunile vechi” va deveni și mai complicat. Acesta este și cel mai mare motiv pentru care reîncărcarea clasei este atât de dificilă. Pentru a elimina eventual versiunile vechi, va trebui să ne asigurăm că, odată ce noul context este creat, toate referințele la vechile clase și obiecte sunt abandonate. Cum ne descurcăm cu eleganță?
Metoda main
de aici va avea o reținere a obiectului context și aceasta este singura legătură către toate lucrurile care trebuie abandonate. Dacă întrerupem acea legătură, obiectul context și clasa context și obiectul serviciu... vor fi toate supuse colectorului de gunoi.
O mică explicație despre motivul pentru care, în mod normal, clasele sunt atât de persistente și nu se colectează gunoiul:
- În mod normal, încărcăm toate clasele noastre în încărcătorul de clasă Java implicit.
- Relația clasă-încărcător de clasă este o relație bidirecțională, încărcătorul de clasă păstrând, de asemenea, în cache toate clasele pe care le-a încărcat.
- Deci, atâta timp cât încărcătorul de clasă este încă conectat la orice fir live, totul (toate clasele încărcate) va fi imun la colectorul de gunoi.
- Acestea fiind spuse, dacă nu putem separa codul pe care vrem să-l reîncărcăm de codul deja încărcat de încărcătorul de clasă implicit, noile noastre modificări de cod nu vor fi niciodată aplicate în timpul rulării.
Cu acest exemplu, vedem că reîncărcarea tuturor claselor aplicației este de fapt destul de ușoară. Scopul este doar de a menține o conexiune subțire, care poate fi eliminată, de la firul de execuție live la încărcătorul de clasă dinamică în uz. Dar dacă vrem ca unele obiecte (și clasele lor) să nu fie reîncărcate și să fie reutilizate între ciclurile de reîncărcare? Să ne uităm la următorul exemplu.
Exemplul 4: Separarea spațiilor de clasă persistente și reîncărcate
Iată codul sursă..
Metoda main
:
public static void main(String[] args) { ConnectionPool pool = new ConnectionPool(); for (;;) { Object context = createContext(pool); invokeService(context); ThreadUtil.sleep(2000); } }
Deci, puteți vedea că truc aici este încărcarea clasei ConnectionPool
și instanțiarea acesteia în afara ciclului de reîncărcare, păstrând-o în spațiul persistent și treceți referința la obiectele Context
Metoda createContext
este, de asemenea, puțin diferită:
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; }
De acum înainte, vom numi obiectele și clasele care sunt reîncărcate cu fiecare ciclu „spațiu reîncărcat” și altele - obiectele și clasele nereciclate și nereînnoite în timpul ciclurilor de reîncărcare - „spațiul persistent”. Va trebui să fim foarte clari despre ce obiecte sau clase rămân în ce spațiu, trasând astfel o linie de separare între aceste două spații.
După cum se vede din imagine, nu numai obiectul Context
și obiectul UserService
se referă la obiectul ConnectionPool
, dar și clasele Context
și UserService
se referă la clasa ConnectionPool
. Aceasta este o situație foarte periculoasă care duce adesea la confuzie și eșec. Clasa ConnectionPool
nu trebuie să fie încărcată de DynamicClassLoader
, trebuie să existe o singură clasă ConnectionPool
în memorie, care este cea încărcată de ClassLoader
implicit. Acesta este un exemplu de ce este atât de important să fiți atenți atunci când proiectați o arhitectură de reîncărcare a clasei în Java.
Ce se întâmplă dacă DynamicClassLoader
încarcă accidental clasa ConnectionPool
? Apoi obiectul ConnectionPool
din spațiul persistent nu poate fi transmis obiectului Context
, deoarece obiectul Context
se așteaptă la un obiect dintr-o clasă diferită, care se numește și ConnectionPool
, dar este de fapt o clasă diferită!
Deci, cum împiedicăm DynamicClassLoader
nostru să încarce clasa ConnectionPool
? În loc să folosească DynamicClassLoader
, acest exemplu folosește o subclasă numită: ExceptingClassLoader
, care va transmite încărcarea superclassloader-ului pe baza unei funcție de condiție:
(className) -> className.contains("$Connection")
Dacă nu folosim ExceptingClassLoader
aici, atunci DynamicClassLoader
ar încărca clasa ConnectionPool
deoarece acea clasă se află în folderul „ target/classes
”. O altă modalitate de a preveni ca clasa ConnectionPool
să fie preluată de DynamicClassLoader
este să compilați clasa ConnectionPool
într-un folder diferit, poate într-un modul diferit, și va fi compilat separat.
Reguli pentru alegerea spațiului
Acum, sarcina de încărcare a clasei Java devine foarte confuză. Cum determinăm ce clase ar trebui să fie în spațiul persistent și ce clase în spațiul reîncărcat? Iată regulile:
- O clasă din spațiul reîncărcat poate face referire la o clasă din spațiul persistent, dar o clasă din spațiul persistat nu poate face referire niciodată la o clasă din spațiul reîncărcat. În exemplul anterior, clasa
Context
reîncărcabilă face referire la clasaConnectionPool
persistentă, darConnectionPool
nu are nicio referință laContext
- O clasă poate exista în oricare spațiu dacă nu face referire la nicio clasă din celălalt spațiu. De exemplu, o clasă de utilitate cu toate metodele statice precum
StringUtils
poate fi încărcată o dată în spațiul persistent și încărcată separat în spațiul reîncărcat.
Deci puteți vedea că regulile nu sunt foarte restrictive. Cu excepția claselor de încrucișare care au obiecte referite în cele două spații, toate celelalte clase pot fi utilizate în mod liber fie în spațiul persistent, fie în spațiul reîncărcat sau ambele. Desigur, doar clasele din spațiul reîncărcat se vor bucura de a fi reîncărcate cu cicluri de reîncărcare.
Deci, cea mai dificilă problemă cu reîncărcarea clasei este rezolvată. În exemplul următor, vom încerca să aplicăm această tehnică unei aplicații web simple și să ne bucurăm de reîncărcarea claselor Java la fel ca orice limbaj de scripting.
Exemplul 5: Mica agenda telefonica
Iată codul sursă..
Acest exemplu va fi foarte asemănător cu cum ar trebui să arate o aplicație web normală. Este o aplicație cu o singură pagină cu server web încorporat AngularJS, SQLite, Maven și Jetty.
Iată spațiul reîncărcat din structura serverului web:
Serverul web nu va deține referințe la servlet-urile reale, care trebuie să rămână în spațiul reîncărcat, pentru a fi reîncărcate. Ceea ce deține sunt servlet-uri stub, care, cu fiecare apel la metoda sa de service, vor rezolva servlet-ul real în contextul real pentru a rula.
Acest exemplu introduce, de asemenea, un nou obiect ReloadingWebContext
, care furnizează serverului web toate valorile ca un Context normal, dar deține intern referințe la un obiect context real care poate fi reîncărcat de un DynamicClassLoader
. Acest ReloadingWebContext
oferă servlet-uri stub serverului web.
ReloadingWebContext
va fi învelișul contextului real și:
- Va reîncărca contextul real atunci când este apelat un HTTP GET la „/”.
- Va furniza servlet-uri stub serverului web.
- Va seta valori și va invoca metode de fiecare dată când contextul real este inițializat sau distrus.
- Poate fi configurat pentru a reîncărca sau nu contextul și care încărcător de clasă este folosit pentru reîncărcare. Acest lucru va ajuta la rularea aplicației în producție.
Deoarece este foarte important să înțelegem cum izolăm spațiul persistent și spațiul reîncărcat, iată cele două clase care se încrucișează între cele două spații:
Clasa qj.util.funct.F0
pentru obiect public F0<Connection> connF
în Context
- Obiectul Funcție, va returna o Conexiune de fiecare dată când funcția este invocată. Această clasă se află în pachetul qj.util, care este exclus din
DynamicClassLoader
.
Clasa java.sql.Connection
pentru obiect public F0<Connection> connF
în Context
- Obiect de conexiune SQL normal. Această clasă nu se află în calea clasei
DynamicClassLoader
, așa că nu va fi preluată.
rezumat
În acest tutorial de clase Java, am văzut cum să reîncărcăm o singură clasă, să reîncărcăm o singură clasă continuu, să reîncărcăm un întreg spațiu de mai multe clase și să reîncărcăm mai multe clase separat de clasele care trebuie să fie persistente. Cu aceste instrumente, factorul cheie pentru a obține o reîncărcare fiabilă a clasei este acela de a avea un design super curat. Apoi vă puteți manipula liber clasele și întregul JVM.
Implementarea reîncărcării clasei Java nu este cel mai ușor lucru din lume. Dar dacă dai o șansă și la un moment dat găsești că orele tale sunt încărcate din mers, atunci aproape că ai ajuns deja. Va mai rămâne foarte puțin de făcut înainte de a obține un design curat complet superb pentru sistemul dumneavoastră.
Mult succes prietenilor mei și bucurați-vă de noua voastră putere!