Sisään Java-kehitysprojektit , tyypilliseen työnkulkuun kuuluu palvelimen uudelleenkäynnistys jokaisen luokan muutoksen yhteydessä, eikä kukaan valittaa siitä. Se on tosiasia Java-kehityksestä. Olemme työskennelleet niin jo ensimmäisestä päivästäni Java-palvelussa. Mutta onko Java-luokan uudelleenlataus niin vaikeaa? Ja voisiko tuo ongelma olla sekä haastava että jännittävä ratkaista taitava Java-kehittäjä ? Tässä Java-luokan opetusohjelmassa yritän ratkaista ongelman, auttaa sinua saamaan kaikki lennon luokan uudelleenlatauksen edut ja parantamaan tuottavuuttasi valtavasti.
Java-luokan uudelleenlataamisesta ei keskustella usein, ja tätä prosessia on tutkittu hyvin vähän. Olen täällä muuttamaan sitä. Tämä Java-luokkien opetusohjelma antaa vaiheittaisen selityksen tästä prosessista ja auttaa sinua hallitsemaan tämän uskomattoman tekniikan. Muista, että Java-luokan uudelleenlatauksen toteuttaminen vaatii paljon huolellisuutta, mutta sen oppiminen vie sinut suuriin liigoihin sekä Java-kehittäjänä että ohjelmistoarkkitehtina. Se ei myöskään satuta ymmärtämään kuinka välttää 10 yleisintä Java-virhettä .
Kaikki tämän opetusohjelman lähdekoodi on ladattu GitHubiin tässä .
Tarvitset koodin suorittamiseksi, kun noudatat tätä opetusohjelmaa Maven , Mennä ja joko Pimennys tai IntelliJ IDEA .
mvn eclipse:eclipse
luoda Eclipse-projektitiedostot.target/classes
.pom
tiedosto.Alt+B E
run_example*.bat
. Aseta IntelliJ: n kääntäjän automaattinen kääntäminen tosi-arvoksi. Sitten, aina kun vaihdat mitä tahansa Java-tiedostoa, IntelliJ kääntää sen automaattisesti.Ensimmäinen esimerkki antaa sinulle yleisen käsityksen Java-luokan kuormaajasta. Tässä on lähdekoodi.
Seuraavat User
luokan määritelmä:
public static class User { public static int age = 10; }
Voimme tehdä seuraavaa:
public static void main(String[] args) { Class userClass1 = User.class; Class userClass2 = new DynamicClassLoader('target/classes') .load('qj.blog.classreloading.example1.StaticInt$User'); ...
Tässä opetusohjelmassa on kaksi User
luokat ladattu muistiin. userClass1
ladataan JVM: n oletusluokan latauslaitteella ja userClass2
käyttämällä DynamicClassLoader
, mukautettua luokan kuormaajaa, jonka lähdekoodi on myös GitHub-projektissa, ja jonka kuvaan yksityiskohtaisesti alla.
Tässä on loput main
menetelmä:
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)); }
Ja tulos:
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
Kuten näette täällä, vaikka User
luokilla on sama nimi, ne ovat itse asiassa kahta erilaista luokkaa, ja niitä voidaan hallita ja manipuloida itsenäisesti. Ikäarvo, vaikka se on ilmoitettu staattisena, on olemassa kahdessa versiossa, joka liitetään erikseen kullekin luokalle, ja sitä voidaan muuttaa myös itsenäisesti.
Normaalissa Java-ohjelmassa ClassLoader
on portaali, joka tuo luokkia JVM: ään. Kun yksi luokka vaatii toisen luokan lataamista, lataaminen on ClassLoader
: n tehtävä.
Tässä Java-luokan esimerkissä mukautettu ClassLoader
nimetty DynamicClassLoader
käytetään lataamaan User
toisen version luokassa. Jos DynamicClassLoader
: n sijaan käytimme taas oletusluokan kuormaajaa (komennolla StaticInt.class.getClassLoader()
), niin sama User
class käytetään, koska kaikki ladatut luokat tallennetaan välimuistiin.
DynamicClassLoader
Normaalissa Java-ohjelmassa voi olla useita luokkakuormaajia. Se, joka lataa pääluokan, ClassLoader
, on oletusarvo, ja koodistasi voit luoda ja käyttää niin monta luokkakuormaajaa kuin haluat. Tämä on siis avain luokan uudelleenlataukseen Java-sovelluksessa. DynamicClassLoader
on mahdollisesti koko tämän opetusohjelman tärkein osa, joten meidän on ymmärrettävä, kuinka dynaaminen luokan lataus toimii, ennen kuin voimme saavuttaa tavoitteemme.
Toisin kuin ClassLoader
: n oletuskäyttäytyminen, DynamicClassLoader
perii aggressiivisemman strategian. Normaali luokan lataaja antaisi vanhemmalleen ClassLoader
prioriteetti ja vain kuormaluokat, joita sen vanhempi ei voi ladata. Se sopii normaaleihin olosuhteisiin, mutta ei meidän tapauksessamme. Sen sijaan DynamicClassLoader
yrittää käydä läpi kaikki luokkansa polut ja ratkaista kohdeluokan, ennen kuin se luopuu oikeudesta vanhempaansa.
Yllä olevassa esimerkissämme DynamicClassLoader
luodaan vain yhdellä luokan polulla: 'target/classes'
(nykyisessä hakemistossamme), joten se pystyy lataamaan kaikki kyseisessä paikassa asuvat luokat. Kaikissa luokissa, joita ei ole, sen on viitattava vanhempaan luokkaohjelmaan. Esimerkiksi meidän on ladattava String
luokka StaticInt
luokka, ja luokan kuormaajalla ei ole pääsyä rt.jar
JRE-kansiossamme, joten String
käytetään vanhemman luokan kuormaajaluokkaa.
Seuraava koodi on peräisin AggressiveClassLoader
, DynamicClassLoader
emoluokka ja näyttää missä tämä käyttäytyminen on määritelty.
byte[] newClassData = loadNewClass(name); if (newClassData != null) { loadedClasses.add(name); return loadClass(newClassData, name); } else { unavaiClasses.add(name); return parent.loadClass(name); }
Ota huomioon seuraavat DynamicClassLoader
-ominaisuudet:
DynamicClassLoader
voidaan kerätä roskiin yhdessä kaikkien ladattujen luokkien ja objektien kanssa.Mahdollisuuden ladata ja käyttää kahta saman luokan versiota ajattelemme nyt vanhan version poistamista käytöstä ja uuden korvaamista sen korvaamiseksi. Seuraavassa esimerkissä teemme juuri niin ... jatkuvasti.
Tämä seuraava Java-esimerkki osoittaa, että JRE voi ladata ja ladata luokkia ikuisesti, vanhojen luokkien ollessa kaatopaikalla, roskat kerätään ja upouusi luokka ladataan kiintolevyltä ja otetaan käyttöön. Tässä on lähdekoodi.
Tässä on pääsilmukka:
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); } }
Kahden sekunnin välein vanha User
luokka poistetaan, uusi ladataan ja sen menetelmä hobby
vedottu.
Tässä on User
luokan määritelmä:
@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'); // } }
Suorita tämä sovellus, yritä kommentoida User
-koodissa ilmoitettua koodia ja poistaa kommentti luokassa. Huomaat, että aina käytetään uusinta määritelmää.
Tässä on esimerkki tuotoksesta:
... Play Football Play Football Play Football Play Basketball Play Basketball Play Basketball
Joka kerta, kun DynamicClassLoader
on luotu, se lataa User
luokka target/classes
kansio, jossa olemme asettaneet Eclipse tai IntelliJ tuottamaan uusimman luokkatiedoston. Kaikki vanhat DynamicClassLoader
s ja vanhat User
luokat irrotetaan ja ne joutuvat roskakoriin.
Jos olet perehtynyt JVM HotSpotiin, tässä on huomionarvoista, että luokan rakennetta voidaan myös muuttaa ja ladata uudelleen: playFootball
menetelmä on poistettava ja playBasketball
menetelmä lisätty. Tämä eroaa HotSpotista, joka sallii vain menetelmän sisällön muuttamisen, tai luokkaa ei voida ladata uudelleen.
Nyt kun voimme ladata luokan uudelleen, on aika yrittää ladata useita luokkia kerralla. Kokeillaan seuraavassa esimerkissä.
Tämän esimerkin tulos on sama kuin esimerkissä 2, mutta se näyttää kuinka tämä käyttäytyminen voidaan toteuttaa sovelluksen tyyppisemmässä rakenteessa, jossa on konteksti-, palvelu- ja malliobjekteja. Tämän esimerkin lähdekoodi on melko suuri, joten olen osoittanut vain osan siitä täällä. Koko lähdekoodi on tässä .
Tässä on main
menetelmä:
public static void main(String[] args) { for (;;) { Object context = createContext(); invokeHobbyService(context); ThreadUtil.sleep(2000); } }
Ja menetelmä 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; }
Menetelmä invokeHobbyService
:
private static void invokeHobbyService(Object context) { Object hobbyService = getFieldValue('hobbyService', context); invoke('hobby', hobbyService); }
Ja tässä on Context
luokka:
public static class Context { public HobbyService hobbyService = new HobbyService(); public void init() { // Init your services here hobbyService.user = new User(); } }
Ja HobbyService
luokka:
public static class HobbyService { public User user; public void hobby() { user.hobby(); } }
Context
luokka tässä esimerkissä on paljon monimutkaisempi kuin User
luokka edellisissä esimerkeissä: sillä on linkkejä muihin luokkiin ja sillä on init
menetelmä kutsutaan jokaiseksi se on instantisoitu. Pohjimmiltaan se on hyvin samanlainen kuin reaalimaailman sovelluksen kontekstiluokat (joka seuraa sovelluksen moduuleja ja tekee riippuvuusinjektioita). Joten pystymme lataamaan tämän Context
luokka yhdessä kaikkien siihen liittyvien luokkien kanssa on hieno askel kohti tämän tekniikan soveltamista tosielämässä.
Kun luokkien ja objektien määrä kasvaa, myös vaihe 'vanhojen versioiden pudottaminen' vaikeutuu. Tämä on myös suurin syy siihen, miksi luokan uudelleenlataus on niin vaikeaa. Jos haluat pudottaa vanhat versiot, meidän on varmistettava, että kun uusi konteksti on luotu, kaikki viittaukset vanhoihin luokkiin ja esineisiin hylätään. Kuinka voimme käsitellä tätä tyylikkäästi?
main
menetelmä tässä pitää sisällön kontekstiobjektin ja se on ainoa linkki kaikkiin pudotettaviin asioihin. Jos katkaiset linkin, kontekstiobjekti ja kontekstiluokka sekä palveluobjekti… altistuvat kaikki roskakorille.
Pieni selitys siitä, miksi luokat yleensä ovat niin sitkeitä eivätkä kerää roskia:
Tässä esimerkissä näemme, että kaikkien sovellusten luokkien uudelleenlataus on itse asiassa melko helppoa. Tavoitteena on vain pitää ohut, pudotettava yhteys aktiivisesta säikeestä käytössä olevaan dynaamiseen luokan kuormaajaan. Mutta entä jos haluamme joidenkin esineiden (ja niiden luokkien) olevan ei voidaan ladata uudelleen ja käyttää uudelleen uudelleenlataussyklien välillä? Katsotaanpa seuraavaa esimerkkiä.
main
menetelmä:
public static void main(String[] args) { ConnectionPool pool = new ConnectionPool(); for (;;) { Object context = createContext(pool); invokeService(context); ThreadUtil.sleep(2000); } }
Joten voit nähdä, että temppu tässä ladataan ConnectionPool
luokka ja sen ilmentäminen uudelleenlataussyklin ulkopuolella pitämällä se pysyvässä tilassa ja välittää viittaus Context
esineitä
createContext
menetelmä on myös hieman erilainen:
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; }
Tästä lähtien kutsumme objektien ja luokkien, jotka ladataan jokaisen jakson aikana, 'uudelleen ladattavaksi tilaksi' ja muita - esineitä ja luokkia, joita ei kierrätetä eikä uusita uudelleenlataussyklien aikana - 'pysyväksi tilaksi'. Meidän on oltava hyvin selkeitä siitä, mitkä objektit tai luokat pysyvät missä tilassa, vetämällä siten erotusviiva näiden kahden tilan välille.
Kuten kuvasta näkyy, Context
eivät ole vain esine ja UserService
objekti viittaa ConnectionPool
esine, mutta Context
ja UserService
luokat viittaavat myös ConnectionPool
luokassa. Tämä on erittäin vaarallinen tilanne, joka johtaa usein sekaannukseen ja epäonnistumiseen. ConnectionPool
luokkaa ei saa ladata DynamicClassLoader
, vain yksi ConnectionPool
luokka muistiin, joka on oletusarvoisesti ladattu ClassLoader
Tämä on yksi esimerkki siitä, miksi on niin tärkeää olla varovainen suunniteltaessa luokan uudelleenlatausarkkitehtuuria Javalassa.
Entä jos DynamicClassLoader
lataa vahingossa ConnectionPool
luokka? Sitten ConnectionPool
objektia pysyvästä avaruudesta ei voida siirtää Context
esine, koska Context
object odottaa toisen luokan objektia, joka on myös nimetty ConnectionPool
, mutta on itse asiassa erilainen luokka!
Joten miten estämme DynamicClassLoader
lataamasta ConnectionPool
luokka? Tässä esimerkissä käytetään DynamicClassLoader
: n käyttämisen sijaan sen alaluokkaa nimeltä: ExceptingClassLoader
, joka siirtää latauksen superlatauslaitteelle ehtofunktion perusteella:
(className) -> className.contains('$Connection')
Jos emme käytä ExceptingClassLoader
täällä, sitten DynamicClassLoader
lataa ConnectionPool
luokka, koska kyseinen luokka sijaitsee 'target/classes
' kansio. Toinen tapa estää ConnectionPool
luokka noutaa DynamicClassLoader
on koota ConnectionPool
luokka toiseen kansioon, ehkä eri moduuliin, ja se kootaan erikseen.
Nyt Java-luokan lataustyö muuttuu todella hämmentäväksi. Kuinka määritämme, minkä luokkien tulisi olla pysyvässä tilassa ja mitkä luokissa uudelleen ladattavassa tilassa? Tässä ovat säännöt:
Context
luokan viittaukset jatkuvat ConnectionPool
luokka, mutta ConnectionPool
ei viittaa Context
StringUtils
voidaan ladata kerran pysyvään tilaan ja ladata erikseen uudelleen ladattavaan tilaan.Joten voit nähdä, että säännöt eivät ole kovin rajoittavia. Lukuun ottamatta ylitysluokkia, joiden kohteisiin viitataan kahden tilan yli, kaikkia muita luokkia voidaan vapaasti käyttää joko pysyvässä tilassa tai uudelleen ladattavassa tilassa tai molemmissa. Tietenkin vain ladattavan tilan luokat nauttivat lataamisesta uudelleenlataussyklien avulla.
Joten käsitellään haastavinta luokan uudelleenlatauksen ongelmaa. Seuraavassa esimerkissä yritämme soveltaa tätä tekniikkaa yksinkertaiseen verkkosovellukseen ja nauttia Java-luokkien lataamisesta aivan kuten mikä tahansa komentosarjakieli.
Tämä esimerkki on hyvin samanlainen kuin miltä normaalin verkkosovelluksen pitäisi näyttää. Se on yhden sivun sovellus, jossa on AngularJS, SQLite, Maven ja Laiturin sulautettu verkkopalvelin .
Tässä on ladattava tila verkkopalvelimen rakenteessa:
Verkkopalvelimessa ei ole viitteitä todellisiin palvelinsovelluksiin, joiden on pysyttävä ladattavassa tilassa, jotta ne voidaan ladata uudelleen. Se pitää sisällään tynkäpalvelimia, jotka jokaisen palvelumenetelmän kutsun yhteydessä ratkaisevat todellisen servletin todellisessa yhteydessä suoritettavaksi.
Tämä esimerkki tuo myös uuden objektin ReloadingWebContext
, joka tarjoaa verkkopalvelimelle kaikki arvot kuten tavallinen konteksti, mutta pitää sisäisesti viitteitä todelliseen kontekstiobjektiin, jonka DynamicClassLoader
voi ladata uudelleen. Juuri tämä ReloadingWebContext
jotka tarjoavat tynkäpalvelimia web-palvelimelle.
ReloadingWebContext
on todellisen kontekstin kääre, ja:
Koska on erittäin tärkeää ymmärtää, kuinka eristämme pysyvän tilan ja ladattavan tilan, tässä on kaksi luokkaa, jotka ylittävät kahden tilan:
Luokka qj.util.funct.F0
kohteelle public F0 connF
sisään Context
DynamicClassLoader
-ryhmään. Luokka java.sql.Connection
kohteelle public F0 connF
sisään Context
DynamicClassLoader
-luokan polulla, joten sitä ei oteta.Tässä Java-luokkien opetusohjelmassa olemme nähneet, kuinka voit ladata yhden luokan uudelleen, ladata yhden luokan jatkuvasti, ladata koko luokan useita luokkia ja ladata useita luokkia erillään luokista, joita on jatkettava. Näillä työkaluilla avainasemassa luotettavan luokan uudelleenlatauksen saavuttamisessa on erittäin puhdas muotoilu. Sitten voit vapaasti manipuloida luokkiasi ja koko JVM: ää.
Java-luokan uudelleenlatauksen toteuttaminen ei ole helpoin asia maailmassa. Mutta jos annat sille laukauksen ja jostain hetkestä löydät luokkasi ladattavan lennossa, niin olet melkein siellä jo. Tehtävää on jäljellä hyvin vähän, ennen kuin saavutat täysin upean puhtaan suunnittelun järjestelmällesi.
Onnea ystäväni ja nauti uudesta supervoimastasi!