Pohled pod kapotu JVM – vytváření i dekompilace metod s využitím nástroje Javassist

9. 7. 2013
Doba čtení: 23 minut

Sdílet

V dnešním článku o jazyce Java i o virtuálním stroji Javy budeme pokračovat v popisu nástroje Javassist, s nímž jsme se seznámili v předchozích dvou dílech. Dnes si ukážeme způsob vytváření nových metod a přidání těchto metod do libovolné třídy. Nezapomeneme ani na nízkoúrovňovou operaci – dekompilaci metod.

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ů

12. Odkazy na Internetu

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");
    }

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.MethodIn­fo.getDescriptor() vrátí deskriptor metody (část její signatury)
3 javassist.bytecode.MethodIn­fo.getAccessFlags() vrátí přístupová práva a další modifikátory metody (STATIC…)
4 javassist.bytecode.MethodIn­fo.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.CodeIte­rator.hasNext() oznamuje, zda existuje další instrukce
4 javassist.bytecode.CodeIte­rator.getCodeLength() vrací celkovou délku kódu
5 javassist.bytecode.CodeIte­rator.byteAt() vrací hodnotu bajtu na aktuální pozici
6 javassist.bytecode.CodeIte­rator.u16bitAt() vrací hodnotu (bezznaménkového) 16bitového slova na aktuální pozici
7 javassist.bytecode.CodeIte­rator.s16bitAt() vrací hodnotu 16bitového slova se znaménkem na aktuální pozici
8 javassist.bytecode.CodeIte­rator.s32bitAt() vrací hodnotu 32bitového slova se znaménkem na aktuální pozici

Asi nejzajímavější je fakt, že metoda javassist.bytecode.CodeIte­rator.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é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é:

ict ve školství 24

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.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ů:

12. 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

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.