文章目录

  • 1、简介
  • 2、依赖
  • 3、ASM API基础知识
  • <1>、基于事件的API
  • <2>、基于树的API
  • 4、使用基于事件的ASM API
  • <1>、使用字段
  • <2>、使用方法
  • <3>、使用类
  • 5、使用修改后的类
  • <1>、使用TraceClassVisitor
  • <2>、使用Java Instrumentation
  • 6、结论


1、简介

本篇文章将通过添加字段,添加方法和更改现有方法的行为来了解如何使用ASM库来操作现有Java类。

2、依赖

需要在maven工程中导入ASM依赖

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

3、ASM API基础知识

ASM API提供了两种与Java类交互的样式,用于转换和生成:基于事件和基于树

<1>、基于事件的API

此API主要基于访问者模式,与处理XML文档的SAX解析模型类似。核心部分包含了以下部件:

  • ClassReader:帮助读取类文件,是转换类的开始
  • ClassVisitor:提供在读取原始类文件后用于转换类的方法
  • ClassWriter:用于输出类转换的最终产品

在ClassVisitor中,我们拥有所有访问者的方法,用于触及给定Java类的不同组件(字段、方法等)。通过提供ClassVisitor子类来实现给定类中的任何更改。

由于需要保留有关Java约定和结果字节码的输出类的完整性,因此该类需要严格的顺序,在该顺序中应调用其方法已生成正确的输出。

基于事件的API中的ClassVisitor方法按一下顺序调用:

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

<2>、基于树的API

此API是一个面向对象的API,类似于处理XML文档的JAXB模型
它仍然基于事件的API,但是它引入了ClassNode类。该类充当类结构的入口点。

4、使用基于事件的ASM API

将使用ASM修改java.lang.Integer类,需要在这一点上要把握一个基本概念:在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类的基础,此外还添加了一个字段和一个方法。

<1>、使用字段

创建用于向Integer类添加字段的ClassVisitor

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方法,首先检查计划添加的字段是否已经存在,并设置一个标志来指示状态。

仍然需要将方法调用转发给父类——这需要在为类中的每个字段调用visitFiled方法时发生。无法转发呼叫意味着不会向该类写入任何字段。

还可以修改现有字段的可见性或类型:

@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方法的所需值来修改现有字段(例如:将私有字段转换为公共字段)

<2>、使用方法

在ASM API中生成整个方法比在类中的其他操作更复杂。这涉及大量的低级字节码操作,因此超出了本文的范围。

但是,对于大多数实际应用,我们可以修改现有方法以使其更易于访问(可能使其公开以便可以覆盖或重载)或修改类以使其可扩展。

让我们将toUnsignedString方法设为public:

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

就像我们为场修改做的那样,只是拦截访问方法并改变我们想要的参数。

在这种情况下,我们使用org.objectweb.asm.Opcodes包中的访问修饰符来更改方法的可见性。然后插入我们的ClassVisitor:

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

<3>、使用类

与修改方法一样,我们通过拦截适当的访问者方法来修改类。在这种情况下拦截访问,这是访问者层次结构中的第一个方法:

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

重写visit方法,将Cloneable接口添加到Integer类支持的接口数组中。将其插入就像我们的适配器的所有其他用途一样。

5、使用修改后的类

所以修改了Integer类。现在我们需要能够加载和使用该类的修改版本。

除了简单地将writer.toByteArray的输出作为类文件写入磁盘之外,还有一些其他方法可以与自定义Integer类进行交互。

<1>、使用TraceClassVisitor

ASM库提供了TraceClassVisitor实用程序类,我们将使用它来内省修改后的类。因此,可以确认我们的变化已经发生。

因为TraceClassVisitor是ClassVisitor,我们可以将它用作标准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());
}

在这里做的是适应ClassVisitor,传递给我们的更早PublicizeMethodAdapter与TraceClassVisitor。

所有访问现在都将使用跟踪器完成,然后可以打印出已转换类的内容,显示对其进行的任何修改。

虽然ASM文档声明TraceClassVisitor可以打印到提供给构造函数的PrintWriter,但这似乎在最新版本的ASM中无法正常工作。

幸运的是,我们可以访问类中的底层打印机,并能够在重写的visitEnd方法中手动打印出跟踪器的文本内容。

<2>、使用Java Instrumentation

这是一个更优雅的解决方案,允许我们通过Instrumentation在更接近的级别上使用JVM

为了检测java.lang.Integer类,编写了一个代理程序,该代理程序将配置未JVM的命令行参数。代理需要两个组件:

  • 实现名为premain的方法的类
  • ClassFileTransformer的一个实现,将有条件地提供我们类的修改版本
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;
            }
        });
    }
}

现在使用Maven jar插件在JAR清单文件中定义premain实现类:

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

到目前为止,构建和打包我们的代码会生成可以作为代理加载的jar,要在假设的“YouClass.class”中使用自定义的Integer类:

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

6、结论

  • 虽然这里单独实现了转换,但是ASM允许我们将多个适配器链接在一起以实现类的复杂转换。
  • 除了已经看到到ASM库的一些强大的功能——消除了可能遇到的第三方库甚至标准JDK类的许多限制。
  • ASM在一些最流行的库(Spring,AspectJ,JDK等)的引擎下呗广泛使用,以便在运行中执行许多“魔术”。