Hlavní navigace

Pohled pod kapotu JVM – změna systémových tříd s využitím nástroje Javassist

3. 9. 2013
Doba čtení: 20 minut

Sdílet

V této části seriálu o programovacím jazyce Java si na dvojici demonstračních příkladů ukážeme, jakým způsobem lze modifikovat bajtkód systémových tříd s využitím nástroje Javassist i to, jak je následně možné tyto třídy použít v nově spuštěné JVM. Taktéž si vysvětlíme činnost přepínače -Xbootclasspath.

Obsah

1. Pohled pod kapotu JVM – změna systémových tříd s využitím nástroje Javassist

2. Příklad testující chování metody java.util.Random.nextInt()

3. Původní podoba bajtkódu metody java.util.Random.nextInt()

4. Modifikace metody java.util.Random.nextInt() tak, aby se vracela konstanta

5. Podoba upraveného bajtkódu metody java.util.Random.nextInt()

6. Parametr -Xbootclasspath použitý při spouštění JVM

7. Spuštění testu s načtením upravené třídy java.util.Random

8. Modifikace metody java.util.Random.nextInt() tak, aby se vracela sekvence čísel (přidání skrytého atributu)

9. Podoba upraveného bajtkódu metody java.util.Random.nextInt()

10. Spuštění testu s načtením upravené třídy java.util.Random

11. Repositář se zdrojovými kódy všech demonstračních i testovacích příkladů

12. Odkazy na Internetu

1. Pohled pod kapotu JVM – změna systémových tříd 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 si řekneme, jakým způsobem je možné využít nástroj Javassist pro modifikaci systémových tříd, tj. (zjednodušeně řečeno) takových tříd, které jsou načteny virtuálním strojem Javy ještě před tím, než je inicializována aplikace spuštěná v rámci JVM. Tyto třídy není možné s využitím nástroje Javassist jednoduše změnit v již běžící JVM, a to z toho důvodu, že samotný Javassist je na těchto třídách závislý. Jediný oficiálně dostupný způsob změny těchto tříd spočívá v implementaci JVM TI agenta, což je téma, kterému jsme se věnovali v předchozích částech tohoto seriálu. Ovšem nástroj Javassist samozřejmě dokáže modifikovat bajtkód systémových tříd a následně umožňuje jejich modifikovanou podobu uložit na disk (do nového souboru .class) a posléze načíst změněnou třídu či třídy při inicializaci nového virtuálního stroje Javy.

Jen pro připomenutí si ukažme, jaké třídy jsou načteny v případě, že se z příkazového řádku zadá příkaz:

java -agentpath:./libjvmtiagent.so Test

kde se za libjvmtiagent.so doplní jméno jednoduchého JVM TI agenta, který na standardní výstup vypisuje všechny načítané třídy. Nás nyní budou zajímat ty třídy, které se do virtuálního stroje Javy načtou ještě před načtením testovací třídy Test. Následující výpis je sice závislý na konkrétní verzi JVM, nicméně základ zůstává na všech typech JVM shodný:

