Obsah
1. Nástroje pro vytváření a manipulaci s bajtkódem JVM
3. BCEL: Byte Code Engineering Library
6. Demonstrační příklad – generování bajtkódu třídy s využitím nástroje Javassist
7. Překlad a spuštění demonstračního příkladu i vygenerované třídy
8. Výsledná podoba bajtkódu vygenerované třídy, porovnání s kompilovanou třídou
9. Repositář se zdrojovými kódy demonstračního příkladu i pomocných skriptů
1. Nástroje pro vytváření a manipulaci s bajtkódem JVM
Další poměrně zajímavou problematikou, kterou se v seriálu o programovacím jazyku Java i o virtuálním stroji Javy budeme zabývat, bude popis nástrojů a knihoven určených pro manipulaci s bajtkódem JVM nebo dokonce k vytváření zcela nového bajtkódu, který nemá žádný předobraz ve zdrojových kódech Javy. Nástrojů pro vytváření či pro manipulaci s bajkódem virtuálního jazyka Java existuje poměrně velké množství, což je pochopitelné, když si uvědomíme, že JVM již dávno není platformou určenou pouze pro provozování aplikací psaných jen v programovacím jazyku Java, ale najdeme mnoho dalších jazyků i nástrojů, které JVM využívají. To znamená, že soubory .class, které jsou zpracovávány v JVM, nevznikají jen překladem zdrojových textů napsaných v Javě, protože se může jednat o bajtkód přeložený assemblerem typu Jasmin či některým generátorem kódu popsaným v dalším textu.
Samotné čtení, analýza a vytváření bajtkódu, popř. jeho cílená modifikace, může být prováděna z mnoha důvodů, mezi něž patří například:
- Zjištění pokrytí kódu testy, což vyžaduje zásahy do kódu jednotlivých metod (nástroje Cobertura, EMMA či JaCoCo)
- Transformace kódu v některých nástrojích podporujících aspektově orientované programování
- Implementace interpretru či překladače bez toho, aby musel programátor nutně znát všechna úskalí binárního bajtkódu
- Transformace API z důvodu zvýšení bezpečnosti
- Implementace testů využívajících substituované API
- Statická analýza a hledání potenciálních chyb nástrojemFindBugs
V dalších kapitolách si velmi stručně popíšeme vlastnosti některých nástrojů určených pro manipulaci s bajtkódem.
2. ASM
Jedním z nejznámějších a současně i nejpoužívanějších nástrojů umožňujících relativně snadnou manipulaci s bajtkódem je nástroj nazvaný ASM (http://asm.ow2.org/ – podívejte se na pěkně navržené logo tohoto nástroje). Práce s ASM je založena na transformaci bajtkódu, která je prováděna postupně načítáním bajtkódu vstupního a postupným generováním bajtkódu výstupního (při přenosu informací ze vstupního bajtkódu do bajtkódu výstupního se na tato data aplikuje zvolená transformace či modifikace). Tento přístup má své přednosti, ale i zápory. Mezi přednosti patří fakt, že není zapotřebí si udržovat v operační paměti celou strukturu bajtkódu (ať již jedné třídy či celého balíčku, …), což je pro některé účely více než dostačující. Příkladem může být vložení volání nějaké logovací metody na začátek každé metody všech tříd – zde skutečně nepotřebujeme mít v operační paměti k dispozici celou třídu či dokonce celý balíček.
„Proudové zpracování“, tj. manipulace s bajtkódem přímo v průběhu jeho načítání a následného ukládání, je navíc velmi rychlé mj. i z toho důvodu, že ASM nemusí složitě budovat celou hierarchii objektů způsobem, jakým to dělají například dále popsané nástroje BCEL či Javassist. Při provádění složitějších manipulací s bajtkódem však může být výhodnější mít k dispozici struktury celých tříd. Přestavme si například nástroj, který kód tříd globálně zpracuje na základě nějaké anotace (či obecně nějakých metadat) přiložených ke třídě či k balíčku. I tento režim je možné v novějších verzích nástroje ASM využít (ASM tedy ve skutečnosti dokáže pracovat ve dvou módech), i když si myslím, že „proudový režim“ zmíněný v předchozím odstavci je v API ASM navržen lepším a především přehlednějším způsobem.
3. BCEL: Byte Code Engineering Library
Jedním z historicky nejstarších programů umožňujících manipulaci s bajtkódem na vyšší úrovni je nástroj (či spíše přesněji řečeno knihovna) nazvaná BCEL, neboli Byte Code Engineering Library). Tato knihovna umožňuje nejprve načíst celý bajtkód do operační paměti a posléze s tímto bajtkódem různým způsobem manipulovat. Vzhledem k tomu, že se bajtkód při načítání převádí na vnitřní reprezentaci (různé typy rozhraní, tříd a výčtových typů), je manipulace s bajtkódem pomocí knihovny BCEL poměrně pomalá, což se však většinou negativně projeví až při práci s rozsáhlejšími projekty. Větší problém pro vývojáře představuje poněkud nepřehledné aplikační programové rozhraní knihovny BCEL a s tím související horší dokumentace. I když se tato knihovna používá poměrně často, je pro první seznámení se s možnostmi manipulace s bajtkódem podle mě výhodnější použít knihovnu Javassist popsanou v šesté kapitole.
Rozdíl mezi knihovnou BCEL a knihovnou ASM (zde je myšlen především „proudový režim“ zpracování bajtkódu) se v určitém pohledu podobá rozdílu mezi způsobem zpracování XML souborů – buď lze použít DOM (Document Object Model) (analogie k BCEL) nebo SAX (Simple API for XML) (analogie k ASM v proudovém režimu). Knihovnu ASM je však taktéž možné využívat v „režimu DOM“, podobně jako BCEL, i když se API obou knihoven od sebe dosti liší.
4. Byteman
Třetím nástrojem, o němž se dnes prozatím alespoň ve stručnosti zmíníme, je nástroj nesoucí název Byteman. I tento nástroj dokáže manipulovat s javovským bajtkódem, ovšem jedná se většinou o manipulaci založenou na textových souborech, do nichž se zapisují pravidla aplikovaná na jednotlivé třídy, rozhraní či skupiny tříd/rozhraní. Díky této podpoře souborů s pravidly je možné mnoho požadovaných modifikací zapsat deklarativně, bez nutnosti vytvářet program v Javě, což bylo nutné v případě použití nástrojů ASM či BCEL. Zajímavý a pro mnoho aplikací i důležitý je fakt, že se změny v bajtkódu mohou provádět přímo v běžícím virtuálním stroji Javy, takže je například možné změnit chování aplikace bez nutnosti ji restartovat (popř. bez nutnosti například restartu aplikačního serveru). Na tuto vlastnost jsou sice vývojáři zvyklí v případě skriptovacích jazyků, ovšem v oblasti Javy se jedná o možná poněkud neobvyklé chování :-)
Nástrojem Byteman se budeme podrobněji zabývat později, dnes si jen pro zajímavost ukažme, jak vypadá soubor s pravidly:
# rule skeleton RULE <rule name> CLASS <class name> METHOD <method name> BIND <bindings> IF <condition> DO <actions> ENDRULE
5. Javassist
Čtvrtým nástrojem, s nímž se začneme do větších podrobností seznamovat v navazující části tohoto seriálu, je nástroj nesoucí název Javassist. Tento nástroj můžeme přirovnat k již zmíněné knihovně BCEL, protože i Javassist nabízí vývojářům API určené pro vytváření či pro modifikaci bajtkódu jednotlivých tříd či rozhraní. Ve skutečnosti je však API nabízené nástrojem Javassist podle mého názoru mnohem jednodušší (či možná lépe řečeno pochopitelnější), než tomu je v případě BCEL, což je mj. zajištěno i tím, že se jedná o rozhraní, v němž je možné využít jak vysokoúrovňové konstrukce (vytvoření metody z řetězce), tak i konstrukce nízkoúrovňové (postupné doplňování jednotlivých „strojových“ instrukcí JVM do těl metod). Vysokoúrovňové i nízkoúrovňové konstrukce je samozřejmě možné navzájem kombinovat, čehož je možné využít například při programování interpretrů či překladačů.
Příklad vytvoření nové metody s využitím vysokoúrovňových konstrukcí:
private static void addAddMethod(CtClass cc) throws CannotCompileException { CtMethod add = CtMethod.make( "public static int add(int x, int y)" + "{" + " return x+y;" + "}", cc); cc.addMethod(add); }
Příklad práce na nižší úrovni, tj. na úrovni jednotlivých kódů instrukcí:
MethodInfo minfo = method.getMethodInfo(); ConstPool constPool = minfo.getConstPool(); Bytecode b = new Bytecode(constPool, 2, 0); // stacksize == 2 b.addIconst(42); b.addReturn(CtClass.intType); CodeAttribute codeAttribute = b.toCodeAttribute();
6. Demonstrační příklad – generování bajtkódu třídy s využitím nástroje Javassist
I bez podrobného popisu vlastností nástroje Javassist je možné si možnosti tohoto nástroje vyzkoušet na jednoduchém demonstračním příkladu nazvaného ClassGenerationTest1. Tento demonstrační příklad po svém spuštění vytvoří bytekód třídy GeneratedClass, který bude mj. obsahovat i statickou metodu main(), jenž na standardní výstup vypíše obligátní text „Hello world!“ s využitím metody System.out.println(). V tomto demonstračním příkladu jsou ukázány čtyři vlastnosti nástroje Javassist – způsob získání třídy z takzvaného poolu tříd (ClassPool.makeClass()), přidání nové metody do třídy (CtClass.addMethod()), deklarace těla metody přímo pomocí řetězce (CtMethod.make(), existují však i další možnosti) a konečně uložení bajtkódu vygenerované třídy na disk (CtClass.writeFile()). Následuje výpis okomentovaného zdrojového kódu tohoto demonstračního příkladu:
import java.io.IOException; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.NotFoundException; /** * Test moznosti nastroje Javassist - vygenerovani jednoduche tridy. * * @author Pavel Tisnovsky */ public class ClassGenerationTest1 { /** * 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(\"Hello world! \");" + "}"; /** * Vytvoreni metody main() z jejiho zdrojoveho kodu. * * @param generatedClass * predstavuje vytvarenou tridu * @throws CannotCompileException * vyhozena v pripade chyby ve zdrojovem kodu */ private static void addMainMethod(CtClass generatedClass) throws CannotCompileException { CtMethod methodMain = CtMethod.make(MAIN_METHOD_SOURCE_TEXT, generatedClass); generatedClass.addMethod(methodMain); } /** * 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 cc = pool.makeClass("GeneratedClass"); // pridani metody do teto tridy addMainMethod(cc); // ulozeni bajtkodu na disk cc.writeFile(); } /** * Spusteni generatoru tridy. * * @param args nevyuzito */ public static void main(String[] args) { System.out.println("class generation begin"); try { generateClass(); } catch (CannotCompileException e) { e.printStackTrace(); } catch (NotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } System.out.println("class generation end"); } }
7. Překlad a spuštění demonstračního příkladu i vygenerované třídy
Demonstrační příklad ClassGenerationTest1 se přeloží následujícím způsobem:
javac -cp javassist.jar ClassGenerationTest1.java
Povšimněte si, že je nutné zadat cestu k Java archivu javassist.jar obsahujícího všechny třídy a rozhraní nástroje Javassist. Tento archiv lze získat buď překladem nebo přímo ze stránek tohoto projektu: http://www.jboss.org/javassist/.
O spuštění přeloženého demonstračního příkladu se postará příkaz:
java -cp .:javassist.jar ClassGenerationTest1
(opět je zde nutné zadat cestu k Java archivu javassist.jar, ale i cestu do aktuálního adresáře).
Po spuštění by se měl v aktuálním adresáři objevit nový soubor nazvaný GeneratedClass.class obsahující bajtkód nové třídy. Tuto třídu, resp. přesněji řečeno její statickou metodu main lze samozřejmě spustit, a to zcela stejným způsobem jako bajtkód třídy vzniklý přímo překladačem Javy:
java GeneratedClass Hello world!
8. Výsledná podoba bajtkódu vygenerované třídy, porovnání s kompilovanou třídou
Podívejme se nyní na interní strukturu bajtkódu vytvořeného pomocí našeho demonstračního příkladu ClassGenerationTest1. Pro dekompilaci bajtkódu do čitelné podoby je možné použít standardní nástroj javap:
javap -c -private GeneratedClass > GeneratedClass.asm
Výsledkem by měl být následující text (indexy konstant v constant poolu se však mohou lišit):
Compiled from "GeneratedClass.java" public class GeneratedClass extends java.lang.Object{ public static void main(java.lang.String[]); Code: 0: getstatic #14; // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #16; // String Hello world! 5: invokevirtual #22; // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return public GeneratedClass(); Code: 0: aload_0 1: invokespecial #27; // Method java/lang/Object."<init>":()V 4: return }
Z výpisu je patrné, že nástroj Javassist skutečně vytvořil třídu obsahující statickou metodu main(), v níž se vypisuje text „Hello world!“ na standardní výstup. Kromě toho byl vytvořen i implicitní bezparametrický konstruktor volající (taktéž bezparametrický) konstruktor předka.
Pro porovnání si ukažme, jak vypadá bajtkód třídy Hello přeložené standardním překladačem javac. Zdrojový kód této třídy je jednoduchý:
public class Hello { public static void main(String[] args) { System.out.println("Hello world!"); } }
Po překladu pomocí javac Hello.java a zpětném překladu s využitím javap -c Hello dostaneme následující výstup:
Compiled from "Hello.java" public class Hello extends java.lang.Object{ public Hello(); Code: 0: aload_0 1: invokespecial #1; // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #2; // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3; // String Hello world! 5: invokevirtual #4; // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return }
Při porovnání obou „disassemblovaných“ bajtkódů zjistíme, že se jedná o prakticky totožné programy, které se liší pouze jinou posloupností deklarace jednotlivých metod (konstruktoru a metody main) a taktéž odlišnou posloupností záznamů uložených v constant poolu – to jsou však jen malé změny, které nemají žádný vliv na běh aplikací. Pro úplnost si ještě ukažme rozdíly mezi oběma bajtkódy v případě, že se ručně prohodí deklarace konstruktoru a metody main:
9. Repositář se zdrojovými kódy demonstračního příkladu i pomocných skriptů
Zdrojové kódy dnes popsaného demonstračního příkladu ClassGenerationTest1 a všech pomocných skriptů jsou, jak se již v tomto seriálu stalo zvykem, 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ů:
# | Zdrojový soubor/skript | Umístění souboru v repositáři |
---|---|---|
1 | ClassGenerationTest1.java | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/008931c9e149/javassist/ClassGenerationTest1/ClassGenerationTest1.java |
2 | buildClassGenerator.sh | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/008931c9e149/javassist/ClassGenerationTest1/buildClassGenerator.sh |
3 | runClassGenerator.sh | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/008931c9e149/javassist/ClassGenerationTest1/runClassGenerator.sh |
4 | runGeneratedClass.sh | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/008931c9e149/javassist/ClassGenerationTest1/runGeneratedClass.sh |
5 | disassembleGeneratedClass.sh | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/008931c9e149/javassist/ClassGenerationTest1/disassembleGeneratedClass.sh |
6 | Hello.java | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/008931c9e149/javassist/ClassGenerationTest1/Hello.java |
10. 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