You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java 17下如何实现类似Lombok的@LogThis注解处理器修改方法体?

实现@LogThis注解处理器(类似Lombok的字节码修改逻辑)

1. 核心原理:跳出普通APT限制,介入编译流程修改字节码

普通注解处理器(APT)仅能生成新Java文件,无法修改已有类的方法逻辑。要实现类似Lombok的效果,必须通过Java编译器插件或字节码操作库,在编译阶段直接修改类的字节码(或抽象语法树AST)。

2. 步骤拆解

2.1 定义@LogThis注解

创建标记注解,用于标记需要修改的方法:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE) // 仅保留在源码阶段,编译后无需留存
public @interface LogThis {
}

2.2 使用ASM处理字节码(Java 17兼容)

ASM是轻量级字节码操作库,可直接修改class文件内容。我们需要拦截带有@LogThis注解的方法,替换其原有方法体。

2.2.1 添加ASM依赖

在Maven的pom.xml中引入ASM:

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>9.5</version>
</dependency>
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm-commons</artifactId>
    <version>9.5</version>
</dependency>

2.2.2 实现ClassVisitor替换方法体

编写ClassVisitor遍历类方法,识别@LogThis注解并替换方法逻辑:

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

public class LogThisClassVisitor extends ClassVisitor {
    public LogThisClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM9, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        return new LogThisMethodVisitor(mv, access, desc);
    }

    private static class LogThisMethodVisitor extends MethodVisitor {
        private final int access;
        private final String desc;
        private String methodName;

        public LogThisMethodVisitor(MethodVisitor mv, int access, String desc) {
            super(Opcodes.ASM9, mv);
            this.access = access;
            this.desc = desc;
        }

        @Override
        public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
            this.methodName = name;
            super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
        }

        @Override
        public void visitAnnotation(String desc, boolean visible) {
            // 匹配@LogThis注解的内部表示
            if ("Lcom/yourpackage/LogThis;".equals(desc)) {
                // 替换原有方法体为日志调用+默认值返回
                this.mv = new MethodVisitor(Opcodes.ASM9) {
                    @Override
                    public void visitCode() {
                        // 插入日志调用:Logger.log("方法调用信息")
                        mv.visitFieldInsn(Opcodes.GETSTATIC, "com/yourpackage/Logger", "INSTANCE", "Ljava/util/logging/Logger;");
                        mv.visitLdcInsn("Method " + methodName + " triggered");
                        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/util/logging/Logger", "info", "(Ljava/lang/String;)V", false);

                        // 根据返回类型生成默认值
                        Type returnType = Type.getReturnType(LogThisMethodVisitor.this.desc);
                        switch (returnType.getSort()) {
                            case Type.VOID:
                                mv.visitInsn(Opcodes.RETURN);
                                break;
                            case Type.BOOLEAN:
                            case Type.CHAR:
                            case Type.BYTE:
                            case Type.SHORT:
                            case Type.INT:
                                mv.visitInsn(Opcodes.ICONST_0);
                                mv.visitInsn(Opcodes.IRETURN);
                                break;
                            case Type.LONG:
                                mv.visitInsn(Opcodes.LCONST_0);
                                mv.visitInsn(Opcodes.LRETURN);
                                break;
                            case Type.FLOAT:
                                mv.visitInsn(Opcodes.FCONST_0);
                                mv.visitInsn(Opcodes.FRETURN);
                                break;
                            case Type.DOUBLE:
                                mv.visitInsn(Opcodes.DCONST_0);
                                mv.visitInsn(Opcodes.DRETURN);
                                break;
                            case Type.OBJECT:
                            case Type.ARRAY:
                                mv.visitInsn(Opcodes.ACONST_NULL);
                                mv.visitInsn(Opcodes.ARETURN);
                                break;
                        }
                        mv.visitMaxs(2, 0);
                    }
                };
            }
            super.visitAnnotation(desc, visible);
        }
    }
}

2.3 实现编译器插件,介入编译流程

编写注解处理器,监听编译完成事件,触发字节码修改:

import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;
import com.sun.tools.javac.api.BasicJavacTask;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.main.JavaCompiler;
import com.sun.tools.javac.util.Context;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.TypeElement;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Set;

public class LogThisProcessor extends AbstractProcessor {
    private JavacFileManager fileManager;
    private Context context;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        context = ((BasicJavacTask) processingEnv.getCompiler()).getContext();
        fileManager = JavacFileManager.getFileManager(context);
        // 添加编译任务监听器,在类编译完成后修改字节码
        JavaCompiler compiler = JavaCompiler.instance(context);
        compiler.addTaskListener(new TaskListener() {
            @Override
            public void finished(TaskEvent e) {
                if (e.getKind() == TaskEvent.Kind.CLASS) {
                    Symbol.ClassSymbol classSymbol = (Symbol.ClassSymbol) e.getTypeElement();
                    processClass(classSymbol);
                }
            }
        });
    }

    private void processClass(Symbol.ClassSymbol classSymbol) {
        String className = classSymbol.getQualifiedName().toString().replace('.', '/');
        File classFile = new File(fileManager.getDestinationDir(), className + ".class");
        if (!classFile.exists()) return;

        try {
            // 读取原class文件,修改后写入
            ClassReader cr = new ClassReader(classFile.getAbsolutePath());
            ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
            LogThisClassVisitor cv = new LogThisClassVisitor(cw);
            cr.accept(cv, ClassReader.SKIP_DEBUG);

            try (FileOutputStream fos = new FileOutputStream(classFile)) {
                fos.write(cw.toByteArray());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 无需生成新文件,仅监听编译事件修改字节码
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Set.of("com.yourpackage.LogThis");
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported(); // 适配Java 17
    }
}

2.4 注册处理器

创建META-INF/services/javax.annotation.processing.Processor文件,写入处理器全类名:

com.yourpackage.LogThisProcessor

3. 测试验证

编写测试类验证效果:

import com.yourpackage.LogThis;

public class Test {
    @LogThis
    int sum(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        Test t = new Test();
        System.out.println(t.sum(1, 2)); // 输出0,同时打印日志
    }
}

编译后,sum方法的字节码会被替换为日志调用和默认值返回逻辑。

关键注意事项

  • Java 17模块化适配:若使用模块化项目,需添加编译参数开放javac内部API,例如--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED。
  • ASM版本兼容性:必须使用9.x及以上版本的ASM,确保支持Java 17的字节码特性。
  • 字节码合法性:替换方法体时需正确设置操作数栈和局部变量表大小,避免字节码验证失败。

内容的提问来源于stack exchange,提问作者Aviv Assayag

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 23:49:58