背景引入
在使用spring-boot开发过程中,自定义属性映射yml中自定义配置时有个有趣的现象:在yml文件里点住配置项(ctrl)就会跳转到对应的java类属性,很好奇这是什么黑科技;
下来研究一番终于大概知道了其一点内在乾坤,首先我们需要引入该依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
然后我们看看该依赖引入的jar包结构,在jar包路径下有个META-INF下有个services目录,这个目录很熟悉即java SPI服务发现规范目录;
于是查阅 javax.annotation.processing.Processor 相关的资料,该SPI实现类会在编辑期间即javac期间被编译器调用【参考】;这下子幡然醒悟,有点类型编译期间动态代理生成的模式(其原理是 spring-boot-configuration-processor相关组件在工程编译期生成了一个文件:spring-configuration-metadata.json,然后开发工具根据这个文件来做对应的代码跳转);
手写一个注解处理器,注解处理器的开发模式一般是处理器提供方在一个工程,能够独立编译和打包;然后再在其他工程里引入这打包好的依赖,因为提供方我们在编译和打包的时候需要设置屏蔽javac注解处理器,但是在使用方工程里我们要开启javac注解处理器;
注解处理器工程
工程大致结构如下
自定义注解(ApiAnnotation)
package com.xx.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target( {ElementType.TYPE, ElementType.METHOD})
public @interface ApiAnnotation {
String author() default "XX";
}
这里需要注意的是注解里的元注解 @Retention 要标记在为源码生效即:@Retention(RetentionPolicy.SOURCE)
注解处理器处理类(DemoProcessor)
这个类写法一般要集成 AbstractProcessor,然后覆盖他的一些方法(摘自其他文档)
- init(ProcessingEnvironment processingEnvironment):
每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements, Types和Filer。后面我们将看到详细的内容。 - process(Set<? extends TypeElement> annotations, RoundEnvironment env):
这相当于每个处理器的主函数main()。扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。 - getSupportedAnnotationTypes():
- 这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,你在这里定义你的注解处理器注册到哪些注解上。
- getSupportedSourceVersion():
- 用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 6的话,你也可以返回SourceVersion.RELEASE_6。我推荐你使用前者。
在Java 7中,你也可以使用注解来代替getSupportedAnnotationTypes()和getSupportedSourceVersion(),像这样:
@SupportedAnnotationTypes(“com.starcor.annotation.RouterAnnotation”),其中括号内内容表示要处理的注解名称,要写全名。
我们自定义一个如下,简单打印一下注解名称:
package com.xx;
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.TypeElement;
@SupportedAnnotationTypes( {"com.xx.annotation.ApiAnnotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class DemoProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for(TypeElement typeElement : annotations){
System.out.println("getQualifiedName--" + typeElement.getQualifiedName());
}
return false;
}
}
其中支持的源码版本和注解类型也可以通过注解指定,如本例的
@SupportedAnnotationTypes( {"com.xx.annotation.ApiAnnotation"})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
SPI配置文件
我们需要在规范的SPI路径下新增我们的处理器类名,即MATA-INF/services下新增了文件名为 javax.annotation.processing.Processor 的文件,内容是我们的真实处理类(Processor的子类)
pom编译配置
你会发现在不修改pom配置文件直接执行compile以上的goal你会发现工程编译不过,并会提示:java: java.util.ServiceConfigurationError: javax.annotation.processing.Processor: Provider com.xx.DemoProcessor not found
java: 编译器 (1.8.0_261) 中出现异常错误。如果在 Bug Database (http://bugs.java.com) 中没有找到该错误, 请通过 Java Bug 报告页 (http://bugreport.java.com) 建立该 Java 编译器 Bug。请在报告中附上您的程序和以下诊断信息。谢谢。
java: java.util.ServiceConfigurationError: javax.annotation.processing.Processor: Provider com.xx.DemoProcessor not found
java: at java.util.ServiceLoader.fail(ServiceLoader.java:239)
java: at java.util.ServiceLoader.access$300(ServiceLoader.java:185)
java: at java.util.ServiceLoader$LazyIterator.nextService(ServiceLoader.java:372)
java: at java.util.ServiceLoader$LazyIterator.next(ServiceLoader.java:404)
java: at java.util.ServiceLoader$1.next(ServiceLoader.java:480)
java: at org.jetbrains.jps.javac.Iterators$9.next(Iterators.java:165)
这是因为默认情况下javac是开启了注解处理器,发现SPI目录下有指定的处理器就回去找,但是我们执行的目的就是要把自定义的处理器编译出来,这就形成了一个类似“死锁”的场景,最终找不到类;
所以我们要在编译的时候关闭掉编译处理器
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<!--Disable annotation processing for ourselves,关闭注解处理器-->
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</plugin>
</plugins>
</build>
这样就可以编译成功了,然后我们install到本地并deploy到远程仓库供其他工厂引入使用;
新建一个工程:refdemo
在refdemo工程里引入依赖
<dependencies>
<dependency>
<groupId>com.uu</groupId>
<artifactId>processor</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
java代码用注解标记上
package com.uu;
import com.xx.annotation.ApiAnnotation;
@ApiAnnotation
public class Demo {
}
执行compile
打印出了:"getQualifiedName--com.xx.annotation.ApiAnnotation"
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ refdemo ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ refdemo ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 1 source file to E:\code-opensource\code-demoparent\annotationprocessor\refdemo\target\classes
getQualifiedName--com.xx.annotation.ApiAnnotation
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.687 s
[INFO] Finished at: 2021-02-08T11:10:24+08:00
[INFO] ------------------------------------------------------------------------
[WARNING] The requested profile "nexus" could not be activated because it does not exist.
源代码可以参考:https://gitee.com/aqu415/code-demoparent/tree/develop/annotationprocessor
我们用到的lombok里面就是用这个特性