我们都用过Lombok 可以快速的进行实体类的setter/getter/toString/hash/construct等等方法的自动编编译字节码生成。但我们只加入了一个注解
@Data
或@Setter
是怎么生成对应方法的字节码的呢?下面听我慢慢分析。
参考源码: 源码
JAVAC 编译过程
从SunJavac的代码来看,编译过程大致可以分为三个过程,分别是:
·解析与填充符号表过程。
·插入式注解处理器的注解处理过程。
·分析与字节码生成过程。
Javac 编译过程如下:
从 Javac 代码的总体结构来看,编译过程大致可以分为 1 个准备过程和 3 个处理过程,它们分别如下所示。
- 准备过程:初始化插入式注解处理器。
- 解析与填充符号表过程,包括:词法、语法分析;填充符号表。
- 插入式注解处理器的注解处理过程。
- 分析与字节码生成过程。
JDK 5 之后,Java 语言提供了对注解(Annotations)的支持,注解在设计上原本是与普通的 Java 代码一样,都只会在程序运行期间发挥作用的。但在 JDK 6 中又提出并通过了 JSR-269 提案,该提案设计了一组被称为“插入式注解处理器”的标准 API,可以提前至编译期对代码中的特定注解进行处理,从而影响到前端编译器的工作过程。我们可以把插入式注解处理器看作是一组编译器的插件,当这些插件工作时,允许读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行过修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止。
Lombok就是通过插入式注解来实现的:
JDK 5 之后,Java 语言提供了对注解(Annotations)的支持,注解在设计上原本是与普通的 Java 代码一样,都只会在程序运行期间发挥作用的。但在 JDK 6 中又提出并通过了 JSR-269 提案,该提案设计了一组被称为“插入式注解处理器”的标准 API,可以提前至编译期对代码中的特定注解进行处理,从而影响到前端编译器的工作过程。我们可以把插入式注解处理器看作是一组编译器的插件,当这些插件工作时,允许读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行过修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止。
实现案例
通过上述的原理知识,我们来实际操作一把!模仿Lombok的@Slf4j 自动在编译的字节码中生成Logger log = LogFactory.getLogger(“xxxxx”);在代码中我们可以直接使用log.info/log.error/…去打印日志。
最终的代码效果如下:
@AllensLog
//@Singleton
public class Test {
public static void main(String[] args) {
log.info("xxxxxxxxx");
log.error("xxxxxxxxxerrorxxxxxxxxxxx");
}
}
1.定义注解
class:AllensLog
在任何的实现类中加入此注解编译过后会生成Logger log = LogFactory.getLogger("xxxxx");
@Target(ElementType.TYPE)
@Documented
@Retention(RetentionPolicy.SOURCE)
public @interface AllensLog {
String value() default "";
}
@Target(ElementType.TYPE)
指定注解可以在类中的哪个位置使用。ElementType.TYPE
表示可以在类上使用此注解,ElementType.FIELD
表示可以在属性上使用次注解,ElementType.METHOD表示可以在方法上使用次注解等等点进源码看下就知道了。我们这里注解明显要放在类上,让整个类拥有使用log对象记录日志的能力。@Documented
标记文档,不用管@Retention(RetentionPolicy.SOURCE)
这个标识表示注解在编译器生效
2.AbstractProcessor
自定义的注解处理器需要继承 AbstractProcessor 这个类,基本的框架大体如下:
package com.allens.netty;
import com.google.auto.service.AutoService;
import com.sun.tools.javac.api.JavacTrees;
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.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.util.Set;
@SupportedAnnotationTypes("com.allens.netty.AllensLog")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(LogProcess.class)
public class LogProcess extends AbstractProcessor {
//private static final Logger log = LoggerFactory.getLogger("MercuryoaLogProcess");
private TreeMaker treeMaker;
private JavacTrees trees;
private Names names;
public LogProcess() {
}
public synchronized void init (ProcessingEnvironment processingEnv) {
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.trees = JavacTrees.instance(processingEnv);
this.names = Names.instance(context);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(AllensLog.class);
set.forEach(element -> {
final JCTree jcTree = this.trees.getTree(element);
jcTree.accept(new TreeTranslator() { // 访问者访问JCTree上类定义的JCClassDecl
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
if (jcTree.getPreferredPosition() == jcClassDecl.pos) {
jcClassDecl.defs = jcClassDecl.defs.prepend(makeLoggerInstanceDecl(jcClassDecl, element));
}
super.visitClassDef(jcClassDecl);
}
});
});
return false;
}
private JCTree.JCExpression memberAccess (String components) {
String[] componentsArr = components.split("\\.");
Name name = this.getNameFromString(componentsArr[0]);
JCTree.JCExpression expression = this.treeMaker.Ident(name);
for (int i = 1; i < componentsArr.length; i ++) {
expression = this.treeMaker.Select((JCTree.JCExpression) expression,
this.getNameFromString(componentsArr[i]));
}
return (JCTree.JCExpression) expression;
}
private Name getNameFromString (String str) {
return this.names.fromString(str);
}
private JCTree.JCVariableDecl makeLoggerInstanceDecl (JCTree.JCClassDecl jcClassDecl, Element element) {
JCTree.JCExpressionStatement var = this.treeMaker.Exec(this.treeMaker.Apply(
List.of(
this.memberAccess("java.lang.String"),
this.memberAccess("java.lang.Class")),
this.memberAccess("org.slf4j.LoggerFactory.getLogger"),
List.of(this.memberAccess(jcClassDecl.getSimpleName().toString() + ".class")))
);
// 定义静态变量 log
return this.treeMaker.VarDef(this.treeMaker.Modifiers(26L),
this.names.fromString("log"), // 指定静态变量名称
this.memberAccess("org.slf4j.Logger"), // 指定Logger 类型,这里是slf4j
var.getExpression() // 表达式赋值给静态变量
);
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
@SupportedAnnotationTypes
代表了这个注解处理器对哪些注解感兴趣,可以使用星号*作为通配符代表对所有的注解都感兴趣。
@SupportedSourceVersion
指出这个注解处理器可以处理哪些版本的 Java 代码。
init() 用于获取编译阶段的一些环境信息。
process() 可以编写处理语法树的具体逻辑。如果不需要改变或添加抽象语法树中的内容,process() 方法就可以返回一个值为 false 的布尔值,通知编译器这个轮次中的代码未发生变化。
3.APT (Annotation Processing Tool)
利用插入式注解处理器在编译阶段修改语法树,需要用到 Javac 中的注解处理工具 APT(Annotation Processing Tool),这是 Sun 为了帮助注解的处理过程而提供的工具,APT 被设计为操作 Java 源文件,而不是编译后的类。
本文使用的是 JDK 8,Javac 相关的源码存放在 tools.jar 中,要在程序中使用的话就必须把这个库放到类路径上。注意,到了 JDK 9 时,整个 JDK 所有的 Java 类库都采用模块化进行重构划分,Javac 编译器就被挪到了 jdk.compiler 模块,并且对该模块的访问进行了严格的限制。
4. JCTree 语法树
2.5 JCTree 语法树
com.sun.tools.javac.tree.JCTree
是语法树元素的基类,包含以下重要的子类:
JCStatement
:声明语法树节点,常见的子类如下
JCBlock
:语句块语法树节点
JCReturn:return
语句语法树节点
JCClassDecl
:类定义语法树节点
JCVariableDecl
:字段/变量定义语法树节点JCMethodDecl
:方法定义语法树节点JCModifiers
:访问标志语法树节点JCExpression
:表达式语法树节点,常见的子类如下
JCAssign
:赋值语句语法树节点
JCIdent
:标识符语法树节点,可以是变量,类型,关键字等
JCAssign
:赋值语句语法树节点
JCIdent
:标识符语法树节点,可以是变量,类型,关键字等
JCTree
利用的是访问者模式,将数据与数据的处理进行解耦。部分源码如下:
public abstract class JCTree implements Tree, Cloneable, DiagnosticPosition {
public int pos = -1;
public abstract void accept(JCTree.Visitor visitor);
}
利用访问者 TreeTranslator
,可以访问 JCTree
上的类定义节点 JCClassDecl
,进而可以获取类中的成员变量、方法等节点并进行修改。
编码过程中,可以利用 javax.annotation.processing.Messager
来打印编译过程的相关信息。
注意,Messager
的 printMessage
方法在打印 log 的时候会自动过滤重复的 log 信息。
比起打印日志,利用 IDEA 工具对编译过程进行 debug,对 JCTree 语法树会有更为直观的认识。
文末提供了在 IDEA 中调试插入式注解处理器的配置。
Maven 配置
由于需要在编译阶段修改 Java 语法树,需要调用语法树相关的 API,因此将 JDK 目录下的 tools.jar 引入当前项目。
<dependency>
<groupId>com.perfma.wrapped</groupId>
<artifactId>com.sun.tools</artifactId>
<version>1.8.0_jdk8u275-b01_linux_x64</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0.1</version>
</dependency>
lombok-processor 项目采用 Java SPI 机制,使其自定义的插入式注解处理器对 lombok-app 项目生效。由于 lombok-processor 项目在编译期间需要排除掉自身的插入式注解处理器,因此配置 maven resource 以过滤掉 SPI 文件,等到打包的时候,再将 SPI 文件加入 lombok-processor 项目的 jar 包中。
mavn build插件配置
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>META-INF/**/*</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>process-META</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>target/classes</outputDirectory>
<resources>
<resource>
<directory>${basedir}/src/main/resources/</directory>
<includes>
<include>**/*</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
JAVA SPI
下面简单说下JAVA SPI的定义:
什么是SPI
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。
SPI和API的使用场景
API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。简而言之就是加载自己如果有多个外部实现,使用SPI加载自己需要的那个外部实现即可,类似阿里的DUBBO SPI。
这里不做过多介绍,如果感兴趣清查看Java SPI详解
最后在resources文件夹中加入META-INF.servicesjavax.annotation.processing.Processor
文件,在文件内添加:
com.allens.netty.LogProcess
最终目录结构如下:
最终结果
测试类:
@AllensLog
//@Singleton
public class Test {
public static void main(String[] args) {
log.info("xxxxxxxxx");
log.error("xxxxxxxxxerrorxxxxxxxxxxx");
}
}
我们可以看到Test.java 编译过后,在代码中已经生成了 private static final Logger log = LoggerFactory.getLogger(Test.class);
由于idea识别不了语法树导致Idea界面上是报错的,但不影响运行。
运行结果:
我找了很多的文档也没找到解决办法,lombok之所以不报错是因为idea安装了lombok插件之后可以解析到AnnotaionProcesser解析过后的语法树,自然能够识别lombok语法。有两种解决办法:
① 我们自己写个插件让idea识别我们的注解
② 下载lombok源码,增加解析的规则,打包修改过后lombok,idea安装之后就可以识别了。
当然这两种方法我只试过第二种,我后续也会深入研究下。请关注我,后续我会发出我适配lombok的教程。
参考文章:
Java SPI详解
Lombok 原理与实现