JVM TI agent: Agent_OnLoad
JVM TI agent: JVM TI version is correct
JVM TI agent: Got VM init event
Class load:   java.lang.System;
Class load:   java.nio.charset.Charset;
Class load:   java.lang.String;
Class prepare:java.lang.ClassNotFoundException;
Class load:   java.net.URLClassLoader$1;
Class prepare:java.net.URLClassLoader$1;
Class load:   sun.misc.URLClassPath$3;
Class prepare:sun.misc.URLClassPath$3;
Class load:   sun.misc.URLClassPath$Loader;
Class load:   sun.misc.URLClassPath$JarLoader;
Class prepare:sun.misc.URLClassPath$Loader;
Class prepare:sun.misc.URLClassPath$JarLoader;
Class prepare:java.lang.StringBuffer;
Class prepare:java.lang.Short;
Class load:   sun.misc.URLClassPath$JarLoader$1;
Class prepare:sun.misc.URLClassPath$JarLoader$1;
Class load:   sun.misc.FileURLMapper;
Class prepare:sun.misc.FileURLMapper;
Class load:   java.util.zip.ZipConstants;
Class load:   java.util.zip.ZipFile;
Class load:   java.util.jar.JarFile;
Class prepare:java.util.zip.ZipConstants;
Class prepare:java.util.zip.ZipFile;
Class prepare:java.util.jar.JarFile;
Class load:   sun.misc.JavaUtilJarAccess;
Class load:   java.util.jar.JavaUtilJarAccessImpl;
Class prepare:sun.misc.JavaUtilJarAccess;
Class prepare:java.util.jar.JavaUtilJarAccessImpl;
Class load:   sun.misc.JarIndex;
Class prepare:sun.misc.JarIndex;
Class load:   sun.misc.ExtensionDependency;
Class prepare:sun.misc.ExtensionDependency;
Class load:   java.util.zip.ZipEntry;
Class prepare:java.util.zip.ZipEntry;
Class load:   java.util.jar.JarEntry;
Class load:   java.util.jar.JarFile$JarFileEntry;
Class prepare:java.util.jar.JarEntry;
Class prepare:java.util.jar.JarFile$JarFileEntry;
Class load:   java.io.DataInput;
Class load:   java.io.DataInputStream;
Class prepare:java.io.DataInput;
Class prepare:java.io.DataInputStream;
Class load:   java.util.zip.ZipFile$ZipFileInputStream;
Class prepare:java.util.zip.ZipFile$ZipFileInputStream;
Class load:   java.security.PrivilegedActionException;
Class prepare:java.security.PrivilegedActionException;
Class load:   sun.misc.URLClassPath$FileLoader;
Class prepare:sun.misc.URLClassPath$FileLoader;
Class load:   sun.misc.Resource;
Class load:   sun.misc.URLClassPath$FileLoader$1;
Class prepare:sun.misc.Resource;
Class prepare:sun.misc.URLClassPath$FileLoader$1;
Class load:   sun.nio.ByteBuffered;
Class load:   java.security.CodeSource;
Class prepare:java.security.CodeSource;
Class load:   java.security.PermissionCollection;
Class load:   java.security.Permissions;
Class prepare:java.security.PermissionCollection;
Class prepare:java.security.Permissions;
Class load:   java.net.URLConnection;
Class load:   sun.net.www.URLConnection;
Class load:   sun.net.www.protocol.file.FileURLConnection;
Class prepare:java.net.URLConnection;
Class prepare:sun.net.www.URLConnection;
Class prepare:sun.net.www.protocol.file.FileURLConnection;
Class load:   java.net.ContentHandler;
Class load:   java.net.UnknownContentHandler;
Class prepare:java.net.ContentHandler;
Class prepare:java.net.UnknownContentHandler;
Class load:   sun.net.www.MessageHeader;
Class prepare:sun.net.www.MessageHeader;
Class load:   java.io.FilePermission;
Class prepare:java.io.FilePermission;
Class load:   java.io.FilePermission$1;
Class prepare:java.io.FilePermission$1;
Class load:   java.security.Policy;
Class load:   sun.security.provider.PolicyFile;
Class prepare:java.security.Policy;
Class prepare:sun.security.provider.PolicyFile;
Class load:   java.security.Policy$UnsupportedEmptyCollection;
Class prepare:java.security.Policy$UnsupportedEmptyCollection;
Class load:   java.io.FilePermissionCollection;
Class prepare:java.io.FilePermissionCollection;
Class load:   java.security.AllPermission;
Class load:   java.security.UnresolvedPermission;
Class load:   java.security.BasicPermissionCollection;
Class prepare:java.security.BasicPermissionCollection;
Class prepare:java.security.ProtectionDomain;
Class load:   sun.misc.JavaSecurityProtectionDomainAccess;
Class load:   java.security.ProtectionDomain$2;
Class prepare:sun.misc.JavaSecurityProtectionDomainAccess;
Class prepare:java.security.ProtectionDomain$2;
Class load:   java.security.ProtectionDomain$Key;
Class prepare:java.security.ProtectionDomain$Key;
Class load:   java.security.Principal;
Class load:   java.security.cert.Certificate;
Class load:   java.lang.Object;
Class load:   Test;
Class prepare:Test;

