Pohled pod kapotu JVM – přímé generování instrukcí bajtkódu s využitím nástroje Javassist

16. 7. 2013
Doba čtení: 25 minut

Sdílet

V dnešní části seriálu o programovacím jazyce Java i o virtuálním stroji Javy se již potřetí budeme věnovat popisu možností nástroje Javassist. Dnes si ukážeme, jak lze přímo generovat jednotlivé „strojové“ instrukce bajtkódu s využitím příslušných pomocných tříd Javassistu.

Obsah

1. Pohled pod kapotu JVM – přímé generování instrukcí bajtkódu s využitím nástroje Javassist: sekvence instrukcí jako atribut přiřazený k metodě

2. Vygenerování metody bez přiřazeného atributu code (metoda bez instrukcí)

3. Zdrojový kód demonstračního příkladu ClassGenerationTest6

4. Výpis bajtkódu vygenerovaného demonstračním příkladem ClassGenerationTest6

5. Spuštění třídy vygenerované demonstračním příkladem ClassGenerationTest6

6. Tvorba kódu metod na úrovni jednotlivých „strojových“ instrukcí

7. Vygenerování metody int foo()

8. Vygenerování metody int add(int, int)

9. Zdrojový kód demonstračního příkladu ClassGenerationTest7

10. Výpis bajtkódu vygenerovaného demonstračním příkladem ClassGenerationTest7

11. Repositář se zdrojovými kódy obou dnešních demonstračních příkladů

12. Odkazy na Internetu

1. Pohled pod kapotu JVM – přímé generování instrukcí bajtkódu s využitím nástroje Javassist: sekvence instrukcí jako atribut přiřazený k metodě

V dnešní části seriálu o programovacím jazyce Java i o virtuálním stroji Javy si ukážeme způsob vytváření nových metod na nižší úrovni abstrakce, konkrétně na úrovni jednotlivých instrukcí bajtkódu. Navážeme tak na část předchozí, v níž jsme se seznámili se způsobem tvorby metod v případě, že máme k dispozici textovou podobu jejich těl, tj. bloků obsahujících lokální deklarace, lokální proměnné i jednotlivé (strukturované) příkazy. Nejdříve si však ve stručnosti zopakujme, jak vlastně vypadá struktura bajtkódu každé přeložené třídy či rozhraní, tj. struktura každého souboru s koncovkou .class. Celý soubor je rozdělen do několika sekcí, které jsou dále rozdělovány do dalších podsekcí. Na nejvyšší úrovni se nachází sekce s hlavičkou bajtkódu, constant poolem, seznamem datových položek, kódy jednotlivých metod a konečně dalšími metadaty (atributy) třídy:

+-----------------------------------------+
|   Hlavička bajtkódu                     |
|                                         |
|  +-------------------------+---------+  |
|  | Magická konstanta       | 4 bajty |  |
|  +-------------------------+---------+  |
|  | Minoritní verze formátu | 2 bajty |  |
|  +-------------------------+---------+  |
|  | Majoritní verze formátu | 2 bajty |  |
|  +-------------------------+---------+  |
|                                         |
+-----------------------------------------+
|   Constant pool                         |
|                                         |
|  +-------------------------+---------+  |
|  | Počet záznamů v poolu   | 2 bajty |  |
|  +-------------------------+---------+  |
|  | záznam #1               | x bajtů |  |
|  | záznam #2               | x bajtů |  |
|  | ...                     |         |  |
|  | záznam #n               | x bajtů |  |
|  +-------------------------+---------+  |
|                                         |
+-----------------------------------------+
|   Definice příznaků třídy/rozhraní      |
|                                         |
|  +-------------------------+---------+  |
|  | Bitové pole s příznaky  | 2 bajty |  |
|  +-------------------------+---------+  |
|                                         |
+-----------------------------------------+
|   Jméno třídy a nadtřídy                |
|                                         |
|  +-------------------------+---------+  |
|  | Index do constant poolu | 2 bajty |  |
|  +-------------------------+---------+  |
|  | Index do constant poolu | 2 bajty |  |
|  +-------------------------+---------+  |
|                                         |
+-----------------------------------------+
|   Implementovaná rozhraní               |
|                                         |
|  +-------------------------+---------+  |
|  | Počet jmen rozhraní     | 2 bajty |  |
|  +-------------------------+---------+  |
|  | Index do const. poolu #1| 2 bajty |  |
|  | Index do const. poolu #2| 2 bajty |  |
|  | ...                     |         |  |
|  | Index do const. poolu #n| 2 bajty |  |
|  +-------------------------+---------+  |
|                                         |
+-----------------------------------------+
|   Datové položky deklarované ve třídě   |
|   .....                                 |
|   .....                                 |
|   .....                                 |
+-----------------------------------------+
|   Kódy jednotlivých metod               |
|   .....                                 |
|   .....                                 |
|   .....                                 |
+-----------------------------------------+
|   Další metadata třídy                  |
|   (atributy třídy)                      |
|   .....                                 |
|   .....                                 |
|   .....                                 |
+-----------------------------------------+

