1. 简介

本文将介绍如何使用ASM对现有Java类进行字节码操作,包括添加字段、添加方法以及修改现有方法的行为。ASM作为Java字节码操作领域的瑞士军刀,能让你绕过源码直接操作底层字节码,实现各种骚操作。

2. 依赖配置

首先在pom.xml中添加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>

最新版本可在Maven中央仓库获取:asmasm-util

3. ASM API核心概念

ASM提供两种操作Java类的方式:事件驱动模型和树形模型。

3.1 事件驱动API

该API基于访问者模式实现类似XML解析中的SAX模型。核心组件包括:

  • ClassReader – 读取类文件的入口,是类转换的起点
  • ClassVisitor – 提供修改类结构的方法集合
  • ClassWriter – 输出转换后的字节码

⚠️ 注意:ClassVisitor的方法调用必须遵循严格顺序,否则会生成错误的字节码:

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

3.2 树形API

该API更接近面向对象风格类似XML处理中的JAXB模型。它在事件驱动API基础上引入了ClassNode根类,作为类结构的入口点。

4. 事件驱动API实战

我们将修改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);
    }
}

4.1 字段操作

创建添加字段的适配器:

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方法检查字段是否存在,并转发方法调用(否则字段不会被写入):

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

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包,避免IDE自动引入其他版本。

集成适配器完成字段添加:

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

4.2 方法操作

完整生成方法涉及大量字节码操作,这里演示修改方法可见性的简单场景。将toUnsignedString0方法改为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);
   }
}

集成方法修改器:

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

4.3 类操作

通过重写visit方法添加接口实现,使Integer类实现Cloneable:

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

5. 使用修改后的类

5.1 使用TraceClassVisitor验证

TraceClassVisitor打印字节码修改结果,用于验证变更:

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

⚠️ 注意:最新版ASM中直接打印可能失效,需手动获取文本内容。

5.2 使用Java Instrumentation

通过Instrumentation实现运行时类替换,步骤:

  1. 创建带premain方法的代理类
  2. 实现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;
            }
        });
    }
}

配置MANIFEST.MF:

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

启动应用时加载代理:

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

6. 总结

本文演示了ASM的基础操作,实际应用中可以链式组合多个适配器实现复杂转换。除了基本操作,ASM还支持注解、泛型和内部类处理。

Spring、AspectJ等知名框架都在底层使用ASM实现各种"黑科技"。掌握ASM能让你突破源码限制,直接操作字节码实现各种酷炫功能。

完整代码示例见GitHub项目


原始标题:A Guide to Java Bytecode Manipulation with ASM