Co se však stane v případě, že si v Javassistu vynutíme změnu nějaké systémové třídy? Není nic jednoduššího, než si to vyzkoušet:

        // ziskat vychozi class pool
        ClassPool pool = ClassPool.getDefault();
 
        // objekt predstavujici menenou (modifikovanou) tridu
        CtClass classToModify = pool.get("java.util.Random");
 
        // tridu nebude mozne dale menit
        classToModify.freeze();
 
        // pokus o nacteni zmenene tridy
        classToModify.toClass();

Spuštění tohoto kódu nedopadne nejlépe:

javassist.CannotCompileException: by java.lang.SecurityException: Prohibited package name: java.util
        at javassist.ClassPool.toClass(ClassPool.java:1099)
        at javassist.ClassPool.toClass(ClassPool.java:1042)
        at javassist.ClassPool.toClass(ClassPool.java:1000)
        at javassist.CtClass.toClass(CtClass.java:1140)
        at SystemClassModificationTest2.modifyRandomClass(SystemClassModificationTest2.java:41)
        at SystemClassModificationTest2.main(SystemClassModificationTest2.java:83)
Caused by: java.lang.SecurityException: Prohibited package name: java.util
        at java.lang.ClassLoader.preDefineClass(ClassLoader.java:479)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:614)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:465)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at javassist.ClassPool.toClass2(ClassPool.java:1112)
        at javassist.ClassPool.toClass(ClassPool.java:1093)
        ... 5 more

Vidíme, že tuto operaci nepovolil samotný virtuální stroj Javy.

2. Příklad testující chování metody java.util.Random.nextInt()

Dnešní demonstrační příklady měnící bajtkódy systémových tříd (resp. jediné třídy) jsou založeny na reálném problému, který v minulosti vznikl při snaze o testování aplikace, v níž se používala řada pseudonáhodných čísel generovaných s využitím třídy java.util.Random. Aby se testy snáze psaly, bylo nutné třídu java.util.Random vhodně upravit takovým způsobem, aby se namísto sekvence pseudonáhodných hodnot vracela buď série konstant nebo jednoduchá aritmetická posloupnost.

My si tento reálný problém trošku zjednodušíme a budeme vlastnosti třídy java.util.Random testovat následujícím příkladem, který po svém spuštění vytvoří monochromatický rastrový obrázek o velikosti 256×256 pixelů, který je vyplněn „šumem“, jehož hodnoty jsou získány metodou java.util.Random.nextInt(). Obrázek je typu TYPE_BYTE_GRAY, což znamená, že každý pixel je představován celočíselnou hodnotou 0..255 udávající světlost pixelu od zcela černé až po bílou. Z tohoto důvodu je vyplnění bitmapy velmi jednoduše dosažitelné přes java.awt.image.DataBuffer a není tak zapotřebí používat mnohem pomalejší metodu java.awt.image.BufferedIma­ge.setRGB(int, int, int) :

import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.io.File;
import java.io.IOException;
import java.util.Random;
 
import javax.imageio.ImageIO;
 
/**
 * Trida, v niz se otestuje funkce metody Random.nextInt().
 *
 * @author Pavel Tisnovsky
 */
public class TestRandom {
 
    /**
     * Jmeno souboru s vygenerovanou bitmapou.
     */
    private static final String OUTPUT_FILE_NAME = "test.png";
 
    /**
     * Horizontalni rozmer bitmapy.
     */
    private static final int IMAGE_HEIGHT = 256;
 
    /**
     * Vertikalni rozmer bitmapy.
     */
    private static final int IMAGE_WIDTH = 256;
 
