Pohled pod kapotu JVM – nástroje pro vytváření a manipulaci s bajtkódem

Pavel Tišnovský 25. 6. 2013

Od dnešní části seriálu o jazyce Java se začneme věnovat další problematice související s JVM. Postupně si totiž popíšeme způsob využití nástrojů určených pro vytváření a/nebo manipulaci s bajtkódem zpracovávaným virtuálním strojem Javy. Dnešní demonstrační příklad bude postaven na nástroji Javassist.

Obsah

1. Nástroje pro vytváření a manipulaci s bajtkódem JVM

2. ASM

3. BCEL: Byte Code Engineering Library

4. Byteman

5. Javassist

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ů

10. Odkazy na Internetu

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:

  1. Zjištění pokrytí kódu testy, což vyžaduje zásahy do kódu jednotlivých metod (nástroje Cobertura, EMMA či JaCoCo)
  2. Transformace kódu v některých nástrojích podporujících aspektově orientované programování
  3. Implementace interpretru či překladače bez toho, aby musel programátor nutně znát všechna úskalí binárního bajtkódu
  4. Transformace API z důvodu zvýšení bezpečnosti
  5. Implementace testů využívajících substituované API
  6. 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:

widgety

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.or­g/people/ptisnovs/jvm-tools/. V následující tabulce najdete odkazy na prozatím nejnovější verze těchto zdrojových kódů:

10. Odkazy na Internetu

  1. Open Source ByteCode Libraries in Java
    http://java-source.net/open-source/bytecode-libraries
  2. ASM Home page
    http://asm.ow2.org/
  3. Seznam nástrojů využívajících projekt ASM
    http://asm.ow2.org/users.html
  4. ObjectWeb ASM (Wikipedia)
    http://en.wikipedia.org/wi­ki/ObjectWeb_ASM
  5. Java Bytecode BCEL vs ASM
    http://james.onegoodcooki­e.com/2005/10/26/java-bytecode-bcel-vs-asm/
  6. BCEL Home page
    http://commons.apache.org/bcel/
  7. Byte Code Engineering Library (před verzí 5.0)
    http://bcel.sourceforge.net/
  8. Byte Code Engineering Library (verze >= 5.0)
    http://commons.apache.org/pro­per/commons-bcel/
  9. BCEL Manual
    http://commons.apache.org/bcel/ma­nual.html
  10. Byte Code Engineering Library (Wikipedia)
    http://en.wikipedia.org/wiki/BCEL
  11. BCEL Tutorial
    http://www.smfsupport.com/sup­port/java/bcel-tutorial!/
  12. Bytecode Engineering
    http://book.chinaunix.net/spe­cial/ebook/Core_Java2_Volu­me2AF/0131118269/ch13lev1sec6­.html
  13. Bytecode Outline plugin for Eclipse (screenshoty + info)
    http://asm.ow2.org/eclipse/index.html
  14. Javassist
    http://www.jboss.org/javassist/
  15. Byteman
    http://www.jboss.org/byteman
  16. Java programming dynamics, Part 7: Bytecode engineering with BCEL
    http://www.ibm.com/develo­perworks/java/library/j-dyn0414/
  17. The JavaTM Virtual Machine Specification, Second Edition
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/VMSpec­TOC.doc.html
  18. The class File Format
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/Clas­sFile.doc.html
  19. javap – The Java Class File Disassembler
    http://docs.oracle.com/ja­vase/1.4.2/docs/tooldocs/win­dows/javap.html
  20. javap-java-1.6.0-openjdk(1) – Linux man page
    http://linux.die.net/man/1/javap-java-1.6.0-openjdk
  21. Using javap
    http://www.idevelopment.in­fo/data/Programming/java/mis­cellaneous_java/Using_javap­.html
  22. Examine class files with the javap command
    http://www.techrepublic.com/ar­ticle/examine-class-files-with-the-javap-command/5815354
  23. aspectj (Eclipse)
    http://www.eclipse.org/aspectj/
  24. Aspect-oriented programming (Wikipedia)
    http://en.wikipedia.org/wi­ki/Aspect_oriented_program­ming
  25. AspectJ (Wikipedia)
    http://en.wikipedia.org/wiki/AspectJ
  26. EMMA: a free Java code coverage tool
    http://emma.sourceforge.net/
  27. Cobertura
    http://cobertura.sourceforge.net/
  28. jclasslib bytecode viewer
    http://www.ej-technologies.com/products/jclas­slib/overview.html
Našli jste v článku chybu?
DigiZone.cz: Test LG 55UH750V aneb Cena/výkon

Test LG 55UH750V aneb Cena/výkon

Vitalia.cz: 5 důvodů, proč jet na výlov rybníka

5 důvodů, proč jet na výlov rybníka

Vitalia.cz: Jaký je rozdíl mezi brambůrky a chipsy?

Jaký je rozdíl mezi brambůrky a chipsy?

Vitalia.cz: Inspekce našla nelegální sklad v SAPĚ. Zase

Inspekce našla nelegální sklad v SAPĚ. Zase

Podnikatel.cz: Udělali jsme velkou chybu, napsal Čupr

Udělali jsme velkou chybu, napsal Čupr

Podnikatel.cz: Babišovi se nedá věřit, stěžovali si hospodští

Babišovi se nedá věřit, stěžovali si hospodští

DigiZone.cz: Rapl: seriál, který vás smíří s ČT

Rapl: seriál, který vás smíří s ČT

Vitalia.cz: Muž, který miluje příliš. Ženám neimponuje

Muž, který miluje příliš. Ženám neimponuje

DigiZone.cz: DVB-T2 ověřeno: seznam TV zveřejněn

DVB-T2 ověřeno: seznam TV zveřejněn

DigiZone.cz: Mordparta: trochu podchlazený 87. revír

Mordparta: trochu podchlazený 87. revír

Vitalia.cz: Kterou dýni můžete jíst za syrova?

Kterou dýni můžete jíst za syrova?

Vitalia.cz: Test dětských svačinek: Tyhle ne!

Test dětských svačinek: Tyhle ne!

120na80.cz: Co je padesátkrát sladší než cukr?

Co je padesátkrát sladší než cukr?

Lupa.cz: Jak levné procesory změnily svět?

Jak levné procesory změnily svět?

Podnikatel.cz: Dva měsíce na EET. Budou stačit?

Dva měsíce na EET. Budou stačit?

Vitalia.cz: Tohle jsou nejlepší česká piva podle odborníků

Tohle jsou nejlepší česká piva podle odborníků

Podnikatel.cz: Takhle se prodávají mražené potraviny

Takhle se prodávají mražené potraviny

DigiZone.cz: Numan Two: rozhlasový přijímač s CD

Numan Two: rozhlasový přijímač s CD

DigiZone.cz: Wimbledon na Nova Sport až do 2019

Wimbledon na Nova Sport až do 2019

Vitalia.cz: Jsou vegani a vyrábějí nemléko

Jsou vegani a vyrábějí nemléko