En guide til Java Bytecode Manipulation med ASM

1. Introduksjon

I denne artikkelen vil vi se på hvordan du bruker ASM-biblioteket for å manipulere en eksisterende Java-klasse ved å legge til felt, legge til metoder og endre atferden til eksisterende metoder.

2. Avhengigheter

Vi må legge til ASM-avhengigheter i vår pom.xml:

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

Vi kan få de nyeste versjonene av asm og asm-util fra Maven Central.

3. Grunnleggende om ASM API

ASM API tilbyr to stiler for interaksjon med Java-klasser for transformasjon og generering: hendelsesbasert og trebasert.

3.1. Hendelsesbasert API

Denne API-en er tungt basert på Besøkende mønster og er lignende følelse som SAX-analyseringsmodellen for behandling av XML-dokumenter. Den består, i kjernen, av følgende komponenter:

  • ClassReader - hjelper til med å lese klassefiler og er begynnelsen på å transformere en klasse
  • ClassVisitor - gir metodene som brukes til å transformere klassen etter å ha lest råklassefilene
  • ClassWriter - brukes til å levere det endelige produktet av klassetransformasjonen

Det er i ClassVisitor at vi har alle besøkende metoder som vi vil bruke til å berøre de forskjellige komponentene (felt, metoder osv.) i en gitt Java-klasse. Vi gjør dette av gir en underklasse av ClassVisitorå implementere eventuelle endringer i en gitt klasse.

På grunn av behovet for å bevare integriteten til utgangsklassen angående Java-konvensjoner og den resulterende bykoden, krever denne klassen en streng rekkefølge der metodene skal kalles for å generere riktig produksjon.

De ClassVisitor metoder i det hendelsesbaserte API kalles i følgende rekkefølge:

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

3.2. Trebasert API

Denne API-en er en mer objektorientert API og er analog med JAXB-modellen for behandling av XML-dokumenter.

Det er fortsatt basert på det hendelsesbaserte API-et, men det introduserer ClassNode rotklasse. Denne klassen fungerer som inngangspunkt i klassestrukturen.

4. Arbeide med det hendelsesbaserte ASM API

Vi endrer java.lang. heltall klasse med ASM. Og vi må forstå et grunnleggende konsept på dette punktet: de ClassVisitor klasse inneholder alle nødvendige besøkende metoder for å opprette eller endre alle delene av en klasse.

Vi trenger bare å overstyre den nødvendige besøkende metoden for å implementere endringene våre. La oss starte med å sette opp forutsetningskomponentene:

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

Vi bruker dette som grunnlag for å legge til Klonbar grensesnitt til aksjen Heltall klasse, og vi legger også til et felt og en metode.

4.1. Arbeide med felt

La oss lage våre ClassVisitor som vi bruker til å legge til et felt i Heltall klasse:

offentlig klasse AddFieldAdapter utvider ClassVisitor {private String fieldName; privat streng felt Standard; privat int tilgang = 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; }} 

Neste, la oss overstyre visitField metode, hvor vi først sjekk om feltet vi planlegger å legge til allerede eksisterer og sett et flagg for å indikere statusen.

Vi må fortsatt videresend metodekallingen til foreldreklassen - dette må skje som visitField metoden kalles for hvert felt i klassen. Unnlatelse av å videresende samtalen betyr at ingen felt vil bli skrevet til klassen.

Denne metoden lar oss også endre synligheten eller typen av eksisterende felt:

@Override public FieldVisitor visitField (int access, String name, String desc, String signature, Object value) {if (name.equals (fieldName)) {isFieldPresent = true; } returner cv.visitField (tilgang, navn, beskrivelse, signatur, verdi); } 

Vi sjekker først flagget satt i det tidligere visitField metode og kalle visitField metoden igjen, denne gangen med navn, tilgangsmodifikator og beskrivelse. Denne metoden returnerer en forekomst av FieldVisitor.

De visitEnd metoden er den siste metoden som kalles i rekkefølge etter besøkende metoder. Dette er den anbefalte posisjonen til gjennomføre feltinnsettingslogikken.

Så må vi ringe visitEnd metoden på dette objektet til signal om at vi er ferdige med å besøke dette feltet:

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

Det er viktig å være sikker på at alle ASM-komponentene som brukes kommer fra org.objectweb.asm pakke - mange biblioteker bruker ASM-biblioteket internt, og IDE-er kan automatisk sette inn de medfølgende ASM-bibliotekene.

Vi bruker nå adapteren vår i addField metode, skaffe en transformert versjon av java.lang. heltallmed vårt tilleggsfelt:

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

Vi har overstyrt visitField og visitEnd metoder.

Alt som skal gjøres angående felt skjer med visitField metode. Dette betyr at vi også kan endre eksisterende felt (for eksempel å transformere et privat felt til publikum) ved å endre de ønskede verdiene som sendes til visitField metode.

4.2. Arbeide med metoder

Å generere hele metoder i ASM API er mer involvert enn andre operasjoner i klassen. Dette involverer en betydelig mengde byte-kodemanipulering på lavt nivå, og som et resultat er det utenfor omfanget av denne artikkelen.

For de fleste praktiske bruksområder kan vi imidlertid gjøre det endre en eksisterende metode for å gjøre den mer tilgjengelig (kanskje gjør det offentlig slik at det kan overstyres eller overbelastes) eller endre en klasse for å gjøre den utvidbar.

