Een gids voor Java-bytecode-manipulatie met ASM

1. Inleiding

In dit artikel zullen we bekijken hoe u de ASM-bibliotheek kunt gebruiken voor het manipuleren van een bestaande Java-klasse door velden toe te voegen, methoden toe te voegen en het gedrag van bestaande methoden te veranderen.

2. Afhankelijkheden

We moeten de ASM-afhankelijkheden toevoegen aan onze pom.xml:

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

We kunnen de nieuwste versies van asm en asm-util krijgen van Maven Central.

3. ASM API Basics

De ASM API biedt twee manieren van interactie met Java-klassen voor transformatie en generatie: op gebeurtenissen gebaseerd en op een boomstructuur.

3.1. Event-gebaseerde API

Deze API is zwaar gebaseerd op de Bezoeker patroon en is vergelijkbaar met het SAX-parseermodel van het verwerken van XML-documenten. Het bestaat in wezen uit de volgende componenten:

  • ClassReader - helpt bij het lezen van klasbestanden en is het begin van het transformeren van een klas
  • KlasseBezoeker - biedt de methoden die worden gebruikt om de klasse te transformeren na het lezen van de onbewerkte klassebestanden
  • ClassWriter - wordt gebruikt om het eindproduct van de klassetransformatie uit te voeren

Het is in de KlasseBezoeker dat we alle bezoekersmethoden hebben die we zullen gebruiken om de verschillende componenten (velden, methoden, enz.) van een bepaalde Java-klasse aan te raken. We doen dit door het verstrekken van een subklasse van KlasseBezoekerom eventuele wijzigingen in een bepaalde klasse door te voeren.

Vanwege de noodzaak om de integriteit van de uitvoerklasse te behouden met betrekking tot Java-conventies en de resulterende bytecode, vereist deze klasse een strikte volgorde waarin de methoden moeten worden aangeroepen om de juiste output te genereren.

De KlasseBezoeker methoden in de op gebeurtenissen gebaseerde API worden aangeroepen in de volgende volgorde:

bezoek visitSource? bezoekOuterClass? (visitAnnotation | visitAttribute) * (visitInnerClass | visitField | visitMethod) * visitEnd

3.2. Tree-gebaseerde API

Deze API is een meer objectgeoriënteerd API en is analoog aan het JAXB-model van het verwerken van XML-documenten.

Het is nog steeds gebaseerd op de op gebeurtenissen gebaseerde API, maar introduceert de ClassNode root-klasse. Deze klasse dient als het toegangspunt tot de klassenstructuur.

4. Werken met de op gebeurtenissen gebaseerde ASM API

We zullen het java.lang.Geheel getal les met ASM. En we moeten op dit punt een fundamenteel concept begrijpen: de KlasseBezoeker class bevat alle noodzakelijke bezoekersmethodes om alle onderdelen van een class te creëren of te wijzigen.

We hoeven alleen de noodzakelijke bezoekersmethode te overschrijven om onze wijzigingen door te voeren. Laten we beginnen met het instellen van de vereiste componenten:

openbare klasse CustomClassWriter {statische String className = "java.lang.Integer"; static String cloneableInterface = "java / lang / Cloneable"; ClassReader-lezer; ClassWriter schrijver; openbare CustomClassWriter () {reader = nieuwe ClassReader (className); writer = nieuwe ClassWriter (lezer, 0); }}

We gebruiken dit als basis om de Te klonen interface naar de voorraad Geheel getal class, en we voegen ook een veld en een methode toe.

4.1. Werken met velden

Laten we onze KlasseBezoeker die we zullen gebruiken om een ​​veld toe te voegen aan de Geheel getal klasse:

public class AddFieldAdapter breidt ClassVisitor {private String fieldName uit; private String fieldDefault; private int access = org.objectweb.asm.Opcodes.ACC_PUBLIC; private boolean isFieldPresent; openbare AddFieldAdapter (String fieldName, int fieldAccess, ClassVisitor cv) {super (ASM4, cv); this.cv = cv; this.fieldName = fieldName; this.access = fieldAccess; }} 

Laten we het volgende doen overschrijven de visitField methode, waar we eerst controleer of het veld dat we willen toevoegen al bestaat en plaats een vlag om de status aan te geven.

