En guide til Java Bytecode Manipulation med ASM

1. Introduktion

I denne artikel ser vi på, hvordan du bruger ASM-biblioteket til manipulation af en eksisterende Java-klasse ved at tilføje felter, tilføje metoder og ændre adfærd for eksisterende metoder.

2. Afhængigheder

Vi skal tilføje ASM-afhængigheder til vores pom.xml:

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

Vi kan få de nyeste versioner af asm og asm-util fra Maven Central.

3. Grundlæggende om ASM API

ASM API giver to stilarter til interaktion med Java-klasser til transformation og generation: begivenhedsbaseret og træbaseret.

3.1. Begivenhedsbaseret API

Denne API er tungt baseret på Besøgende mønster og er svarer til SAX-parseringsmodellen behandling af XML-dokumenter. Den består i sin kerne af følgende komponenter:

  • ClassReader - hjælper med at læse klassefiler og er begyndelsen på at transformere en klasse
  • ClassVisitor - giver de metoder, der bruges til at transformere klassen efter at have læst de rå klassefiler
  • ClassWriter - bruges til at afgive det endelige produkt af klassetransformationen

Det er i ClassVisitor at vi har alle de besøgende metoder, som vi bruger til at røre ved de forskellige komponenter (felter, metoder osv.) i en given Java-klasse. Vi gør dette ved giver en underklasse af ClassVisitorat implementere eventuelle ændringer i en given klasse.

På grund af behovet for at bevare integriteten af ​​outputklassen vedrørende Java-konventioner og den deraf følgende bytode, kræver denne klasse en streng rækkefølge, hvor dens metoder skal kaldes for at generere korrekt output.

Det ClassVisitor metoder i den hændelsesbaserede API kaldes i følgende rækkefølge:

besøg visitSource? visitOuterClass? (visitAnnotation | visitAttribute) * (visitInnerClass | visitField | visitMethod) * visitEnd

3.2. Træbaseret API

Denne API er en mere objektorienteret API og er analog med JAXB-modellen behandling af XML-dokumenter.

Det er stadig baseret på den hændelsesbaserede API, men det introducerer ClassNode rodklasse. Denne klasse fungerer som indgangspunkt i klassestrukturen.

4. Arbejde med den hændelsesbaserede ASM API

Vi ændrer java.lang. heltal klasse med ASM. Og vi er nødt til at forstå et grundlæggende koncept på dette tidspunkt: det ClassVisitor klasse indeholder alle de nødvendige besøgsmetoder til at oprette eller ændre alle delene i en klasse.

Vi behøver kun at tilsidesætte den nødvendige besøgende metode til at gennemføre vores ændringer. Lad os starte med at opsætte de forudsatte komponenter:

offentlig klasse CustomClassWriter {static String className = "java.lang.Integer"; statisk streng cloneableInterface = "java / lang / Cloneable"; ClassReader-læser; ClassWriter-forfatter; offentlig CustomClassWriter () {reader = new ClassReader (className); forfatter = ny ClassWriter (læser, 0); }}

Vi bruger dette som grundlag for at tilføje Klonabel interface til bestanden Heltal klasse, og vi tilføjer også et felt og en metode.

4.1. Arbejde med felter

Lad os skabe vores ClassVisitor som vi bruger til at føje et felt til Heltal klasse:

offentlig klasse AddFieldAdapter udvider ClassVisitor {private String fieldName; privat streng feltstandard; privat int adgang = org.objectweb.asm.Opcodes.ACC_PUBLIC; privat boolsk isFieldPresent; public AddFieldAdapter (String fieldName, int fieldAccess, ClassVisitor cv) {super (ASM4, cv); this.cv = cv; this.fieldName = fieldName; this.access = fieldAccess; }} 

Lad os derefter tilsidesætte visitField metode, hvor vi først Kontroller, om det felt, vi planlægger at tilføje, allerede findes, og indstil et flag, der angiver status.

Det skal vi stadig videresend metodekaldet til overordnet klasse - dette skal ske som visitField metode kaldes for hvert felt i klassen. Manglende videresendelse af opkaldet betyder, at ingen felter vil blive skrevet til klassen.

Denne metode giver os også mulighed for ændre synligheden eller typen af ​​eksisterende felter:

