Obsah
1. Pohled pod kapotu JVM – vytváření i dekompilace metod s využitím nástroje Javassist
2. Vytvoření bajtkódu metody přímo ze zdrojového kódu
3. Konstrukce bajtkódu metody se specifikací návratové hodnoty, parametrů a těla metody
4. Konstrukce bajtkódu metody přímým zápisem instrukcí JVM
5. Zdrojový kód demonstračního příkladu ClassGenerationTest4
6. Výsledná podoba bajtkódu třídy vygenerované příkladem ClassGenerationTest4
7. Zpětné získání informací o libovolné metodě vybrané třídy
8. Přečtení instrukcí tvořících tělo vybrané metody a dekódování instrukcí do čitelné podoby
9. Zdrojový kód demonstračního příkladu ClassGenerationTest5
10. Ukázka výpisu bajtkódu metod vytvořených demonstračním příkladem ClassGenerationTest5
11. Repositář se zdrojovými kódy obou demonstračních příkladů
1. Pohled pod kapotu JVM – vytváření i dekompilace metod s využitím nástroje Javassist
V dnešní části seriálu o programovacím jazyku Java i o virtuálním stroji Javy navážeme na část předchozí, v níž jsme si ukázali způsob vytváření nových atributů a přidávání těchto atributů k vytvářené (generované) třídě, přesněji řečeno k jejímu bajtkódu. Práce s atributy je v podstatě velmi jednoduchá, protože u každého atributu musíme zadat jen jeho jméno, datový typ, modifikátory (STATIC, PUBLIC, VOLATILE atd.) a popř. i počáteční hodnotu, kterou je atribut naplněn v rámci inicializace třídy ve statickém bloku (platí pro statické atributy) či při konstrukci instance třídy (nestatické atributy). Dnes se budeme zabývat složitější problematikou, konkrétně přidáním nových metod do generovaných tříd. U každé metody je nutné znát a tím pádem i zadat mnohem větší množství údajů, než tomu bylo v případě atributů. Kromě jména metody a modifikátorů je nutné zadat i její návratový typ, typy parametrů metody a samozřejmě i její tělo.
Kromě tří základních způsobů vytváření nových metod se seznámíme i s opačnou problematikou. Jedná se o získání všech důležitých informací o již existující metodě vybrané třídy, a to nezávisle na tom, zda byla tato metoda přeložena klasickým překladačem Javy, tj. příkazem javac, nebo zda byla vygenerována některým dalším nástrojem, samozřejmě včetně Javassist. Vzhledem k tomu, že se při překladu ztratí původní zdrojový kód (resp. mohou na něj být pouze uvedeny odkazy ve formě jména souboru a čísla řádku), je při dekompilaci metody získána sekvence instrukcí tvořících její tělo. Tyto instrukce lze vypsat různým způsobem, buď jen jako seznam operačních kódů těchto instrukcí nebo ve formě jmen instrukcí. I pro tento případ existují v nástroji Javassist příslušné třídy a metody, což znamená, že vytvoření prohlížeče bajtkódu je skutečně poměrně jednoduché.
2. Vytvoření bajtkódu metody přímo ze zdrojového kódu
Jak jsme si již řekli v předchozích částech tohoto seriálu, je nástroj Javassist navržen takovým způsobem, že obsahuje API sloužící pro tvorbu tříd/metod/atributů na vyšší úrovni i na úrovni nižší. Pojmem „vyšší úroveň“ je zde myšlena práce se zdrojovým kódem představujícím například tělo metody, zatímco na nižší úrovni je možné manipulovat přímo s jednotlivými instrukcemi bajtkódu, či dokonce s jednotlivými bajty (což je někdy taktéž zapotřebí). S generováním metody s využitím vysokoúrovňového API nástroje Javassist jsme se již seznámili, protože jsme v předchozích třech demonstračních příkladech vytvářeli metodu main() takovým způsobem, že se ve formě řetězce zadala jak hlavička metody, tak i její tělo, a to ve stejném formátu, jakoby se jednalo o standardní zdrojový kód odpovídající specifikaci jazyka Java:
/** * Zdrojovy kod metody main(), ktery bude nasledne zkompilovan * do bajtkodu a zakomponovan do vytvorene tridy. */ private static final String MAIN_METHOD_SOURCE_TEXT = "public static void main(String[] args)" + "{" + " System.out.println(add(1,2));" + " System.out.println(foo());" + " System.out.println(bar());" + "}"; /** * Vytvoreni metody main() z jejiho zdrojoveho kodu. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void addMethodMain(CtClass generatedClass) throws CannotCompileException { CtMethod methodMain = CtMethod.make(MAIN_METHOD_SOURCE_TEXT, generatedClass); generatedClass.addMethod(methodMain); }
Stejným způsobem lze samozřejmě vytvořit i metodu s jinými parametry či s jiným návratovým typem než void. Uveďme si jednoduchý příklad metody, která sečte hodnoty svých dvou celočíselných parametrů a následně vrátí výsledek tohoto součtu:
/** * Vytvoreni metody public static int add(int x, int y). * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void addMethodAdd(CtClass generatedClass) throws CannotCompileException { CtMethod addMethod = CtMethod.make( "public static int add(int x, int y)" + "{" + " return x+y;" + "}", generatedClass); generatedClass.addMethod(addMethod); }
3. Konstrukce bajtkódu metody se specifikací návratové hodnoty, parametrů a těla metody
Mohlo by se zdát, že nástroj Javassist je vlastně pouze rozhraním ke standardnímu překladači Javy (javac), ve skutečnosti však tomu tak není, protože nové metody je možné vytvářet i dalšími způsoby, které již programátorům umožňují s bajtkódy výsledných metod lépe manipulovat. Kromě CtMethod.make() je možné kostru bajtkódu nové metody vytvořit i přímým zavoláním konstruktoru new CtMethod(). Tomuto konstruktoru je nutné předat čtyři základní parametry popisující metodu – její návratový typ, jméno metody, typy (ne jména!) všech parametrů metody a konečně třídu, v níž má být metoda deklarována. Následně je možné změnit modifikátory metody s využitím CtMethod.setModifiers(), což je obdoba minule popsaného příkazu CtField.setModifiers(). Tělo metody se v tomto případě zadává přes CtMethod.setBody(). Ukažme si jednoduchý příklad, v němž je tělo metody představováno jediným příkazem. V tomto případě se nemusí okolo příkazu zapisovat složené závorky:
/** * Vytvoreni bezparametricke staticke metody foo() vracejici int. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void constructMethodFoo(CtClass generatedClass) throws CannotCompileException { CtClass returnType = CtClass.intType; CtClass[] parameterTypes = {}; // u metody je nutne znat jeji jmeno, navratovou hodnotu i typy parametru CtMethod fooMethod = new CtMethod(returnType, "foo", parameterTypes, generatedClass); // zmena modifikatoru fooMethod.setModifiers(Modifier.STATIC | Modifier.PUBLIC); // telo metody muze byt slozeno z jednoho vyrazu fooMethod.setBody("return 42;"); generatedClass.addMethod(fooMethod); }
Pokud je tělo metody složeno z více příkazů, je vhodné okolo těchto příkazů zapsat složené závorky a vytvořit tak příkazový blok. To je ukázáno na další metodě (nyní neveřejné) nazvané bar. Tato metoda obsahuje dvě lokální proměnné a vrací výsledek výpočtu s využitím těchto proměnných. Zde je nutné upozornit na to, že se při generování bajtkódu metody nebudou provádět žádné optimalizace, což znamená, že uvedené výpočty budou převedeny do instrukcí bajtkódu, i když by to ve skutečnosti nemuselo být nutné:
/** * Vytvoreni bezparametricke staticke metody bar() vracejici float. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void constructMethodBar(CtClass generatedClass) throws CannotCompileException { CtClass returnType = CtClass.floatType; CtClass[] parameterTypes = {}; // u metody je nutne znat jeji jmeno, navratovou hodnotu i typy parametru CtMethod fooMethod = new CtMethod(returnType, "bar", parameterTypes, generatedClass); // zmena modifikatoru fooMethod.setModifiers(Modifier.STATIC | Modifier.PRIVATE); // telo metody muze byt slozeno z celeho bloku fooMethod.setBody( "{" + " float x = 1/8f;" + " float y = 336;" + " return x * y;" + "}" ); generatedClass.addMethod(fooMethod); }
4. Konstrukce bajtkódu metody přímým zápisem instrukcí JVM
Třetí způsob konstrukce metody již používá nízkoúrovňové API nástroje Javassist. Využívá se zde faktu, že instrukce tvořící tělo metody jsou uloženy v jednom z jejích atributů. Může se to sice zdát podivné, ale je tomu skutečně tak, ostatně některé metody nemají (a nemohou) mít žádné tělo. Instrukce tvořící tělo přeložené metody jsou uloženy v atributu nazvaném Code a v nástroji Javassist je možné hodnotu tohoto atributu nastavit s využitím příkazu MethodInfo.setCodeAttribute(), kterému se předá instance třídy CodeAttribute. Bližší informace o třídě MethodInfo budou uvedeny v dalších kapitolách, způsob vytváření bajtkódu metod bude podrobněji vysvětlen v navazující části tohoto seriálu. Zde si jen ukažme krátký úryvek kódu, který vytvoří tělo metody, které by se v Javě mohlo zapsat takto: return 42;:
MethodInfo minfo = method.getMethodInfo(); ConstPool constPool = minfo.getConstPool(); Bytecode b = new Bytecode(constPool, 2, 0); // stacksize == 2 // pridani instrukce pro ulozeni celociselne konstanty na zasobnik b.addIconst(42); // pridani instrukce pro ukonceni metody a vraceni hodnoty ulozene na zasobniku b.addReturn(CtClass.intType); // ziskani atributu metody s nazvem "code" z vytvoreneho bajtkodu CodeAttribute codeAttribute = b.toCodeAttribute(); // prirazeni atributu k metode minfo.setCodeAttribute(codeAttribute);
5. Zdrojový kód demonstračního příkladu ClassGenerationTest4
První dva způsoby konstrukce nových metod popsané v předchozích kapitolách jsou implementovány v dnešním prvním demonstračním příkladu nazvaném ClassGenerationTest4 (příklady jsou číslovány průběžně od předminulé části tohoto seriálu). Tento příklad po svém spuštění vytvoří kostru třídy nazvané „GeneratedClass4“ a následně se v této třídě vytvoří čtveřice statických metod. Samozřejmě se jedná o metodu main(), dále pak o metodu add() následovanou dvojicí bezparametrických metod nazvaných foo() a bar(). Následně je vygenerovaná třída „GeneratedClass4“ uložena do souboru „GeneratedClass4.class“ a je připravena pro svou inicializaci a spuštění v rámci virtuálního stroje Javy. Následuje výpis celého kódu výše popsaného demonstračního příkladu:
import java.io.IOException; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.Modifier; import javassist.NotFoundException; /** * Test moznosti nastroje Javassist - vygenerovani jednoduche tridy * s metodou main a nekolika dalsimi metodami. * * @author Pavel Tisnovsky */ public class ClassGenerationTest4 { /** * Jmeno vygenerovane tridy. */ private static final String GENERATED_CLASS_NAME = "GeneratedClass4"; /** * Zdrojovy kod metody main(), ktery bude nasledne zkompilovan * do bajtkodu a zakomponovan do vytvorene tridy. */ private static final String MAIN_METHOD_SOURCE_TEXT = "public static void main(String[] args)" + "{" + " System.out.println(add(1,2));" + " System.out.println(foo());" + " System.out.println(bar());" + "}"; /** * Vytvoreni metody main() z jejiho zdrojoveho kodu. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void addMethodMain(CtClass generatedClass) throws CannotCompileException { CtMethod methodMain = CtMethod.make(MAIN_METHOD_SOURCE_TEXT, generatedClass); generatedClass.addMethod(methodMain); } /** * Vytvoreni metody public static int add(int x, int y). * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void addMethodAdd(CtClass generatedClass) throws CannotCompileException { CtMethod addMethod = CtMethod.make( "public static int add(int x, int y)" + "{" + " return x+y;" + "}", generatedClass); generatedClass.addMethod(addMethod); } /** * Vytvoreni bezparametricke staticke metody foo() vracejici int. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void constructMethodFoo(CtClass generatedClass) throws CannotCompileException { CtClass returnType = CtClass.intType; CtClass[] parameterTypes = {}; // u metody je nutne znat jeji jmeno, navratovou hodnotu i typy parametru CtMethod fooMethod = new CtMethod(returnType, "foo", parameterTypes, generatedClass); // zmena modifikatoru fooMethod.setModifiers(Modifier.STATIC | Modifier.PUBLIC); // telo metody muze byt slozeno z jednoho vyrazu fooMethod.setBody("return 42;"); generatedClass.addMethod(fooMethod); } /** * Vytvoreni bezparametricke staticke metody bar() vracejici float. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void constructMethodBar(CtClass generatedClass) throws CannotCompileException { CtClass returnType = CtClass.floatType; CtClass[] parameterTypes = {}; // u metody je nutne znat jeji jmeno, navratovou hodnotu i typy parametru CtMethod fooMethod = new CtMethod(returnType, "bar", parameterTypes, generatedClass); // zmena modifikatoru fooMethod.setModifiers(Modifier.STATIC | Modifier.PRIVATE); // telo metody muze byt slozeno z celeho bloku fooMethod.setBody( "{" + " float x = 1/8f;" + " float y = 336;" + " return x * y;" + "}" ); generatedClass.addMethod(fooMethod); } /** * Vytvoreni tridy s metodou main(). * * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu metody main() * @throws IOException * pokud dojde k chybe pri zapisu bajtkodu na disk * @throws NotFoundException * pokud dojde k chybe pri zapisu bajtkodu na disk */ private static void generateClass() throws CannotCompileException, NotFoundException, IOException { // ziskat vychozi class pool ClassPool pool = ClassPool.getDefault(); // vytvoreni nove verejne tridy CtClass generatedClass = pool.makeClass(GENERATED_CLASS_NAME); // pridani metody add do teto tridy addMethodAdd(generatedClass); // konstrukce nove metody foo() constructMethodFoo(generatedClass); // konstrukce nove metody bar() constructMethodBar(generatedClass); // pridani metody do teto tridy addMethodMain(generatedClass); // ulozeni bajtkodu na disk generatedClass.writeFile(); } /** * Spusteni generatoru tridy. * * @param args nevyuzito */ public static void main(String[] args) { System.out.println("class generation begin: " + GENERATED_CLASS_NAME); try { generateClass(); } catch (CannotCompileException e) { e.printStackTrace(); } catch (NotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } System.out.println("class generation end: " + GENERATED_CLASS_NAME); } }
6. Výsledná podoba bajtkódu třídy vygenerované příkladem ClassGenerationTest4
Demonstrační příklad ClassGenerationTest4, jehož zdrojový text byl vypsán v předchozí kapitole, se přeloží následujícím skriptem, jenž předpokládá přítomnost archivu javassist.jar v aktuálním adresáři:
javac -cp javassist.jar ClassGenerationTest4.java
Spuštění přeloženého příkladu se provede příkazem:
java -cp .:javassist.jar ClassGenerationTest4
Po spuštění ClassGenerationTest4 by se v aktuálním adresáři měl vytvořit soubor nazvaný GeneratedClass4.class obsahující nově vytvořený bajtkód. Jeho obsah je možné si prohlédnout po zadání příkazu:
javap -c -private GeneratedClass4
Ve výpisu obsahu bajtkódu můžeme vidět těla všech čtyř explicitně vytvořených metod a současně i tělo automaticky vygenerovaného konstruktoru. Povšimněte si, že u metody private static float bar() skutečně nedošlo k žádné optimalizaci bajtkódu, protože ten zde obsahuje jak obě lokální proměnné, tak i výpočet nad nimi. Ve skutečnosti by bylo možné celé tělo této metody nahradit pouze dvěma instrukcemi: fstore (42)+freturn:
Compiled from "GeneratedClass4.java" public class GeneratedClass4 extends java.lang.Object{ public static int add(int, int); Code: 0: iload_0 1: iload_1 2: iadd 3: ireturn public static int foo(); Code: 0: bipush 42 2: ireturn private static float bar(); Code: 0: ldc #12; //float 0.125f 2: fstore_0 3: sipush 336 6: i2f 7: fstore_1 8: fload_0 9: fload_1 10: fmul 11: freturn public static void main(java.lang.String[]); Code: 0: getstatic #20; //Field java/lang/System.out:Ljava/io/PrintStream; 3: iconst_1 4: iconst_2 5: invokestatic #22; //Method add:(II)I 8: invokevirtual #28; //Method java/io/PrintStream.println:(I)V 11: getstatic #20; //Field java/lang/System.out:Ljava/io/PrintStream; 14: invokestatic #30; //Method foo:()I 17: invokevirtual #28; //Method java/io/PrintStream.println:(I)V 20: getstatic #20; //Field java/lang/System.out:Ljava/io/PrintStream; 23: invokestatic #32; //Method bar:()F 26: invokevirtual #35; //Method java/io/PrintStream.println:(F)V 29: return public GeneratedClass4(); Code: 0: aload_0 1: invokespecial #39; //Method java/lang/Object."<init>":()V 4: return }
7. Zpětné získání informací o libovolné metodě vybrané třídy
Nyní se budeme zabývat opačnou problematikou, než je vytváření nových metod. Mnohdy je totiž nástroj Javassist používán v případech, kdy je nutné získat bajtkód nějaké metody a popř. tento bajtkód i modifikovat (typickým příkladem je již zmíněné volání logovací funkce). My si dnes ukážeme, jak lze vypsat informace o libovolné metodě. Pro jednoduchost a současně i pro větší názornost se budou získávat informace o čtyřech metodách, které již dobře známe – main, add, foo a bar. První věcí, kterou je nutné udělat, je takzvaně „rozmrazit“ vygenerovanou třídu, aby bylo možné přistupovat k jejím složkám. Pokud by se rozmrazení neprovedlo příkazem CtClass.defrost(), došlo by při běhu demonstračního příkladu k běhové výjimce. Jakmile je třída rozmrazena, lze zavolat uživatelskou metodu printMethodStructures():
/** * Spusteni generatoru tridy. * * @param args nevyuzito */ public static void main(String[] args) { System.out.println("class generation begin: " + GENERATED_CLASS_NAME); try { CtClass generatedClass = generateClass(); // dulezite - generovana trida nesmi byt "zmrazena" generatedClass.defrost(); printMethodStructures(generatedClass); } catch (CannotCompileException e) { e.printStackTrace(); } catch (NotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (BadBytecode e) { e.printStackTrace(); } System.out.println("class generation end: " + GENERATED_CLASS_NAME); } }
Uživatelská metoda printMethodStructures() je ve skutečnosti velmi jednoduchá, protože pro všechny čtyři zkoumané metody zavolá printMethodStructure():
/** * Vypis struktury vybranych metod z generovane tridy. * * @param generatedClass * predstavuje vytvarenou tridu * @throws NotFoundException * vyhozena, pokud metoda nebyla nalezena * @throws BadBytecode */ private static void printMethodStructures(CtClass generatedClass) throws NotFoundException, BadBytecode { printMethodStructure(generatedClass, "main"); printMethodStructure(generatedClass, "add"); printMethodStructure(generatedClass, "foo"); printMethodStructure(generatedClass, "bar"); }
V printMethodStructure() se nejprve získá instance třídy CtMethod představující obraz zkoumané metody. Následně se přes CtMethod.getMethodInfo() získá instance třídy MethodInfo obsahující, jak již název této třídy napovídá, informace o zkoumané metodě. My využijeme následující čtveřici getterů, které jsou ve třídě MethodInfo deklarovány:
# | Metoda | Popis |
---|---|---|
1 | javassist.bytecode.MethodInfo.getName() | vrátí jméno metody |
2 | javassist.bytecode.MethodInfo.getDescriptor() | vrátí deskriptor metody (část její signatury) |
3 | javassist.bytecode.MethodInfo.getAccessFlags() | vrátí přístupová práva a další modifikátory metody (STATIC…) |
4 | javassist.bytecode.MethodInfo.getCodeAttribute() | vrátí atribut metody reprezentující její tělo |
V následujícím úryvku zdrojového kódu si povšimněte, jak lze velmi snadno převést modifikátory metody na řetězec s využitím Modifier.toString():
/** * Vypis struktury vybrane metody. * * @param generatedClass * predstavuje vytvarenou tridu * @param methodName * jmeno metody, jejiz struktura se ma vypsat * @throws NotFoundException * vyhozena, pokud metoda nebyla nalezena * @throws BadBytecode * vyhozena, pokud se nalezne neplatna instrukce v bytekodu */ private static void printMethodStructure(CtClass generatedClass, String methodName) throws NotFoundException, BadBytecode { System.out.println("Method '" + methodName + "' structure:"); CtMethod method = generatedClass.getDeclaredMethod(methodName); if (method == null) { System.out.println(" not found!"); return; } MethodInfo methodInfo = method.getMethodInfo(); System.out.println(" real name: " + methodInfo.getName()); System.out.println(" descriptor: " + methodInfo.getDescriptor()); System.out.println(" access flags: " + Modifier.toString(methodInfo.getAccessFlags())); System.out.println(" method body:"); printMethodBody(methodInfo); System.out.println(); }
8. Přečtení instrukcí tvořících tělo vybrané metody a dekódování instrukcí do čitelné podoby
Nyní se dostáváme k snad nejzajímavější části dnešního článku – ke zkoumání sekvence instrukcí tvořících tělo zkoumané metody. Jak jsme si již řekli ve čtvrté kapitole, je tato sekvence instrukcí uložená v atributu nazvaném Code. Hodnotu tohoto atributu získáme pomocí CodeAttribute ca = methodInfo.getCodeAttribute();. Jednotlivé bajty tvořící hodnotu atributu Code by však bylo velmi namáhavé zkoumat ručně, proto využijeme další třídy z nástroje Javassist. Tato třída se jmenuje CodeIterator a její instanci získáme jednoduše: CodeAttribute.iterator(). Z mnoha metod, které tato třída nabízí, nás bude prozatím zajímat tato osmice:
# | Metoda | Popis |
---|---|---|
1 | javassist.bytecode.CodeIterator.begin() | přesun na začátek bajtkódu |
2 | javassist.bytecode.CodeIterator.next() | přesun na další instrukci |
3 | javassist.bytecode.CodeIterator.hasNext() | oznamuje, zda existuje další instrukce |
4 | javassist.bytecode.CodeIterator.getCodeLength() | vrací celkovou délku kódu |
5 | javassist.bytecode.CodeIterator.byteAt() | vrací hodnotu bajtu na aktuální pozici |
6 | javassist.bytecode.CodeIterator.u16bitAt() | vrací hodnotu (bezznaménkového) 16bitového slova na aktuální pozici |
7 | javassist.bytecode.CodeIterator.s16bitAt() | vrací hodnotu 16bitového slova se znaménkem na aktuální pozici |
8 | javassist.bytecode.CodeIterator.s32bitAt() | vrací hodnotu 32bitového slova se znaménkem na aktuální pozici |
Asi nejzajímavější je fakt, že metoda javassist.bytecode.CodeIterator.next() posune iterátor na další instrukci a nikoli na další bajt. To je důležité, protože jednotlivé instrukce mohou mít různou délku, kterou by bylo nutné zkoumat a nějakým způsobem reflektovat. Ovšem vzhledem k tomu, že každá instrukce bajtkódu má operační znak dlouhý přesně jeden bajt (liší se počet parametrů), je možné celý průchod bajtkódem implementovat velmi jednoduše následující smyčkou, která vypíše pouze hodnotu bajtů tvořících operační kódy instrukcí (a nikoli parametry – tyto bajty jsou ignorovány):
CodeIterator iterator = ca.iterator(); while (iterator.hasNext()) { int index = iterator.next(); int opcode = iterator.byteAt(index); System.out.println(opcode); }
Ovšem číselné hodnoty kódů jednotlivých instrukcí jsou pro člověka nečitelné; bylo by lepší je převést na mnemotechnické zkratky instrukcí. I na to je v nástroji Javassist myšleno, protože ve třídě javassist.bytecode.Mnemonic se nachází pole řetězců s mnemotechnickými zkratkami všech instrukcí. Opět je zde využito faktu, že každá instrukce má délku operačního kódu přesně jeden bajt, takže stačí vytvořit pole s 256 řetězci, i když ve skutečnosti jsou některé operační kódy prozatím neobsazeny:
package javassist.bytecode; public interface Mnemonic { String[] OPCODE = { "nop", /* 0*/ "aconst_null", /* 1*/ "iconst_m1", /* 2*/ "iconst_0", /* 3*/ ... ... ... "ifnonnull", /* 199*/ "goto_w", /* 200*/ "jsr_w" /* 201*/ }; }
Podívejme se nyní na uživatelskou metodu printMethodBody(), která vlastně představuje ústřední část našeho jednoduchého „disassembleru“. V této metodě se prochází přes jednotlivé instrukce bajtkódu (metoda CodeIterator.next() může přeskočit o více než jeden bajt) a následně jsou vypsány mnemotechnické zkratky všech přečtených instrukcí:
/** * Vypis instrukci tvoricich telo vybrane metody. * * @throws NotFoundException * vyhozena, pokud metoda nebyla nalezena * @throws BadBytecode * vyhozena, pokud se nalezne neplatna instrukce v bytekodu */ private static void printMethodBody(MethodInfo methodInfo) throws BadBytecode { CodeAttribute ca = methodInfo.getCodeAttribute(); CodeIterator iterator = ca.iterator(); while (iterator.hasNext()) { int index = iterator.next(); int opcode = iterator.byteAt(index); System.out.println(" " + Mnemonic.OPCODE[opcode]); } }
9. Zdrojový kód demonstračního příkladu ClassGenerationTest5
Uživatelské metody popsané v sedmé i v osmé kapitole, jsou součástí dnešního druhého demonstračního příkladu pojmenovaného ClassGenerationTest5. V tomto příkladu se nejdříve vytvoří čtveřice metod zcela shodným způsobem, jako tomu bylo i v předchozím demonstračním příkladu ClassGenerationTest4 a následně jsou vypsány informace o všech těchto čtyřech metodách. Následuje výpis zdrojového kódu dnešního druhého demonstračního příkladu:
import java.io.IOException; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.Modifier; import javassist.NotFoundException; import javassist.bytecode.BadBytecode; import javassist.bytecode.CodeAttribute; import javassist.bytecode.CodeIterator; import javassist.bytecode.MethodInfo; import javassist.bytecode.Mnemonic; /** * Test moznosti nastroje Javassist - vygenerovani jednoduche tridy * s metodou main a nekolika dalsimi metodami. * * @author Pavel Tisnovsky */ public class ClassGenerationTest5 { /** * Jmeno vygenerovane tridy. */ private static final String GENERATED_CLASS_NAME = "GeneratedClass5"; /** * Zdrojovy kod metody main(), ktery bude nasledne zkompilovan * do bajtkodu a zakomponovan do vytvorene tridy. */ private static final String MAIN_METHOD_SOURCE_TEXT = "public static void main(String[] args)" + "{" + " System.out.println(add(1,2));" + " System.out.println(foo());" + " System.out.println(bar());" + "}"; /** * Vytvoreni metody main() z jejiho zdrojoveho kodu. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void addMethodMain(CtClass generatedClass) throws CannotCompileException { CtMethod methodMain = CtMethod.make(MAIN_METHOD_SOURCE_TEXT, generatedClass); generatedClass.addMethod(methodMain); } /** * Vytvoreni metody public static int add(int x, int y). * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void addMethodAdd(CtClass generatedClass) throws CannotCompileException { CtMethod addMethod = CtMethod.make( "public static int add(int x, int y)" + "{" + " return x+y;" + "}", generatedClass); generatedClass.addMethod(addMethod); } /** * Vytvoreni bezparametricke staticke metody foo() vracejici int. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void constructMethodFoo(CtClass generatedClass) throws CannotCompileException { CtClass returnType = CtClass.intType; CtClass[] parameterTypes = {}; // u metody je nutne znat jeji jmeno, navratovou hodnotu i typy parametru CtMethod fooMethod = new CtMethod(returnType, "foo", parameterTypes, generatedClass); // zmena modifikatoru fooMethod.setModifiers(Modifier.STATIC | Modifier.PUBLIC); // telo metody muze byt slozeno z jednoho vyrazu fooMethod.setBody("return 42;"); generatedClass.addMethod(fooMethod); } /** * Vytvoreni bezparametricke staticke metody bar() vracejici float. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void constructMethodBar(CtClass generatedClass) throws CannotCompileException { CtClass returnType = CtClass.floatType; CtClass[] parameterTypes = {}; // u metody je nutne znat jeji jmeno, navratovou hodnotu i typy parametru CtMethod fooMethod = new CtMethod(returnType, "bar", parameterTypes, generatedClass); // zmena modifikatoru fooMethod.setModifiers(Modifier.STATIC | Modifier.PRIVATE); // telo metody muze byt slozeno z celeho bloku fooMethod.setBody( "{" + " float x = 1/8f;" + " float y = 336;" + " return x * y;" + "}" ); generatedClass.addMethod(fooMethod); } /** * Vytvoreni tridy s metodou main(). * * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu metody main() * @throws IOException * pokud dojde k chybe pri zapisu bajtkodu na disk * @throws NotFoundException * pokud dojde k chybe pri zapisu bajtkodu na disk */ private static CtClass generateClass() throws CannotCompileException, NotFoundException, IOException { // ziskat vychozi class pool ClassPool pool = ClassPool.getDefault(); // vytvoreni nove verejne tridy CtClass generatedClass = pool.makeClass(GENERATED_CLASS_NAME); // pridani metody add do teto tridy addMethodAdd(generatedClass); // konstrukce nove metody foo() constructMethodFoo(generatedClass); // konstrukce nove metody bar() constructMethodBar(generatedClass); // pridani metody do teto tridy addMethodMain(generatedClass); // ulozeni bajtkodu na disk generatedClass.writeFile(); return generatedClass; } /** * Vypis struktury vybrane metody. * * @param generatedClass * predstavuje vytvarenou tridu * @param methodName * jmeno metody, jejiz struktura se ma vypsat * @throws NotFoundException * vyhozena, pokud metoda nebyla nalezena * @throws BadBytecode * vyhozena, pokud se nalezne neplatna instrukce v bytekodu */ private static void printMethodStructure(CtClass generatedClass, String methodName) throws NotFoundException, BadBytecode { System.out.println("Method '" + methodName + "' structure:"); CtMethod method = generatedClass.getDeclaredMethod(methodName); if (method == null) { System.out.println(" not found!"); return; } MethodInfo methodInfo = method.getMethodInfo(); System.out.println(" real name: " + methodInfo.getName()); System.out.println(" descriptor: " + methodInfo.getDescriptor()); System.out.println(" access flags: " + Modifier.toString(methodInfo.getAccessFlags())); System.out.println(" method body:"); printMethodBody(methodInfo); System.out.println(); } /** * Vypis instrukci tvoricich telo vybrane metody. * * @throws NotFoundException * vyhozena, pokud metoda nebyla nalezena * @throws BadBytecode * vyhozena, pokud se nalezne neplatna instrukce v bytekodu */ private static void printMethodBody(MethodInfo methodInfo) throws BadBytecode { CodeAttribute ca = methodInfo.getCodeAttribute(); CodeIterator iterator = ca.iterator(); while (iterator.hasNext()) { int index = iterator.next(); int opcode = iterator.byteAt(index); System.out.println(" " + Mnemonic.OPCODE[opcode]); } } /** * Vypis struktury vybranych metod z generovane tridy. * * @param generatedClass * predstavuje vytvarenou tridu * @throws NotFoundException * vyhozena, pokud metoda nebyla nalezena * @throws BadBytecode */ private static void printMethodStructures(CtClass generatedClass) throws NotFoundException, BadBytecode { printMethodStructure(generatedClass, "main"); printMethodStructure(generatedClass, "add"); printMethodStructure(generatedClass, "foo"); printMethodStructure(generatedClass, "bar"); } /** * Spusteni generatoru tridy. * * @param args nevyuzito */ public static void main(String[] args) { System.out.println("class generation begin: " + GENERATED_CLASS_NAME); try { CtClass generatedClass = generateClass(); // dulezite - generovana trida nesmi byt "zmrazena" generatedClass.defrost(); printMethodStructures(generatedClass); } catch (CannotCompileException e) { e.printStackTrace(); } catch (NotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (BadBytecode e) { e.printStackTrace(); } System.out.println("class generation end: " + GENERATED_CLASS_NAME); } }
10. Ukázka výpisu bajtkódu metod vytvořených demonstračním příkladem ClassGenerationTest5
Překlad dnešního druhého demonstračního příkladu ClassGenerationTest5 se provede podobným způsobem, s jakým jsme se již setkali u příkladu předchozího:
javac -cp javassist.jar ClassGenerationTest5.java
I jeho spuštění je téměř shodné:
java -cp .:javassist.jar ClassGenerationTest5
Po spuštění třídy ClassGenerationTest5 se na standardní výstup vypíšou informace o všech čtyřech zkoumaných metodách. Můžete si pro větší zajímavost tento výpis zkusit porovnat s (prozatím) podrobnějším výpisem uvedeným v šesté kapitole:
class generation begin: GeneratedClass5 Method 'main' structure: real name: main descriptor: ([Ljava/lang/String;)V access flags: public static method body: getstatic iconst_1 iconst_2 invokestatic invokevirtual getstatic invokestatic invokevirtual getstatic invokestatic invokevirtual return Method 'add' structure: real name: add descriptor: (II)I access flags: public static method body: iload_0 iload_1 iadd ireturn Method 'foo' structure: real name: foo descriptor: ()I access flags: public static method body: bipush ireturn Method 'bar' structure: real name: bar descriptor: ()F access flags: private static method body: ldc fstore_0 sipush i2f fstore_1 fload_0 fload_1 fmul freturn class generation end: GeneratedClass5
11. Repositář se zdrojovými kódy obou demonstračních příkladů
Následuje – v tomto seriálu již tradiční – kapitola s odkazy na zdrojové kódy :-) Oba dnes popsané demonstrační příklady jsou společně s dalšími pomocnými skripty uloženy do Mercurial repositáře dostupného na adrese http://icedtea.classpath.org/people/ptisnovs/jvm-tools/. V následující tabulce najdete odkazy na prozatím nejnovější verze těchto zdrojových kódů:
12. Odkazy na Internetu
- Open Source ByteCode Libraries in Java
http://java-source.net/open-source/bytecode-libraries - ASM Home page
http://asm.ow2.org/ - Seznam nástrojů využívajících projekt ASM
http://asm.ow2.org/users.html - ObjectWeb ASM (Wikipedia)
http://en.wikipedia.org/wiki/ObjectWeb_ASM - Java Bytecode BCEL vs ASM
http://james.onegoodcookie.com/2005/10/26/java-bytecode-bcel-vs-asm/ - BCEL Home page
http://commons.apache.org/bcel/ - Byte Code Engineering Library (před verzí 5.0)
http://bcel.sourceforge.net/ - Byte Code Engineering Library (verze >= 5.0)
http://commons.apache.org/proper/commons-bcel/ - BCEL Manual
http://commons.apache.org/bcel/manual.html - Byte Code Engineering Library (Wikipedia)
http://en.wikipedia.org/wiki/BCEL - BCEL Tutorial
http://www.smfsupport.com/support/java/bcel-tutorial!/ - Bytecode Engineering
http://book.chinaunix.net/special/ebook/Core_Java2_Volume2AF/0131118269/ch13lev1sec6.html - Bytecode Outline plugin for Eclipse (screenshoty + info)
http://asm.ow2.org/eclipse/index.html - Javassist
http://www.jboss.org/javassist/ - Byteman
http://www.jboss.org/byteman - Java programming dynamics, Part 7: Bytecode engineering with BCEL
http://www.ibm.com/developerworks/java/library/j-dyn0414/ - The JavaTM Virtual Machine Specification, Second Edition
http://java.sun.com/docs/books/jvms/second_edition/html/VMSpecTOC.doc.html - The class File Format
http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html - javap – The Java Class File Disassembler
http://docs.oracle.com/javase/1.4.2/docs/tooldocs/windows/javap.html - javap-java-1.6.0-openjdk(1) – Linux man page
http://linux.die.net/man/1/javap-java-1.6.0-openjdk - Using javap
http://www.idevelopment.info/data/Programming/java/miscellaneous_java/Using_javap.html - Examine class files with the javap command
http://www.techrepublic.com/article/examine-class-files-with-the-javap-command/5815354 - aspectj (Eclipse)
http://www.eclipse.org/aspectj/ - Aspect-oriented programming (Wikipedia)
http://en.wikipedia.org/wiki/Aspect_oriented_programming - AspectJ (Wikipedia)
http://en.wikipedia.org/wiki/AspectJ - EMMA: a free Java code coverage tool
http://emma.sourceforge.net/ - Cobertura
http://cobertura.sourceforge.net/ - jclasslib bytecode viewer
http://www.ej-technologies.com/products/jclasslib/overview.html