我们都用过Lombok 可以快速的进行实体类的setter/getter/toString/hash/construct等等方法的自动编编译字节码生成。但我们只加入了一个注解
@Data
或@Setter
是怎么生成对应方法的字节码的呢?下面听我慢慢分析。
参考源码: 源码
从SunJavac的代码来看,编译过程大致可以分为三个过程,分别是:
·解析与填充符号表过程。
·插入式注解处理器的注解处理过程。
·分析与字节码生成过程。
从 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");
}
}
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)
这个标识表示注解在编译器生效自定义的注解处理器需要继承 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 的布尔值,通知编译器这个轮次中的代码未发生变化。
利用插入式注解处理器在编译阶段修改语法树,需要用到 Javac 中的注解处理工具 APT(Annotation Processing Tool),这是 Sun 为了帮助注解的处理过程而提供的工具,APT 被设计为操作 Java 源文件,而不是编译后的类。
本文使用的是 JDK 8,Javac 相关的源码存放在 tools.jar 中,要在程序中使用的话就必须把这个库放到类路径上。注意,到了 JDK 9 时,整个 JDK 所有的 Java 类库都采用模块化进行重构划分,Javac 编译器就被挪到了 jdk.compiler 模块,并且对该模块的访问进行了严格的限制。
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 中调试插入式注解处理器的配置。
由于需要在编译阶段修改 Java 语法树,需要调用语法树相关的 API,因此将 JDK 目录下的 tools.jar 引入当前项目。
<dependency>
<groupId>com.perfma.wrappedgroupId>
<artifactId>com.sun.toolsartifactId>
<version>1.8.0_jdk8u275-b01_linux_x64version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
dependency>
<dependency>
<groupId>com.google.auto.servicegroupId>
<artifactId>auto-serviceartifactId>
<version>1.0.1version>
dependency>
lombok-processor 项目采用 Java SPI 机制,使其自定义的插入式注解处理器对 lombok-app 项目生效。由于 lombok-processor 项目在编译期间需要排除掉自身的插入式注解处理器,因此配置 maven resource 以过滤掉 SPI 文件,等到打包的时候,再将 SPI 文件加入 lombok-processor 项目的 jar 包中。
mavn build插件配置
<build>
<resources>
<resource>
<directory>src/main/resourcesdirectory>
<excludes>
<exclude>META-INF/**/*exclude>
excludes>
resource>
resources>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-resources-pluginartifactId>
<version>2.6version>
<executions>
<execution>
<id>process-METAid>
<phase>prepare-packagephase>
<goals>
<goal>copy-resourcesgoal>
goals>
<configuration>
<outputDirectory>target/classesoutputDirectory>
<resources>
<resource>
<directory>${basedir}/src/main/resources/directory>
<includes>
<include>**/*include>
includes>
resource>
resources>
configuration>
execution>
executions>
plugin>
plugins>
build>
下面简单说下JAVA SPI的定义:
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 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 原理与实现