注解处理器(AnnotationProcessorTool)作为Java的一个高级语法特性,合理使用能给我们的开发带来极大便利。
例如Spring、Lombok以及Jetbrains自带的一些注解及其对应功能都是基于APT实现的。
Lombok可以利用APT自动检测你代码中的 @Data
@AllArgsConstructor
等注解,干预Java的编译过程,从而实现自动生成一些代码来简化开发,提高源代码的可读性。
基本知识
在Java中,javax.lang.model.AnnotatedConstruct
是APT中大多数类的最顶级父类,例如TypeElement、TypeMirror等都是它的子类。
现在我们介绍一下Element和TypeMirror这两个接口。
回忆一下面向对象中类与对象的区别:类是一个抽象的概念,它为其对应的所有对象定义了共同的属性和方法。而对象是类的实例,例如Student类可以有多个对象,Student类定义了其所有对象的Name和Age属性,那么对于不同的对象,它们都有这两个属性,但是它们各自的值可以不一样。
现在可以简单理解为TypeMirror是Element的镜像,也就是说TypeMirror是对Element的抽象,Element是与其对应的TypeMirror的实现。(TypeMirror是类,Element是对象)
Element
Element接口的方法
- TypeMirror asType() 获取Element对应的TypeMirror
- ElementKind getKind() 获取Element的类型,ElementKind是一个枚举
- Set<Modifier> getModifiers() 获取Element的关键字,Modifier也是一个枚举
- Name getSimpleName() 获取Element的简单名称,如果这个Element是一个方法,那么SimpleName就是方法名,类、接口、变量等以此类推
- Element getEnclosingElement() 获取定义这个Element的Element,这个比较复杂,放在下面介绍
- List<? extends Element> getEnclosedElements() 获取Element中的所有子Element,例如对某个TypeElement使用此方法可以得到这个类中的成员变量、构造函数、方法等Element
- List<? extends AnnotationMirror> getAnnotationMirrors() 获取修饰此Element的所有注解镜像
- <A extends Annotation> A getAnnotation(Class<A> annotationType) 获取修饰此Element的注解,不存在返回null
- <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationType) 作用同上,可以得到多个同类型的注解
Element接口的子类
- PackageElement 包元素
- TypeElement 类元素,通常是类、枚举、接口、注解(枚举是类的一种,注解是接口的一种)
- VariableElement 变量元素,通常是类的成员变量、方法的参数
- ExecutableElement 可执行元素,通常是构造函数和方法
- TypeParameterElement 泛型参数元素,例如List<E>中的E
- Parameterizable 可泛型参数化的,例如类和方法都能使用<K, V, T>来定义泛型,因此 Parameterizable 是 ExecutableElement 和 TypeElement 的父类
- QualifiedNameable 可命名的,是PackageElement 和 TypeElement 的父类
上面的 VariableElement 和 TypeParameterElement 是 Element 的直接子类。
首先来看一段代码:
public class Person<T> { // TypeElement, TypeParameterElement
String name; // VariableElement
int age; // VariableElement
String sex; // VariableElement
public void get() { // ExecutableElement
System.out.println("name:" + name + " age:" + age + " sex:"+ sex);
}
public void sayHello(String to) { // ExecutableElement, VariableElement
System.out.println("Hello, " + to + "!");
}
}
看到这里你应该大致了解这些子类的区别了,但是 TypeParameterElement 和方法中的 VariableElement都需要通过其他Element得到。
例如,通常你能得到 TypeElement 和 ExecutableElement,你可以通过 TypeElement 得到这个类的泛型参数集合,通过 ExecutableElement 可以得到这个可执行元素的参数元素集合 VariableElement。
getEnclosingElement()
这个方法比较复杂,先看看JDK里的注释是怎么写的:
简单来说,这个方法的返回值取决于具体是哪种Element的子类。
通常会用这个方法从 ExecutableElement 中得到定义这个构造函数或方法的类,例如对Person类的构造函数Element使用这个方法,可以得到Person类的TypeElement。
如果是从 TypeParameterElement 中调用,得到的是这个泛型的具体类型,例如对List<Person>的 TypeParameterElement 使用这个方法可以得到Person的TypeElement。
如果从一个类或接口的 TypeElement 中调用,得到的是他们的包元素,也就是 PackageElement。
至于提到的record component和module是后续JDK更新加入的新元素,有兴趣可以自己了解一下。
TypeMirror
TypeMirror是一个抽象的概念,与Element相比可提供的信息就很少了。
TypeMirror接口的方法
- TypeKind getKind() 获取TypeMirror的类型
- List<? extends AnnotationMirror> getAnnotationMirrors() 修饰该类的接口镜像
- <A extends Annotation> A getAnnotation(Class<A> annotationType) 同Element中的方法
- <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationType) 同上
TypeMirror的子类
- PrimitiveType 基本类型,例如int、float、char等
- NullType 空类型
- ArrayType 数组类型,例如String[]
- DeclaredType 通常是 TypeElement 对应的 TypeMirror 的类型。
- TypeVariable 泛型参数类型,是 TypeParameterElement 对应的 TypeMirror 的类型。
- WildcardType 通配符类型,例如
?
? extends T
? super T
- ExecutableType 可执行类型,也就是 ExecutableElement 对应的 TypeMirror 的类型。
- NoType 无类型,通常是包、模块、空类型的TypeMirror类型
- ErrorType 错误类型,通常表示一个类无法被正确的转换为TypeMirror
还有其他的UnionType、IntersectionType用的比较少,感兴趣可以自己了解一下。
NullType、ArrayType 、DeclaredType、TypeVariable 是 Reference 的子类,而 Reference 才是 TypeMirror 的直接子类。
ErrorType 是 DeclaredType的子类。
TypeMirror常见问题
无父类的父类的TypeMirror
对应 TypeElement 可以通过 getSuperClass() 方法得到它的父类的TypeMirror,如果它没有父类,得到的则是 Object 类的 TypeMirror。
Object类的父类的TypeMirror
Object类是Java中最顶级的类,也就意味着它没有父类。如果你对 Object类的 TypeElement 使用 getSuperClass() 方法,会得到一个 NoType,它是 TypeMirror 的子类。
TypeMirror的 toString() 方法
你可以通过这个方法得到这个 TypeMirror 对应的类的完整包名。
需要注意的是,这个方法返回的有可能含有其他符号,例如 ExecutableElement 的 TypeMirror$toString 只后是小括号()+返回值类名,例如”()java.lang.String”。
而对于NoType来说,toString() 返回的是字符串none。
准备工作
先准备一个注解,例如:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Component {
}
然后创建一个新的类,继承 javax.annotation.processing.AbstractProcessor
类,实现process()与getSupportedAnnotationTypes()两个基本的方法。
为了方便,这里直接引入@AutoService注解,它可以自动在META-INF中生成对应的service配置。
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0-rc6</version>
</dependency>
用法如下所示
@AutoService(Processor.class)
public class MyTestAPT extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return Sets.newHashSet(
Component.class.getCanonicalName()
);
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
// return super.getSupportedSourceVersion();
}
}
这就是一个最简单的注解处理器了,如果需要在其他项目使用,直接引入依赖就行。
maven依赖的scope建议设置为provided。
如果是gradle,请使用annotationProcessor来引入依赖。
下面对这些方法逐一介绍。
process()
这个方法有两个参数,一个是 Set<? extends TypeElement> annotations
,另一个是 RoundEnvironment roundEnv
。
首先annotations
这个集合储存的是被编译项目中存在的所有受支持的注解,受支持的注解由下面的getSupportedAnnotationTypes方法提供。
例如一个SpringBoot项目你使用了@SpringBootApplication与@Autowired,这个集合中保存的就是这两个注解对应的TypeElement
类。
RoundEnvironment中有两个get方法,分别是:
- getRootElements 获取所有被注解处理器支持的注解所修饰的元素
- getElementsAnnotatedWith 获取所有被指定注解修饰的元素
举个例子说明这两个方法,例如有下面的代码:
public interface UserService {
UserEntity getUserById(Long userId);
}
@Component
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
public UserEntity getUserById(Long userId) {
return null;
}
}
此时调用getRootElements方法,可以得到的是UserServiceImpl 和 UserService,因为UserServiceImpl 继承了 UserService,因此即使UserService没有注解,但是它的实现类存在,这也算是一个RootElement。
如果调用getElementsAnnotatedWith方法获取被@Compoent注解修饰的元素,此时只能得到UserServiceImpl。
getSupportedAnnotationTypes()
这是一个很重要的方法,它的返回值是一个String类型的集合。
这个集合存放的也就是对应注解类的完整路径,一般用getCanonicalName()来获取,至于它和一般的getName()的区别,可以自行百度了解一下。
这个方法的作用是告诉你的注解处理器,当编译时在项目中存在至少一个在这个集合中的注解时,需要调用process()方法。
也就是说,只有在这个集合中的注解才会参与注解处理。
常用方法
你已经了解了APT及其基本模型,现在可以开始你的创作了。
下面介绍一些常用的方法来辅助开发。
JavaPoet
通常注解处理器会被用来自动生成一些代码,但是生成代码的过程用字符串拼接很麻烦,可以尝试一下JavaPoet库,通过简单的Java代码来生成标准格式的代码。
在注解处理器中,你可以按照如下写法在org.example.generated包内生成一个GeneratedClass。
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
try {
TypeSpec build = TypeSpec.classBuilder("GeneratedClass")
.addModifiers(Modifier.PUBLIC)
.addMethod(MethodSpec.methodBuilder("say")
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.get(String.class))
.addStatement(CodeBlock.of("return \"Hello\"")).build())
.build();
JavaFile.builder("org.example.generated", build).build().writeTo(this.filer);
} catch (IOException e) {
throw new RuntimeException(e);
}
return true;
}
生成的GeneratedClass代码如下:
public class GeneratedClass {
public String say() {
return "Hello";
}
}
你可以直接在你的项目中使用这个生成的类,前提是你执行了compile或者build命令让注解处理器先生成这些类。
这些生成的类和包会随着你的项目一起打包出去,因此引入的注解处理器scope为provided即可,它们只会在你编译时生效。
常见问题
注解处理器是一个比较麻烦的东西,例如项目需要调用生成的代码,就需要先让注解处理器生成对应的代码,才能进行项目的下一步编译。
如果你打算写一个注解处理器,建议把项目分成两个不同的module,分别是compiler和annotation。
annotation主要用于存放你的自定义注解以及一些通用的工具类。
compiler就是你实现AbstractProcessor的模块了,如果是gradle项目,直接把这个模块作为annotationProcessor引入即可。如果是maven项目,直接以provided的模式引入即可。
Compilation failed: internal java compiler error
错误信息大致如下:
java: java.util.ServiceConfigurationError: javax.annotation.processing.Processor: Provider org.example.MyTestAPT not found
这个错误一般出现在打包编译一个注解处理器项目时,当你完成一个注解处理器的编写后,你希望把它打包出来给别的项目使用,但是编译时可能会出现这个错误。
例如你的项目使用了lombok,打包时就需要让lombok先做一些操作,然后才能输出你的项目class文件,这是建立在你已经引入了lombok为依赖的前提。
这个错误的主要原因是打包时maven会认为你的项目也需要使用你的注解处理器,但此时你的项目还没有被打包,而打包你的项目又需要用到你的项目中的注解处理器,这样就产生循环依赖了。
解决的办法也很简单,在maven打包时让proc参数为none即可,也就是忽略所有的注解处理器,这样打包时就不会产生循环依赖了。
在compiler模块中的pom.xml加入下面的片段即可:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<proc>none</proc>
</configuration>
</plugin>
</plugins>
</build>
加上这个插件后一定要记得先clean再install,否则可能不会生效。
不过这个方法也有一定的缺陷,它直接让所有的注解处理器不生效,这样的话如果你的compiler模块使用了lombok或其他含有注解处理器的库也同样不会生效。
如果你没有使用任何其他的注解处理器,用上面的方法即可。
当然你也可以只屏蔽你自己的注解处理器,稍作修改即可:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArgs>
<arg>-proc:none</arg>
<arg>-Aorg.example.MyTestAPT</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
利用proc的参数可以指定关闭哪些注解处理器,在-A的后面紧跟注解处理器的包名和类型即可。
结束语
Java的注解处理器远不止这么简单,例如其中的Element、TypeMirror等都没有详细介绍,但通过这篇文章了解一下它们简单的用法,实际上就应该可以应对大部分场景了。
这一部分内容我自己也并不完全明白,本文仅供学习参考,如有错误欢迎指出。