Java提供了一个javac -processor命令支持处理标注有特定注解的类,来生成新的源文件,并对新生成的源文件重复执行。执行的命令大概是这样的:
javac -XprintRounds -processor com.keyniu.anno.processor.ToStringProcessor com.keyniu.anno.processor.Point
本文的目标是用一个案例来讲解注解处理器的使用,我们会定义一个@ToString注解,创建注解处理器,为所有标注了@ToString注解的类生成toString工具方法。
这里需要特别说明的是javac -processor只支持生成新的文件,无法在原来的文件里做修改。
1. 定义ToString注解
首先我们需要定义一个注解,用来标注后续要生成toString方法的类。@ToString的逻辑很简单,这里我们只把它定义为一个标记注解。
package com.keyniu.anno.processor;import java.lang.annotation.*;@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ToString {
}
定义@ToString注解之后,我们就可以把它用在想要自动生成toString方法的类上,比如我们有一个Point类的定义,我们希望为Point类生成toString方法,可以在Point上添加@ToString注解
package com.keyniu.anno.processor;@ToString
public class Point {private int x;private int y;public int getX(Point this) {return x;}public int getY() {return y;}
}
2. 创建注解处理器
要想生成代码,我们还需要定义注解处理器,来处理代码的生成。注解处理器需要继承AbstractProcessor类,通过注解能指定支持的注解、代码版本号。下面的代码展示了整个处理过程,我们来解释一下运行流程:
- 入参annotations是当前注解处理器支持的注解类型,@SupportedAnnotationTypes可以指定通配符,所以annotaions可以有多个注解类,不过这个案例中,注解只有@ToString
- 通过RoundEnvironment.getElementsAnnotatedWith查找标注了@ToString的Element,它有3个子类,TypeElement(类、接口)、VariableElement(字段、参数)、ExecutableElement(方法、构造器)
- 这里我们只关心的类类型(TypeElement)
- 使用processingEnv.getFiler().createSourceFile创建生成的类文件,此处我们要生成的是com.keyniu.anno.processor.StringUtils类
- 创建文件输出流PrintWriter,用于后续写入.java文件
- 后续要做的就是通过字符串拼接,生成.java文件的内容了,先定义包,设置import,然后定义类,最后是定义方法的代码,这个过程中可以使用TypeElement的元数据
- PrintWriter关闭后,新的.java文件就会倍生成,新生成的类,会重新走一边注解处理的过程
package com.keyniu.anno.processor;import java.io.PrintWriter;
import java.util.Iterator;
import java.util.Set;
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.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.tools.JavaFileObject;@SupportedAnnotationTypes({"com.keyniu.anno.processor.ToString"})
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class ToStringProcessor extends AbstractProcessor {public ToStringProcessor() {}public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {try {Set<? extends Element> es = roundEnv.getElementsAnnotatedWith(ToString.class); // 步骤2,寻找标准了@ToString的所有ElementIterator var4 = es.iterator();while(var4.hasNext()) {Element e = (Element)var4.next();if (e instanceof TypeElement te) { // 步骤3,我们只关心注解了@ToString的TypeElementJavaFileObject jfo = this.processingEnv.getFiler().createSourceFile("com.keyniu.anno.processor.StringUtils", new Element[0]); // 步骤4PrintWriter out = new PrintWriter(jfo.openWriter()); // 步骤5try {this.printClass(out); // 步骤6this.printMethod(te, out);this.printClassSuffix(out);} catch (Throwable var12) {try {out.close();} catch (Throwable var11) {var12.addSuppressed(var11);}throw var12;}out.close();}}return false;} catch (Exception var13) {var13.printStackTrace();return false;}}private void printClass(PrintWriter out) {out.println("package com.keyniu.anno.processor;");out.println("");out.println("import java.lang.StringBuilder;");out.println("");out.println("public class StringUtils {");}private void printClassSuffix(PrintWriter out) {out.println("}");}private void printMethod(TypeElement te, PrintWriter out) {String indent = " ";StringBuilder methodCode = new StringBuilder();methodCode.append(indent + "public static java.lang.String toString(" + te.getQualifiedName() + " i) {");methodCode.append("\n");methodCode.append(indent + indent + "StringBuilder sb = new StringBuilder();");methodCode.append("\n");Iterator var5 = te.getEnclosedElements().iterator();while(var5.hasNext()) {Element e = (Element)var5.next();if (e instanceof VariableElement ve) {String field = ve.getSimpleName().toString();methodCode.append(indent + indent + "sb.append(\"" + field + ":\");").append("\n");methodCode.append(indent + indent + "sb.append(i.get" + field.substring(0, 1).toUpperCase() + field.substring(1) + "());").append("\n");}}methodCode.append(indent + indent + "return sb.toString();\n");methodCode.append(indent + "}");out.println(methodCode);}
}
3. 调用注解处理器
在注解类(ToString)、注解处理器(ToStringProcessor)和使用注解的类(Point)都定义完成后我们就可以开始调用javac -processor类。首先要做的是编译ToString和ToStringProcessor类
javac com/keyniu/anno/processor/ToString.java
javac com/keyniu/anno/processor/ToStringProcessor.java
然后就可以使用-processor引用ToStringProcessor类了,当然你要保证ToStringProcessor类在classpath下可访问
D:\Workspace\HelloJava17\src\main\java>javac -XprintRounds -processor com.keyniu.anno.processor.ToStringProcessor com.keyniu.anno.processor.Point
执行结束后,你会看到在D:\Workspace\HelloJava17\src\main\java\com\keyniu\anno\processor下新生成了一个StringUtils类,生成的代码如下
package com.keyniu.anno.processor;import java.lang.StringBuilder;public class StringUtils {public static java.lang.String toString(com.keyniu.anno.processor.Point i) {StringBuilder sb = new StringBuilder();sb.append("x:");sb.append(i.getX());sb.append("y:");sb.append(i.getY());return sb.toString();}
}
4. 提供Maven支持
应该承认javac -processor确实能用了,但是为编译过程额外添加了一个步骤,带来了额外的负担,而且生成的代码和我们用户代码混杂在一起了。通过Maven的maven-compiler-plugin插件,能让这个过程自动化,并为生成的代码提供单独的目录。为了让这个过程可行,我们现在将项目拆分为两个,anno-processing提供ToString定义、ToStringProcessor注解处理器定义
<groupId>com.keyniu</groupId>
<artifactId>anno-processing</artifactId>
<version>1.0-SNAPSHOT</version>
在客户端工程,提供Point定义,引用anno-processing的依赖, GAV和依赖定义如下
<groupId>com.randy.graalvm</groupId>
<artifactId>swing</artifactId>
<version>1.0-SNAPSHOT</version><dependencies><dependency><groupId>com.keyniu</groupId><artifactId>anno-processing</artifactId><version>1.0-SNAPSHOT</version></dependency>
</dependencies>
紧接着要做的是在swing项目中,添加maven-compiler-plugin插件,定义生成文件保存的目录(generatedSourcesDirectory),以及注解处理器(annotationProcessor)
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.12.1</version><configuration><source>17</source><target>17</target><encoding>UTF-8</encoding><generatedSourcesDirectory>${project.build.directory}/generated-sources/</generatedSourcesDirectory><annotationProcessors><annotationProcessor>com.keyniu.anno.processor.ToStringProcessor</annotationProcessor></annotationProcessors></configuration></plugin></plugins>
</build>
在这些配置都完成后,就可以正常的通过mvn package编译打包了,运行后能看到target目录下多了一个generated-sources,并且在classes文件夹下包含了StringUtils编译后的.class文件
事情做到这一步,应该说我们定义的ToStringProcessor和ToString已经能满足特定场景下的时候了,不过它并不支持修改,自能新生成一个类来扩展现有类的能力,仍然显得不那么完美。
下一篇我们会讲解lombok的实现原理,怎么在类加载时使用字节码操作类库动态的修改Class的实现。
A. 参考资料
- Java Annotation Processing and Creating a Builder | Baeldung