Svi smo čuli i mnogi od nas koriste Hibernate, Spring, Groovy, AspectJ.
Za Groovy znamo da je dinamički jezik i da radi na Javinoj virtualnoj mašini,
ali ćesto nemamo predstavu kako je sve to implementirano.
Kada koristimo Hibernate, on kao da magično presretne pozive metoda naših
entiteta i po potrebi dovlači podatke iz baze.
Spring i AspectJ nude aspekte koji menjaju bajtkod naših klasa dodavajući im
nove funkcionalnosti.
Kako svi oni uspevaju u tome?
Odgovor je - svi oni vrše manipulaciju bajtkodom.
Ovaj članak prikazuje šta Javina virtuelna mašina nudi po pitanju dinamičkog
generisanja bajtkoda i predstavlja uvod u korišćenje biblioteki za manipulaciju
bajtkodom.
Kao prvo, bajtkod predstavlja binarni niz instrukcija Javine virtualne mašine.
Ako pogledamo Javinu klasu ClassLoader, videćemo da ona ima metod defineClass
koji prihvata bajtkod u obliku niza bajtova i vraća instancu klase Class.
Ukoliko ovaj niz bajtova potiče iz fajla, onda se dešava ono što već znamo, a to
je učitavanje klase iz .class fajla. Međutim, ništa nas ne sprečava da taj niz
bajtova dinamički generišemo i time proizvedemo novu klasu potpuno dinamički.
Dakle, Java ima ugrađenu mogućnost za ovo na najnižem nivou, i sve što je
potrebno je da generišemo niz bajtova.
Pošto bi ovo bilo vrlo mukotrpno, čak i da dobro poznajemo skup instrukcija
virtualne mašine, možemo koristiti neku od biblioteka.
Pre opisa ovih biblioteka, treba primetiti jednu bitnu stvar - Java nam
omogućuje da generišemo proizvoljne klase na osnovu bajtkoda. Šta je sa
modifikacijom već učitanih klasa? Odgovor je - osim u slučaju kad modifikacija
menja samo kod unutar neke metode (tj. ne dodaje nove metode, polja, ili ne
menja potpise postojećih medoda, ili natklase postojećih klasa) nije moguće
promeniti već učitanu klasu. Jednom kad se klasa učita nije je moguće ukloniti
iz memorije, niti modifikivati, osim u prethodno opisanom slučaju modifikacija
unutar metoda, koji je podržan u novijim verzijama Javine virtualne mašine da bi
se korisnicima olakšalo debagovanje koda (i realizuje se korišćenjem Javinog
debug API-ja).
Za sve ove biblioteke je zajedničko da nam olakšavaju proces generisanja klase,
tako što ne moramo ručno da pravimo niz bajtova koji predstavlja bajtkod. Sve
one omogućuju definisanje imena klase, potpisa njenih metoda, međutim kad dođemo
do implementacije samog tela metoda, mnoge od ovih biblioteka zahtevaju unos
pojedinačnih instrukcija Javine virtualne mašine.
Evo primera biblioteke Apache BCEL (koju inače koristi AspectJ za implementaciju
aspekata)
... InstructionList il = new InstructionList(); il.append(new PUSH(cp, "Hello, world")); ...
Ovde vidimo emitovanje instrukcije PUSH. Ovakav način rada, zasigurno nije
jednostavan, i zahteva dobro poznavanje instrukcija Javine virtualne mašine.
(Mada AspectJ očigledno ne unosi ručno instrukciju po instrukciju, već kopira
odgovarajuće instrukcije iz aspekta u odgovarajuće klase)
Rešenje nam pruža biblioteka JBoss Javassist. Ona nam, osim što nam omogućuje
rad direktno sa bajtkodom, pruža i mogućnost rada bez potrebe za poznavanjem i
korišćenjem bajtkoda direktno.
Pogledajmo primere korišćenja ove sjajne biblioteke. Sve što je potrebno je
dodati javassist.jar u classpath.
import javassist.*;
public class Main {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass myClassMaker = pool.makeClass("my.pkg.MyClass");
Class myClass = pointClassMaker.toClass();
Object myObject = pointClass.newInstance();
System.out.println(myObject.getClass());
}
}
Preko instance Javassist-ove klase ClassPool imamo pristup svim mogućnostima ove
biblioteke.
Prvo što ćemo uraditi je da napravimo novu, praznu klasu, pozivom makeClass. U
tom trenutku možemo koristiti myClassMaker.toBytecode() da dobijemo niz bajtova
koji predstavljaju bajtkod i zatim odatle proizvesti objekat Class, međutim
Javassist to radi za nas, metodom toClass().
Od trenutka kad dobijemo instancu Javine klase Class, koristimo standardni Javin
način za instanciranje klase.
Dakle dinamički smo generisali i instancirali našu prvu klasu, uz pomoć nekoliko
linija koda! Za sada je ona potpuno prazna i samo ispisujemo ime klase naše
instance, da bi smo se uverili da je u pitanju klasa my.pkg.MyClass.
Sada ćemo dodati novi metod našoj klasi. Videli smo da druge biblioteke
zahtevaju da se telo metoda predstavi kao niz instrukcija virtualne mašine.
Javassist nam omogućuje da telo metode predstavimo kao tekst, i sam ga prevodi u
bajtkod!
String methodBody = "public void print() { System.out.println(\"Hello Javassist!\"); }";
myClassMaker.addMethod(CtNewMethod.make(methodBody, myClassMaker));
...
myClass.getDeclaredMethod("print").invoke(myObject);
Da bismo pristupili novokreiranom metodu u našem primeru, moramo koristiti
refleksiju i uveravamo se da klasa zaista sadrži metod print. Kad bi nova klasa
nasledila postojeću klasu (što možemo postići sa
myClassMaker.setSuperclass(pool.get("pkg.PostojecaKlasa")); ), i redefinisali
postojeći metod onda bismo lako mogli isprobati klasu u akciji:
CtClass stackClassMaker = pool.makeClass("my.pkg.MyStack");
String methodBody2 = "public Object push(Object o) { " +
" System.out.println(\"Hello Stack push: \" + o);" +
" return super.push(o);}";
stackClassMaker.setSuperclass(pool.get("java.util.Stack"));
stackClassMaker.addMethod(CtNewMethod.make(methodBody2, stackClassMaker));
Class myStackClass = stackClassMaker.toClass();
Stack stack = (Stack) myStackClass.newInstance();
stack.push(123);
Ovde definišemo novu klasu my.pkg.MyStack koja nasleđuje Javinu klasu Stack, i redefiniše metod add. Posle poziva metoda add trebali bismo da vidimo odgovarajući ispis u konzoli. Ovaj primer je interesantan jer vrlo podseća na ono što radi Hibernate - od postojeće klase entiteta pravi novu klasu koja je nasleđuje, redefiniše sve metode koje želi da presretne i u telu redefinisanih metoda vrši željene aktivnosti i zatim po potrebi poziva originalnu metodu. Korisnici i dalje rade sa osnovnom klasom (kao u ovom primeru sa klasom Stack) i ne moraju da znaju da rade zapravo sa klasom koja je nasleđuje (slično kao kad rade sa interfesom i ne moraju da znaju koja ga klasa implementira). Hibernate za implementaciju ovoga ne koristi Javassist, već biblioteku CGLIB, koja nema širok spektar mogućnosti kao Javassist, ali ima posebnu podršku za prethodni slučaj, i omogućava implementaciju tela metoda bez korišćenja stringa, već direktno u Javi.
Kao što smo već napomenuli Java ne omogućava dinamičku promenu bajtkoda već
učitanih klasa. Međutim, način da se to prevaziđe je omogućavanje presretanja
učitavanja klase i vršenja željenih izmena u tom trenutku. Java pruža mogućnost
registracije koda, koji implementira jednostavni interfejs ClassFileTransformer,
i koji se uključuje u proces učitavanja klasa vršeći željene izmene na bajtkodu.
Biblioteke koje žele da podrže manipulaciju bajtkodom postojećih klasa bez
pravljenja novih klasa, koriste ovu mogućnost. Da bi se ovo aktiviralo potrebno
je program startovati sa dodatnim argumentom -javaagent koji ukazuje na jar fajl
u kome se nalazi klasa koja implementira ClassFileTransformer.
Na primer, AspectJ koristi ovu mogućnost da bi podržao dodavanje aspekata
postojećim klasama u vreme njihovog učitavanja.
Posle nekoliko primera korišćenja Javassista mogli ste steći osećaj kakve sve
mogućnosti pruža ova biblioteka. Javassist može napraviti sve što zamislimo, od
dodavanja metoda, polja, natklasi, interfejsa koje klasa implementira, promene
imena ili potpisa postojećih metoda itd.
Samo treba zapamtiti da kad menjamo postojeće klase, dobijamo novi objekat
Class, zato što Java ne omogućava promenu već učitanih klasa.
Da li ćete koristiti manipulaciju bajtkodom u vašem sledećem prоjektu? Verovatno
ne. Cilj ovog članka je pre svega da objasni šta pruža Javina virtualna mašina
po pitanju dinamičkog generisanja koda i pokaže kako biblioteke i frejmvorci
koje koristimo svaki dan implementiraju svoje naizgled magične funkcije.