La oss gjøre metoden toUnsignedString offentlig:

public class PublicizeMethodAdapter utvider ClassVisitor {publicizeMethodAdapter (int api, ClassVisitor cv) {super (ASM4, cv); this.cv = cv; } offentlig MethodVisitor visitMethod (int-tilgang, strengnavn, strengbeskrivelse, streng-signatur, streng [] unntak) {if (name.equals ("toUnsignedString0")) {return cv.visitMethod (ACC_PUBLIC + ACC_STATIC, name, desc, signature, unntak); } returner cv.visitMethod (tilgang, navn, beskrivelse, signatur, unntak); }} 

Som vi gjorde for feltendring, gjorde vi bare avskjær besøksmetoden og endre parametrene vi ønsker.

I dette tilfellet bruker vi tilgangsmodifikatorene i org.objectweb.asm.Opcodes pakke til endre synligheten til metoden. Vi kobler deretter til ClassVisitor:

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

4.3. Arbeide med klasser

På samme måte som endringsmetoder, vi endre klasser ved å fange opp den riktige besøksmetoden. I dette tilfellet avskjærer vi besøk, som er den aller første metoden i besøkshierarkiet:

public class AddInterfaceAdapter utvider ClassVisitor {public AddInterfaceAdapter (ClassVisitor cv) {super (ASM4, cv); } @ Overstyr offentlig ugyldig besøk (int versjon, int tilgang, strengnavn, streng signatur, streng supernavn, streng [] grensesnitt) {streng [] holder = ny streng [grensesnitt.lengde + 1]; holding [holding.length - 1] = cloneableInterface; System.arraycopy (grensesnitt, 0, holder, 0, grensesnitt.lengde); cv.visit (V1_8, tilgang, navn, signatur, supernavn, holder); }} 

Vi overstyrer besøk metode for å legge til Klonbar grensesnitt til en rekke grensesnitt som skal støttes av Heltall klasse. Vi kobler dette inn akkurat som alle andre bruksområder for adapterne våre.

5. Bruke den modifiserte klassen

Så vi har endret Heltall klasse. Nå må vi kunne laste og bruke den modifiserte versjonen av klassen.

I tillegg til å bare skrive utdataene fra writer.toByteArray til disk som en klassefil, er det noen andre måter å samhandle med vår tilpassede Heltall klasse.

5.1. Bruker TraceClassVisitor

ASM-biblioteket tilbyr TraceClassVisitor verktøy klasse som vi vil bruke til introspekter den modifiserte klassen. Dermed kan vi bekreft at endringene våre har skjedd.

Fordi det TraceClassVisitor er en ClassVisitor, kan vi bruke den som en 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); } offentlige MethodVisitor visitMethod (int tilgang, strengnavn, strengbeskrivelse, streng signatur, streng [] unntak) {if (name.equals ("toUnsignedString0")) {System.out.println ("Besøk usignert metode"); returner tracer.visitMethod (ACC_PUBLIC + ACC_STATIC, navn, beskrivelse, signatur, unntak); } returner tracer.visitMethod (tilgang, navn, beskrivelse, signatur, unntak); } offentlig ugyldig visitEnd () {tracer.visitEnd (); System.out.println (tracer.p.getText ()); } 

Det vi har gjort her er å tilpasse ClassVisitor at vi gikk til vår tidligere PublicizeMethodAdapter med TraceClassVisitor.

Alt besøket vil nå gjøres med sporeren vår, som deretter kan skrive ut innholdet i den transformerte klassen, og vise eventuelle endringer vi har gjort på den.

Mens ASM-dokumentasjonen sier at TraceClassVisitor kan skrive ut til PrintWriter som er levert til konstruktøren, ser dette ikke ut til å fungere skikkelig i den siste versjonen av ASM.

Heldigvis har vi tilgang til den underliggende skriveren i klassen og kunne manuelt skrive ut sporingsinnholdet i overstyrt visitEnd metode.

5.2. Bruke Java Instrumentation

Dette er en mer elegant løsning som lar oss jobbe med JVM på et nærmere nivå via Instrumentation.

Å instrumentere java.lang. heltall klasse, vi skriv en agent som vil bli konfigurert som en kommandolinjeparameter med JVM. Agenten krever to komponenter:

  • En klasse som implementerer en metode som heter premain
  • En implementering av ClassFileTransformer der vi vil betinget levere den modifiserte versjonen av klassen vår
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 nå vår premain implementeringsklasse i en JAR-manifestfil ved hjelp av Maven jar-plugin:

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

Å bygge og pakke koden vår så langt produserer glasset som vi kan laste som agent. For å bruke vår tilpassede Heltall klasse i en hypotetisk “YourClass.class“:

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

6. Konklusjon

Mens vi implementerte transformasjonene våre her hver for seg, tillater ASM oss å koble flere adaptere sammen for å oppnå komplekse transformasjoner av klasser.

I tillegg til de grunnleggende transformasjonene vi undersøkte her, støtter ASM også interaksjoner med merknader, generiske og indre klasser.

Vi har sett noe av kraften til ASM-biblioteket - det fjerner mange begrensninger vi kan støte på med tredjepartsbiblioteker og til og med standard JDK-klasser.

ASM brukes mye under panseret på noen av de mest populære bibliotekene (Spring, AspectJ, JDK, etc.) for å utføre mye "magi" på farten.

Du finner kildekoden for denne artikkelen i GitHub-prosjektet.