    /**
     * Vytvoreni nove bitmapy se stupni sedi.
     *
     * @return nove vytvorena bitmapa
     */
    private static BufferedImage createImage() {
        return new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_BYTE_GRAY);
    }
 
    /**
     * Zapis bitmapy na disk ve formatu PNG.
     * 
     * @param image
     *            testovaci bitmapa
     * @throws IOException
     */
    private static void writeImage(BufferedImage image) throws IOException {
        ImageIO.write(image, "png", new File(OUTPUT_FILE_NAME));
    }
 
    /**
     * Ziskani objektu typu DataBuffer obsahujiciho hodnoty jednotlivych pixelu.
     * 
     * @param image
     *            testovaci bitmapa
     * @return objekt typu DatabBuffer pro predanou bitmapu
     */
    private static DataBuffer getImageDataBuffer(BufferedImage image) {
        Raster raster = image.getRaster();
        return raster.getDataBuffer();
    }
 
    /**
     * Vyplneni bitmapy sumem ziskanym funkci Random.nextInt().
     * 
     * @param image
     *            testovaci bitmapa
     * @param dataBuffer
     *            objekt typu DatabBuffer pro predanou bitmapu
     * @param random
     *            instance tridy Random pouzita pro generovani sumu
     */
    private static void fillImageByNoise(BufferedImage image, DataBuffer dataBuffer, Random random) {
        int i=0;
        for (int y = 0; y < image.getHeight(); y++) {
            for (int x = 0; x < image.getWidth(); x++) {
                dataBuffer.setElem(i, random.nextInt());
                i++;
            }
        }
    }
 
    /**
     * Spusteni testu tridy java.util.Random.
     */
    public static void main(String[] args) throws IOException {
        // vytvoreni bitmapy
        BufferedImage image = createImage();
 
        // ziskani objektu obsahujiciho hodnoty pixelu bitmapy
        DataBuffer dataBuffer = getImageDataBuffer(image);
 
        // vytvoreni instance tridy java.util.Random, kterou budeme
        // pouzivat v testu
        Random random = new Random(42L);
 
        // vyplneni bitmapy sumem ziskanym funkci Random.nextInt()
        fillImageByNoise(image, dataBuffer, random);
 
        // zapis bitmapy na disk
        writeImage(image);
    }
 
}

Po spuštění tohoto příkladu by se měl na disku vytvořit soubor se jménem „test.png“:

Obrázek 1: Obrázek vytvořený testovací třídou TestRandom v případě, že se použije standardní nezměněná třída java.util.Random.

3. Původní podoba bajtkódu metody java.util.Random.nextInt()

Můžeme se podívat na to, jak je vlastně metoda java.util.Random.nextInt() implementována (a to kvůli tréninku bez sledování jejího zdrojového kódu :-). Vidíme, že Random.nextInt() interně volá další metodu Random.next(32), přičemž konstanta 32 specifikuje počet bitů náhodného čísla:

Compiled from "Random.java"
public class java.util.Random extends java.lang.Object implements java.io.Serializable{
 
static final long serialVersionUID;
 
public int nextInt();
  Code:
   0:           aload_0
   1:           bipush          32
   3:           invokevirtual   #225; //Method next:(I)I
   6:           ireturn

Bajtkód metody Random.next(bits) sice může vypadat poněkud tajemně, ale jeho smysl je velmi dobře vysvětlen v dokumentaci. Provádí se totiž následující operace, a to atomicky (s využitím třídy AtomicLong zajišťující atomickou změnu své hodnoty):

(seed * 25214903917L + 11L) & ((1L << 48) - 1)
return (int)(seed >>> (48 - bits)).
 
protected int next(int);
  Code:
   0:           aload_0
   1:           getfield        #202; //Field seed:Ljava/util/concurrent/atomic/AtomicLong;
   4:           astore          6
   6:           aload           6
   8:           invokevirtual   #229; //Method java/util/concurrent/atomic/AtomicLong.get:()J
   11:          lstore_2
   12:          lload_2
   13:          ldc2_w          #103; //long 25214903917l
   16:          lmul
   17:          ldc2_w          #101; //long 11l
   20:          ladd
   21:          ldc2_w          #105; //long 281474976710655l (==FFFFFFFFFFFF, maska)
   24:          land
   25:          lstore          4
   27:          aload           6
   29:          lload_2
   30:          lload           4
   32:          invokevirtual   #232; //Method java/util/concurrent/atomic/AtomicLong.compareAndSet:(JJ)Z
   35:          ifeq            6
   38:          lload           4
   40:          bipush          48
   42:          iload_1
   43:          isub
   44:          lushr
   45:          l2i
   46:          ireturn
 
}

4. Modifikace metody java.util.Random.nextInt() tak, aby se vracela konstanta

