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
相关产品推荐
相关产品推荐

