写在前面
记得第一次接触到Lombok是拉一个新项目时,用IDEA打开都是一片红的,第一印象是不太好的。不过后面整个团队都用起来之后也还不错的。说节省了时间吧,在使用中好像也没什么感觉,有点像空格和tab一样,都是一下子的事,可能是习惯问题吧。回到主题,比起争论lombok的使用与否,更加好奇它实现的过程。
一、注解处理器(Annotation Processor Tool)是什么
APT全称是Pluggable Annotation Processing API(可插入注释处理API),是JSR269规范提供的一套API,可以在编译期自定义我们期望的编译输出,APT并不能修改已存在的源码,它只能添加新源码,帮我们做一些检查代码,生成模板化代码或者配置文件等。我们常见的应用有Lombok,yml文件ctrl+鼠标左键定位到具体文件(注解处理器生成spring-configuration-metadata.json),google的AutoService(下文会介绍)等.
二、运行流程以及基本原理
从Javac代码的总体结构来看,编译过程大致可以分为1个准备过程和3个处理过程,它们分别如下 所示。
- 准备过程:初始化插入式注解处理器。
- 解析与填充符号表过程,包括词法、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树,填充符号表。产生符号地址和符号信息。
- 插入式注解处理器的注解处理过程。
- 分析与字节码生成过程,包括标注检查、数据流及控制流分析、解语法糖、字节码生成。
如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个Round,也就是上图的回环过程
把上述处理过程对应到代码中,Javac编译动作的入口是 com.sun.tools.javac.main.JavaCompiler类,上述3个过程的代码逻辑集中在这个类的compile()和compile2()方法里,整个编译过程主要的处理由下图中标注的8个方法来完成
三、自定义Lombok的项目构建
创建一个maven项目,引入相关jar包和设置maven-compiler-plugin,关于为什么要设置proc:none可以看Maven annotation processing processor not found
<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<!--不使用注释处理器,只编译源文件-->
<compilerArgument>-proc:none</compilerArgument>
<source>1.8</source>
<target>1.8</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
复制代码
四、Setter、Getter注解处理器
1. 目录树
├─java
│ │ complie.sh
│ │
│ └─com
│ └─hinotoyk
│ ├─jsr269
│ │ Getter.java
│ │ GetterProcessor.java
│ │ Setter.java
│ │ SetterProcessor.java
│ │
│ └─test
│ TestDemo.java
│
└─resources
└─META-INF
└─services
javax.annotation.processing.Processor
复制代码
2. 创建Getter、Setter注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Getter {
}
复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Setter {
}
复制代码
3. 创建Getter、Setter对应的注解处理器
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.*;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;
//对Getter感兴趣
@SupportedAnnotationTypes("com.hinotoyk.jsr269.Getter")
//支持的版本,使用1.8就写这个
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends AbstractProcessor {
// 编译时期输入日志的
private Messager messager;
// 将Element转换为JCTree的工具,提供了待处理的抽象语法树
private JavacTrees trees;
// 封装了创建AST节点的一些方法
private TreeMaker treeMaker;
// 提供了创建标识符的方法
private Names names;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.trees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 获取被@Getter注解标记的所有元素(这个元素可能是类、变量、方法等等)
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Getter.class);
set.forEach(element -> {
// 将Element转换为JCTree
JCTree jcTree = trees.getTree(element);
jcTree.accept(new TreeTranslator() {
/***
* JCTree.Visitor有很多方法,我们可以通过重写对应的方法,(从该方法的形参中)来获取到我们想要的信息:
* 如: 重写visitClassDef方法, 获取到类的信息;
* 重写visitMethodDef方法, 获取到方法的信息;
* 重写visitVarDef方法, 获取到变量的信息;
* @param jcClassDecl
*/
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
//创建一个变量语法树节点的List
List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
// 遍历defs,即是类定义的详细语句,包括字段、方法的定义等等
for (JCTree tree : jcClassDecl.defs) {
if (tree.getKind().equals(Tree.Kind.VARIABLE)) {
JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree;
jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
}
}
// 对于变量进行生成方法的操作
jcVariableDeclList.forEach(jcVariableDecl -> {
messager.printMessage(Diagnostic.Kind.NOTE, "get " + jcVariableDecl.getName() + " has been processed");
treeMaker.pos = jcVariableDecl.pos;
//类里的前面追加生成的Getter方法
jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
});
super.visitClassDef(jcClassDecl);
}
});
});
//我们有修改过AST,所以返回true
return true;
}
private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
/***
* JCStatement:声明语法树节点,常见的子类如下
* JCBlock:语句块语法树节点
* JCReturn:return语句语法树节点
* JCClassDecl:类定义语法树节点
* JCVariableDecl:字段/变量定义语法树节点
* JCMethodDecl:方法定义语法树节点
* JCModifiers:访问标志语法树节点
* JCExpression:表达式语法树节点,常见的子类如下
* JCAssign:赋值语句语法树节点
* JCIdent:标识符语法树节点,可以是变量,类型,关键字等等
*/
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));
JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
return treeMaker.MethodDef(
treeMaker.Modifiers(Flags.PUBLIC),//mods:访问标志
getNewMethodName(jcVariableDecl.getName()),//name:方法名
jcVariableDecl.vartype,//restype:返回类型
List.nil(),//typarams:泛型参数列表
List.nil(),//params:参数列表
List.nil(),//thrown:异常声明列表
body,//方法体
null);
}
private Name getNewMethodName(Name name) {
String s = name.toString();
return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
}
}
复制代码
Settter注解处理器和Getter的大同小异,只是生成set方法的步骤不太一样,详细可查看openjdk-7 TreeMaker
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.*;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;
@SupportedAnnotationTypes("com.hinotoyk.jsr269.Setter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SetterProcessor extends AbstractProcessor {
private Messager messager;
private JavacTrees trees;
private TreeMaker treeMaker;
private Names names;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.trees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Setter.class);
set.forEach(element -> {
JCTree jcTree = trees.getTree(element);
jcTree.accept(new TreeTranslator() {
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
for (JCTree tree : jcClassDecl.defs) {
if (tree.getKind().equals(Tree.Kind.VARIABLE)) {
JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree;
jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
}
}
jcVariableDeclList.forEach(jcVariableDecl -> {
messager.printMessage(Diagnostic.Kind.NOTE, "set " + jcVariableDecl.getName() + " has been processed");
treeMaker.pos = jcVariableDecl.pos;
jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));
});
super.visitClassDef(jcClassDecl);
}
});
});
return true;
}
private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
statements.append(
treeMaker.Exec(
treeMaker.Assign(
treeMaker.Select(
treeMaker.Ident(names.fromString("this")),
names.fromString(jcVariableDecl.name.toString())
),
treeMaker.Ident(names.fromString(jcVariableDecl.name.toString()))
)
)
);
JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
// 生成入参
JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), jcVariableDecl.getName(),jcVariableDecl.vartype, null);
List<JCTree.JCVariableDecl> paramList = List.of(param);
return treeMaker.MethodDef(
treeMaker.Modifiers(Flags.PUBLIC), // 方法限定值
setNewMethodName(jcVariableDecl.getName()), // 方法名
treeMaker.Type(new Type.JCVoidType()), // 返回类型
List.nil(),
paramList, // 入参
List.nil(),
body,
null
);
}
private Name setNewMethodName(Name name) {
String s = name.toString();
return names.fromString("set" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
}
}
复制代码
4.创建测试Class
import com.hinotoyk.jsr269.Getter;
import com.hinotoyk.jsr269.Setter;
@Setter
@Getter
public class TestDemo {
private String name;
public static void main(String[] args) {
TestDemo testDemo = new TestDemo();
testDemo.setName("yk");
System.out.println(testDemo.getName());
}
}
复制代码
5.运行
直接运行肯定会报错的,因为实际上这应该是分开两个工程的,一个是注解处理器工程,一个是业务工程。我们需要用已经编译好的注解处理器工程在编译期去处理其他的工程,一起编译的话肯定会出错嘛,按照这个思路写成shell脚本如下:
#!/usr/bin bash
if [ -d classes ]; then
rm -rf classes;
fi
#创建输出目录
mkdir classes
#先编译注解处理器相关的类,并且-d指定输出到classes文件夹
javac -encoding UTF-8 -cp ${JAVA_HOME}/lib/tools.jar com/hinotoyk/jsr269/*.java -d classes/
#-cp 指定class路径(运行时用全限定类名) -processor 即指定用上面编译出来的注解类去编译测试类,并且class文件也指定到classes文件夹
javac -cp classes -processor com.hinotoyk.jsr269.GetterProcessor,com.hinotoyk.jsr269.SetterProcessor com/hinotoyk/test/TestDemo.java -d classes/
#-cp 指定class路径,然后运行
java -cp classes com.hinotoyk.test.TestDemo
复制代码
可以清楚的看到,执行脚本之后可以正常的运行了,正确打印yk
6.打包(SPI机制)
SPI全称为(Service Provider Interface) ,是JDK内置的一种服务提供发现机制;主要被框架的开发人员使用,比如java.sql.Driver接口,数据库厂商实现此接口即可,当然要想让系统知道具体实现类的存在,还需要使用固定的存放规则,需要在resources下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类
打包的操作比较简单,java有一个SPI机制,包括JDBC,Spring,Dubbo都有大量使用。比如大家熟悉的例子,可以看Mysql连接的jar包,对应的目录下就有
我们在resources目录下创建一个META-INF文件夹,再在下面创建一个service文件夹,最后创建一个名为javax.annotation.processing.Processor的文件之后,在这个文件里面写上全限定名的实现类即可,多个实现类的话按enter分行填写就行了
└─resources
└─META-INF
└─services
└─javax.annotation.processing.Processor
复制代码
com.hinotoyk.jsr269.GetterProcessor
com.hinotoyk.jsr269.SetterProcessor
复制代码
打包前把测试用的那个类注释一下,否则编译会报错
打包:clean->complie->package->install
注解处理器SPI的具体实现可以自行查看源码,路径如下:
com.sun.tools.javac.main.JavaCompiler->compile()->this.initProcessAnnotations()->
this.procEnvImpl.setProcessors()->this.initProcessorIterator()->JavacProcessingEnvironment.ServiceIterator()
五、工程项目引用自定义Lombok
1.新建maven项目并引入依赖
<dependency>
<groupId>com.hinotoyk.jsr269</groupId>
<artifactId>jsr269Demo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
复制代码
2.创建并运行Demo
package com.hinotoyk.nichijou.Test;
import com.hinotoyk.jsr269.Getter;
import com.hinotoyk.jsr269.Setter;
@Getter
@Setter
public class Demo {
private String name;
public Demo(){
}
public static void main(String[] args) {
Demo a = new Demo();
a.setName("ykkkkkk");
System.out.println(a.getName());
}
}
复制代码
直接运行即可,结果如图
六、番外:AutoService的使用
AutoService的github,readme就有教程
1.删除MATE-INF文件夹、修改pom文件
删除我们上文中创建的MATE-INF文件夹
添加对应的依赖,以及修改一下maven的编译插件
<?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.hinotoyk.jsr269</groupId>
<artifactId>jsr269Demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>jsr269Demo</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service-annotations</artifactId>
<version>1.0-rc7</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0-rc7</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
复制代码
2.添加autoService注解
在SetterProcessor和getterProcessor类上都添加@AutoService(Processor.class)
3.打包
正常的clean->complie->package->install即可
我们看看编译输出,实际上也是通过注解处理器帮我们自动生成这么的一个文件
写在最后
这篇文章终于摸出来了,写这篇文章的时候了解到了许多知识点,还包括了以前没接触过编译方面的知识,查看了很多技术大佬的文章,也粗看了一遍《深入理解Java虚拟机》里面的相关篇章,收益匪浅。
以上。
参考资料
Lombok原理分析与功能实现
Java-JSR-269-插入式注解处理器
JVM系列六(自定义插入式注解器)
javac命令初窥
java注解处理器——在编译期修改语法树
《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》