@Override public FieldVisitor visitField (int adgang, streng navn, streng beskrivelse, streng signatur, objekt værdi) {if (name.equals (fieldName)) {isFieldPresent = true; } returnere cv.visitField (adgang, navn, beskrivelse, signatur, værdi); } 

Vi kontrollerer først det flag, der er sat i det tidligere visitField metode og kalde visitField metode igen, denne gang med navn, adgangsmodifikator og beskrivelse. Denne metode returnerer en forekomst af FieldVisitor.

Det visitEnd metode er den sidste metode, der kaldes i rækkefølge efter besøgsmetoderne. Dette er den anbefalede position til udføre feltindsættelseslogikken.

Så er vi nødt til at ringe til visitEnd metode på dette objekt til signal om, at vi er færdige med at besøge dette felt:

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

Det er vigtigt at være sikker på, at alle anvendte ASM-komponenter kommer fra org.objectweb.asm pakke - mange biblioteker bruger ASM-biblioteket internt, og IDE'er kunne automatisk indsætte de medfølgende ASM-biblioteker.

Vi bruger nu vores adapter i addField metode, opnåelse af en transformeret version af java.lang. heltalmed vores tilføjede felt:

offentlig klasse CustomClassWriter {AddFieldAdapter addFieldAdapter; // ... offentlig byte [] addField () {addFieldAdapter = ny AddFieldAdapter ("aNewBooleanField", org.objectweb.asm.Opcodes.ACC_PUBLIC, forfatter); reader.accept (addFieldAdapter, 0); returner writer.toByteArray (); }}

Vi har tilsidesat visitField og visitEnd metoder.

Alt, hvad der skal gøres vedrørende felter, sker med visitField metode. Dette betyder, at vi også kan ændre eksisterende felter (f.eks. Omdanne et privat felt til offentligheden) ved at ændre de ønskede værdier, der sendes til visitField metode.

4.2. Arbejde med metoder

Generering af hele metoder i ASM API er mere involveret end andre operationer i klassen. Dette involverer en betydelig mængde byte-kodemanipulation på lavt niveau og er derfor uden for denne artikels anvendelsesområde.

Til de fleste praktiske anvendelser kan vi dog enten ændre en eksisterende metode for at gøre den mere tilgængelig (måske gør det offentligt, så det kan tilsidesættes eller overbelastes) eller ændre en klasse for at gøre den udvidelig.

Lad os gøre metoden toUnsignedString offentlig:

public class PublicizeMethodAdapter udvider ClassVisitor {publicizeMethodAdapter (int api, ClassVisitor cv) {super (ASM4, cv); this.cv = cv; } offentlig MethodVisitor visitMethod (int adgang, streng navn, streng desc, streng signatur, streng [] undtagelser) {if (name.equals ("toUnsignedString0")) {return cv.visitMethod (ACC_PUBLIC + ACC_STATIC, navn, desc, signatur, undtagelser); } returner cv.visitMethod (adgang, navn, beskrivelse, underskrift, undtagelser); }} 

Som vi gjorde for feltændring, gjorde vi blot aflytte besøgsmetoden og ændre de parametre, vi ønsker.

I dette tilfælde bruger vi adgangsmodifikatorerne i org.objectweb.asm.Opcodes pakke til ændre synligheden af ​​metoden. Vi tilslutter derefter vores ClassVisitor:

offentlig byte [] publicizeMethod () {pubMethAdapter = ny PublicizeMethodAdapter (forfatter); reader.accept (pubMethAdapter, 0); returner writer.toByteArray (); } 

4.3. Arbejde med klasser

På samme måde som at ændre metoder, vi ændre klasser ved at opfange den passende gæstemetode. I dette tilfælde aflytter vi besøg, som er den allerførste metode i besøgshierarkiet:

offentlig klasse AddInterfaceAdapter udvider ClassVisitor {public AddInterfaceAdapter (ClassVisitor cv) {super (ASM4, cv); } @Override offentligt ugyldigt besøg (int version, int adgang, streng navn, streng signatur, streng supernavn, streng [] grænseflader) {streng [] holder = ny streng [grænseflader.længde + 1]; holding [holding.length - 1] = cloneableInterface; System.arraycopy (grænseflader, 0, hold, 0, grænseflader.længde); cv.visit (V1_8, adgang, navn, signatur, supernavn, bedrift); }} 

Vi tilsidesætter besøg metode til at tilføje Klonabel interface til den række af grænseflader, der skal understøttes af Heltal klasse. Vi tilslutter dette ligesom alle andre anvendelser af vores adaptere.

5. Brug af den modificerede klasse

Så vi har ændret Heltal klasse. Nu skal vi være i stand til at indlæse og bruge den ændrede version af klassen.

Ud over blot at skrive output af writer.toByteArray til disk som en klassefil, er der nogle andre måder at interagere med vores tilpassede Heltal klasse.

5.1. Bruger TraceClassVisitor

ASM-biblioteket leverer TraceClassVisitor utility klasse, som vi vil bruge til introspekter den modificerede klasse. Således kan vi bekræft, at vores ændringer er sket.

Fordi TraceClassVisitor er en ClassVisitor, vi kan bruge det som drop-in erstatning for en standard ClassVisitor:

PrintWriter pw = ny PrintWriter (System.out); public PublicizeMethodAdapter (ClassVisitor cv) {super (ASM4, cv); this.cv = cv; tracer = ny TraceClassVisitor (cv, pw); } offentlig MethodVisitor visitMethod (int adgang, streng navn, streng beskrivelse, streng signatur, streng [] undtagelser) {if (name.equals ("toUnsignedString0")) {System.out.println ("Besøg usigneret metode"); returner tracer.visitMethod (ACC_PUBLIC + ACC_STATIC, navn, beskrivelse, underskrift, undtagelser); } returner tracer.visitMethod (adgang, navn, beskrivelse, underskrift, undtagelser); } offentligt ugyldigt visitEnd () {tracer.visitEnd (); System.out.println (tracer.p.getText ()); } 

Det, vi har gjort her, er at tilpasse ClassVisitor at vi gik til vores tidligere PublicizeMethodAdapter med TraceClassVisitor.

Alt besøg vil nu blive gjort med vores sporingsenhed, som derefter kan udskrive indholdet af den transformerede klasse og vise de ændringer, vi har foretaget til den.

Mens ASM-dokumentationen siger, at TraceClassVisitor kan udskrive til PrintWriter der leveres til konstruktøren, ser det ikke ud til at fungere korrekt i den nyeste version af ASM.

Heldigvis har vi adgang til den underliggende printer i klassen og kunne manuelt udskrive sporers tekstindhold i vores tilsidesatte visitEnd metode.

5.2. Brug af Java-instrumentering

Dette er en mere elegant løsning, der giver os mulighed for at arbejde med JVM på et tættere niveau via Instrumentation.

At instrumentere java.lang. heltal klasse, vi skriv en agent, der konfigureres som en kommandolinjeparameter med JVM. Agenten kræver to komponenter:

  • En klasse, der implementerer en navngivet metode premain
  • En implementering af ClassFileTransformer hvor vi betinget leverer den ændrede version af vores klasse
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) kaster IllegalClassFormatException {if (name.equals ("java / lang / Integer")) {CustomClassWriter cr = new CustomClassWriter (b); return cr.addField ();} return b;}}); }}

Vi definerer nu vores premain implementeringsklasse i en JAR-manifestfil ved hjælp af Maven jar-pluginet:

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

Opbygning og emballering af vores kode hidtil producerer den krukke, som vi kan lægge som agent. At bruge vores tilpassede Heltal klasse i en hypotetisk ”YourClass.class“:

java YourClass -javaagent: "/ sti / til / theAgentJar.jar"

6. Konklusion

Mens vi implementerede vores transformationer her individuelt, giver ASM os mulighed for at kæde flere adaptere sammen for at opnå komplekse transformationer af klasser.

Ud over de grundlæggende transformationer, vi undersøgte her, understøtter ASM også interaktioner med annotationer, generiske og indre klasser.

Vi har set noget af styrken i ASM-biblioteket - det fjerner mange begrænsninger, vi kan støde på med tredjepartsbiblioteker og endda standard JDK-klasser.

ASM bruges i vid udstrækning under hætte på nogle af de mest populære biblioteker (Spring, AspectJ, JDK osv.) Til at udføre en masse "magi" på farten.

Du kan finde kildekoden til denne artikel i GitHub-projektet.


$config[zx-auto] not found$config[zx-overlay] not found