如何使RetentionPolicy.SOURCE级Java自定义注解处理器生效?
问题背景
我开发了一个RetentionPolicy为SOURCE的Java自定义注解处理器,想实现类似Lombok Getter/Setter的代码生成功能——给标记@ClickElement的WebElement字段自动生成对应的click方法(比如给googleSearchBtn生成clickGoogleSearchBtn()),但始终不生效。原本以为必须给Lombok提交代码才能实现这类功能,后来了解到可以自己实现注解处理器,已经尝试了以下修复措施:
- 在Eclipse和IntelliJ IDEA中启用注解处理
- 使用统一版本的JDK
- 在pom.xml中配置annotationProcessorPaths
期望生成的代码:
public void clickGoogleSearchBtn() { googleSearchBtn.click(); }
核心错误排查与修复
1. 注解路径不匹配(最致命问题)
处理器类ClickMethodProcessor上的@SupportedAnnotationTypes注解写的是com.automationdemo1.annotations.ClickElement,但实际注解类ClickElement的包是com.annotations.ClickElement,路径完全不匹配,导致处理器根本识别不到目标注解。直接修正这个路径:
@SupportedAnnotationTypes("com.annotations.ClickElement")
2. 语法树修改方式兼容性问题
直接通过classTree.defs = classTree.defs.prepend(clickMethod)修改语法树的方式,在部分Javac版本中可能无法正确将修改提交给编译器。换成TreeTranslator遍历修改语法树,兼容性更好:
// 替换原来的generateClickMethod方法中的语法树修改逻辑 javacTrees.getTree(enclosingClass).accept(new TreeTranslator() { @Override public void visitClassDef(JCTree.JCClassDecl jcClassDecl) { JCTree.JCMethodDecl clickMethod = createClickMethod(methodName, fieldName); jcClassDecl.defs = jcClassDecl.defs.prepend(clickMethod); super.visitClassDef(jcClassDecl); } });
3. WebElement类型判断不够严谨
直接用element.asType().toString().equals("org.openqa.selenium.WebElement")判断类型,遇到类型别名、泛型或者类加载器差异时会失效。改用Types工具类做类型匹配:
private boolean isWebElement(Element element) { TypeElement webElementType = processingEnv.getElementUtils().getTypeElement("org.openqa.selenium.WebElement"); if (webElementType == null) { messager.printMessage(Diagnostic.Kind.WARNING, "未找到WebElement类型,请检查selenium依赖"); return false; } TypeMirror webElementMirror = webElementType.asType(); return typeUtils.isAssignable(element.asType(), webElementMirror); }
修复后的完整处理器代码
package com.annotations.processors; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.Processor; 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.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import javax.tools.Diagnostic; import com.annotations.ClickElement; import com.google.auto.service.AutoService; import com.sun.tools.javac.api.JavacTrees; import com.sun.tools.javac.code.Flags; import com.sun.tools.javac.code.TypeTag; import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.tree.TreeTranslator; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.List; import com.sun.tools.javac.util.Name; import com.sun.tools.javac.util.Names; @SupportedAnnotationTypes("com.annotations.ClickElement") @SupportedSourceVersion(SourceVersion.RELEASE_21) @AutoService(Processor.class) public class ClickMethodProcessor extends AbstractProcessor { private Messager messager; private JavacTrees javacTrees; private TreeMaker treeMaker; private Names names; private Types typeUtils; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); messager = processingEnv.getMessager(); JavacProcessingEnvironment javacProcessingEnv = (JavacProcessingEnvironment) processingEnv; Context context = javacProcessingEnv.getContext(); javacTrees = JavacTrees.instance(javacProcessingEnv); treeMaker = TreeMaker.instance(context); names = Names.instance(context); typeUtils = processingEnv.getTypeUtils(); messager.printMessage(Diagnostic.Kind.NOTE, "ClickMethodProcessor初始化完成"); } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(ClickElement.class)) { if (element instanceof VariableElement && isWebElement(element)) { generateClickMethod((VariableElement) element); } } return true; } private boolean isWebElement(Element element) { TypeElement webElementType = processingEnv.getElementUtils().getTypeElement("org.openqa.selenium.WebElement"); if (webElementType == null) { messager.printMessage(Diagnostic.Kind.WARNING, "未找到WebElement类型,请检查selenium依赖"); return false; } TypeMirror webElementMirror = webElementType.asType(); return typeUtils.isAssignable(element.asType(), webElementMirror); } private void generateClickMethod(VariableElement field) { TypeElement enclosingClass = (TypeElement) field.getEnclosingElement(); String fieldName = field.getSimpleName().toString(); Name methodName = names.fromString("click" + capitalize(fieldName)); javacTrees.getTree(enclosingClass).accept(new TreeTranslator() { @Override public void visitClassDef(JCTree.JCClassDecl jcClassDecl) { JCTree.JCMethodDecl clickMethod = createClickMethod(methodName, fieldName); jcClassDecl.defs = jcClassDecl.defs.prepend(clickMethod); super.visitClassDef(jcClassDecl); } }); messager.printMessage(Diagnostic.Kind.NOTE, "已生成方法:" + methodName); } private JCTree.JCMethodDecl createClickMethod(Name methodName, String fieldName) { JCTree.JCExpression returnType = treeMaker.TypeIdent(TypeTag.VOID); JCTree.JCExpressionStatement clickStatement = treeMaker.Exec( treeMaker.Apply( List.nil(), treeMaker.Select( treeMaker.Ident(names.fromString(fieldName)), names.fromString("click") ), List.nil() ) ); JCTree.JCBlock methodBody = treeMaker.Block(0, List.of(clickStatement)); return treeMaker.MethodDef( treeMaker.Modifiers(Flags.PUBLIC), methodName, returnType, List.nil(), List.nil(), List.nil(), methodBody, null ); } private String capitalize(String str) { return Character.toUpperCase(str.charAt(0)) + str.substring(1); } }
额外配置与验证步骤
1. Maven依赖与编译配置
确保auto-service依赖正确引入(用来自动生成META-INF/services/javax.annotation.processing.Processor文件),同时配置注解处理器路径:
<dependencies> <dependency> <groupId>com.google.auto.service</groupId> <artifactId>auto-service</artifactId> <version>1.1.1</version> <optional>true</optional> </dependency> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>4.15.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.12.1</version> <configuration> <source>21</source> <target>21</target> <annotationProcessorPaths> <path> <groupId>com.google.auto.service</groupId> <artifactId>auto-service</artifactId> <version>1.1.1</version> </path> <!-- 替换成你自己的注解处理器模块坐标 --> <path> <groupId>your-group-id</groupId> <artifactId>your-annotation-processor-artifact</artifactId> <version>your-version</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build>
2. IDE配置细节
- IntelliJ IDEA:打开
Settings > Build, Execution, Deployment > Compiler > Annotation Processors,勾选Enable annotation processing,同时开启Store generated sources relative to module content root,这样生成的代码能被IDE识别并提示。 - Eclipse:右键项目 →
Properties > Java Compiler > Annotation Processing,勾选Enable annotation processing,配置生成目录为target/generated-sources/annotations。
3. 验证方法
执行mvn clean compile,查看控制台日志是否有ClickMethodProcessor初始化完成和已生成方法:clickGoogleSearchBtn的提示;或者直接打开编译后的DemoPage.class文件,检查是否包含期望生成的click方法。
内容的提问来源于stack exchange,提问作者Mihir Bhagat

