Vodič za manipulaciju Java bajt kodom s ASM-om

1. Uvod

U ovom ćemo članku pogledati kako se koristi ASM knjižnica za manipulaciju postojećom klasom Java dodavanjem polja, dodavanjem metoda i promjenom ponašanja postojećih metoda.

2. Ovisnosti

Moramo dodati ASM ovisnosti u našu pom.xml:

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

Najnovije verzije asm i asm-util možemo dobiti od Maven Central.

3. Osnove ASM API-ja

ASM API pruža dva stila interakcije s Java klasama za transformaciju i generiranje: temeljenu na događajima i na stablu.

3.1. API zasnovan na događajima

Ovaj API je jako bazirano na Posjetitelj uzorak i je sličan po osjećaju kao model raščlanjivanja SAX obrade XML dokumenata. U svojoj se srži sastoji od sljedećih komponenata:

  • ClassReader - pomaže u čitanju datoteka klase i početak je transformacije klase
  • ClassVisitor - pruža metode korištene za transformiranje klase nakon čitanja sirovih datoteka klase
  • ClassWriter - koristi se za dobivanje konačnog proizvoda transformacije klase

To je u ClassVisitor da imamo sve metode posjetitelja koje ćemo koristiti za dodirivanje različitih komponenata (polja, metoda itd.) određene Java klase. To radimo do pružajući podrazred od ClassVisitorza provedbu bilo kakvih promjena u datoj klasi.

Zbog potrebe za očuvanjem integriteta izlazne klase u vezi s Java konvencijama i rezultirajućim bajt kodom, ova klasa zahtijeva a strogim redoslijedom kojim bi se trebale nazivati ​​njegove metode za generiranje ispravnih rezultata.

The ClassVisitor metode u API-ju temeljenom na događajima pozivaju se sljedećim redoslijedom:

posjetite visitSource? visitOuterClass? (visitAnnotation | visitAttribute) * (visitInnerClass | visitField | visitMethod) * visitEnd

3.2. API zasnovan na stablu

Ovaj API je više objektno orijentirana API i jest analogno modelu JAXB obrade XML dokumenata.

Još uvijek se temelji na API-ju temeljenom na događajima, ali uvodi ClassNode korijenska klasa. Ova klasa služi kao ulazna točka u strukturu klase.

4. Rad s ASM API-jem temeljenim na događajima

Izmijenit ćemo java.lang.Integer razred s ASM-om. I u ovom trenutku moramo shvatiti temeljni koncept: the ClassVisitor klasa sadrži sve potrebne metode posjetitelja za stvaranje ili izmjenu svih dijelova klase.

Moramo nadjačati potrebnu metodu posjetitelja da bismo primijenili naše promjene. Počnimo s postavljanjem preduvjetnih komponenata:

javna klasa CustomClassWriter {static String className = "java.lang.Integer"; statički niz cloneableInterface = "java / lang / Cloneable"; Čitač ClassReader; Pisac ClassWriter-a; javni CustomClassWriter () {čitač = novi ClassReader (ime klase); pisac = novi ClassWriter (čitač, 0); }}

Ovo koristimo kao osnovu za dodavanje Klonirajući sučelje s dionicom Cijeli broj klase, a dodajemo i polje i metodu.

4.1. Rad s poljima

Stvorimo svoje ClassVisitor koje ćemo upotrijebiti za dodavanje polja u Cijeli broj razred:

javna klasa AddFieldAdapter proširuje ClassVisitor {private String fieldName; private String fieldDefault; privatni int pristup = org.objectweb.asm.Opcodes.ACC_PUBLIC; private boolean isFieldPresent; javni AddFieldAdapter (String fieldName, int fieldAccess, ClassVisitor cv) {super (ASM4, cv); ovo.cv = cv; this.fieldName = PoljeName; this.access = fieldAccess; }} 

Dalje, idemo nadjačati visitField metoda, gdje smo prvi provjerite postoji li polje koje planiramo dodati i postavite zastavicu koja označava status.

Još uvijek moramo prosljeđivanje poziva metode roditeljskoj klasi - ovo se mora dogoditi kao visitField metoda se poziva za svako polje u klasi. Ako ne proslijedi poziv, znači da u razred neće biti upisana polja.

Ova metoda nam također omogućuje izmijeniti vidljivost ili vrstu postojećih polja:

@Override public FieldVisitor visitField (int pristup, naziv niza, opis niza, potpis niza, vrijednost objekta) {if (name.equals (fieldName)) {isFieldPresent = true; } return cv.visitField (pristup, ime, opis, potpis, vrijednost); } 

Prvo provjeravamo zastavicu postavljenu u ranijem visitField metodu i pozovite visitField ponovo metoda, ovaj put pružajući ime, modifikator pristupa i opis. Ova metoda vraća primjerak FieldVisitor.

The visitEnd metoda je zadnja metoda koja se zove redoslijedom metoda posjetitelja. Ovo je preporučeni položaj za provesti logiku umetanja polja.

Zatim, moramo nazvati visitEnd metoda na ovom objektu do signal da smo gotovi s posjetom ovom polju:

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

Važno je biti siguran da sve korištene ASM komponente dolaze iz org.objectweb.asm paket - puno knjižnica interno koristi ASM knjižnicu i IDE-ovi mogu automatski umetnuti priložene ASM knjižnice.

Sada koristimo svoj adapter u addField metoda, dobivanje transformirane verzije java.lang.Integers našim dodanim poljem:

javna klasa CustomClassWriter {AddFieldAdapter addFieldAdapter; // ... javni bajt [] addField () {addFieldAdapter = novi AddFieldAdapter ("aNewBooleanField", org.objectweb.asm.Opcodes.ACC_PUBLIC, pisac); reader.accept (addFieldAdapter, 0); povratak Writer.toByteArray (); }}

Nadjačali smo visitField i visitEnd metode.

Sve što se može učiniti u vezi s poljima događa se s visitField metoda. To znači da također možemo modificirati postojeća polja (recimo, transformirati privatno polje u javno) promjenom željenih vrijednosti proslijeđenih u visitField metoda.

4.2. Rad s metodama

Generiranje cjelovitih metoda u ASM API-u više je uključeno od ostalih operacija u klasi. To uključuje značajnu količinu manipulacije bajt-kodom niske razine i kao rezultat toga izvan je dosega ovog članka.

Međutim, za većinu praktičnih primjena možemo izmijeniti postojeću metodu kako bi bila dostupnija (možda objaviti tako da se može nadjačati ili preopteretiti) ili izmijeniti klasu kako bi je učinili proširivom.

Učinimo javnim metodu toUnsignedString:

javna klasa PublicizeMethodAdapter proširuje ClassVisitor {public PublicizeMethodAdapter (int api, ClassVisitor cv) {super (ASM4, cv); ovo.cv = cv; } public MethodVisitor visitMethod (int pristup, naziv niza, opis niza, potpis niza, niz [] iznimke) {if (name.equals ("toUnsignedString0")) {return cv.visitMethod (ACC_PUBLIC + ACC_STATIC, ime, opis, potpis, iznimke); } return cv.visitMethod (pristup, ime, opis, potpis, iznimke); }} 

Kao i za izmjenu polja, samo smo presretnite metodu posjeta i promijenite parametre koje želimo.

U ovom slučaju koristimo modifikatore pristupa u org.objectweb.asm.Opcodes paket do promijeniti vidljivost metode. Zatim priključujemo naš ClassVisitor:

javni bajt [] publicizeMethod () {pubMethAdapter = novi PublicizeMethodAdapter (pisač); reader.accept (pubMethAdapter, 0); povratak Writer.toByteArray (); } 

4.3. Rad s nastavom

U skladu s istim linijama kao i modificiranje metoda, mi modificirati nastavu presretanjem odgovarajuće metode posjetitelja. U ovom slučaju presrećemo posjetiti, što je prva metoda u hijerarhiji posjetitelja:

javna klasa AddInterfaceAdapter proširuje ClassVisitor {javni AddInterfaceAdapter (ClassVisitor cv) {super (ASM4, cv); } @Override javni void posjet (int verzija, int pristup, naziv niza, potpis niza, niz SuperName, String [] sučelja) {String [] držanje = novi niz [interfaces.length + 1]; držanje [holding.length - 1] = cloneableInterface; System.arraycopy (sučelja, 0, zadržavanje, 0, sučelja.dužina); cv.visit (V1_8, pristup, ime, potpis, superName, posjedovanje); }} 

Nadjačavamo posjetiti metoda za dodavanje Klonirajući sučelje za niz sučelja koje treba podržati Cijeli broj razred. Priključujemo to baš kao i sve druge namjene naših adaptera.

5. Korištenje modificirane klase

Tako smo izmijenili Cijeli broj razred. Sada moramo biti u mogućnosti učitati i koristiti modificiranu verziju klase.

Uz jednostavno pisanje rezultata pisac.toByteArray na disk kao datoteku klase, postoje neki drugi načini interakcije s našim prilagođenim Cijeli broj razred.

5.1. Koristiti TraceClassVisitor

ASM knjižnica nudi TraceClassVisitor klasa korisnosti na koju ćemo se naviknuti proučite modificiranu klasu. Tako možemo potvrditi da su se dogodile naše promjene.

Jer TraceClassVisitor je ClassVisitor, možemo ga koristiti kao zamjensku zamjenu za standard ClassVisitor:

PrintWriter pw = novi PrintWriter (System.out); javni PublicizeMethodAdapter (ClassVisitor cv) {super (ASM4, cv); ovo.cv = cv; tracer = novi TraceClassVisitor (cv, pw); } public MethodVisitor visitMethod (pristup int, naziv niza, opis niza, potpis niza, niz [] izuzetaka) {if (name.equals ("toUnsignedString0")) {System.out.println ("Posjeta nepotpisane metode"); vratiti tracer.visitMethod (ACC_PUBLIC + ACC_STATIC, ime, opis, potpis, iznimke); } return tracer.visitMethod (pristup, ime, opis, potpis, iznimke); } javna praznina visitEnd () {tracer.visitEnd (); System.out.println (tracer.p.getText ()); } 

Ono što smo ovdje učinili je prilagoditi ClassVisitor da smo prešli na naše ranije PublicizeMethodAdapter s TraceClassVisitor.

Sva posjeta sada će se obaviti s našim tragom, koji zatim može ispisati sadržaj transformirane klase, pokazujući sve izmjene koje smo na njoj napravili.

Dok ASM dokumentacija navodi da TraceClassVisitor može ispisati na PrintWriter koji se isporučuje konstruktoru, čini se da ovo ne radi ispravno u najnovijoj verziji ASM-a.

Srećom, imamo pristup osnovnom pisaču u klasi i uspjeli smo ručno ispisati sadržaj teksta tragača u našoj nadjačanoj visitEnd metoda.

5.2. Korištenje Java instrumentacije

Ovo je elegantnije rješenje koje nam omogućuje bliskiji rad s JVM-om putem Instrumentacije.

Za instrumentaciju java.lang.Integer razred, mi napišite agenta koji će biti konfiguriran kao parametar naredbenog retka s JVM-om. Sredstvo zahtijeva dvije komponente:

  • Klasa koja implementira metodu imenovanu premain
  • Provedba ClassFileTransformer u kojem ćemo uvjetno dostaviti modificiranu verziju naše klase
javna klasa Premain {javna statička praznina premain (String agentArgs, Instrumentation inst) {inst.addTransformer (new ClassFileTransformer () {@ Nadjačaj javni bajt [] transformacija (ClassLoader l, naziv niza, klasa c, ProtectionDomain d, byte [] b) baca IllegalClassFormatException {if (name.equals ("java / lang / Integer")) {CustomClassWriter cr = new CustomClassWriter (b); return cr.addField ();} return b;}}); }}

Sada definiramo svoje premain klasa implementacije u datoteci JAR manifesta pomoću dodatka Maven jar:

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

Izgradnjom i pakiranjem našeg koda do sada se dobiva tegla koju možemo napuniti kao sredstvo. Da se koristimo našim prilagođenim Cijeli broj razred u hipotetičkom “YourClass.class“:

java YourClass -javaagent: "/ put / do /AgentAgent.jar"

6. Zaključak

Iako smo ovdje pojedinačno implementirali svoje transformacije, ASM nam omogućuje povezivanje više adaptora kako bismo postigli složene transformacije klasa.

Pored osnovnih transformacija koje smo ovdje ispitali, ASM također podržava interakcije s napomenama, generičkim podacima i unutarnjim klasama.

Vidjeli smo nešto snage ASM biblioteke - uklanja mnoga ograničenja koja bismo mogli naići kod neovisnih knjižnica, pa čak i standardnih JDK klasa.

ASM se naširoko koristi ispod hauba nekih od najpopularnijih knjižnica (Spring, AspectJ, JDK, itd.) Kako bi izveo puno "magije" u letu.

Izvorni kod za ovaj članak možete pronaći u projektu GitHub.