Nás dnes bude zajímat především způsob uložení instrukcí tvořících těla jednotlivých metod. Poněkud překvapivě jsou tato velmi důležitá data uložena ve formě atributů přiřazených k jednotlivým metodám. Ke každé datové položce či metodě totiž může být přiřazen libovolný počet atributů, které umožňují uložení dalších informací o této datové položce/metodě, resp. o jejích vlastnostech. Počet atributů je libovolný, samozřejmě může být i nulový (to se ostatně týká většiny nefinálních datových položek, kterým žádný atribut nebývá přiřazen).

Každý atribut se obecně skládá z dvojice obsahující jméno atributu a jeho hodnotu. Jméno atributu je zadáno nepřímo jako odkaz do constant poolu, protože se v mnoha případech můžeme setkat s tím, že shodný atribut je použit u většího množství datových položek (jedná se například o atribut se jménem ConstantValue odkazující na konstantu uloženou též v poolu). Následující tabulka obsahuje jména a stručný popis atributů popsaných přímo ve specifikaci virtuálního stroje Javy:

# Jméno atributu Význam
1 ConstantValue odkaz na konstantu u finální datové položky
2 Code instrukce tvořící tělo metody (právě tento atribut dnes využijeme)
3 Exceptions informace o výjimkách, které může metoda vyhazovat
4 InnerClasses informace o vnitřních třídách (jedná se o atribut třídy)
5 EnclosingMethod informace uváděná u vnitřních a anonymních tříd
6 Synthetic atribut bez hodnoty (délka=0) uváděný u syntetických tříd, metod či datových položek
7 Signature atribut obsahující odkaz na signaturu třídy, metody či datové položky
8 SourceFile atribut obsahující odkaz na jméno zdrojového souboru (uložené v constant poolu)
9 LineNumberTable atribut obsahující pole s mapováním mezi instrukcemi metody a odpovídajícím zdrojovým řádkem
10 LocalVariableTable atribut obsahující ladicí informace o lokálních proměnných
11 LocalVariableTypeTable atribut obsahující ladicí informace o typech lokálních proměnných
11 Deprecated atribut bez hodnoty (délka=0) uváděný u tříd, metod a datových položek s anotací @Deprecated

Jak je z předchozí tabulky patrné, jsou instrukce tvořící těla metod uloženy do atributu s názvem Code. Hodnotu tohoto atributu však nebudeme muset vytvářet ručně bajt po bajtu, protože budeme moci využít možností nabízených knihovnou Javassist, především pak její třídou Bytecode a Opcode.

2. Vygenerování metody bez přiřazeného atributu code (metoda bez instrukcí)

Nástroj Javassist podporuje vytváření nových metod s následným přidáním těchto metod do libovolné třídy. Jakým způsobem se nové metody mohou vytvořit jsme si ukázali v předchozí části tohoto seriálu. Ovšem prozatím všechny námi vytvářené metody obsahovaly i tělo, tj. příkazový blok. Jinými slovy to znamená, že ke všem prozatím vytvářeným metodám byl nástrojem Javassist vygenerován i atribut se jménem Code obsahující příslušný seznam instrukcí vzniklých překladem těl metod. Ve skutečnosti však některé metody nemusí mít nutně přiřazen atribut Code. Jedná se například o abstraktní metody (v abstraktních třídách) či metody deklarované v nějakém rozhraní.