Našim prvním úkolem bude úprava metody java.util.Random.nextInt() takovým způsobem, aby se namísto sekvence pseudonáhodných hodnot vracela konstantní hodnota, dejme tomu pro jednoduchost nula. To je velmi snadné, protože pouze nahradíme původní bajtkód metody Random.nextInt() bajtkódem novým. Tento bajtkód vznikne překladem výrazu return 0;, což za nás provede nástroj Javassist automaticky:

import java.io.IOException;
 
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
 
 
 
/**
 * Test moznosti nastroje Javassist - zmena chovani standardni (systemove)
 * tridy java.util.Random.
 *
 * @author Pavel Tisnovsky
 */
public class SystemClassModificationTest1 {
 
    /**
     * Modifikace metody Random.nextInt().
     *
     * @throws CannotCompileException
     *             vyhozena v pripade, ze se nepodaril preklad metody 
     * @throws NotFoundException
     *             vyhozena, pokud trida ci metoda nebyla nalezena
     * @throws IOException 
     *             vyhozena v pripade, ze se nepodarilo ulozit vysledny bajtkod do souboru
     */
    private static void modifyRandomClass() throws CannotCompileException, NotFoundException, IOException {
        // ziskat vychozi class pool
        ClassPool pool = ClassPool.getDefault();
 
        // objekt predstavujici menenou (modifikovanou) tridu
        CtClass classToModify = pool.get("java.util.Random");
 
        // zmena tela metody nextInt()
        changeMethodNextInt(classToModify);
 
        // tridu nebude mozne dale menit
        classToModify.freeze();
 
        // ulozeni modifikovane tridy do souboru
        classToModify.writeFile(".");
    }
 
    /**
     * Zmena tela metody nextInt().
     *
     * @param classToModify
     *            modifikovana trida
     * @throws NotFoundException
     *             vyhozena, pokud metoda nebyla nalezena
     * @throws CannotCompileException
     *             vyhozena v pripade, ze se nepodaril preklad metody
     */
    private static void changeMethodNextInt(CtClass classToModify) throws NotFoundException, CannotCompileException {
        CtMethod method = classToModify.getMethod("nextInt", "()I");
        method.setBody("return 0;");
    }
 
