Hlavní navigace

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:

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?

28. 6. 2013 21:12

Funguje to tak, že při novém volání metody se již použije nový kód, ale pokud se injektuje změněná metoda ve chvíli, kdy ještě v nějakém vláknu běží (sleep, aktivní čekání na událost, výpočet), tak tato vlákna ještě doběhnou se starším kódem. Ale to je spíš vlastnost samotné JVM než bytemana.

28. 6. 2013 19:16

Jak to dopadne, když Byteman v běžící JVM změní bajtkód, který předtím JVM zkompilovala do nativního kódu nebo zoptimalizovala? Je šance dát nějak JVM vědět, že se bajtkód změnil, nebo bude JVM změny udělané Bytemanem ignorovat a spokojeně si poběží (nebo poleží) nad původním kódem?

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Vitalia.cz: Říká amoleta - a myslí palačinka

Říká amoleta - a myslí palačinka

Lupa.cz: Kdo pochopí vtip, může jít do ČT vyvíjet weby

Kdo pochopí vtip, může jít do ČT vyvíjet weby

Root.cz: Telegram spustil anonymní blog Telegraph

Telegram spustil anonymní blog Telegraph

Vitalia.cz: „Připluly“ z Německa a možná obsahují jed

„Připluly“ z Německa a možná obsahují jed

Lupa.cz: Teletext je „internetem hipsterů“

Teletext je „internetem hipsterů“

120na80.cz: Vitaminová abeceda

Vitaminová abeceda

120na80.cz: Jak oddálit Alzheimera?

Jak oddálit Alzheimera?

Podnikatel.cz: Podnikatelům dorazí varování od BSA

Podnikatelům dorazí varování od BSA

DigiZone.cz: Recenze Westworld: zavraždit a...

Recenze Westworld: zavraždit a...

Podnikatel.cz: EET: Totálně nezvládli metodologii projektu

EET: Totálně nezvládli metodologii projektu

Lupa.cz: Proč firmy málo chrání data? Chovají se logicky

Proč firmy málo chrání data? Chovají se logicky

Vitalia.cz: 9 největších mýtů o mase

9 největších mýtů o mase

Podnikatel.cz: Zavře krám u #EET Malá pokladna a Teeta?

Zavře krám u #EET Malá pokladna a Teeta?

Vitalia.cz: Jsou čajové sáčky toxické?

Jsou čajové sáčky toxické?

Podnikatel.cz: Babiše přesvědčila 89letá podnikatelka?!

Babiše přesvědčila 89letá podnikatelka?!

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí

Vitalia.cz: Znáte „černý detox“? Ani to nezkoušejte

Znáte „černý detox“? Ani to nezkoušejte

Lupa.cz: Propustili je z Avastu, už po nich sahá ESET

Propustili je z Avastu, už po nich sahá ESET

Vitalia.cz: To není kašel! Správná diagnóza zachrání život

To není kašel! Správná diagnóza zachrání život