We moeten nog steeds stuur de methodeaanroep door naar de bovenliggende klasse - dit moet gebeuren als de visitField methode wordt aangeroepen voor elk veld in de klasse. Als de oproep niet wordt doorgeschakeld, worden er geen velden naar de klas geschreven.

Deze methode stelt ons ook in staat om de zichtbaarheid of het type van bestaande velden wijzigen:

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

We controleren eerst de vlag die is ingesteld in de eerdere visitField methode en roep het visitField methode opnieuw, deze keer met de naam, toegangsmodificator en beschrijving. Deze methode retourneert een exemplaar van Veldbezoeker.

De visitEnd methode is de laatste methode die wordt aangeroepen in volgorde van de bezoekersmethodes. Dit is de aanbevolen positie om voer de veldinvoeglogica uit.

Vervolgens moeten we het visitEnd methode op dit object naar geef aan dat we klaar zijn met het bezoeken van dit veld:

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

Het is belangrijk om er zeker van te zijn dat alle gebruikte ASM-componenten afkomstig zijn van het org.objectweb.asm pakket - veel bibliotheken gebruiken de ASM-bibliotheek intern en IDE's kunnen de gebundelde ASM-bibliotheken automatisch invoegen.

We gebruiken nu onze adapter in de veld toevoegen methode, het verkrijgen van een getransformeerde versie van java.lang.Geheel getalmet ons toegevoegde veld:

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

We hebben het visitField en visitEnd methoden.

Alles wat u met betrekking tot velden moet doen, gebeurt met de visitField methode. Dit betekent dat we ook bestaande velden kunnen wijzigen (bijvoorbeeld een privéveld naar het publiek transformeren) door de gewenste waarden te wijzigen die worden doorgegeven aan de visitField methode.

4.2. Werken met methoden

Het genereren van hele methoden in de ASM API is meer betrokken dan andere bewerkingen in de klas. Dit omvat een aanzienlijke hoeveelheid bytecode-manipulatie op laag niveau en valt daarom buiten het bestek van dit artikel.

Voor de meeste praktische toepassingen kunnen we echter beide een bestaande methode aanpassen om deze toegankelijker te maken (maak het misschien openbaar zodat het kan worden overschreven of overbelast) of wijzig een klasse om deze uitbreidbaar te maken.

Laten we de methode toUnsignedString openbaar maken:

public class PublicizeMethodAdapter breidt ClassVisitor uit {public PublicizeMethodAdapter (int api, ClassVisitor cv) {super (ASM4, cv); this.cv = cv; } public MethodVisitor visitMethod (int access, String name, String desc, String signature, String [] uitzonderingen) {if (name.equals ("toUnsignedString0")) {return cv.visitMethod (ACC_PUBLIC + ACC_STATIC, naam, desc, handtekening, uitzonderingen); } return cv.visitMethod (toegang, naam, desc, handtekening, uitzonderingen); }} 

Zoals we deden voor de veldwijziging, we alleen onderschep de bezoekmethode en verander de gewenste parameters.

In dit geval gebruiken we de toegangsmodificatoren in het org.objectweb.asm.Opcodes pakket naar verander de zichtbaarheid van de methode. We pluggen dan onze KlasseBezoeker:

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

4.3. Werken met klassen

Op dezelfde manier als het wijzigen van methoden, wij klassen wijzigen door de juiste bezoekersmethode te onderscheppen. In dit geval onderscheppen we bezoek, de allereerste methode in de bezoekershiërarchie:

openbare klasse AddInterfaceAdapter breidt ClassVisitor {openbare AddInterfaceAdapter (ClassVisitor cv) {super (ASM4, cv) uit; } @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, toegang, naam, handtekening, superName, holding); }} 

We overschrijven de bezoek methode om het Te klonen interface met de reeks interfaces die worden ondersteund door de Geheel getal klasse. We pluggen deze in, net als alle andere toepassingen van onze adapters.

5. Gebruik van de gewijzigde klasse

Dus we hebben het Geheel getal klasse. Nu moeten we de gewijzigde versie van de klasse kunnen laden en gebruiken.

