Sprievodca manipuláciou Java Bytecode s ASM

1. Úvod

V tomto článku sa pozrieme na to, ako používať knižnicu ASM na manipuláciu s existujúcou triedou Java pridaním polí, pridaním metód a zmenou správania existujúcich metód.

2. Závislosti

Musíme pridať závislosti ASM do našej pom.xml:

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

Najnovšie verzie asm a asm-util môžeme získať z Maven Central.

3. Základy ASM API

ASM API poskytuje dva štýly interakcie s triedami Java na transformáciu a generovanie: na základe udalostí a na báze stromov.

3.1. Rozhranie API založené na udalostiach

Toto API je veľmi rozšírené založený na Návštevník vzor a je v dojme podobné modelu analýzy SAX spracovania XML dokumentov. Vo svojej podstate sa skladá z nasledujúcich komponentov:

  • ClassReader - pomáha čítať súbory triedy a je začiatkom transformácie triedy
  • ClassVisitor - poskytuje metódy použité na transformáciu triedy po prečítaní nespracovaných súborov triedy
  • ClassWriter - slúži na výstup finálneho produktu transformácie triedy

Je to v ClassVisitor že máme všetky metódy návštevníka, ktoré použijeme na dotknutie sa rôznych komponentov (polí, metód atď.) danej triedy Java. Robíme to poskytujúce podtriedu ClassVisitorimplementovať akékoľvek zmeny v danej triede.

Z dôvodu potreby zachovania integrity výstupnej triedy týkajúcej sa konvencií Java a výsledného bytecode vyžaduje táto trieda a v prísnom poradí, v akom by sa mali volať jej metódy vygenerovať správny výstup.

The ClassVisitor metódy v API založenom na udalostiach sa volajú v nasledujúcom poradí:

navštíviť visitSource? visitOuterClass? (visitAnnotation | visitAttribute) * (visitInnerClass | visitField | visitMethod) * visitEnd

3.2. Stromové API

Toto API je viac objektovo orientované API a je analogicky k modelu JAXB spracovania XML dokumentov.

Stále je založený na API založenom na udalostiach, ale predstavuje ClassNode koreňová trieda. Táto trieda slúži ako vstupný bod do štruktúry triedy.

4. Práca s ASM API založeným na udalostiach

Upravíme java.lang.Integer triedy s ASM. V tomto bode musíme pochopiť základný koncept: the ClassVisitor trieda obsahuje všetky potrebné metódy návštevníka na vytvorenie alebo úpravu všetkých častí triedy.

Na vykonanie našich zmien stačí prepísať nevyhnutný spôsob návštevnosti. Začnime nastavením nevyhnutných komponentov:

verejná trieda CustomClassWriter {statický reťazec className = "java.lang.Integer"; static String cloneableInterface = "java / lang / Cloneable"; Čítačka ClassReader; Spisovateľ ClassWriter; public CustomClassWriter () {reader = new ClassReader (className); spisovateľ = nový ClassWriter (čítačka, 0); }}

Používame to ako základ na pridanie Cloneable rozhranie k akcii Celé číslo triedy a pridáme tiež pole a metódu.

4.1. Práca s poľami

Poďme vytvoriť naše ClassVisitor že použijeme na pridanie poľa do Celé číslo trieda:

verejná trieda AddFieldAdapter rozširuje ClassVisitor {private String fieldName; privátne pole reťazca Predvolené; private int access = org.objectweb.asm.Opcodes.ACC_PUBLIC; private boolean isFieldPresent; public AddFieldAdapter (String fieldName, int fieldAccess, ClassVisitor cv) {super (ASM4, cv); this.cv = cv; this.fieldName = fieldName; this.access = fieldAccess; }} 

Ďalej, poďme prepísať visitField metóda, kde sme ako prví skontrolujte, či pole, ktoré plánujeme pridať, už existuje a nastavte príznak označujúci stav.

Stále musíme preposlať volanie metódy materskej triede - toto sa musí stať ako visitField metóda sa volá pre každé pole v triede. Ak hovor nepresmerujete, znamená to, že do triedy nebudú zapísané žiadne polia.

Táto metóda nám to tiež umožňuje upraviť viditeľnosť alebo typ existujúcich polí:

@Override public FieldVisitor visitField (int access, String name, String desc, String signature, Object value) {if (name.equals (fieldName)) {isFieldPresent = true; } návrat cv.visitField (prístup, meno, popis, podpis, hodnota); } 

