Útmutató a Java Bytecode Manipulációhoz ASM-mel

1. Bemutatkozás

Ebben a cikkben megvizsgáljuk az ASM könyvtár használatát egy meglévő Java osztály manipulálásához mezők hozzáadásával, módszerek hozzáadásával és a meglévő módszerek viselkedésének megváltoztatásával.

2. Függőségek

Hozzá kell adnunk az ASM-függőségeket a sajátunkhoz pom.xml:

 org.ow2.asm asm 6.0 org.ow2.asm asm-util 6.0 

A Maven Central-tól beszerezhetjük az asm és az asm-util legújabb verzióit.

3. Az ASM API alapjai

Az ASM API kétféle stílust kínál a Java osztályokkal való interakcióhoz az átalakításhoz és a generáláshoz: eseményalapú és faalapú.

3.1. Eseményalapú API

Ez az API erősen alapján Látogató minta és van érzésében hasonló a SAX elemzési modellhez az XML dokumentumok feldolgozását. Lényegében a következő összetevőkből áll:

  • ClassReader - segít elolvasni az osztályfájlokat, és elkezdi az osztály átalakítását
  • ClassVisitor - bemutatja az osztály átalakítására használt módszereket a nyers osztályfájlok elolvasása után
  • ClassWriter - az osztályátalakítás végtermékének kibocsátására szolgál

Ebben van ClassVisitor hogy rendelkezünk az összes olyan látogatói módszerrel, amelyet az adott Java osztály különböző összetevőinek (mezők, módszerek stb.) megérintésére használunk. Ezt megcsináljuk alosztályának biztosítása ClassVisitoraz adott osztály bármely változásának végrehajtására.

Mivel szükség van a kimeneti osztály integritásának megőrzésére a Java konvenciókkal és az ebből eredő byte-kóddal kapcsolatban, ehhez az osztályhoz a szigorú sorrend, amelyben módszereit meg kell hívni hogy megfelelő kimenetet generáljon.

A ClassVisitor az eseményalapú API metódusait a következő sorrendben hívjuk meg:

látogasson el a visitSource oldalra? visitOuterClass? (visitAnnotation | visitAttribute) * (visitInnerClass | visitField | visitMethod) * visitEnd

3.2. Faalapú API

Ez az API a objektum-orientáltabb API és van analóg a JAXB modellel az XML dokumentumok feldolgozását.

Még mindig az eseményalapú API-n alapul, de bevezeti a ClassNode gyökérosztály. Ez az osztály az osztálystruktúra belépési pontjaként szolgál.

4. Munka az eseményalapú ASM API-val

Módosítjuk a java.lang.Integer osztály az ASM-mel. És ezen a ponton fel kell fognunk egy alapvető fogalmat: a ClassVisitor osztály tartalmazza az összes szükséges látogatói módszert az osztály összes részének létrehozásához vagy módosításához.

Csak a szükséges látogatói módszert kell felülírnunk a változásaink végrehajtásához. Kezdjük az előfeltételek beállításával:

public class CustomClassWriter {static String className = "java.lang.Integer"; statikus karakterlánc cloneableInterface = "java / lang / Cloneable"; ClassReader olvasó; ClassWriter író; public CustomClassWriter () {olvasó = new ClassReader (className); író = új ClassWriter (olvasó, 0); }}

Ezt vesszük alapul a Klónozható interfész az állományhoz Egész szám osztály, és hozzáadunk egy mezőt és egy metódust is.

4.1. Munka mezőkkel

Hozzuk létre a sajátunkat ClassVisitor amellyel hozzáadunk egy mezőt a Egész szám osztály:

public class Az AddFieldAdapter kiterjeszti a ClassVisitor {private String fieldName; private String fieldDefault; privát int hozzáférés = org.objectweb.asm.Opcodes.ACC_PUBLIC; privát logikai isFieldPresent; public AddFieldAdapter (String fieldName, int fieldAccess, ClassVisitor cv) {szuper (ASM4, cv); ez.cv = cv; this.fieldName = mezőNév; this.access = fieldAccess; }} 

Ezután menjünk felülírja a visitField módszer, ahol először ellenőrizze, hogy a hozzáadni kívánt mező már létezik-e, és állítson be egy jelölőt az állapot jelzésére.

Még mindig muszáj továbbítsa a metódushívást a szülő osztálynak - ennek úgy kell történnie, mint a visitField metódust hívnak meg az osztály minden mezőjére. A hívás továbbításának elmulasztása azt jelenti, hogy nem írnak mezőket az osztálynak.

Ez a módszer lehetővé teszi számunkra azt is módosítsa a meglévő mezők láthatóságát vagy típusát:

@Orride public FieldVisitor visitField (int access, String name, String desc, String signature, Object value) {if (név.egyenlő (mezőnév)) {isFieldPresent = true; } return cv.visitField (hozzáférés, név, leírás, aláírás, érték); } 

Először a korábbiakban beállított zászlót ellenőrizzük visitField metódust és hívja meg a visitField módszerrel, ezúttal megadva a nevet, a hozzáférés módosítóját és a leírást. Ez a módszer a FieldVisitor.

A visitEnd módszer az utolsó úgynevezett módszer a látogatói módszerek sorrendjében. Ez az ajánlott pozíció hajtsa végre a mezőbeillesztési logikát.

Akkor hívnunk kell a visitEnd metódus ezen az objektumon jelezzük, hogy befejeztük a mező meglátogatását:

@Orride public void visitEnd () {if (! IsFieldPresent) {FieldVisitor fv = cv.visitField (access, fieldName, fieldType, null, null); if (fv! = null) {fv.visitEnd (); }} cv.visitEnd (); } 

Fontos megbizonyosodni arról, hogy az összes használt ASM-összetevő származik-e org.objectweb.asm csomag - sok könyvtár használja az ASM könyvtárat belsőleg, és az IDE-k automatikusan beilleszthetik a mellékelt ASM könyvtárakat.

Most az adapterünket használjuk a addField módszer, átalakított változatának megszerzése java.lang.Integerhozzáadott mezőnkkel:

public class CustomClassWriter {AddFieldAdapter addFieldAdapter; // ... nyilvános bájt [] addField () {addFieldAdapter = new AddFieldAdapter ("aNewBooleanField", org.objectweb.asm.Opcodes.ACC_PUBLIC, író); reader.accept (addFieldAdapter, 0); return író.toByteArray (); }}

Felülírtuk a visitField és visitEnd mód.

A mezőkkel kapcsolatos minden tennivaló a visitField módszer. Ez azt jelenti, hogy a meglévő mezőket is módosíthatjuk (például egy privát mező nyilvánosá alakítását) azáltal, hogy megváltoztatjuk a visitField módszer.

4.2. Munka módszerekkel

Teljes metódusok létrehozása az ASM API-ban nagyobb szerepet játszik, mint az osztály többi művelete. Ez jelentős mennyiségű alacsony szintű bájtkód-manipulációval jár, és ennek következtében meghaladja a cikk kereteit.

A legtöbb gyakorlati felhasználásra azonban bármelyiket megtehetjük módosítson egy meglévő módszert annak hozzáférhetőbbé tétele érdekében (talán nyilvánosságra hozza, hogy felülírható vagy túlterhelhető legyen) vagy módosítson egy osztályt, hogy kibővíthető legyen.

Tegyük nyilvánossá a toUnsignedString metódust:

public class PublicizeMethodAdapter kiterjeszti a ClassVisitor {public PublicizeMethodAdapter (int api, ClassVisitor cv) {super (ASM4, cv); ez.cv = cv; } public MethodVisitor visitMethod (int hozzáférés, String neve, String desc, String aláírás, String [] kivételek) {if (név.egyenlő ("toUnsignedString0")) {return cv.visitMethod (ACC_PUBLIC + ACC_STATIC, név, desc, aláírás, kivételek); } return cv.visitMethod (hozzáférés, név, leírás, aláírás, kivételek); }} 

Csakúgy, mint a terepi módosításhoz, mi is elfogja a látogatási módszert és változtassa meg a kívánt paramétereket.

Ebben az esetben a hozzáférési módosítókat használjuk a org.objectweb.asm.Opcodes csomagot változtassa meg a módszer láthatóságát. Ezután csatlakoztatjuk a készülékünket ClassVisitor:

public byte [] publicizeMethod () {pubMethAdapter = new PublicizeMethodAdapter (író); reader.accept (pubMethAdapter, 0); return író.toByteArray (); } 

4.3. Munka osztályokkal

A módszerek módosításával azonos vonalon mi osztályok módosítása a megfelelő látogatói módszer elfogásával. Ebben az esetben elfogunk látogatás, amely a látogatói hierarchia legelső módszere:

public class AddInterfaceAdapter kiterjeszti a ClassVisitor {public AddInterfaceAdapter (ClassVisitor cv) {super (ASM4, cv); } @Orride public void visit (int verzió, int hozzáférés, String neve, String aláírás, String superName, String [] interfészek) {String [] holding = new String [interfaces.length + 1]; holding [holding.length - 1] = cloneableInterface; System.arraycopy (interfészek, 0, tartás, 0, interfészek.hossz); cv.visit (V1_8, hozzáférés, név, aláírás, superName, holding); }} 

Felülírjuk a látogatás metódus a Klónozható interfész az interfészek tömbjéhez, amelyet a Egész szám osztály. Csatlakoztatjuk ezt ugyanúgy, mint adaptereink összes többi felhasználását.

5. A módosított osztály használata

Tehát módosítottuk a Egész szám osztály. Most képesnek kell lennünk betölteni és használni az osztály módosított verzióját.

Amellett, hogy egyszerűen megírja a kimenetet író.toByteArray osztályfájlként a lemezre más módon is léphet kapcsolatba testreszabott fájljainkkal Egész szám osztály.

5.1. Használni a TraceClassVisitor

Az ASM könyvtár biztosítja a TraceClassVisitor hasznossági osztály, amelyet használni fogunk vizsgálja meg a módosított osztályt. Így megtehetjük erősítse meg, hogy változásaink megtörténtek.

Mert a TraceClassVisitor egy ClassVisitor, használhatjuk a szabvány bepattanására ClassVisitor:

PrintWriter pw = new PrintWriter (System.out); public PublicizeMethodAdapter (ClassVisitor cv) {szuper (ASM4, cv); ez.cv = cv; nyomjelző = új TraceClassVisitor (cv, pw); } public MethodVisitor visitMethod (int hozzáférés, String név, String desc, String aláírás, String [] kivételek) {if (név.egyenlő ("toUnsignedString0")) {System.out.println ("Aláíratlan módszer megtekintése"); return tracer.visitMethod (ACC_PUBLIC + ACC_STATIC, név, leírás, aláírás, kivételek); } return tracer.visitMethod (hozzáférés, név, leírás, aláírás, kivételek); } public void visitEnd () {tracer.visitEnd (); System.out.println (tracer.p.getText ()); } 

Amit itt tettünk, az a ClassVisitor hogy átmentünk a korábbiakhoz PublicizeMethodAdapter a ... val TraceClassVisitor.

Az összes látogatást a nyomkövetőnkkel végezzük, amely kinyomtatja az átalakított osztály tartalmát, bemutatva az általa elvégzett módosításokat.

Míg az ASM dokumentációja kimondja, hogy a TraceClassVisitor kinyomtathatja a PrintWriter amelyet a kivitelezőnek szállítottunk, úgy tűnik, hogy ez nem működik megfelelően az ASM legújabb verziójában.

Szerencsére hozzáférünk az osztály alapjául szolgáló nyomtatóhoz, és manuálisan kinyomtathattuk a nyomkövető szöveges tartalmát felülírva visitEnd módszer.

5.2. Java Instrumentation használata

Ez egy elegánsabb megoldás, amely lehetővé teszi számunkra, hogy a JVM-mel szorosabban működjünk együtt az Instrumentation segítségével.

A hangszereléshez java.lang.Integer osztály, mi írjon egy ügynököt, amelyet parancssori paraméterként konfigurálnak a JVM-mel. Az ügynöknek két összetevőre van szüksége:

  • Osztály, amely egy nevű módszert valósít meg premain
  • A ClassFileTransformer amelyben feltételesen megadjuk osztályunk módosított változatát
public class Premain {public static void premain (String agentArgs, Instrumentation inst) {inst.addTransformer (new ClassFileTransformer () {@Orride public byte [] transzformáció (ClassLoader l, String név, c osztály, ProtectionDomain d, byte [] b) dobja az IllegalClassFormatException {if (név.egyenlő ("java / lang / Integer")) {CustomClassWriter cr = új CustomClassWriter (b); return cr.addField ();} return b;}}); }}

Most meghatározzuk a sajátunkat premain megvalósítási osztály egy JAR nyilvántartási fájlban a Maven jar plugin segítségével:

 org.apache.maven.plugins maven-jar-plugin 2.4 com.baeldung.examples.asm.instrumentation.Premain true 

Az eddigi kódunk felépítése és csomagolása előállítja a korsót, amelyet ügynökként tölthetünk be. Testreszabott használatunkhoz Egész szám osztály hipotetikusYourClass.class“:

java YourClass -javaagent: "/ path / to / theAgentJar.jar"

6. Következtetés

Míg az átalakításokat itt külön-külön hajtottuk végre, az ASM lehetővé teszi számunkra, hogy több adaptert láncoljunk össze az osztályok összetett átalakításainak elérése érdekében.

Az itt vizsgált alaptranszformációk mellett az ASM támogatja az annotációkkal, generikusokkal és belső osztályokkal való interakciókat is.

Láttuk az ASM könyvtár néhány erejét - eltávolít egy csomó korlátozást, amellyel harmadik fél könyvtárakkal, sőt a standard JDK osztályokkal is találkozhatunk.

Az ASM-et széles körben használják a legnépszerűbb könyvtárak (Spring, AspectJ, JDK stb.) Motorházteteje alatt, hogy sok „varázslatot” hajtsanak végre menet közben.

A cikk forráskódját a GitHub projektben találja meg.