Java >> Java tutorial >  >> Tag >> byte

En guide til Java Bytecode Manipulation med ASM

1. Introduktion

I denne artikel vil vi se på, hvordan man bruger ASM-biblioteket til at manipulere en eksisterende Java-klasse ved at tilføje felter, tilføje metoder og ændre adfærden af ​​eksisterende metoder.

2. Afhængigheder

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

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>6.0</version>
</dependency>
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm-util</artifactId>
    <version>6.0</version>
</dependency>

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

3. Grundlæggende om ASM API

ASM API'en giver to stilarter til at interagere med Java-klasser til transformation og generering:begivenhedsbaseret og træbaseret.

3.1. Hændelsesbaseret API

Denne API er stærkt baseret på Visitor mønster og er lignende i fornemmelsen til SAX-parsing-modellen behandling af XML-dokumenter. Den består i sin kerne af følgende komponenter:

  • Klasselæser – hjælper med at læse klassefiler og er begyndelsen på at transformere en klasse
  • Klassebesøger – giver de metoder, der bruges til at transformere klassen efter at have læst de rå klassefiler
  • Klasseskriver – bruges til at udlæse det endelige produkt af klassetransformationen

Det er i ClassVisitor at vi har alle de besøgende metoder, som vi vil bruge til at røre ved de forskellige komponenter (felter, metoder osv.) i en given Java-klasse. Det gør vi ved at levere en underklasse af Klassebesøger at implementere ændringer i en given klasse.

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

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

visit
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 af behandling af XML-dokumenter.

Den er stadig baseret på den begivenhedsbaserede API, men den introducerer ClassNode rodklasse. Denne klasse fungerer som indgangspunktet til klassestrukturen.

4. Arbejde med den begivenhedsbaserede ASM API

Vi ændrer java.lang.Integer klasse med ASM. Og vi er nødt til at forstå et grundlæggende koncept på dette tidspunkt:ClassVisitor klasse indeholder alle de nødvendige besøgende metoder til at oprette eller ændre alle dele af en klasse .

Vi behøver kun at tilsidesætte den nødvendige besøgsmetode for at implementere vores ændringer. Lad os starte med at opsætte de nødvendige komponenter:

public class CustomClassWriter {

    static String className = "java.lang.Integer"; 
    static String cloneableInterface = "java/lang/Cloneable";
    ClassReader reader;
    ClassWriter writer;

    public CustomClassWriter() {
        reader = new ClassReader(className);
        writer = new ClassWriter(reader, 0);
    }
}

Vi bruger dette som grundlag for at tilføje den Klonbare grænseflade til aktien Heltal klasse, og vi tilføjer også et felt og en metode.

4.1. Arbejde med felter

Lad os oprette vores ClassVisitor som vi bruger til at tilføje et felt til heltal klasse:

public class AddFieldAdapter extends ClassVisitor {
    private String fieldName;
    private String fieldDefault;
    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;
    }
}

Lad os derefter tilsidesætte besøgsfeltet metode , hvor vi først kontrollerer, om det felt, vi planlægger at tilføje, allerede eksisterer og sætter et flag for at angive status .

Vi skal stadig viderestille metodekaldet til den overordnede klasse — dette skal ske som visitField metode kaldes for hvert felt i klassen. Hvis opkaldet ikke viderestilles, betyder det, at der ikke bliver skrevet felter til klassen.

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

@Override
public FieldVisitor visitField(
  int access, String name, String desc, String signature, Object value) {
    if (name.equals(fieldName)) {
        isFieldPresent = true;
    }
    return cv.visitField(access, name, desc, signature, value); 
}

Vi tjekker først flaget i det tidligere besøgsfelt metode og kald visitField metode igen, denne gang med navn, adgangsmodifikator og beskrivelse. Denne metode returnerer en forekomst af FieldVisitor.

The visitEnd metode er den sidste metode kaldet i rækkefølge efter besøgsmetoderne. Dette er den anbefalede position for at udføre feltindsættelseslogikken .

Derefter skal vi ringe til visitEnd metode på dette objekt for at signalere, at vi er færdige med at besøge dette felt:

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

Det er vigtigt at være sikker på, at alle de 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å en transformeret version af java.lang.Integer med vores tilføjede felt:

public class CustomClassWriter {
    AddFieldAdapter addFieldAdapter;
    //...
    public byte[] addField() {
        addFieldAdapter = new AddFieldAdapter(
          "aNewBooleanField",
          org.objectweb.asm.Opcodes.ACC_PUBLIC,
          writer);
        reader.accept(addFieldAdapter, 0);
        return writer.toByteArray();
    }
}

