Ръководство за манипулация на байт кодове на Java с ASM

1. Въведение

В тази статия ще разгледаме как да използваме ASM библиотеката за манипулиране на съществуващ клас Java чрез добавяне на полета, добавяне на методи и промяна на поведението на съществуващите методи.

2. Зависимости

Трябва да добавим ASM зависимостите към нашия pom.xml :

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

Можем да получим най-новите версии на asm и asm-util от Maven Central.

3. Основи на API на ASM

ASM API осигурява два стила на взаимодействие с Java класове за трансформация и генериране: базиран на събития и дървовиден.

3.1. API, базиран на събития

Този API е силно базиран на шаблона на посетителя и е подобен по усещане на модела за синтактичен анализ на SAX за обработка на XML документи. Той се състои в основата си от следните компоненти:

  • ClassReader - помага за четене на файлове с класове и е началото на трансформирането на клас
  • ClassVisitor - предоставя методите, използвани за трансформиране на класа след четене на суровите файлове на класа
  • ClassWriter - използва се за извеждане на крайния продукт от преобразуването на класа

В ClassVisitor имаме всички посетителски методи, които ще използваме, за да докоснем различните компоненти (полета, методи и т.н.) на даден Java клас. Правим това, като предоставяме подклас на ClassVisitor, за да приложим всякакви промени в даден клас.

Поради необходимостта да се запази целостта на изходния клас, отнасящ се до Java конвенциите и получения байтов код, този клас изисква строг ред, в който неговите методи трябва да бъдат извикани, за да генерират правилен изход.

Методите ClassVisitor в API, базиран на събития, се извикват в следния ред:

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

3.2. API, базиран на дърво

Този API е по -обектно ориентиран API и е аналог на JAXB модела за обработка на XML документи.

Той все още се основава на API, базиран на събития, но въвежда коренния клас ClassNode . Този клас служи като входна точка в структурата на класа.

4. Работа с ASM API, базиран на събития

Ще модифицираме класа java.lang.Integer с ASM. И ние трябва да се разбере на основен принцип в този момент: на ClassVisitor клас съдържа всички необходими методи посетителите, за да създадете или промените всички части на един клас .

Трябва само да заменим необходимия метод за посетители, за да приложим промените си. Нека започнем с настройка на необходимите компоненти:

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); } }

Използваме това като основа, за да добавим интерфейса Cloneable към класа Integer на склад , а също така добавяме поле и метод.

4.1. Работа с полета

Нека създадем нашия ClassVisitor , който ще използваме за добавяне на поле към класа Integer :

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; } } 

На следващо място, нека да я отменят visitField метод , където за първи път се провери дали областта планираме да добавим вече съществува и да зададете флаг за показване на състоянието .

Все още трябва да препратим извикването на метода към родителския клас - това трябва да се случи, тъй като методът visitField се извиква за всяко поле в класа. Неуспешното пренасочване на обаждането означава, че няма да бъдат записани полета в класа.

Този метод също ни позволява да модифицираме видимостта или типа на съществуващите полета :

@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); } 

Първо проверяваме флага, зададен в по- ранния метод visitField и извикваме метода visitField отново, този път предоставяйки име, модификатор на достъп и описание. Този метод връща екземпляр на FieldVisitor.

Методът visitEnd е последният метод, извикан по реда на методите на посетителя. Това е препоръчителната позиция за извършване на логиката за вмъкване на полето .

След това трябва да извикаме метода visitEnd на този обект, за да сигнализираме, че сме приключили с посещението на това поле:

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

Важно е да сте сигурни, че всички използвани ASM компоненти идват от пакета org.objectweb.asm - много библиотеки използват ASM библиотеката вътрешно и IDE могат автоматично да вмъкват включените в комплекта ASM библиотеки.

Сега използваме нашия адаптер в метода addField , получавайки трансформирана версия на java.lang.Integer с нашето добавено поле:

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(); } }

Ние сме заменили visitField и visitEnd методи.

Всичко, което трябва да се направи по отношение на полетата, се случва с метода visitField . Това означава, че можем също да модифицираме съществуващи полета (да речем, трансформиране на частно поле в публично) чрез промяна на желаните стойности, предадени на метода visitField .

4.2. Работа с методи

Генерирането на цели методи в ASM API е по-ангажирано от други операции в класа. Това включва значително количество манипулация на байтов код с ниско ниво и в резултат е извън обхвата на тази статия.

За повечето практически приложения обаче можем или да модифицираме съществуващ метод, за да го направим по-достъпен (може би да го направим публичен, за да може да бъде заменен или претоварен) или да модифицираме клас, за да го направим разширяем .

Нека направим метода toUnsignedString публичен:

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); } } 

Както направихме за модификацията на полето, ние просто прихващаме метода за посещение и променяме желаните от нас параметри .

In this case, we use the access modifiers in the org.objectweb.asm.Opcodes package to change the visibility of the method. We then plug in our ClassVisitor:

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

4.3. Working With Classes

Along the same lines as modifying methods, we modify classes by intercepting the appropriate visitor method. In this case, we intercept visit, which is the very first method in the visitor hierarchy:

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); } } 

We override the visit method to add the Cloneable interface to the array of interfaces to be supported by the Integer class. We plug this in just like all the other uses of our adapters.

5. Using the Modified Class

So we've modified the Integer class. Now we need to be able to load and use the modified version of the class.

In addition to simply writing the output of writer.toByteArray to disk as a class file, there are some other ways to interact with our customized Integer class.

5.1. Using the TraceClassVisitor

The ASM library provides the TraceClassVisitor utility class that we'll use to introspect the modified class. Thus we can confirm that our changes have happened.

Because the TraceClassVisitor is a ClassVisitor, we can use it as a drop-in replacement for a 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()); } 

What we have done here is to adapt the ClassVisitor that we passed to our earlier PublicizeMethodAdapter with the TraceClassVisitor.

All the visiting will now be done with our tracer, which then can print out the content of the transformed class, showing any modifications we've made to it.

While the ASM documentation states that the TraceClassVisitor can print out to the PrintWriter that's supplied to the constructor, this doesn't appear to work properly in the latest version of ASM.

Fortunately, we have access to the underlying printer in the class and were able to manually print out the tracer's text contents in our overridden visitEnd method.

5.2. Using Java Instrumentation

This is a more elegant solution that allows us to work with the JVM at a closer level via Instrumentation.

To instrument the java.lang.Integer class, we write an agent that will be configured as a command line parameter with the JVM. The agent requires two components:

  • A class that implements a method named premain
  • An implementation of ClassFileTransformer in which we'll conditionally supply the modified version of our class
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; } }); } }

We now define our premain implementation class in a JAR manifest file using the Maven jar plugin:

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

Building and packaging our code so far produces the jar that we can load as an agent. To use our customized Integer class in a hypothetical “YourClass.class“:

java YourClass -javaagent:"/path/to/theAgentJar.jar"

6. Conclusion

While we implemented our transformations here individually, ASM allows us to chain multiple adapters together to achieve complex transformations of classes.

In addition to the basic transformations we examined here, ASM also supports interactions with annotations, generics, and inner classes.

We've seen some of the power of the ASM library — it removes a lot of limitations we might encounter with third-party libraries and even standard JDK classes.

ASM is widely used under the hood of some of the most popular libraries (Spring, AspectJ, JDK, etc.) to perform a lot of “magic” on the fly.

Можете да намерите изходния код за тази статия в проекта GitHub.