Naast het simpelweg schrijven van de output van writer.toByteArray naar schijf als een klassebestand, zijn er enkele andere manieren om te communiceren met onze custom Geheel getal klasse.

5.1. De ... gebruiken TraceClassVisitor

De ASM-bibliotheek biedt de TraceClassVisitor utility-klasse die we zullen gebruiken introspecteer de gewijzigde klasse. Dus we kunnen bevestig dat onze wijzigingen zijn doorgevoerd.

Omdat de TraceClassVisitor is een KlasseBezoeker, kunnen we het gebruiken als drop-in vervanging voor een standaard KlasseBezoeker:

PrintWriter pw = nieuwe PrintWriter (System.out); openbare PublicizeMethodAdapter (ClassVisitor cv) {super (ASM4, cv); this.cv = cv; tracer = nieuwe TraceClassVisitor (cv, pw); } public MethodVisitor visitMethod (int access, String name, String desc, String signature, String [] uitzonderingen) {if (name.equals ("toUnsignedString0")) {System.out.println ("Bezoekende niet-ondertekende methode"); retourneer tracer.visitMethod (ACC_PUBLIC + ACC_STATIC, naam, desc, handtekening, uitzonderingen); } retourneer tracer.visitMethod (toegang, naam, desc, handtekening, uitzonderingen); } openbare ongeldige visitEnd () {tracer.visitEnd (); System.out.println (tracer.p.getText ()); } 

Wat we hier hebben gedaan, is het aanpassen van het KlasseBezoeker dat we zijn doorgegeven aan onze eerdere PublicizeMethodAdapter met de TraceClassVisitor.

Al het bezoek wordt nu gedaan met onze tracer, die dan de inhoud van de getransformeerde klasse kan afdrukken, met alle wijzigingen die we erin hebben aangebracht.

Hoewel in de ASM-documentatie staat dat het TraceClassVisitor kan afdrukken naar de PrintWriter dat aan de constructor wordt geleverd, lijkt dit niet goed te werken in de laatste versie van ASM.

Gelukkig hebben we toegang tot de onderliggende printer in de klas en konden we de tekstinhoud van de tracer handmatig afdrukken in onze overschreven visitEnd methode.

5.2. Java-instrumentatie gebruiken

Dit is een elegantere oplossing die ons in staat stelt om via instrumentatie op een dichter niveau met de JVM te werken.

Om het java.lang.Geheel getal klasse, wij schrijf een agent die zal worden geconfigureerd als een opdrachtregelparameter met de JVM. De agent heeft twee componenten nodig:

  • Een klasse die een methode implementeert met de naam premain
  • Een implementatie van ClassFileTransformer waarin we voorwaardelijk de aangepaste versie van onze klas zullen leveren
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) gooit IllegalClassFormatException {if (name.equals ("java / lang / Integer")) {CustomClassWriter cr = nieuwe CustomClassWriter (b); return cr.addField ();} return b;}}); }}

We definiëren nu onze premain implementatieklasse in een JAR-manifestbestand met behulp van de Maven jar-plug-in:

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

Het bouwen en verpakken van onze code tot nu toe levert de pot op die we als agent kunnen laden. Om onze op maat gemaakte Geheel getal les in een hypothetische "YourClass.class“:

java YourClass -javaagent: "/ pad / naar / theAgentJar.jar"

6. Conclusie

Hoewel we onze transformaties hier afzonderlijk hebben geïmplementeerd, stelt ASM ons in staat om meerdere adapters aan elkaar te koppelen om complexe transformaties van klassen te bereiken.

Naast de basistransformaties die we hier hebben onderzocht, ondersteunt ASM ook interacties met annotaties, generieke termen en innerlijke klassen.

We hebben een deel van de kracht van de ASM-bibliotheek gezien - het verwijdert veel beperkingen die we kunnen tegenkomen bij bibliotheken van derden en zelfs standaard JDK-klassen.

ASM wordt veel gebruikt onder de motorkap van enkele van de meest populaire bibliotheken (Spring, AspectJ, JDK, enz.) Om tijdens de vlucht veel "magie" uit te voeren.

Je kunt de broncode voor dit artikel vinden in het GitHub-project.


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