一文解读|Java编译期注解处理器AbstractProcessor
阿里妹导读
本文围绕编译器注解都是如何运行的呢? 又是怎么自动生成代码的呢?做出了详细介绍。
概述
运行时注解:通过反射在运行时动态处理注解的逻辑
编译时注解:通过注解处理器在编译期动态处理相关逻辑
平时我们接触的框架大部分都是运行时注解,比如:@Autowire @Resoure @Bean 等等。
我们今天来详细介绍一下,不过在介绍之前,可以先简单了解一下Java注解的基本概念:
Java注解:https://blog.csdn.net/u010634066/article/details/80384125
注解处理器
注解处理流程
在java编译器中构建;
编译器开始执行未执行过的注解处理器;
循环处理注解元素(Element),找到被该注解所修饰的类,方法,或者属性;
生成对应的类,并写入文件;
判断是否所有的注解处理器都已执行完毕,如果没有,继续下一个注解处理器的执行(回到步骤1)。
AbstractProcessor
getSupportedOptions()
"name","age"}) ({
public class SzzTestProcessor extends AbstractProcessor {
}
不过貌似该接口并没有什么用处。
String resultPath = processingEnv.getOptions().get(参数);
实际上这个获取的参数是编译期通过入参 -Akey=name 设置的,跟getSupportedOptions没有什么关系。
getSupportedAnnotationTypes
获取当前的注解处理类能够处理哪些注解类型,默认实现是从SupportedAnnotationTypes注解里面获取;
注解值是个字符串数组 String [] ;
匹配上的注解,会通过当前的注解处理类的 process方法传入。
"*") (
(SourceVersion.RELEASE_11)
public class PrintingProcessor extends AbstractProcessor {
}
又或者可以直接重写这个接口:
@Override
public ImmutableSet<String> getSupportedAnnotationTypes() {
return ImmutableSet.of(AutoService.class.getName());
}
最终他们生效的地方就是用来做过滤,因为处理的时候会获取到所有的注解,然后根据这个配置来获取自己能够处理的注解。
getSupportedSourceVersion
(SourceVersion.RELEASE_11)
public class PrintingProcessor extends AbstractProcessor {
}
或者重写(推荐 , 获取最新的版本)
public SourceVersion getSupportedSourceVersion() {
//设置为能够支持最新版本
return SourceVersion.latestSupported();
}
init初始化
init是初始化方法,这个方法传入了ProcessingEnvironment 对象。一般我们不需要去重写它,直接使用抽象类就行了。
当然你也可以根据自己的需求来重写
public synchronized void init(ProcessingEnvironment pe) {
super.init(pe);
System.out.println("SzzTestProcessor.init.....");
// 可以获取到编译器参数(下面两个是一样的)
System.out.println(processingEnv.getOptions());
System.out.println(pe.getOptions());
}
可以获取到很多信息,例如获取编译器自定义参数, 自定义参数的设置请看下面的 如何给编译期设置入参 部分
process 处理方法
如果返回 true,则这些注解不会被后续 Processor 处理;
如果返回 false,则这些注解可以被后续的 Processor 处理。
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("SzzTestProcessor.process.....;");
return false;
}
如何注册注解处理器
然后在需要使用到注解处理器的Module引用。
1、在resource/META-INF.services文件夹下创建一个名为javax.annotation.processing.Processor的文件;里面的内容就是你的注解处理器的全限定类名;
服务配置文件不正确, 或构造处理程序对象javax.annotation.processing.Processor: Provider org.example.SzzTestProcessor not found时抛出异常错误
如果是用Maven编译的话,请加上如下配置 <compilerArgument>-proc:none</compilerArgument>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</execution>
<execution>
<id>compile-project</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
3、注解处理器打包成功,就可以提供给别的Module使用了
(Processor.class)
public class SzzBuildProcessor extends AbstractProcessor {
}
如何调试编译期代码
Maven相关配置(指定生效的Processor)
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<!-- 主动设置生成的源码的文件夹路径,默认的就是下面的地址。一般不需要主动设置除非你有自己的需求 -->
<generatedSourcesDirectory>${project.build.directory} /generated-sources/</generatedSourcesDirectory>
<!-- 指定生效的注解处理器,这里设置之后,只会有下面配置的注解处理器生效; 一般情况也不用主动配置,可以将下面的全部删除 -->
<annotationProcessors>
<annotationProcessor>
org.example.SzzTestProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>
注意事项
自定义注解处理器范例
范例一:自动生成Build构造器
public class Company {
private String name;
private String email ;
}
public class Personal {
private String name;
private String age;
}
我们想创建对应的构建器帮助类来更流畅地实例化POJO类
Company company = new CompanyBuilder()
.setName("ali").build();
Personal personal = new PersonalBuilder()
.setName("szz").build();
2. 需求分析
1、定义一个 @BuildProperty 注解,在需要生成对应的setXX方法的方法上标记注解
public class CompanyBuilder {
private Company object = new Company();
public Company build() {
return object;
}
public CompanyBuilder setName(java.lang.String value) {
object.setName(value);
return this;
}
}
3、编码
// 注解用在方法上
// 尽在Source处理期间可用,运行期不可用
public BuildProperty {
}
注解处理器
// 只处理这个注解;
public class SzzBuildProcessor extends AbstractProcessor {
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("SzzBuildProcessor.process ;");
for (TypeElement annotation : annotations) {
// 获取所有被该注解 标记过的实例
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);
// 按照需求 检查注解使用的是否正确 以set开头,并且参数只有一个
Map<Boolean, List<Element>> annotatedMethods = annotatedElements.stream().collect(
Collectors.partitioningBy(element ->
((ExecutableType) element.asType()).getParameterTypes().size() == 1
&& element.getSimpleName().toString().startsWith("set")));
List<Element> setters = annotatedMethods.get(true);
List<Element> otherMethods = annotatedMethods.get(false);
// 打印注解使用错误的case
otherMethods.forEach(element ->
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@BuilderProperty 注解必须放到方法上并且是set开头的单参数方法", element));
if (setters.isEmpty()) {
continue;
}
Map<String ,List<Element>> groupMap = new HashMap();
// 按照全限定类名分组。一个类创建一个Build
setters.forEach(setter ->{
// 全限定类名
String className = ((TypeElement) setter
.getEnclosingElement()).getQualifiedName().toString();
List<Element> elements = groupMap.get(className);
if(elements != null){
elements.add(setter);
}else {
List<Element> newElements = new ArrayList<>();
newElements.add(setter);
groupMap.put(className,newElements);
}
});
groupMap.forEach((groupSetterKey,groupSettervalue)->{
//获取 类名SimpleName 和 set方法的入参
Map<String, String> setterMap = groupSettervalue.stream().collect(Collectors.toMap(
setter -> setter.getSimpleName().toString(),
setter -> ((ExecutableType) setter.asType())
.getParameterTypes().get(0).toString()
));
try {
// 组装XXXBuild类。并创建对应的类文件
writeBuilderFile(groupSetterKey,setterMap);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
// 返回false 表示 当前处理器处理了之后 其他的处理器也可以接着处理,返回true表示,我处理完了之后其他处理器不再处理
return true;
}
private void writeBuilderFile(
String className, Map<String, String> setterMap)
throws IOException {
String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}
String simpleClassName = className.substring(lastDot + 1);
String builderClassName = className + "Builder";
String builderSimpleClassName = builderClassName
.substring(lastDot + 1);
JavaFileObject builderFile = processingEnv.getFiler()
.createSourceFile(builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
if (packageName != null) {
out.print("package ");
out.print(packageName);
out.println(";");
out.println();
}
out.print("public class ");
out.print(builderSimpleClassName);
out.println(" {");
out.println();
out.print(" private ");
out.print(simpleClassName);
out.print(" object = new ");
out.print(simpleClassName);
out.println("();");
out.println();
out.print(" public ");
out.print(simpleClassName);
out.println(" build() {");
out.println(" return object;");
out.println(" }");
out.println();
setterMap.entrySet().forEach(setter -> {
String methodName = setter.getKey();
String argumentType = setter.getValue();
out.print(" public ");
out.print(builderSimpleClassName);
out.print(" ");
out.print(methodName);
out.print("(");
out.print(argumentType);
out.println(" value) {");
out.print(" object.");
out.print(methodName);
out.println("(value);");
out.println(" return this;");
out.println(" }");
out.println();
});
out.println("}");
}
}
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
System.out.println("----------");
System.out.println(processingEnv.getOptions());
}
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
4、注册注解处理器
主要参数就是
<compilerArgument>-proc:none</compilerArgument>
如下所示
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</execution>
<execution>
<id>compile-project</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
6、执行编译打包
8、Demo Module 进行编译,会自动生成BuildCompany类
Demo Module 编译之后,就会在target文件夹生成BuildXXX类。并且只有我们用注解BuildProperty标记了的方法才会生成对应的方法。
而且如果注解BuildProperty使用的方式不对,我们也会打印出来了异常。
如何给编译期设置入参
String verify = processingEnv.getOptions().get("自定义key");
注意这个获取到的编译器参数只能获取的是以-A开头的参数,因为是过滤之后的
微信扫码关注该文公众号作者