1. Introducción
En este artículo, veremos cómo usar la biblioteca ASM para manipular una clase Java existente agregando campos, agregando métodos y cambiando el comportamiento de los métodos existentes.
2. Dependencias
Necesitamos agregar las dependencias de ASM a nuestro pom.xml :
org.ow2.asm asm 6.0 org.ow2.asm asm-util 6.0
Podemos obtener las últimas versiones de asm y asm-util de Maven Central.
3. Conceptos básicos de la API de ASM
La API de ASM proporciona dos estilos de interacción con clases de Java para la transformación y generación: basada en eventos y basada en árboles.
3.1. API basada en eventos
Esta API se basa en gran medida en el patrón Visitor y es similar al modelo de análisis SAX para procesar documentos XML. Se compone, en su esencia, de los siguientes componentes:
- ClassReader : ayuda a leer archivos de clase y es el comienzo de la transformación de una clase
- ClassVisitor : proporciona los métodos utilizados para transformar la clase después de leer los archivos de clase sin procesar
- ClassWriter : se usa para generar el producto final de la transformación de clase
Es en ClassVisitor donde tenemos todos los métodos de visitante que usaremos para tocar los diferentes componentes (campos, métodos, etc.) de una clase Java determinada. Hacemos esto proporcionando una subclase de ClassVisitor para implementar cualquier cambio en una clase determinada.
Debido a la necesidad de preservar la integridad de la clase de salida con respecto a las convenciones de Java y el código de bytes resultante, esta clase requiere un orden estricto en el que se deben llamar sus métodos para generar una salida correcta.
Los métodos ClassVisitor en la API basada en eventos se llaman en el siguiente orden:
visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )* ( visitInnerClass | visitField | visitMethod )* visitEnd
3.2. API basada en árboles
Esta API es una API más orientada a objetos y es análoga al modelo JAXB de procesamiento de documentos XML.
Todavía se basa en la API basada en eventos, pero presenta la clase raíz ClassNode . Esta clase sirve como punto de entrada a la estructura de clases.
4. Trabajar con la API de ASM basada en eventos
Modificaremos la clase java.lang.Integer con ASM. Y necesitamos captar un concepto fundamental en este punto: la clase ClassVisitor contiene todos los métodos de visitante necesarios para crear o modificar todas las partes de una clase .
Solo necesitamos anular el método de visitante necesario para implementar nuestros cambios. Comencemos por configurar los componentes de requisitos previos:
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); } }
Usamos esto como base para agregar la interfaz Cloneable a la clase Stock Integer , y también agregamos un campo y un método.
4.1. Trabajar con campos
Creemos nuestro ClassVisitor que usaremos para agregar un campo a la clase 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; } }
A continuación, vamos a anular la visitField método , en el que primero revisará si el campo tenemos la intención de añadir ya existe y establece un indicador para indicar el estado .
Todavía tenemos que reenviar la llamada al método a la clase principal ; esto debe suceder ya que se llama al método visitField para cada campo de la clase. Si no se reenvía la llamada, no se escribirán campos en la clase.
Este método también nos permite modificar la visibilidad o el tipo de campos existentes :
@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); }
Primero verificamos el conjunto de banderas en el método visitField anterior y volvemos a llamar al método visitField , esta vez proporcionando el nombre, el modificador de acceso y la descripción. Este método devuelve una instancia de FieldVisitor.
El método visitEnd es el último método llamado en el orden de los métodos del visitante. Esta es la posición recomendada para realizar la lógica de inserción de campo .
Luego, necesitamos llamar al método visitEnd en este objeto para indicar que hemos terminado de visitar este campo:
@Override public void visitEnd() { if (!isFieldPresent) { FieldVisitor fv = cv.visitField( access, fieldName, fieldType, null, null); if (fv != null) { fv.visitEnd(); } } cv.visitEnd(); }
Es importante asegurarse de que todos los componentes de ASM utilizados provengan del paquete org.objectweb.asm ; muchas bibliotecas usan la biblioteca de ASM internamente y los IDE pueden insertar automáticamente las bibliotecas de ASM incluidas.
Ahora usamos nuestro adaptador en el método addField , obteniendo una versión transformada de java.lang.Integer con nuestro campo agregado:
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(); } }
Hemos anulado los métodos visitField y visitEnd .
Todo lo que se debe hacer con respecto a los campos ocurre con el método visitField . Esto significa que también podemos modificar campos existentes (por ejemplo, transformar un campo privado en público) cambiando los valores deseados pasados al método visitField .
4.2. Trabajar con métodos
La generación de métodos completos en la API de ASM es más complicada que otras operaciones de la clase. Esto implica una cantidad significativa de manipulación de código de bytes de bajo nivel y, como resultado, está más allá del alcance de este artículo.
Sin embargo, para la mayoría de los usos prácticos, podemos modificar un método existente para hacerlo más accesible (quizás hacerlo público para que pueda anularse o sobrecargarse) o modificar una clase para hacerlo extensible .
Hagamos público el método 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); } }
Como hicimos para la modificación de campo, simplemente interceptamos el método de visita y cambiamos los parámetros que deseamos .
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.
Puede encontrar el código fuente de este artículo en el proyecto GitHub.