Vi har tilsidesat visitField og visitEnd metoder.

Alt, der skal gøres vedrørende felter, sker med visitField metode. Det betyder, at vi også kan ændre eksisterende felter (f.eks. transformere 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 som et resultat uden for denne artikels omfang.

Til de fleste praktiske formål kan vi dog enten ændre en eksisterende metode for at gøre den mere tilgængelig (måske gøre den offentlig, så den kan tilsidesættes eller overbelastes) eller ændre en klasse for at gøre den udvidelsesbar .

Lad os gøre toUnsignedString-metoden offentlig:

public class PublicizeMethodAdapter extends 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,
              exceptions);
        }
        return cv.visitMethod(
          access, name, desc, signature, exceptions);
   }
}

Ligesom vi gjorde for feltmodifikationen, opsnapper vi blot besøgsmetoden og ændrer de parametre, vi ønsker .

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

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

4.3. Arbejde med klasser

På samme måde som ved at ændre metoder, modificerer vi klasser ved at opsnappe den relevante besøgsmetode . I dette tilfælde opsnapper vi besøg , som er den allerførste metode i besøgshierarkiet:

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, access, name, signature, superName, holding);
    }
}

Vi tilsidesætter besøget metode til at tilføje Klonbar grænseflade til rækken af ​​grænseflader, der skal understøttes af Heltal klasse. Vi tilslutter dette ligesom alle andre anvendelser af vores adaptere.

5. Brug af den ændrede klasse

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

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

5.1. Brug af TraceClassVisitor

ASM-biblioteket leverer TraceClassVisitor hjælpeklasse, som vi vil bruge til at introspektere den modificerede klasse . Således kan vi bekræfte, at vores ændringer er sket .

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

PrintWriter pw = new PrintWriter(System.out);

public PublicizeMethodAdapter(ClassVisitor cv) {
    super(ASM4, cv);
    this.cv = cv;
    tracer = new TraceClassVisitor(cv,pw);
}

public MethodVisitor visitMethod(
  int access,
  String name,
  String desc,
  String signature,
  String[] exceptions) {
    if (name.equals("toUnsignedString0")) {
        System.out.println("Visiting unsigned method");
        return tracer.visitMethod(
          ACC_PUBLIC + ACC_STATIC, name, desc, signature, exceptions);
    }
    return tracer.visitMethod(
      access, name, desc, signature, exceptions);
}

public void visitEnd(){
    tracer.visitEnd();
    System.out.println(tracer.p.getText());
}

Det, vi har gjort her, er at tilpasse ClassVisitor som vi overførte til vores tidligere PublicizeMethodAdapter med TraceClassVisitor .

Alle besøg vil nu blive udført med vores sporstof, som derefter kan udskrive indholdet af den transformerede klasse og vise eventuelle ændringer, vi har foretaget til den.

Mens ASM-dokumentationen siger, at TraceClassVisitor kan udskrive til PrintWriter som er leveret til konstruktøren, ser dette ikke ud til at fungere korrekt i den seneste version af ASM.

Heldigvis har vi adgang til den underliggende printer i klassen og var i stand til manuelt at udskrive sporingens tekstindhold i vores tilsidesatte visitEnd metode.

5.2. Brug af Java Instrumentation

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

At instrumentere java.lang.Integer klasse, skriver vi en agent, der vil blive konfigureret som en kommandolinjeparameter med JVM . Agenten kræver to komponenter:

  • En klasse, der implementerer en metode ved navn 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)
              throws 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-plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>2.4</version>
    <configuration>
        <archive>
            <manifestEntries>
                <Premain-Class>
                    com.baeldung.examples.asm.instrumentation.Premain
                </Premain-Class>
                <Can-Retransform-Classes>
                    true
                </Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

Bygning og emballering af vores kode indtil videre producerer den krukke, som vi kan fylde som agent. For at bruge vores tilpassede heltal klasse i en hypotetisk "YourClass.class “:

java YourClass -javaagent:"/path/to/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 har undersøgt her, understøtter ASM også interaktioner med annoteringer, generiske og indre klasser.

Vi har set noget af kraften ved ASM-biblioteket – det fjerner en masse begrænsninger, vi kan støde på med tredjepartsbiblioteker og endda standard JDK-klasser.

ASM er meget brugt under hætten på nogle af de mest populære biblioteker (Spring, AspectJ, JDK osv.) til at udføre en masse "magi" i farten.

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


Java tag