Najprv skontrolujeme príznak nastavený v predchádzajúcom visitField metóda a zavolajte visitField znova, tentokrát s uvedením názvu, modifikátora prístupu a popisu. Táto metóda vracia inštanciu FieldVisitor.

The visitEnd metóda je posledná volaná metóda podľa spôsobu návštevníka. Toto je odporúčaná poloha pre vykonať logiku vkladania polí.

Potom musíme zavolať na visitEnd metóda na tomto objekte signál, že sme ukončili návštevu tohto poľa:

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

Je dôležité si byť istý, že všetky použité komponenty ASM pochádzajú z org.objectweb.asm balíček - veľa knižníc interne používa knižnicu ASM a IDE môžu automaticky vkladať pribalené knižnice ASM.

Teraz používame náš adaptér v addField metóda, získanie transformovanej verzie java.lang.Integers našim pridaným poľom:

verejná trieda CustomClassWriter {AddFieldAdapter addFieldAdapter; // ... public byte [] addField () {addFieldAdapter = new AddFieldAdapter ("aNewBooleanField", org.objectweb.asm.Opcodes.ACC_PUBLIC, spisovateľ); reader.accept (addFieldAdapter, 0); vrátiť spisovateľ.toByteArray (); }}

Prekonali sme visitField a visitEnd metódy.

Všetko, čo sa týka polí, sa deje s visitField metóda. To znamená, že môžeme tiež upraviť existujúce polia (povedzme transformovať súkromné ​​pole na verejné) zmenou požadovaných hodnôt odovzdaných do visitField metóda.

4.2. Práca s metódami

Generovanie celých metód v rozhraní ASM API je viac zapojené ako iné operácie v triede. Zahŕňa to značné množstvo nízkoúrovňovej manipulácie s bajtovými kódmi a vo výsledku je to nad rámec tohto článku.

Na väčšinu praktických použití však môžeme buď upraviť existujúcu metódu tak, aby bola prístupnejšia (možno ho zverejniť, aby bolo možné ho prepísať alebo preťažiť) alebo upraviť triedu tak, aby bola rozšíriteľná.

Poďme zverejniť metódu toUnsignedString:

public class PublicizeMethodAdapter rozširuje ClassVisitor {public PublicizeMethodAdapter (int api, ClassVisitor cv) {super (ASM4, cv); this.cv = cv; } public MethodVisitor visitMethod (int access, String name, String desc, String signature, String [] exceptions) {if (name.equals ("toUnsignedString0")) {return cv.visitMethod (ACC_PUBLIC + ACC_STATIC, name, desc, signature, výnimky); } return cv.visitMethod (prístup, meno, popis, podpis, výnimky); }} 

Ako sme to urobili pri poľnej úprave, iba sme zachytiť spôsob návštevy a zmeniť parametre, ktoré požadujeme.

V tomto prípade použijeme modifikátory prístupu v org.objectweb.asm.Opcodes balíček do zmeniť viditeľnosť metódy. Potom zapojíme naše ClassVisitor:

public byte [] publicizeMethod () {pubMethAdapter = nový PublicizeMethodAdapter (zapisovač); reader.accept (pubMethAdapter, 0); vrátiť spisovateľ.toByteArray (); } 

4.3. Práca s triedami

Rovnako ako modifikácia metód, aj my upravovať triedy zachytávaním vhodných metód pre návštevníkov. V takom prípade zachytíme navštíviť, čo je úplne prvá metóda v hierarchii návštevníkov:

public class AddInterfaceAdapter extends ClassVisitor {public AddInterfaceAdapter (ClassVisitor cv) {super (ASM4, cv); } @Override public void visit (int version, int access, String name, String signature, String superName, String [] interfaces) {String [] holding = new String [interfaces.length + 1]; holding [holding.length - 1] = cloneableInterface; System.arraycopy (interfaces, 0, holding, 0, interfaces.length); cv.visit (V1_8, prístup, meno, podpis, superMeno, držba); }} 

Prepíšeme navštíviť metóda na pridanie Cloneable - rozhranie k rade rozhraní, ktoré má podporovať Celé číslo trieda. Zapájame to rovnako ako všetky ostatné spôsoby použitia našich adaptérov.

5. Používanie upravenej triedy

Takže sme upravili Celé číslo trieda. Teraz musíme byť schopní načítať a používať upravenú verziu triedy.

Okrem jednoduchého zápisu výstupu z spisovateľ.toByteArray na disk ako súbor triedy, existuje niekoľko ďalších spôsobov interakcie s našimi prispôsobenými Celé číslo trieda.

5.1. Pomocou TraceClassVisitor

Knižnica ASM poskytuje TraceClassVisitor úžitková trieda, na ktorú zvykneme prezrieť upravenú triedu. Takto môžeme potvrdzujeme, že došlo k našim zmenám.

Pretože TraceClassVisitor je a ClassVisitor, môžeme ho použiť ako náhradu za štandard ClassVisitor:

PrintWriter pw = nový PrintWriter (System.out); public PublicizeMethodAdapter (ClassVisitor cv) {super (ASM4, cv); this.cv = cv; sledovač = nový TraceClassVisitor (cv, pw); } public MethodVisitor visitMethod (int access, String name, String desc, String signature, String [] exceptions) {if (name.equals ("toUnsignedString0")) {System.out.println ("Hosťujúca nepodpísaná metóda"); návrat tracer.visitMethod (ACC_PUBLIC + ACC_STATIC, meno, popis, podpis, výnimky); } návrat tracer.visitMethod (prístup, meno, popis, podpis, výnimky); } public void visitEnd () {tracer.visitEnd (); System.out.println (tracer.p.getText ()); } 

To, čo sme tu urobili, je prispôsobiť ClassVisitor že sme prešli k nášmu skôr PublicizeMethodAdapter s TraceClassVisitor.

Celá návšteva sa teraz uskutoční pomocou nášho sledovacieho programu, ktorý potom môže vytlačiť obsah transformovanej triedy a ukázať všetky úpravy, ktoré sme v nej vykonali.

Zatiaľ čo dokumentácia ASM uvádza, že TraceClassVisitor je možné vytlačiť do PrintWriter ktorý je dodávaný konštruktoru, zdá sa, že to v najnovšej verzii ASM nefunguje správne.

Našťastie máme prístup k základnej tlačiarni v triede a dokázali sme manuálne vytlačiť textový obsah indikátora v našom prepísanom formáte visitEnd metóda.

5.2. Používanie inštrumentácie Java

Jedná sa o elegantnejšie riešenie, ktoré nám umožňuje pracovať s JVM na bližšej úrovni prostredníctvom prístrojovej techniky.

Ak chcete prístroj java.lang.Integer triedy, my napíšte agenta, ktorý bude nakonfigurovaný ako parameter príkazového riadku s JVM. Agent vyžaduje dve zložky:

  • Trieda, ktorá implementuje metódu s názvom premain
  • Implementácia ClassFileTransformer v ktorom podmienečne dodáme upravenú verziu našej triedy
public class Premain {public static void premain (String agentArgs, Instrumentation inst) {inst.addTransformer (new ClassFileTransformer () {@Override public byte [] transform (ClassLoader l, String name, Class c, ProtectionDomain d, byte [] b) hodí IllegalClassFormatException {if (name.equals ("java / lang / Integer")) {CustomClassWriter cr = new CustomClassWriter (b); return cr.addField ();} return b;}}); }}

Teraz definujeme naše premain trieda implementácie v súbore manifestu JAR pomocou doplnku Maven jar:

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

Pri zostavovaní a balení nášho kódu sa doposiaľ vytvára nádoba, ktorú môžeme načítať ako agent. Ak chcete použiť naše prispôsobené Celé číslo triedy v hypotetickom „YourClass.class“:

java YourClass -javaagent: "/ cesta / k / theAgentJar.jar"

6. Záver

Zatiaľ čo sme tu implementovali naše transformácie individuálne, ASM nám umožňuje reťaziť viac adaptérov dohromady, aby sme dosiahli komplexné transformácie tried.

Okrem základných transformácií, ktoré sme tu preskúmali, podporuje ASM aj interakcie s anotáciami, generikami a vnútornými triedami.

Videli sme časť sily knižnice ASM - odstraňuje veľa obmedzení, s ktorými sa môžeme stretnúť pri knižniciach tretích strán a dokonca aj pri štandardných triedach JDK.

ASM sa často používa pod kapotou najpopulárnejších knižníc (Spring, AspectJ, JDK atď.) Na vykonávanie mnohých „mág“ za behu.

Zdrojový kód tohto článku nájdete v projekte GitHub.