以下是两种 Java 字节码修改方法的代码示例及对比,包含详细注释:


方法 1:直接修改类文件(编译后但加载前)

实现思路
  • 直接操作 .class 文件,修改字节码后生成新的类文件。
  • 适用于需要持久化修改(如生成新类文件供后续使用)。
代码示例
import org.objectweb.asm.*;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;

public class BytecodeModifier {

    public static void main(String[] args) throws IOException {
        // 1. 读取原始类文件(HelloWorld.class)
        ClassReader reader = new ClassReader("HelloWorld");
        
        // 2. 创建ClassWriter,用于生成修改后的字节码
        ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
        
        // 3. 创建ClassVisitor,处理类的结构
        ClassVisitor visitor = new ClassVisitor(ASM9, writer) {
            @Override
            public MethodVisitor visitMethod(
                int access, 
                String name, 
                String descriptor, 
                String signature, 
                String[] exceptions
            ) {
                // 4. 仅修改名为 "sayHello" 的方法
                if ("sayHello".equals(name)) {
                    // 5. 创建MethodVisitor,覆盖原有方法体
                    return new MethodVisitor(ASM9, super.visitMethod(access, name, descriptor, signature, exceptions)) {
                        @Override
                        public void visitCode() {
                            // 6. 在方法开头插入指令:调用 System.out.println("Modified!")
                            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/io/PrintStream", "out", "Ljava/io/PrintStream;");
                            mv.visitLdcInsn("Modified!");
                            mv.visitMethodInsn(
                                Opcodes.INVOKEVIRTUAL, 
                                "java/io/PrintStream", 
                                "println", 
                                "(Ljava/lang/String;)V", 
                                false
                            );
                            // 继续执行原有方法体
                            super.visitCode();
                        }
                    };
                }
                // 其他方法不修改,直接返回
                return super.visitMethod(access, name, descriptor, signature, exceptions);
            }
        };

        // 7. 读取并处理类
        reader.accept(visitor, ClassReader.EXPAND_FRAMES);
        
        // 8. 生成新的字节码并写入文件
        byte[] modifiedBytes = writer.toByteArray();
        Files.write(Paths.get("HelloWorldModified.class"), modifiedBytes);
        System.out.println("类文件修改完成!");
    }
}
说明
  1. ASM版本:使用 ASM9 对应 Java 17+ 的字节码版本。
  2. 修改逻辑:在 sayHello 方法开头插入一条 System.out.println("Modified!") 指令。
  3. 输出结果:生成 HelloWorldModified.class,后续运行时需使用修改后的类文件。

方法 2:加载时修改(通过Instrumentation API)

实现思路
  • 在类加载时动态修改字节码,无需预先生成新文件。
  • 通过 java.lang.instrument 包实现,需以 Java Agent 形式运行。
代码示例
(1) 代理类(Agent)
import java.lang.instrument.Instrumentation;

public class BytecodeAgent {
    public static void premain(String args, Instrumentation inst) {
        System.out.println("Agent已加载,注册转换器...");
        inst.addTransformer(new ClassFileTransformer());
    }
}
(2) 类字节码转换器
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import org.objectweb.asm.*;

public class ClassFileTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(
        ClassLoader loader, 
        String className, 
        Class<?> classBeingRedefined, 
        ProtectionDomain protectionDomain, 
        byte[] classfileBuffer
    ) {
        // 仅处理 "HelloWorld" 类
        if ("HelloWorld".equals(className)) {
            ClassReader reader = new ClassReader(classfileBuffer);
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
            ClassVisitor visitor = new ClassVisitor(ASM9, writer) {
                @Override
                public MethodVisitor visitMethod(
                    int access, 
                    String name, 
                    String descriptor, 
                    String signature, 
                    String[] exceptions
                ) {
                    if ("sayHello".equals(name)) {
                        return new MethodVisitor(ASM9, super.visitMethod(access, name, descriptor, signature, exceptions)) {
                            @Override
                            public void visitCode() {
                                // 插入 System.out.println("Modified!")
                                mv.visitFieldInsn(Opcodes.GETSTATIC, "java/io/PrintStream", "out", "Ljava/io/PrintStream;");
                                mv.visitLdcInsn("Modified!");
                                mv.visitMethodInsn(
                                    Opcodes.INVOKEVIRTUAL, 
                                    "java/io/PrintStream", 
                                    "println", 
                                    "(Ljava/lang/String;)V", 
                                    false
                                );
                                super.visitCode();
                            }
                        };
                    }
                    return super.visitMethod(access, name, descriptor, signature, exceptions);
                }
            };
            reader.accept(visitor, ClassReader.EXPAND_FRAMES);
            return writer.toByteArray();
        }
        return null; // 不修改其他类
    }
}
(3) 启动Agent的配置文件(MANIFEST.MF)
Manifest-Version: 1.0
Premain-Class: BytecodeAgent
Can-Run-In-Parallel: true
运行命令
# 启动时指定Agent(需打包为jar)
java -javaagent:bytecode-agent.jar -cp . HelloWorld

对比表格

维度 直接修改类文件 加载时修改(Instrumentation)
修改时机 编译后但加载前(静态修改) 类加载时动态修改(运行时)
是否持久化修改 是(生成新类文件) 否(仅在内存中生效)
对已加载类的影响 需重新启动才能生效 可修改已加载的类(需支持 retransform)
适用场景 需要持久化修改(如框架预处理) 动态监控、AOP、运行时热修复
性能开销 无运行时开销 运行时有轻微开销(需遍历所有类)
实现复杂度 简单(直接操作文件) 复杂(需处理类加载生命周期、Agent机制)
是否需要Agent 是(必须通过Java Agent加载)
类加载器限制 受限于类加载器权限(如第三方库的类)

总结

  • 直接修改类文件:适合预处理阶段生成新类文件,简单且持久化。
  • 加载时修改:适合动态场景(如AOP、监控),但需处理Agent机制和类加载器问题。
Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