Class loaderi apstrakuju proces učitavanja klasa pre njihovog prvog instanciranja čime ih čine raspoloživim za upotrebu. Svaki put kada se zahteva instanciranje klase ili se klasa statički poziva, class loader u pozadini, transparentno za sistem, pronalazi i učitava zahtevanu klasu u memoriju JVM.
Razlozi za postojanje class loadera se nalaze u samoj osnovi zahteva Java jezika: da bude nezavisan od platforme i da podržava distribuirane sisteme. Jezik koji je nezavisan od platforme ne sme da se oslanja na specifičan fajl sistem za učitavanje biblioteka. Dalje, pošto se očekuje da Java učitava klase koje se nalaze distribuirane po mreži, korišćenje fajl sistema sasvim prestaje da ima smisla. Zato, kada se u programu zahteva instanciranje neke klase, JVM zahteva od class loadera da je učita. Class loader traži klasu na način na koji je već dizajniran: na fajl sistemu, na mreži, na ROM čipu itd.
Java definiše sledeća 3 class loadera:
Bootstrap class loader se koristi od strane JVM za učitavanje klasa koje su neophodne za rad same virtualne mašine.
Reč je o svim osnovnim (core) Java klasama, na pr.: java.*, javax.*, org.omg.* itd.
Klase se najčešće nalaze na predefinisanim lokacijama u fajl sistemu u odgovarajućim
jar bibliotekama. Lokacija zavisi od verzije Jave i od njenog proizvođača. Za Sunovu JVM (Java v1.4.x) reč je o jar arhivama u
folderu %JAVA_HOME%/jre/lib (properti: sun.boot.class.path): rt.jar, l18n.jar, jsse.jar itd.
Bootstrap class loader je implementiran kao deo same virtualne mašine (delom u native kodu) i ne može se instancirati iz Java koda. Samim time se ne vrše sigurnosne provere, jer se smatra da se osnovne klase nalaze u sigurnoj zoni, pa se one i učitavaju brže.
Extension class loader omogućava učitavanje klasa koje nisu navedene u CLASSPATH varijabli. Sunova JVM koristi folder: %JAVA_HOME%/jre/lib/ext (properti: java.ext.dirs) za pronalaženje klasa. Znači, alternativa navođenja jar fajla u CLASSPATH varijabli je njeno smeštanje u navedeni folder.
Extension class loader je definisan u klasi sun.misc.Launcher$ExtClassLoader. On se
retko koristi.
System class loader je zadužen za učitavanje klasa i jar arhiva koje su navedene u CLASSPATH varijabli
(properti: java.class.path). On se takođe koristi za lociranje ulazne tačke u klasi (metod main()),
ali i za učitavanje svih ostalih klasa koje nisu obuhvaćene prethodnim class loaderima.
System class loader je definisan u klasi sun.misc.Launcher$AppClassLoader.
Sledi opis organizacije Javinih class loadera i način rada.
Jasno je da JVM mora nekako da 'zna' kada da koristi neki od navedenih class loadera. Zato su class loaderi organizovani hijerarhijski, pri čemu se koristi model delegiranja (delegate) zahteva kako bi se pronašao pravi class loader.
Dakle, kada JVM dobije zahtev za instaciranjem neke klase, obratiće se prvo System class loaderu. Međutim, System class loader ne pristupa odmah traženju klase. Prvo se primenjuje metod delegiranja: zahtev se prosleđuje sledećem class loaderu u hijerarhiji. Tako zahtev stiže sve do Bootstrap class loadera gde se zaista prvi put i obrađuje, pošto je on poslednji u hijerarhiji.
Ako je klasa pronađena, ona će biti vraćena istim putem nazad. U suportnom, Bootstrap class loader vraća informacija da klasa nije nađena, što je znak Extension class loaderu da on obradi zahtev. Ako i on ne pronađe klasu, zahtev se konačno vraća do System class loadera koji će pokušati da je nađe.
Ako je nakon obrade zahteva klasa pronađena, ona će biti učitana u memoriju JVM. U suprotnom, generiše se
ClassNotFoundException.
Procedura učitavanja nađene klase se može podeliti na sledeće celine:
java.lang.Class na osnovu učitanog bajtkoda<clinit> koga izvršava JVM.
Ova procedura je okvirna i različite implementacije JVM je mogu nadograđivati i menjati. Na primer, klase se mogu keširati radi bržeg učitavanja, verifikacija za jednu klasu se može raditi samo prvi put kada se čita itd.
Java v1.4 donosi i jednu zanimljivu funkcionalnost, a to je mogućnost zamene nekih sistemskih biblioteka novijim (Endorsed Standard Override Mechanism). Pošto Bootstrap class loader nalazi sistemske biblioteke, novije verzije istih koje su navedene u CLASSPATH-u biće ignorisane, jer se već učitane. Pomenuta funkcionalnost omogućava da se ipak koriste novje verzije nekih sistemskih klasa, tako što će biti smeštene u %JAVA_HOME%/lib/endorsed folder.
Ova funkcionalnost zamene je moguća samo za JAXP i CORBA sistemske biblioteke.
Opisani Javini class loaderi koriste i sledeće važne mehanizme:
Učitavanje klasa po zahtevu (lazy loading): nikada se ne učitavaju sve klase koje su pronađene tokom pretrage. Umesto toga, klase se učitavaju po zahtevu, u toku rada programa. Ovaj mehanizam daje bolje performanse, veću efikasnost i bolje korišćenje memorije. Naročito je zanimljiva fleksibilnost ovog mehanizma, pošto je ovako moguće koristiti klase i arhive koje su dodate na putanje za pretragu (na pr. CLASSPATH) nakon inicijalizacije sistema (programa).
Keširanje klasa: kada Sunovi class loaderi učitaju neku klasu, oni je drže u memoriji još neko vreme.
Ipak, garbage collector može da 'pokupi' i takve Class objekte koji se više ne koriste.
Zato postoji JVM opcija za garbage collector koja isključuje uklanjanje Class objekata:
-Xnoclassgc.
Različiti namespace: svaki class loader ima svoj namespace! Dve klase koje su u istom paketu,
ali koje su učitane različitim class loaderima se smatraju da su u različitim paketima: neće imati pristupa
međusobnim default access metodama i promenljivima. Ovo je važan mehanizam i biće detaljno objašnjen kasnije.
Implicitno učitavanje klasa: ako se zahteva klasa koja nasleđuje neku drugu klasu, obe će biti učitane. Isto važi i za bilo koje dve klase gde se jedna referiše na drugu preko varijabli, metoda, nasleđivanja/implementacije ili kastovanjem.
Java omogućava jednostavno kreiranje custom class loadera. Za njihovo kreiranje je predviđena apstraktna klasa
java.lang.ClassLoader. Ona implementira svu neophodnu logiku koja transformiše niz pročitanih
bajtova u Class objekat, čime je njena upotreba značajno olakšana i pojednostavljena. Međutim,
ClassLoader ne implementira mehanizam za pronalaženje i učitavanje fajlova klasa.
Java dolazi sa sledećim implementacijama ClassLoader:
java.security.SecureClassLoader - relativno tanak wrapper oko ClassLoader klase
i dalje ne definiše mehanizam za lociranje fajlova. Predviđen je kao jedna od osnovnih klasa koju bi nasledili korisnički
custom class loaderi.
java.net.URLClassLoader - nasleđuje SecureClassLoader i predstavlja default
mehanizam kako Java traži klase i jar arhive na fajl sistemu ili na mreži. Ova klasa je isto predviđena
kao jedna od osnovnih klasa koju bi nasledili korisnički custom loaderi.
Javini Extension class loader i System class loader su izgrađeni nad ovom klasom, iako je direktno ne nasleđuju.
URLClassLoader je prilično fleksibilan i može da pročita klasu iz fajla na lokalnom sistemu,
HTTP lokacije i FTP lokacije, a zna da radi i sa ZIP/JAR arhivama.
Kako je ovo često korišćen class loader evo kratkih primera korišćenja:
URL[] urls = { new File("putanja").toURL() };
ClassLoader cl = new URLClassLoader(urls);
Object o = cl.loadClass("paket.Foo").newInstance();URL[] urls = { new File("biblioteka.jar").toURL() };
ClassLoader cl = new URLClassLoader(urls);
Object o = cl.loadClass("paket.Foo").newInstance();URL[] urls = { new URL("http", "www.javasvet.net", "/cfolder/") };
ClassLoader cl = new URLClassLoader(urls);
Object o = cl.loadClass("Foo").newInstance();URL[] urls = { new URL("ftp", "username:password@ftp.javasvet.net:", "/") };
ClassLoader cl = new URLClassLoader(urls);
Object o = cl.loadClass("Foo").newInstance();sun.applet.AppletClassLoader - extenduje URLClassLoader, a služi za učitavanje apleta.
java.rmi.server.RMIClassLoader - wrapper oko URLClassLoader koji služi za RMI.
Za standardnu upotrebu dovoljne su Javine implementacije class loadera. Kasnije će biti pokazano kako se one koriste, tj. kako se klase dinamički učitavaju. Ipak, custom class loaderi mogu da nađu interesantne primene. Evo nekih ideja:
Kao što je rečeno, custom class loader se pravi nasleđivanjem klase java.lang.ClassLoader.
Ova apstraktna klasa sadrži sve potrebne funkcionalnosti za rad sa bajt kodom i sa njegovim
pretvaranjem u java.lang.Class instancu tako da je razvijaoc custom class loadera oslobođen velikog posla.
Praktično, ono što treba uraditi je implementirati metod loadClass().
U primeru se nalazi jednostavna implementacija custom class loadera
(FooClassLoader). Njegov zadatak je da prvo potraži zahtevanu klasu na standardan način, konsultujući
Java class loadere (delegiranje). Ako oni ne nađu klasu, onda je FooClassLoader traži u specijalnom folderu,
gde, primera radi, fajlovi klasa imaju ekstenziju '.klasa' umesto standardnog '.class'.
Ako se pogleda implementacija FooClassLoader.loadClass() metode, uočava se sledeći postupak:
Class instanca, smešta se u bafer.
Važno je uočiti postojanje bafera za već nađene klase koje su prethodno bile učitane istom instancom class loadera.
Bafer postoji zbog toga što svaki put kada se zahteva jedna klasa class loader mora da vrati referencu na isti Class
objekat. U suprotnom, sistem bi svaku novu instancu posmatrao kao različitu klasu što bi dovodilo do Exception-a.
Dalje, postojanje bafera (keša) je zgodno i zbog performasi, kako sistem ne bi iznova tražio i proveravao klase koje su već nađene.
Sledeći važan momenat je delegiranje zahteva Javinim class loaderima, prema ranije prikazanoj hijerarhiji. Ovo je jako bitno za sigurnost i stabilnost sistema, kako custom class loader ne bi vratio neke svoje (izmenjene) instance postojećih i/ili sistemskih klasa. Dalje, uočava se da delegiranje nije neophodno i da ne postoji ništa što sprečava custom class loader da ne pozove Javine class loadere. Ovo su važna pitanja, pa će kasnije biti objašnjena sa više detalja.
U ovom primeru treba učitati klasu Ext1 koja se ne nalazi na CLASSPATH-u,
već u pomenutom posebnom folderu, sa promenjenom ekstenzijom. Kod koji koristi FooClassLoader je jednostavan:
FooClassLoader fcl = new FooClassLoader();
Class clazz = fcl.loadClass("Ext1");
Ext ext1 = (Ext) clazz.newInstance();Kako se klasa Ext1 ne nalazi na CLASSPATH-u, FooClassLoader će potražiti
fajl 'Ext1.klasa' u posebnom folderu i odatle učitati klasu. Poslednji red ovog isečka je zanimljiv jer
se kreirana instanca objekta Ext1 kastuje u Ext. Iako deluje ispravno,
nije moguće pisati sledeće:
Ext1 ext1 = (Ext1) clazz.newInstance();Ovako napisan red prouzrokuje java.lang.NoClassDefFoundError.
Razlog je očigledan: klasa Ext1 nije dostupna Javinim class loaderima
koji se koriste za pokretanje i rad programa primera, tako da oni jednostavno ne vide tu klasu.
Odatle se zaključuje da klasa koja se dinamički učitava mora da bude nekog tipa koji je dostupan
na mestu korišćenja, da bi mogla da se kastuje iz Object. Zato u primeru klasa
Ext1 implementira interfejs Ext koji je 'vidljiv' i na mestu korišćenja,
pa kastovanje prolazi bez problema.
Rezultat rada primera je:
+ ext2 Ext2.foo + ext1 > load class: Ext1 > custom class > find class: Ext1 > load class: java.lang.Object > system class > load class: Ext > system class > class loaded ok > load class: java.lang.System > system class > load class: java.io.PrintStream > system class Ext1.foo
Odavde se vidi da se za samo jedan upućen zahtev za instanciranjem FooClassLoader
interno poziva čak 5 puta! Ipak nema mesta zabuni: rečeno je već da class loader implicitno
učitava i sve druge neophodne klase, što ovde praktično znači učitavanje svih klasa koje se koriste u
Ext1: Object, Ext, System i PrintStream.
Zahtevi za učitavanjem ovih klasa se upućuju ponovo FooClassLoader koji ih delegira
Javinim class loaderima. Učitavanje je uspešno jer se sve zahtevane klase nalaze na CLASSPATH-u.
Na prvi pogled se čini da je dinamički kreirana klasa Ext1 iz prethodnog primera
istovetna kao i kada bi bila kreirana na standardni način, kao i svaka klasa koje je na
CLASSPATH-u (na pr. Ext2). Ipak, razlika postoji: klase se nalaze u različitim
namespace-ovima! Svaka klasa 'pamti' referencu na class loader koji ju je učitao
i predstavlja deo njegovog namespace-a.
Da bi se ovo proverilo, prethodni primer je izmenjen
da bi se foo() metod deklarisao sa default access. Pošto su Ext1
i Ext2 u istom paketu kao i glavni program, čini se da će biti moguće
pozvati njihove foo() metode. Rezultat rada programa je:
+ ext2
Ext2.foo
+ ext1
> load class: Ext1
> custom class
> find class: Ext1
> load class: Ext
> system class
> class loaded ok
> load class: java.lang.Object
> system class
Exception in thread "main" java.lang.AbstractMethodError: Ext.foo()V
at RunMe.main(RunMe.java:16)
Program upravo puca u liniji:
ext1.foo();Glavni program je učitan samo Javinim class loaderima. Klasu Ext1 učitava
FooClassLoader, čime se ona nalazi u drugom namespacu. Iako je u pitanju isti paket,
namespace je različit, pa glavni program praktično 'ne vidi' Ext1.foo() metodu.
Umesto toga, poziva se metod Ext.foo(), što, jasno, dovodi do greške - metod je
apstraktan.
Ova greška se ispravlja ako se Ext.foo() i njene implementacije deklarišu kao public.
Rezultat rada glavnog programa je tada istovetan prethodnom primeru.
Ovo je važna osobina class loadera. Znači, iako u istom paketu, klase Ext1 i Ext2 iz primera
se nalaze u različitm namespace-ovima jer su učitane različitim class loaderima.
Za dinamičko učitavanje klasa nije neophodno praviti novi custom class loadere. Ukoliko nema potrebe za nekim specijalnim vidom učitavanja, dovoljno je koristiti neki od Javinih ponuđenih mehanizama za dinamičko učitavanjem klasa. Generalno, u Javi postoje 2 načina za učitavanje klasa:
Class.forName(String)
Najosnovniji način učitavanja klasa. Ova metoda uvek koristi isti class loader koji je učitao i klasu koja radi učitavanje. Dalje, klasa se odmah i inicijalizuje: svi statički blokovi i promenljive.
ClassLoader.loadClass()
Ovaj način podrazumeva korišćenje neke konkretne instance class loadera. To može da bude neki custom class loader,
ali i neka od implementacija koja već postoji u Javi (java.security.SecureClassLoader, java.net.URLClassLoader, itd).
U ovom slučaju se inicijalizacija ne radi na mestu učitavanja, već se ona ostavlja za prvu upotrebu učitane klase.
Navedene razlike nemaju uticaja na obične Java programe. Međutim, u dinamičkim okruženjima, gde postoji razgranata hijerarhija
class loadera i gde neki ne moraju da delegiraju zahtev standardnim Java class loaderima (na pr.: aplikacioni serveri),
ova razlika dolazi do izražaja. Tada Class.forName(String) postaje neupotrebljiv, jer class loader
pozivajuće klase može biti izolovan i da ne vidi klasu koja je regularno na CLASSPATH-u. Ovo je bio jedan od razloga zašto
Java2 donosi proširenja koja se tiču dinamičkog učitavanja klasa. Sledi njihov opis.
Class.forName(String, boolean, ClassLoader) je nova verzija forName metode.
Za razliku od prethodne, korisnik ovde može da utiče na to koji se class loader koristi za učitavanje
klasa. Drugi parametar boolean tipa određuje da li se kreirana instanca inicijalizuje odmah prilikom
učitavanja ili kasnije, pri prvoj njenoj upotrebi. Između stare i nove verzije postoji sledeća relacija:
Class.forName("Foo") == Class.forName("Foo", true, this.getClass().getClassLoader())Drugo proširenje je Thread.currentThread().getContextClassLoader(). Ova metoda vraća class loader
threada. Po defaultu, class loader novo kreiranog threada je class loader roditeljskog
(parent) threada, koji ga je kreirao. Ova vrednost se može promeniti sa setContextClassLoader().
Na ovaj način je moguće dobiti referencu na isti class loader bez obzira na trenutni nivo u hijerarhiji class loadera.
Kombinacijom ova dve funkcionalnosti, na mnogim mestima je sledeći način preporučen za dinamičko učitavanje klasa:
Class.forName("Foo", true, Thread.currentThread().getContextClassLoader());gde se sa drugim parametrom može upravljati inicijalizacijom učitane klase. Često se ovaj preporučeni način proširuje tako što se prvo koristi class loader trenutne klase, pa tek onda onaj definisan u threadu.
Treći primer je nastao proširenjem prethodnog. Sada se u klasu Ext1, koja se i
sama dinamički učitava, uvodi učitavanje klase Misc. Dinamičko učitavanje klase Misc se radi dva puta:
prvi put koristeći Class.forName(), a drugi put getContextClassLoader().
Glavni program, dakle, prvo dinamički učitava klasu Ext1 koristeći FooClassLoader.
Kada se napravi instanca klase Ext1, poziva se njen metod u okviru kojeg se dinamički učitava Misc
klasa na dva različita načina. Na ovaj način se simulira neko dinamičko okruženje koji može imati razgranatu strukturu
class loadera. Učitana klasa se ne instancira, da Ext1 ne bio implicitno zavisan od
Misc. Rezultat rada programa je:
+ ext2 Ext2.foo Misc.foo Misc.foo + ext1 ... (sve isto kao i ranije) ... Ext1.foo #1 > load class: java.lang.Class > system class > load class: Misc > system class #2 > load class: java.lang.Thread > system class > load class: java.lang.ClassLoader > system class
Ovaj primer ilustruje razliku između dva gore pomenuta načina učitavanja. Za prvi način je korišćen stari
oblik Class.forName(). Kako on koristi class loader klase koja radi učitavanje,
ovde se, dakle, opet koristi FooClassLoader, pošto je njime učitana klasa Ext1.
Zato na konzoli postoji ispis "load class: Misc".
Misc klasa se u ovom primeru nalazi na CLASSPATH-u, pa FooClassLoader
može da je učita, jer delegira zahtev Javinim class loaderima. Međutim, da je FooClassLoader bio implementiran
drugačije i da nije delegirao zahtev, Ext1 ne bi mogao da učita klasu Misc iako je na CLASSPATH-u!
Sa drugim načinom (#2) učitavanja ovde nema takvih problema: getContextClassLoader() vraća
referencu na class loader glavne aplikacija (glavnog threada), tako da FooClassLoader uopšte
ne učestvuje u drugom učitavanju klase Misc. Zato i nema nikakvog ispisa na konzoli koji bi
označavao aktivnost instance FooClassLoader.
Metod Ext1.foo() sadrži komentarisan poziv Ext.look(), metode koja se nalazi u osnovnoj klasi.
Šta se dešava kada se ova linija odkomentariše?
Ext1.foo
Exception in thread "main" java.lang.IllegalAccessError: tried to access method Ext.look()V from class Ext1
at Ext1.foo(Ext1.java:7)
at RunMe.main(RunMe.java:16)
Stvar je jasna: zbog različitih namespace-ova, default access metod Ext.look() se ne vidi u Ext1.foo().
Ovo proširenje koristi izolovan custom class loader. Za svrhu ovog primera,
FooClassLoader je izmenjen tako da delegira samo zahteve za učitavanjem osnovnih Java klasa. Zbog toga je
bilo potrebno imati i fajl 'Ext.klasa' u posebnom folderu. Rezultat rada programa je:
+ ext2
Ext2.foo
+ ext1
> load class: Ext1
> find class: Ext1
> load class: Ext
> find class: Ext
> load class: java.lang.Object
> system class
> class loaded ok
> class loaded ok
Exception in thread "main" java.lang.ClassCastException
at RunMe.main(RunMe.java:15)
Program puca u liniji:
Ext ext1 = (Ext) clazz.newInstance();Iako na prvi pogled deluje neobično, jer je tip kastovanja dobar, nema mesta zabuni.
Kastovanje se radi u glavnom programu, pa se kastovanje vrši u klasu Ext koja je na CLASSPATH-u, dok
clazz.newInstance() vraća instancu klase Ext koju je učitao FooClassLoader
iz posebnog foldera. Ovde se, dakle, razlikuju dve Ext klase: jedna koje je učitana Javinim class loaderima
i koja se nalazi na CLASSPATH-u, i druga koju je učitao FooClassLoader iz posebnog foldera koji nije
na CLASSPATH-u. Kako su zato u pitanju različite klase, kastovanje ne može da uspe.
Class loaderi se nalaze u samom centru Javinog sigurnosnog modela. Njihova arhitektura i organizacija rešava potencijalne sigurnosne probleme koristeći sledeće strategije:
Custom class loader korišćen u prethodnim primerima pre nego što pristupi svom svojstvenom načinu pronalaženja klasa prvo delegira zahtev Javinim class loaderima, prema opisanoj hijerarhiji. Očigledno je da to nije obavezno i da je moguće napisati custom class loader koji se uopšte ne obraća Javinim class loaderima. Ovo je potencijalni sigurnosni propust, jer je onda moguće napisati custom class loader koji vraća neke svoje instance sistemskih Java klasa umesto pravih. Lako je zamisliti malicioznu upotrebu ovog potencijalnog propusta, gde se umesto neke često korišćene Java klase učitava njena izmenjena i destruktivna verzija.
Međtutim, kako će to već biti objašnjeno u sledećoj tački, ovo ipak nije sigurnosni problem. Čak šta više, mogućnost da se zanemari hijerarhija je jako važna mogućnost, koja se često koristi. Aplikacioni serveri, na primer, često koriste odvojene class loadere za klase servera i klase korisničkih aplikacija.
U Javi postoji mehanizam koji jednostavno zabranjuje svako custom kreiranje klasa iz java.*
paketa ili podpaketa. Znači, sve sistemske klase su zaštićene i bootstrap class loader je uvek taj
koji ih pronalazi i učitava. Odatle se zaključuje da custom class loader mora da bar delegira
svoj zahtev bootstrap class loaderu! Zato model delegiranja ne sadrži sigurnosni problem.
Kao što je to već pokazano, svaki class loader ima svoj namespace. Ovo je važan sigurnosni zahtev, jer sprečava da custom class loadere povredi sigurnost drugog class loadera, uključujući i Javine.
Security Manager je Javim mehanizam kojim se može zabraniti veliki broj funkcionalnosti. Tako je, na primer, moguće zabraniti otvaranje socketa ka nekoj adresi, ili čitanja fajlova sa neke lokacije. Upotrebom Security Manager je moguće sprečiti učitavanje bilo kojeg class loadera, dobavljanja reference na bilo koji postojeći class loader i promena konteksnog class loadera bilo kojeg threada.
Rad sa Security Managerom se sastoji iz dva koraka: konfigursanje tkzv. policy fajlova i samog njegovog uključenja u aplikaciju.
Sledi lista primera za download.
primer #1