【JDK】自定义注释处理程序插件 AbstractProcessor
1 前言
最近比较好奇,Lombok、MapStruct 里的注解,会在打包的时候会给我们的字节码文件里,写入一些东西甚至生成一些转换的字节码文件,不知道人家是如何做到的,所以抽空看了看,自己写了一个小Demo,来体验下。我们的目标就是引入自己的 AbstractProcessor 的 maven依赖包,然后能给我们生成一些东西来。
参考文档:
MapStruct文档:https://mapstruct.org/documentation/reference-guide/
MapStruct源码:https://github.com/mapstruct/mapstruct
同类文档:https://www.bilibili.com/read/cv22416305/
同类文档:https://blog.csdn.net/begefefsef/article/details/126434950
2 相关知识
我们接触的注解主要分为以下两类:
- 运行时注解:通过反射在运行时动态处理注解的逻辑
- 编译时注解:通过注解处理器在编译期动态处理相关逻辑
平时我们接触的框架大部分都是运行时注解,比如:@Autowire @Resoure @Bean 等等。那么我们平时有接触过哪些编译期注解呢,@Lombok
@AutoService
等等
像这些编译时注解的作用都是自动生成代码,一是为了提高编码的效率,二是避免在运行期大量使用反射,通过在编译期利用反射生成辅助类和方法以供运行时使用。
注解编译期处理流程最关键的一个类就是Processor ,它是注解处理器的接口类,我们所有需要对编译期处理注解的逻辑都需要实现这个Processor接口,当然,AbstractProcessor 抽象类帮我们写好了大部分都流程,所以我们只需要实现这个抽象类就可以很方便的定义一个注解处理器;
注解处理流程由多轮完成。每一轮都从编译器在源文件中搜索注解并选择适合这些注解的 注解处理器(AbstractProcessor) 开始。每个注解处理器依次在相应的源上被调用。
如果在此过程中生成了任何文件,则将以生成的文件作为输入开始另一轮。这个过程一直持续到处理阶段没有新文件生成为止。
注解处理器的处理步骤:
- 在java编译器中构建;
- 编译器开始执行未执行过的注解处理器;
- 循环处理注解元素(Element),找到被该注解所修饰的类,方法,或者属性;
- 生成对应的类,并写入文件;
- 判断是否所有的注解处理器都已执行完毕,如果没有,继续下一个注解处理器的执行(回到步骤1)
3 实践
看完基本的概念后,我们来开始写一个自己的 AbstractProcessor,新建工程哦。我直接贴代码了哈,大家看的时候可以自己动手试试:
3.1 注解
新建个注解,来作为处理的入口,也就是我们要对哪个注解进行处理:
@Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface MyAnnotation { }
3.2 自定义 AbstractProcessor
创建处理类,继承 AbstractProcessor:
import com.google.auto.service.AutoService; import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; import java.io.IOException; import java.io.Writer; import java.util.Set; /** * @author: xjx * @description * 自定义的编译时处理器 */ @AutoService(Processor.class) @SupportedAnnotationTypes("process.MyAnnotation") @SupportedSourceVersion(SourceVersion.RELEASE_8) public class MyCustomProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "=============我执行了"); try { printFile(); } catch (IOException e) { } return false; } private void printFile() throws IOException { this.processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "doing print"); //通过向Filer声明自己想要创建的java文件包名,就能获取其writer JavaFileObject fileObject = this.processingEnv.getFiler().createSourceFile("test.mipa.testSrcFile.myClass1"); Writer writer = fileObject.openWriter(); //实际应用应该需要使用JavaPoet等自动代码生成功能 String classFileStr = "package test.mipa.testSrcFile; \n" + "class myClass1\n{\n" + "private int a = 0;\n" + "int getValue()\n { return this.a; }\n" + "}"; //直接将待生成.java文件以String形式输入writer,编译器会将这个流程产生的文件也进行生成 writer.write(classFileStr); writer.flush(); writer.close(); } }
3.3 部署到本地仓库
我的pom:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.processor</groupId> <artifactId>processor</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.google.auto.service</groupId> <artifactId>auto-service</artifactId> <version>1.1.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.1</version> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> </project>
写好后,我们 install 一下把它安装到本地仓库:
3.4 其他工程引入依赖看看效果
我找了一个我自己的工程,引入:
然后我们 install 一下,看看效果:
4 小结
好啦,体验完毕哈,接下来重点其实就在 AbstractProcessor 的逻辑里,我们想要根据什么生成什么样的东西,比如 MapStruct 它本身定义了很多模板文件,渲染匹配的,抽空还可以再看看哈,暂时就到这里哈,有理解不对的地方欢迎指正哈。