Podívejme se na konkrétní příklad, v němž je metoda int foo() vytvořena jako běžná neabstraktní metoda, abstraktní metoda v abstraktní třídě a konečně jako pouhá hlavička metody, která je součástí rozhraní:

class X {
    int foo() {
        return 42;
    }
}
 
abstract class Y {
    abstract int foo();
}
 
interface Z {
    int foo();
}

Po překladu tříd X a Y i rozhraní Z získáme podle očekávání trojici souborů s koncovkou .class. Tyto soubory lze velmi snadno analyzovat, například v tomto seriálu již popsaným jednoduchým nástrojem decompiler5.c (http://icedtea.classpath­.org/people/ptisnovs/jvm-tools/file/0b0f51cf19d2/de­compiler/decompiler5.c). Pro třídu X snadno zjistíme, že k metodě int foo() je skutečně přiřazený atribut Code:

Magicka konstanta: cafebabe
Cislo verze:       50
Cislo podverze:    0
 
Velikost const. poolu: 14 prvku
 
Class/interface attributes: 0x0020
    ACC_SUPER
 
Class name is stored in constant pool #1
Class            13        X
Super class name is stored in constant pool #2
Class            14        java/lang/Object
 
Number of implemented interfaces: 0
 
Number of declared fields: 0
 
Number of declared methods: 2
  Method name:'<init>'
  Descriptor:  ()V
    Field flags: 0x0000
  Attributes:  1
    Name:  'Code'
    Value: ' 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00 01 00 07 00 00 00 06 00 01 00 00 00 01'
 
  Method name:'foo'
  Descriptor:  ()I
    Field flags: 0x0000
  Attributes:  1
    Name:  'Code'
    Value: ' 00 01 00 01 00 00 00 03 10 2a ac 00 00 00 01 00 07 00 00 00 06 00 01 00 00 00 03'

V přeložené třídě Y sice taktéž existuje metoda int foo(), ovšem tato metoda je abstraktní (viz její příznaky – fields) a tudíž k ní není přiřazen žádný atribut Code (jediný kód je součástí konstruktoru třídy):

Magicka konstanta: cafebabe
Cislo verze:       50
Cislo podverze:    0
 
Velikost const. poolu: 14 prvku
 
Class/interface attributes: 0x0420
    ACC_ABSTRACT
    ACC_SUPER
 
Class name is stored in constant pool #1
Class            13        Y
Super class name is stored in constant pool #2
Class            14        java/lang/Object
 
Number of implemented interfaces: 0
 
Number of declared fields: 0
 
Number of declared methods: 2
  Method name:'<init>'
  Descriptor:  ()V
    Field flags: 0x0000
  Attributes:  1
    Name:  'Code'
    Value: ' 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00 01 00 07 00 00 00 06 00 01 00 00 00 07'
 
  Method name:'foo'
  Descriptor:  ()I
    Field flags: 0x0400
    ACC_ABSTRACT
  Attributes:  0

Podobné je to i v případě rozhraní Z, kde došlo jen k malé změně – nenajdeme zde konstruktor a navíc je metoda int foo() nejenom abstraktní, ale i veřejná (public):

Magicka konstanta: cafebabe
Cislo verze:       50
Cislo podverze:    0
 
Velikost const. poolu: 8 prvku
 
Class/interface attributes: 0x0600
    ACC_ABSTRACT
    ACC_INTERFACE
 
Class name is stored in constant pool #0
Class             7        Z
Super class name is stored in constant pool #1
Class             8        java/lang/Object
 
Number of implemented interfaces: 0
 
Number of declared fields: 0
 
Number of declared methods: 1
  Method name:'foo'
  Descriptor:  ()I
    Field flags: 0x0401
    ACC_PUBLIC
    ACC_ABSTRACT
  Attributes:  0

Vytvoření metody bez přiřazeného atributu Code je v případě nástroje Javassist velmi jednoduché. Kostra metody se vytvoří konstruktorem new CtMethod(), kterému se předá návratový typ metody, jméno metody, typy všech parametrů metody i třída, do níž bude metoda vložena. Následně se metodě přiřadí příslušné příznaky, zde konkrétně příznak statické metody a příznak pro metodu veřejnou. Posledním úkonem je vložení metody do její třídy s využitím volání CtClass.addMethod():

    /**
     * Vytvoreni bezparametricke staticke metody foo() vracejici int.
     * Instrukce tvorici telo metody jsou vytvoreny s vyuzitim tridy Bytecode.
     *
     * @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
        generatedClass.addMethod(fooMethod);
    }

3. Zdrojový kód demonstračního příkladu ClassGenerationTest6

Metoda constructMethodFoo() popsaná v předchozí kapitole je součástí dnešního prvního demonstračního příkladu nazvaného ClassGenerationTest6. Jedná se o velmi jednoduchý příklad, který vlastně oproti oběma příkladům popsaným minule nepřináší nic nového. Povšimněte si, že vytvářená třída není abstraktní a navíc se v metodě main() volá i metoda foo(), která však nemá žádné tělo. Tento fakt nástroj Javassist implicitně nekontroluje, takže je možné s jeho pomocí vytvořit i takové soubory .class, které by nebylo možné vygenerovat s využitím standardního překladače javac:

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 metodou foo(), ktera vsak neobsahuje zadny kod.
 *
 * @author Pavel Tisnovsky
 */
public class ClassGenerationTest6 {
 
    /**
     * Jmeno vygenerovane tridy.
     */
    private static final String GENERATED_CLASS_NAME = "GeneratedClass6";
 
    /**
     * 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(foo());" +
        "}";
 
    /**
     * 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 bezparametricke staticke metody foo() vracejici int.
     * Instrukce tvorici telo metody jsou vytvoreny s vyuzitim tridy Bytecode.
     *
     * @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
        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);
 
        // konstrukce nove metody foo()
        constructMethodFoo(generatedClass);
 
        // pridani metody do teto tridy
        addMethodMain(generatedClass);
 
        // ulozeni bajtkodu na disk
        generatedClass.writeFile();
 
        return generatedClass;
    }
 
    /**
     * 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);
    }
 
}

4. Výpis bajtkódu vygenerovaného demonstračním příkladem ClassGenerationTest6

Podívejme se nyní na interní strukturu bajtkódu třídy GeneratedClass6 vytvořené dnešním prvním demonstračním příkladem ClassGenerationTest6. Interní strukturu vypíšeme jednoduše s využitím standardního nástroje javap:

javap -c -private GeneratedClass6

Získáme následující výstup, na němž je nejzajímavější (i když očekávanou) informací fakt, že metoda foo() skutečně nemá žádné tělo, tj. neobsahuje žádné instrukce bajtkódu, na rozdíl od konstruktoru i od metody main():

Compiled from "GeneratedClass6.java"
public class GeneratedClass6 extends java.lang.Object{
 
public static int foo();
 
public static void main(java.lang.String[]);
  Code:
   0:   getstatic       #16; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   invokestatic    #18; //Method foo:()I
   6:   invokevirtual   #24; //Method java/io/PrintStream.println:(I)V
   9:   return
 
public GeneratedClass6();
  Code:
   0:   aload_0
   1:   invokespecial   #29; //Method java/lang/Object."":()V
   4:   return
 
}

5. Spuštění třídy vygenerované demonstračním příkladem ClassGenerationTest6

Zajímavé bude zjistit, co se stane v případě, že se pokusíme o inicializaci a následné „spuštění“ třídy GeneratedClass6 vygenerované dnešním prvním demonstračním příkladem. Inicializaci a spuštění této třídy (přesněji řečeno zavolání metody public static void main(String[] args)) by měl zajistit tento příkaz:

java GeneratedClass6

Ve skutečnosti však virtuální stroj jazyka Java provádí při načítání každé třídy poměrně velké množství kontrol, v jejichž průběhu se spouští několik typů takzvaných verifikátorů načítaného bajtkódu. Jedním ze základních způsobů verifikace je i zjištění, zda všechny neabstraktní a nenativní metody skutečně obsahují atribut s názvem Code. V případě, že tomu tak není – což je i případ naší třídy GeneratedClass6 – neprojde daný bajtkód verifikačním procesem. O tom se lze velmi snadno přesvědčit, když se podíváme na chybové hlášení vypsané příkazem java GeneratedClass6:

Exception in thread "main" java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file GeneratedClass6
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:634)
        at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:277)
        at java.net.URLClassLoader.access$000(URLClassLoader.java:73)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:212)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:205)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:321)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:294)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:266)
Could not find the main class: GeneratedClass6. Program will exit.

6. Tvorba kódu metod na úrovni jednotlivých „strojových“ instrukcí

Nyní se již konečně budeme zabývat důležitější částí dnešního článku – způsobem vytvoření kódů metod poskládaných z jednotlivých „strojových“ instrukcí bajtkódu. Uvidíme, že se nemusí jednat o příliš složitou problematiku, protože mnoho nízkoúrovňových operací za nás dokáže provést přímo knihovna Javassist. Stručně lze způsob vytvoření těla metod s použitím instrukcí shrnout do několika bodů:

  1. Vytvoření obrazu metody, tj. objektu typu CtMethod (již známe).
  2. Nastavení příznaků metody přes CtMethod.setModifiers() (taktéž již známe).
  3. Přidání metody do příslušné třídy s pomocí CtClass.addMethod().
  4. Získání objektu typu MethodInfo přes CtMethod.getMethodInfo()
  5. Získání objektu typu ConstPool přes MethodInfo.getConstPool()
  6. Vytvoření objektu typu Bytecode (zde se použije instance třídy ConstPool).
  7. Přidání libovolného počtu instrukcí do objektu typu Bytecode.
  8. Převod bajtkódu na atribut přes Bytecode.toCodeAttribute()
  9. Přidání nového atributu k metodě s využitím MethodInfo.setCodeAttribute()

Prakticky všechny výše zmíněné body (kromě bodu číslo 7) jsou implementovány ve dvojici uživatelských metod nazvaných constructMethodFoo() a constructMethodAdd(). První z těchto metod slouží pro konstrukci metody s hlavičkou public static int foo(), druhá pak pro vytvoření metody public static int add(int, int). Obě generované metody se tedy od sebe liší v jednom důležitém ohledu – počtu svých argumentů.

Následuje výpis zdrojového kódu uživatelské metody constructMethodFoo():

    /**
     * Vytvoreni bezparametricke staticke metody foo() vracejici int.
     * Instrukce tvorici telo metody jsou vytvoreny s vyuzitim tridy Bytecode.
     *
     * @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
        generatedClass.addMethod(fooMethod);
 
        MethodInfo methodInfo = fooMethod.getMethodInfo();
 
        ConstPool constPool = methodInfo.getConstPool();
        Bytecode bytecode = generateBytecodeForMethodFoo(constPool);
        CodeAttribute codeAttribute = bytecode.toCodeAttribute();
 
        methodInfo.setCodeAttribute(codeAttribute);
    }

Uživatelská metoda constructMethodAdd() je prakticky stejná s metodou constructMethodFoo(); jediná větší odlišnost spočívá ve specifikaci argumentů vytvářené metody add():

    /**
     * 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 constructMethodAdd(CtClass generatedClass) throws CannotCompileException {
        CtClass returnType = CtClass.intType;
        CtClass[] parameterTypes = {CtClass.intType, CtClass.intType};
 
        // u metody je nutne znat jeji jmeno, navratovou hodnotu i typy parametru
        CtMethod addMethod = new CtMethod(returnType, "add", parameterTypes, generatedClass);
 
        // zmena modifikatoru
        addMethod.setModifiers(Modifier.STATIC | Modifier.PUBLIC);
 
        // telo metody muze byt slozeno z jednoho vyrazu
        generatedClass.addMethod(addMethod);
 
        MethodInfo methodInfo = addMethod.getMethodInfo();
 
        ConstPool constPool = methodInfo.getConstPool();
        Bytecode bytecode = generateBytecodeForMethodAdd(constPool);
        CodeAttribute codeAttribute = bytecode.toCodeAttribute();
 
        methodInfo.setCodeAttribute(codeAttribute);
    }

7. Vygenerování metody int foo()

Zbývá nám programově vytvořit sekvenci instrukcí tvořících těla metod int foo() a int add(int, int). Jednodušší je tělo metody int foo(), protože zde vyžadujeme pouze to, aby metoda jednoduše vrátila celočíselnou konstantu 42. Tuto operaci lze realizovat s využitím dvojice instrukcí. První instrukce uloží zmíněnou celočíselnou konstantu 42 na lokální zásobník operandů vytvořený při vstupu do metody a následně se tato hodnota vrátí s využitím instrukce ireturn. Pro uložení celočíselné konstanty na zásobník operandů slouží instrukce bipush, sipush, ldc, ldc_w a navíc i specializované instrukce iconst_m1, iconst0, iconst1iconst5:

# Instrukce Opkód Data 1 Data 2 Typ na zásobníku Popis
1 iconst_m1 0×02     int uložení konstanty –1 na zásobník
2 iconst0 0×03     int uložení konstanty 0 na zásobník
3 iconst1 0×04     int uložení konstanty 1 na zásobník
4 iconst2 0×05     int uložení konstanty 2 na zásobník
5 iconst3 0×06     int uložení konstanty 3 na zásobník
6 iconst4 0×07     int uložení konstanty 4 na zásobník
7 iconst5 0×08     int uložení konstanty 5 na zásobník
8 bipush 0×10 byteconst   int uložení „byteconst“ na zásobník s konverzí na int
9 sipush 0×11 hi-byte lowbyte int uložení slova hibyte-lowbyte na zásobník s konverzí na int
10 ldc 0×12 index   string/ref/int/float načte konstantu z constant poolu (může se jednat i o referenci)
11 ldc_w 0×13 hi-byte lowbyte string/ref/int/float načte konstantu z constant poolu (index je šestnáctibitový)

My se však většinou nemusíme starat o to, kterou z těchto instrukcí použít, protože je možné jednoduše využít metodu Bytecode.addIconst(), která již potřebnou optimalizaci provede automaticky. V následující metodě si povšimněte, že při vytváření instance třídy Bytecode je nutné ručně uvést maximální velikost zásobníku operandů i počet lokálních proměnných:

    /**
     * Vytvoreni bajtkodu predstavujiciho sekvenci instrukci pro metodu int
     * foo().
     * 
     * @param constPool
     *            tabulka konstant pouzivana metodou
     * @return bajtkod predstavujici sekvenci instrukci pro metodu int foo()
     */
    private static Bytecode generateBytecodeForMethodFoo(ConstPool constPool)
    {
        final int stackSize = 1;
        final int localVars = 0;
        Bytecode bytecode = new Bytecode(constPool, stackSize, localVars);
        // bipush 42
        bytecode.addIconst(42);
        // ireturn
        bytecode.addReturn(CtClass.intType);
        // bajtkod bude obsahovat:
        // bipush 42
        // ireturn
        return bytecode;
    }

8. Vygenerování metody int add(int, int)

Vytvoření bajtkódu metody int add(int, int) je již nepatrně složitější. Tato metoda totiž musí na zásobník operandů uložit hodnotu svých dvou argumentů, provést součet těchto hodnot a výsledek operace vrátit s využitím nám již známé instrukce ireturn. Oba celočíselné argumenty lze na zásobník operandů uložit pomocí instrukcí iload n, kde se za n doplní index příslušného argumentu. Vzhledem k tomu, že se jedná o statickou metodu (do níž se nepředává implicitní argument this), je uložení hodnoty prvního argumentu na zásobník operandů zajištěno instrukcí iload 0 a uložení hodnoty argumentu druhého pak instrukcí iload 1. Následný součet zajistí instrukce iadd (ta nemá žádný parametr). Povšimněte si toho, jak se zvětšila kapacita zásobníku operandů i počet lokálních proměnných:

    /**
     * Vytvoreni bajtkodu predstavujiciho sekvenci instrukci pro metodu int
     * add(int, int).
     * 
     * @param constPool
     *            tabulka konstant pouzivana metodou
     * @return bajtkod predstavujici sekvenci instrukci pro metodu int add(int,
     *         int)
     */
    private static Bytecode generateBytecodeForMethodAdd(ConstPool constPool)
    {
        final int stackSize = 2;
        final int localVars = 2;
        Bytecode bytecode = new Bytecode(constPool, stackSize, localVars);
        // iload 0 nebo iload_0
        bytecode.addIload(0);
        // iload 1 nebo iload_1
        bytecode.addIload(1);
        // iadd
        bytecode.add(Opcode.IADD);
        // ireturn
        bytecode.addReturn(CtClass.intType);
        return bytecode;
    }

Zajímavé je, že některé instrukce je nutné generovat přímo s využitím třídy Opcode, přesněji řečeno konstant deklarovaných v této třídě, zatímco pro vytvoření jiných instrukcí existují příslušné metody ve třídě Bytecode. Tyto metody jsou implementovány z toho důvodu, že na základě konkrétní hodnoty svého argumentu dokážou vybrat tu instrukci bajtkódu, která je optimální z hlediska jeho celkové velikosti, což si ukážeme v kapitole číslo 10. Nic nám však nebrání používat přímo konstanty z třídy Opcode ve všech případech a zabránit tak všem optimalizacím.

9. Zdrojový kód demonstračního příkladu ClassGenerationTest7

Všechny čtyři výše popsané uživatelské metody constructMethodFoo(), constructMethodAdd(), generateBytecodeForMethodFoo() a generateBytecodeForMethodAdd() jsou součástí dnešního druhého demonstračního příkladu nazvaného ClassGenerationTest7. V tomto příkladu se provede dvojice operací. Nejdříve se vytvoří nová třída nazvaná GeneratedClass7 s trojicí vygenerovaných metod main(), foo() a add() a následně se struktura celé této třídy vypíše na standardní výstup s využitím stejného postupu, s jakým jsme se seznámili již v předchozí části tohoto seriálu – zpětným přečtením kódu každé metody s následnou iterací přes získaný kód a výpisem symbolických jmen jednotlivých instrukcí. Následuje výpis celé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.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.BadBytecode;
import javassist.bytecode.Bytecode;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.CodeIterator;
import javassist.bytecode.ConstPool;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.Mnemonic;
import javassist.bytecode.Opcode;
 
 
 
/**
 * Test moznosti nastroje Javassist - vygenerovani jednoduche tridy
 * s metodou main a nekolika dalsimi metodami.
 *
 * @author Pavel Tisnovsky
 */
public class ClassGenerationTest7 {
 
    /**
     * Jmeno vygenerovane tridy.
     */
    private static final String GENERATED_CLASS_NAME = "GeneratedClass7";
 
    /**
     * 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(foo());" +
        "    System.out.println(add(1,2));" +
        "}";
 
    /**
     * 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 bezparametricke staticke metody foo() vracejici int.
     * Instrukce tvorici telo metody jsou vytvoreny s vyuzitim tridy Bytecode.
     *
     * @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
        generatedClass.addMethod(fooMethod);
 
        MethodInfo methodInfo = fooMethod.getMethodInfo();
 
        ConstPool constPool = methodInfo.getConstPool();
        Bytecode bytecode = generateBytecodeForMethodFoo(constPool);
        CodeAttribute codeAttribute = bytecode.toCodeAttribute();
 
        methodInfo.setCodeAttribute(codeAttribute);
    }
 
    /**
     * 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 constructMethodAdd(CtClass generatedClass) throws CannotCompileException {
        CtClass returnType = CtClass.intType;
        CtClass[] parameterTypes = {CtClass.intType, CtClass.intType};
 
        // u metody je nutne znat jeji jmeno, navratovou hodnotu i typy parametru
        CtMethod addMethod = new CtMethod(returnType, "add", parameterTypes, generatedClass);
 
        // zmena modifikatoru
        addMethod.setModifiers(Modifier.STATIC | Modifier.PUBLIC);
 
        // telo metody muze byt slozeno z jednoho vyrazu
        generatedClass.addMethod(addMethod);
 
        MethodInfo methodInfo = addMethod.getMethodInfo();
 
        ConstPool constPool = methodInfo.getConstPool();
        Bytecode bytecode = generateBytecodeForMethodAdd(constPool);
        CodeAttribute codeAttribute = bytecode.toCodeAttribute();
 
        methodInfo.setCodeAttribute(codeAttribute);
    }
 
    /**
     * Vytvoreni bajtkodu predstavujiciho sekvenci instrukci pro metodu int
     * foo().
     * 
     * @param constPool
     *            tabulka konstant pouzivana metodou
     * @return bajtkod predstavujici sekvenci instrukci pro metodu int foo()
     */
    private static Bytecode generateBytecodeForMethodFoo(ConstPool constPool)
    {
        final int stackSize = 1;
        final int localVars = 0;
        Bytecode bytecode = new Bytecode(constPool, stackSize, localVars);
        bytecode.addIconst(42);
        bytecode.addReturn(CtClass.intType);
        // bajtkod bude obsahovat:
        // bipush 42
        // ireturn
        return bytecode;
    }
 
    /**
     * Vytvoreni bajtkodu predstavujiciho sekvenci instrukci pro metodu int
     * add(int, int).
     * 
     * @param constPool
     *            tabulka konstant pouzivana metodou
     * @return bajtkod predstavujici sekvenci instrukci pro metodu int add(int,
     *         int)
     */
    private static Bytecode generateBytecodeForMethodAdd(ConstPool constPool)
    {
        final int stackSize = 2;
        final int localVars = 2;
        Bytecode bytecode = new Bytecode(constPool, stackSize, localVars);
        bytecode.addIload(0);
        bytecode.addIload(1);
        bytecode.add(Opcode.IADD);
        bytecode.addReturn(CtClass.intType);
        return bytecode;
    }
 
    /**
     * 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);
 
        // konstrukce nove metody foo()
        constructMethodFoo(generatedClass);
 
        // konstrukce nove metody add()
        constructMethodAdd(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, "foo");
        printMethodStructure(generatedClass, "add");
    }
 
    /**
     * 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. Výpis bajtkódu vygenerovaného demonstračním příkladem ClassGenerationTest7

Bude jistě zajímavé podívat se na strukturu bajtkódu třídy GeneratedClass7 vygenerované dnešním druhým demonstračním příkladem ClassGenerationTest7. Pro výpis struktury bajtkódu opět využijeme standardní nástroj javap:

javap -c -private GeneratedClass7

Z výpisu uvedeného pod tímto odstavcem je patrné, že se skutečně obě metody foo()add() vytvořily způsobem, který byl vyžadován. Povšimněte si však některých „optimalizací“, které nástroj Javassist provedl. První optimalizací je uložení konstanty 42 na zásobník operandů s využitím instrukce bipush, tj. instrukce sloužící pro uložení bajtu, který je následně expandován na plnohodnotný typ integer. Druhá optimalizace spočívá v použití instrukcí iload0 a iload1 namísto obecných instrukcí iload x. I v tomto případě Javassist zvolil takový typ instrukcí, které vedou k vytvoření co nejkratšího bajtkódu, a to v souladu se specifikací virtuálního stroje Javy (podobné základní optimalizace ostatně provádí i samotný nástroj javac):

bitcoin_skoleni

Compiled from "GeneratedClass7.java"
public class GeneratedClass7 extends java.lang.Object{
public static int foo();
  Code:
   0:   bipush  42
   2:   ireturn
 
public static int add(int, int);
  Code:
   0:   iload_0
   1:   iload_1
   2:   iadd
   3:   ireturn
 
public static void main(java.lang.String[]);
  Code:
   0:   getstatic   #19; //Field java/lang/System.out:Ljava/io/PrintStream;
   3:   invokestatic    #21; //Method foo:()I
   6:   invokevirtual   #27; //Method java/io/PrintStream.println:(I)V
   9:   getstatic   #19; //Field java/lang/System.out:Ljava/io/PrintStream;
   12:  iconst_1
   13:  iconst_2
   14:  invokestatic    #29; //Method add:(II)I
   17:  invokevirtual   #27; //Method java/io/PrintStream.println:(I)V
   20:  return
 
public GeneratedClass7();
  Code:
   0:   aload_0
   1:   invokespecial   #33; //Method java/lang/Object."<init>":()V
   4:   return

}

Tento výpis si můžeme porovnat s analýzou bajtkódu provedenou v samotném demonstračním příkladu:

class generation begin: GeneratedClass7
Method 'main' structure:
    real name:    main
    descriptor:   ([Ljava/lang/String;)V
    access flags: public static
    method body:
        getstatic
        invokestatic
        invokevirtual
        getstatic
        iconst_1
        iconst_2
        invokestatic
        invokevirtual
        return
 
Method 'foo' structure:
    real name:    foo
    descriptor:   ()I
    access flags: public static
    method body:
        bipush
        ireturn
 
Method 'add' structure:
    real name:    add
    descriptor:   (II)I
    access flags: public static
    method body:
        iload_0
        iload_1
        iadd
        ireturn
 
class generation end: GeneratedClass7

11. Repositář se zdrojovými kódy obou dnešních 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.