如何创建具备实际功能的Java自定义注解?
你之前用Aspect没效果,核心是搞混了运行时注解增强和编译期注解生成代码的区别,先理清楚这俩的差异:
为什么Aspect不适合你的需求?
Aspect是基于运行时注解的增强手段,它只能对已存在的方法做拦截、增强,没法生成新的方法。而且你之前的Aspect切点写错了:@After("@annotation(...)")是匹配方法上标注了该注解的方法,但你的@Dragonrider是加在类上的,所以根本匹配不到。就算改对切点(比如@After("within(@com.example.demo.annotations.Dragonrider *)")),也只能在已有方法执行后打印日志,没法生成新的fly方法。
而Lombok的@Getter/@Setter是编译期注解,在Java编译成字节码的阶段,通过注解处理器修改字节码,直接生成新的方法。这才是你需要的方向。
实现类似Lombok的自定义注解步骤
1. 修改注解的保留策略
首先把你的@Dragonrider改成编译期生效的(不需要保留到运行时):
package com.example.demo.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) // 编译期生效,编译后注解会被丢弃 public @interface Dragonrider { String name(); }
2. 编写注解处理器(核心)
注解处理器是编译时执行的代码,用来扫描标注了自定义注解的类,生成或修改字节码。这里用JavaPoet简化代码生成(也可以用ASM直接修改字节码,更接近Lombok的实现)。
第一步:添加依赖(Maven为例)
<dependency> <groupId>com.squareup</groupId> <artifactId>javapoet</artifactId> <version>1.13.0</version> <scope>provided</scope> <!-- 编译时用,不需要打包到项目 --> </dependency>
第二步:实现注解处理器
package com.example.demo.processor; import com.example.demo.annotations.Dragonrider; import com.squareup.javapoet.JavaFile; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.TypeSpec; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import java.io.IOException; import java.util.Set; // 指定要处理的注解全类名 @SupportedAnnotationTypes("com.example.demo.annotations.Dragonrider") // 指定支持的Java版本 @SupportedSourceVersion(SourceVersion.RELEASE_8) public class DragonriderProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // 遍历所有标注了@Dragonrider的类 for (Element element : roundEnv.getElementsAnnotatedWith(Dragonrider.class)) { if (!(element instanceof TypeElement)) continue; TypeElement clazz = (TypeElement) element; String className = clazz.getSimpleName().toString(); String packageName = processingEnv.getElementUtils().getPackageOf(clazz).getQualifiedName().toString(); String riderName = clazz.getAnnotation(Dragonrider.class).name(); // 生成fly方法的代码 MethodSpec flyMethod = MethodSpec.methodBuilder("fly") .addModifiers(Modifier.PUBLIC) .returns(void.class) .addStatement("System.out.println($S + \"'s dragon is soaring high!\")", riderName) .build(); // 生成一个带fly方法的子类(如果要直接修改原类,需要用ASM等字节码工具) TypeSpec subclass = TypeSpec.classBuilder(className + "WithFly") .superclass(clazz.asType()) .addModifiers(Modifier.PUBLIC) .addMethod(flyMethod) .build(); // 将生成的类写入编译目录 try { JavaFile.builder(packageName, subclass) .build() .writeTo(processingEnv.getFiler()); } catch (IOException e) { e.printStackTrace(); } } return true; } }
3. 注册注解处理器
在src/main/resources/META-INF/services下创建文件javax.annotation.processing.Processor,内容为处理器的全类名:
com.example.demo.processor.DragonriderProcessor
4. 编译测试
编译你的项目后,会在编译目录下生成DragonWithFly.java和对应的字节码文件,这个类继承自Dragon,并且带有fly方法。你可以直接使用这个子类:
public class Test { public static void main(String[] args) { DragonWithFly drogon = new DragonWithFly("Drogon", true, 1500.0); drogon.fly(); // 输出:Daenerys's dragon is soaring high! } }
进阶:直接修改原类的字节码
上面的例子是生成子类,如果要像Lombok那样直接给原类添加方法,需要用到ASM或Javassist这类字节码操作库,在编译阶段修改原类的字节码,把fly方法直接注入进去。这部分逻辑更复杂,但原理和注解处理器一致,都是在编译期介入修改字节码。
内容的提问来源于stack exchange,提问作者Victor