    /**
     * Spusteni modifikatoru tridy java.util.Random.
     *
     * @param args nevyuzito
     */
    public static void main(String[] args) {
        try {
            modifyRandomClass();
        }
        catch (CannotCompileException e) {
            e.printStackTrace();
        }
        catch (NotFoundException e) {
            e.printStackTrace();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
 
}

Povšimněte si, že třída, která se má modifikovat, se získá příkazy:

        // ziskat vychozi class pool
        ClassPool pool = ClassPool.getDefault();
 
        // objekt predstavujici menenou (modifikovanou) tridu
        CtClass classToModify = pool.get("java.util.Random");

a její zápis na disk se provede:

        // tridu nebude mozne dale menit
        classToModify.freeze();
 
        // ulozeni modifikovane tridy do souboru
        classToModify.writeFile(".");

Význam tečky použité v posledním příkazu bude vysvětlen v navazující kapitole.

5. Podoba upraveného bajtkódu metody java.util.Random.nextInt()

Demonstrační příklad SystemClassModificationTest1 popsaný ve čtvrté kapitole je nutné přeložit následujícím způsobem, který předpokládá, že se v pracovním adresáři nachází i Java archiv javassist.jar:

javac -cp javassist.jar SystemClassModificationTest1.java

Spuštění příkladu na Linuxu:

java -cp .:javassist.jar SystemClassModificationTest1

Spuštění příkladu na MS Windows:

java -cp .;javassist.jar SystemClassModificationTest1

Nezávisle na tom, zda je příklad spuštěn na Linuxu či ve Windows by se měl v pracovním adresáři vytvořit podadresář java obsahující další podadresář util s novým bajtkódem třídy Random. Ona tečka, o které jsme se v předchozí kapitole zmiňovali totiž určuje absolutní či relativní cestu do adresáře, který je považován za základ CP (tedy „počátek“ hierarchie balíčků). A jelikož jsme modifikovali a uložili bajtkód třídy java.util.Random, je umístěn do ./java/util/Random.class.

Pohledem na vytvořený bajtkód (příkaz javap) se přesvědčíme, zda se modifikace třídy java.util.Random skutečně zdařila:

Compiled from "Random.java"
public class java.util.Random extends java.lang.Object implements java.io.Serializable{
 
static final long serialVersionUID;
 
public int nextInt();
  Code:
   0:           iconst_0
   1:           ireturn
 
}

6. Parametr -Xbootclasspath použitý při spouštění JVM

Nyní se dostáváme k dalšímu problému – jakým způsobem máme virtuálnímu stroji Javy říci, že má načíst naši upravenou třídu java.util.Random a nikoli originální podobu této třídy uložené v souboru rt.jar? Zde nám nepomůže změna classpath (CP), ale musíme použít (nestandardní) přepínač -Xbootclasspath, jehož tři možné podoby najdeme snadno po zápisu příkazu java -X do konzole:

~$ java -X
    -Xmixed           mixed mode execution (default)
    -Xint             interpreted mode execution only
    -Xbootclasspath:<directories and zip/jar files separated by ;>
                      set search path for bootstrap classes and resources
    -Xbootclasspath/a:<directories and zip/jar files separated by ;>
                      append to end of bootstrap class path
    -Xbootclasspath/p:<directories and zip/jar files separated by ;>
                      prepend in front of bootstrap class path
    -Xnoclassgc       disable class garbage collection
    -Xincgc           enable incremental garbage collection
    -Xloggc:<file>    log GC status to a file with time stamps
    -Xbatch           disable background compilation
    -Xms<size>        set initial Java heap size
    -Xmx<size>        set maximum Java heap size
    -Xss<size>        set java thread stack size
    -Xprof            output cpu profiling data
    -Xfuture          enable strictest checks, anticipating future default
    -Xrs              reduce use of OS signals by Java/VM (see documentation)
    -Xcheck:jni       perform additional checks for JNI functions
    -Xshare:off       do not attempt to use shared class data
    -Xshare:auto      use shared class data if possible (default)
    -Xshare:on        require using shared class data, otherwise fail.
 
The -X options are non-standard and subject to change without notice.

Z předchozí nápovědy vyplývá, že použijeme volbu -Xbootclasspath/p:, jelikož potřebujeme, aby naše modifikovaná třída java.util.Random měla přednost před stejně pojmenovanou třídou z archivu rt.jar.

7. Spuštění testu s načtením upravené třídy java.util.Random

Se znalostí funkce přepínače -Xbootclasspath je již nové spuštění demonstračního příkladu TestRandom snadné:

java -Xbootclasspath/p:. TestRandom

Jak to celé funguje? Specifikovali jsme, že před systémovými třídami uloženými v archivu rt.jar budou mít přednost třídy uložené v aktuálním adresáři (tečka). JVM samozřejmě opět hledá třídy v podadresářích v závislosti na balíčku, takže třídu java.util.Random bude nejprve hledat v adresáři ./java/util/ (kde je skutečně uložena) a teprve, kdyby ji zde nenašel, použije stejně pojmenovanou třídu z rt.jar.

Příklad TestRandom by měl opět vytvořit rastrový obrázek „test.png“, který by však měl být celý černý, protože se namísto pseudonáhodných hodnot vracely v metodě Random.nextInt() jen nuly:

Obrázek 2: Obrázek vytvořený testovací třídou TestRandom v případě, že se použije pozměněná třída java.util.Random.

8. Modifikace metody java.util.Random.nextInt() tak, aby se vracela sekvence čísel (přidání skrytého atributu)

Nyní si ukažme, jak je nutné modifikovat třídu java.util.Random takovým způsobem, aby se namísto sekvence pseudonáhodných hodnot vracela v metodě Random.nextInt() aritmetická řada, konkrétně posloupnost celých čísel od 0 do Integer.MAX_VALUE (s přetečením na Integer.MIN_VALUE). Aby to bylo možné, je nejprve nutné do třídy přidat nový atribut, který si bude pamatovat následující hodnotu počitadla, pomocí něhož se bude sekvence čísel tvořit. Přidání nového atributu je jednoduché, jak je ostatně patrné z následujícího úryvku kódu:

    /**
     * Pridani noveho privatniho atributu intCounter.
     *
     * @param classToModify
     *            modifikovana trida
     * @throws CannotCompileException
     *             vyhozena v pripade, ze se nepodarilo pridani noveho atributu
     */
    private static void createAndAddNewAttribute(CtClass classToModify) throws CannotCompileException {
        // vytvoreni a pridani noveho privatniho atributu do tridy
        CtField f = new CtField(CtClass.intType, "intCounter", classToModify);
        f.setModifiers(Modifier.PRIVATE);
        classToModify.addField(f);
    }

Při modifikaci metody Random.nextInt() pro jednoduchost nebudeme brát v úvahu nutnost provádění zvyšování počitadla atomicky (sami se můžete zkusit zamyslet nad tím, co se může stát, když se tato metoda bude volat z více vláken):

    /**
     * Zmena tela metody nextInt().
     *
     * @param classToModify
     *            modifikovana trida
     * @throws NotFoundException
     *             vyhozena, pokud metoda nebyla nalezena
     * @throws CannotCompileException
     *             vyhozena v pripade, ze se nepodaril preklad metody
     */
    private static void changeMethodNextInt(CtClass classToModify) throws NotFoundException, CannotCompileException {
        CtMethod method = classToModify.getMethod("nextInt", "()I");
        method.setBody("return intCounter++;");
    }

Celý demonstrační příklad, který provede modifikaci třídy java.util.Random vypadá takto:

import java.io.IOException;
 
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.NotFoundException;
 
 
 
/**
 * Test moznosti nastroje Javassist - zmena chovani standardni (systemove)
 * tridy java.util.Random.
 *
 * @author Pavel Tisnovsky
 */
public class SystemClassModificationTest2 {
 
    /**
     * Modifikace metody Random.nextInt().
     *
     * @throws CannotCompileException
     *             vyhozena v pripade, ze se nepodaril preklad metody 
     * @throws NotFoundException
     *             vyhozena, pokud trida ci metoda nebyla nalezena
     * @throws IOException 
     *             vyhozena v pripade, ze se nepodarilo ulozit vysledny bajtkod do souboru
     */
    private static void modifyRandomClass() throws CannotCompileException, NotFoundException, IOException {
        // ziskat vychozi class pool
        ClassPool pool = ClassPool.getDefault();
 
        // objekt predstavujici menenou (modifikovanou) tridu
        CtClass classToModify = pool.get("java.util.Random");
 
        // vytvoreni a pridani noveho atributu do tridy
        createAndAddNewAttribute(classToModify);
 
        // zmena tela metody nextInt()
        changeMethodNextInt(classToModify);
 
        // tridu nebude mozne dale menit
        classToModify.freeze();
 
        // ulozeni modifikovane tridy do souboru
        classToModify.writeFile(".");
    }
 
    /**
     * Zmena tela metody nextInt().
     *
     * @param classToModify
     *            modifikovana trida
     * @throws NotFoundException
     *             vyhozena, pokud metoda nebyla nalezena
     * @throws CannotCompileException
     *             vyhozena v pripade, ze se nepodaril preklad metody
     */
    private static void changeMethodNextInt(CtClass classToModify) throws NotFoundException, CannotCompileException {
        CtMethod method = classToModify.getMethod("nextInt", "()I");
        method.setBody("return intCounter++;");
    }
 
    /**
     * Pridani noveho privatniho atributu intCounter.
     *
     * @param classToModify
     *            modifikovana trida
     * @throws CannotCompileException
     *             vyhozena v pripade, ze se nepodarilo pridani noveho atributu
     */
    private static void createAndAddNewAttribute(CtClass classToModify) throws CannotCompileException {
        // vytvoreni a pridani noveho privatniho atributu do tridy
        CtField f = new CtField(CtClass.intType, "intCounter", classToModify);
        f.setModifiers(Modifier.PRIVATE);
        classToModify.addField(f);
    }
 
    /**
     * Spusteni modifikatoru tridy java.util.Random.
     *
     * @param args nevyuzito
     */
    public static void main(String[] args) {
        try {
            modifyRandomClass();
        }
        catch (CannotCompileException e) {
            e.printStackTrace();
        }
        catch (NotFoundException e) {
            e.printStackTrace();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
 
}

9. Podoba upraveného bajtkódu metody java.util.Random.nextInt()

Druhý demonstrační příklad SystemClassModificationTest2 popsaný v předchozí kapitole přeložíme takto:

javac -cp javassist.jar SystemClassModificationTest2.java

Spuštění příkladu na Linuxu:

java -cp .:javassist.jar SystemClassModificationTest2

Spuštění příkladu na MS Windows:

java -cp .;javassist.jar SystemClassModificationTest2

Podívejme se nyní na podobu nového bajtkódu přiřazeného k metodě Random.nextInt(). Vidíme, že se skutečně pracuje s hodnotou nového privátního atributu Random.intCounter (a to bez zaručení atomičnosti prováděných operací):

Compiled from "Random.java"
public class java.util.Random extends java.lang.Object implements java.io.Serializable{
 
static final long serialVersionUID;
 
public int nextInt();
  Code:
   0:           aload_0
   1:           dup
   2:           getfield        #73; //Field intCounter:I
   5:           dup_x1
   6:           iconst_1
   7:           iadd
   8:           putfield        #73; //Field intCounter:I
   11:          ireturn
 
}

Poznámka: instrukce dup_x1 zduplikuje (zkopíruje) položku z TOS (vrcholu zásobníku operandů), ale vloží ji o dvě pozice níž do téhož zásobníku, tudíž jde o kombinaci operace swap+dup.

10. Spuštění testu s načtením upravené třídy java.util.Random

Jakmile máme uloženou novou podobu bajtkódu třídy java.util.Random, můžeme si vyzkoušet znovu spustit demonstrační příklad TestRandom, a to opět s použitím nestandardní volby -Xbootclasspath:

java -Xbootclasspath/p:. TestRandom

Příklad TestRandom by měl opět vytvořit rastrový obrázek „test.png“, který by však měl obsahovat gradientní přechod od černé do bílé, protože se namísto pseudonáhodných hodnot vracela v metodě Random.nextInt() aritmetická řada (která při převodu z int na byte přetéká přesně na hranici mezi jednotlivými řádky bitmapy):

CS24_early

Obrázek 3: Obrázek vytvořený testovací třídou TestRandom v případě, že se použije pozměněná třída java.util.Random.

11. Repositář se zdrojovými kódy všech demonstračních i testovací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 SystemClassModificationTest1SystemClassModificationTest2 jsou společně s testovacím příkladem TestRandom a 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 všech zdrojových kódů i skriptů:

# Zdrojový soubor/skript Umístění souboru v repositáři
1 TestRandom.java http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/4e8c878e4cb0/ja­vassist/SystemClassModifi­cation/TestRandom.java
2 SystemClassModificationTest1.java http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/4e8c878e4cb0/ja­vassist/SystemClassModifi­cation/SystemClassModifica­tionTest1.java
3 SystemClassModificationTest2.java http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/4e8c878e4cb0/ja­vassist/SystemClassModifi­cation/SystemClassModifica­tionTest2.java
4 build.sh http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/4e8c878e4cb0/ja­vassist/SystemClassModifi­cation/build.sh
5 build.bat http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/4e8c878e4cb0/ja­vassist/SystemClassModifi­cation/build.bat
6 modifyClass1.sh http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/4e8c878e4cb0/ja­vassist/SystemClassModifi­cation/modifyClass1.sh
7 modifyClass1.bat http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/4e8c878e4cb0/ja­vassist/SystemClassModifi­cation/modifyClass1.bat
8 modifyClass2.sh http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/4e8c878e4cb0/ja­vassist/SystemClassModifi­cation/modifyClass2.sh
9 modifyClass2.bat http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/4e8c878e4cb0/ja­vassist/SystemClassModifi­cation/modifyClass2.bat
10 runTestRandom.sh http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/4e8c878e4cb0/ja­vassist/SystemClassModifi­cation/runTestRandom.sh
11 runTestRandom.bat http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/file/4e8c878e4cb0/ja­vassist/SystemClassModifi­cation/runTestRandom.bat

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

Byl pro vás článek přínosný?

Autor článku

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