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的所有Element
Iterator var4 = es.iterator();
while(var4.hasNext()) {
Element e = (Element)var4.next();
if (e instanceof TypeElement te) { // 步骤3,我们只关心注解了@ToString的TypeElement
JavaFileObject jfo = this.processingEnv.getFiler().createSourceFile("com.keyniu.anno.processor.StringUtils", new Element[0]); // 步骤4
PrintWriter out = new PrintWriter(jfo.openWriter()); // 步骤5
try {
this.printClass(out); // 步骤6
this.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