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
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ů
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.BufferedImage.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“:
![](https://i.iinfo.cz/images/88/java-image-1.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:
![](https://i.iinfo.cz/images/88/java-image-2.png)
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):
![](https://i.iinfo.cz/images/88/java-image-3.png)
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 SystemClassModificationTest1 i SystemClassModificationTest2 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.org/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ů:
12. Odkazy na Internetu
- Open Source ByteCode Libraries in Java
http://java-source.net/open-source/bytecode-libraries - ASM Home page
http://asm.ow2.org/ - Seznam nástrojů využívajících projekt ASM
http://asm.ow2.org/users.html - ObjectWeb ASM (Wikipedia)
http://en.wikipedia.org/wiki/ObjectWeb_ASM - Java Bytecode BCEL vs ASM
http://james.onegoodcookie.com/2005/10/26/java-bytecode-bcel-vs-asm/ - BCEL Home page
http://commons.apache.org/bcel/ - Byte Code Engineering Library (před verzí 5.0)
http://bcel.sourceforge.net/ - Byte Code Engineering Library (verze >= 5.0)
http://commons.apache.org/proper/commons-bcel/ - BCEL Manual
http://commons.apache.org/bcel/manual.html - Byte Code Engineering Library (Wikipedia)
http://en.wikipedia.org/wiki/BCEL - BCEL Tutorial
http://www.smfsupport.com/support/java/bcel-tutorial!/ - Bytecode Engineering
http://book.chinaunix.net/special/ebook/Core_Java2_Volume2AF/0131118269/ch13lev1sec6.html - Bytecode Outline plugin for Eclipse (screenshoty + info)
http://asm.ow2.org/eclipse/index.html - Javassist
http://www.jboss.org/javassist/ - Byteman
http://www.jboss.org/byteman - Java programming dynamics, Part 7: Bytecode engineering with BCEL
http://www.ibm.com/developerworks/java/library/j-dyn0414/ - The JavaTM Virtual Machine Specification, Second Edition
http://java.sun.com/docs/books/jvms/second_edition/html/VMSpecTOC.doc.html - The class File Format
http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html - javap – The Java Class File Disassembler
http://docs.oracle.com/javase/1.4.2/docs/tooldocs/windows/javap.html - javap-java-1.6.0-openjdk(1) – Linux man page
http://linux.die.net/man/1/javap-java-1.6.0-openjdk - Using javap
http://www.idevelopment.info/data/Programming/java/miscellaneous_java/Using_javap.html - Examine class files with the javap command
http://www.techrepublic.com/article/examine-class-files-with-the-javap-command/5815354 - aspectj (Eclipse)
http://www.eclipse.org/aspectj/ - Aspect-oriented programming (Wikipedia)
http://en.wikipedia.org/wiki/Aspect_oriented_programming - AspectJ (Wikipedia)
http://en.wikipedia.org/wiki/AspectJ - EMMA: a free Java code coverage tool
http://emma.sourceforge.net/ - Cobertura
http://cobertura.sourceforge.net/ - jclasslib bytecode viewer
http://www.ej-technologies.com/products/jclasslib/overview.html