【学习总结】自定义lombok注解
目录:
说明
业务开发中我们项目常用lombok来简化JavaBean的一些样板代码,虽然大家对lombok的褒贬不一,我认为只要公司约定好一些基础的依赖库,就没有多大的问题,适当取舍;不是工具不好用,而是要有合适的使用方法。最近,想要学习一下lombok中对于注解在编译时期就能生效的原理,通常我们都是使用Spring的AOP编程定义一些运行时注解,对于编译时注解还是知之甚少,所以想要通过这样一个机会来了解下lombok的注解原理,在以后的开发中可以适当使用,内部维护lombok版本。
使用场景
公司内部业务有太多的样板代码,每次开发都得去做重复性的处理,这个时候自定义lombok注解就会简化一些重复性的劳动。
原理说明
概述
官网对他的功能的描述:ProjectLombok是一个java库,可以自动插入编辑器和构建工具,为您的java增添色彩。
永远不要再编写另一个getter或equals方法,只要有一个注释,您的类就有一个功能齐全的构建器,自动化您的日志变量,等等。
根据官网的解释我们可以把lombok定位为一个构建工具插件。
编译期优化
概述
Java语言的”编译期“其实是一段”不确定“的操作过程,因为他可能是指一个前端编译器(其实叫”编译器的前端“更准确一些)把 *.java文件转变成 *.class文件;也可能指虚拟机的后端运行期编译器(JIT编译器,Just In Time Compiler)把字节码转变成机器码的过程;还可能是指使用静态提前编译器(AOT编译器,Ahead Of Time Compiler)直接把 *.java文件编译成本地机器代码的过程。下面列举了3类编译过程中比较有代表性的编译器。
(1)前端编译器:Sun的Javac、Eclipse JDT 中的增量式编译器(ECJ)。
(2)JIT编译器:HotSpot VM的C1、C2编译器。
(3)AOT编译器:GNU Compiler for the Java(GCJ)、Excelsior JET。
—— 节选自《深入理解Java虚拟机(第二版)》
我们常用的就是Javac编译器,我们也是基于这个编译器做的自定义注解。
Javac编译器
Javac编译器不像HotSpot虚拟机那样使用C/C++语言实现,它本身是用Java语言实现的,这为纯Java开发程序员提供了便利。编译过程大致分为3个过程:
(1)解析与填充符号表过程。
(2)插入式注解处理器的注解处理过程。
(3)分析与字节码生成过程。
这三个过程如图所示。
我们利用的就是注解处理器。
注解处理器
在JDK1.5之后,Java语言提供了对注解(Annotation)的支持,这些注解与普通代码一样在运行时执行。在JDK1.6中实现了JSR-269规范,提供了一组插入式注解处理器的标准API在编译期间对注解进行处理,我们把他们看做是编译器插件,在这些插件里,可以读取、修改、添加抽象语法树种的任意元素。有了编译器注解处理的标准API后,我们的代码才有可能干涉编译器的行为,由于语法树的任意元素,甚至包括注释代码都可以在插件中访问到,所以通过插入式注解处理器实现的插件在功能上有很大的发挥空间。
Java注解
以@interface定义声明的Java类型,一般会修饰@Target和@Retention注解指定目标和保留策略,目标只该注解修饰的Java语法结构,分为。
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
TYPE,
/** Field declaration (includes enum constants) */
FIELD,
/** Method declaration */
METHOD,
/** Formal parameter declaration */
PARAMETER,
/** Constructor declaration */
CONSTRUCTOR,
/** Local variable declaration */
LOCAL_VARIABLE,
/** Annotation type declaration */
ANNOTATION_TYPE,
/** Package declaration */
PACKAGE,
/**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE
}
保留策略分为。
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
- SOURCE:表示注解的信息会被编译器抛弃,不会留在 class 文件中,注解的信息只会留在源文件中。
- CLASS:表示注解的信息被保留在 class 文件中。当程序编译时,但不会被虚拟机读取在运行的时候。
- RUNTIME:表示注解的信息被保留在 class 文件中。当程序编译时,会被虚拟机保留在运行时。
Lombok自定义注解
1.下载lombok源码
GitHub地址:https://github.com/rzwitserloot/lombok.git
Gitee镜像:https://gitee.com/mirrors_projectlombok/lombok_1.git
2.编译环境
需要JDK9以上版本编译(这里下载的JDK11),配置IDEAjava运行环境
3.源码解读
以@Setter注解为例(定义在src/core/lombok下),在src/core/lombok/javac下定义了HandleSetter类继承抽象类JavacAnnotationHandler,这个是lombok自定以的中间注解处理器,且HandleSetter用注解@Provides 修饰(这个在我们另一篇文章中分享过),会写入SPI文件;
在使用 javac 编译器时(netbeans,maven,gradle),Lombok 会以 annotation processor 方式运行。 Javac 会以 SPI 方式加载所有 jar 包中 META-INF/services/javax.annotation.processing.Processor
文件所列举的类,并以 annotation processor 的方式运行它。对于 Lombok,这个类是 lombok.launch.AnnotationProcessorHider$AnnotationProcessor
,当它被 javac 加载创建后,会执行 init
方法,在这个方法中会启动一个特殊的类加载器 ShadowClassLoader
,加载同 jar 包下所有以 .SCL.lombok
结尾的类(Lombok 为了对 IDE 隐藏这些类,所以不是通常地以 .class 结尾)。其中就包含各式各样的 handler
。
—引自https://blog.csdn.net/weixin_30896825/article/details/101424667?spm=1001.2014.3001.5506
4.定义一个注解
定义注解后可以自定义一个handle,实现handle方法。
5.编译自己的lombok注解
由于clone的lombok项目突然不能编译了,就没有具体操作验证。
Javac自定义注解
注解所在的项目不能与引用注解的项目在一起,因为引用注解需要依赖注解解释器的jar包文件,我们这里直接分开了,应该用maven分子项目也可以实现,这里没有验证。
1.搭建的Maven项目
pom.xml文件如下,
<?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.jsyn</groupId>
<artifactId>custom-processor</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.15</version>
</dependency>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
<!-- 引用的google的APT自动配置库,用了好几种其他方式都没有成功-->
<!--APT自动添加配置库-->
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service-annotations</artifactId>
<version>1.0.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
2.创建注解
主要想要在一个类中插入成员方法,并返回 Hello注解中的name的值
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Hello {
String name() default "World";
}
3.创建解释器
解释器中使用了sun.java的编译器的相关方法,pom中必须引入sun的java工具包tools,其中还涉及一些APT的语法,现在只是简单了解,以后有机会会学习一下其他的用法。
@SupportedAnnotationTypes("com.jsyn.annotation.Hello")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
// google的APT处理注解,用于生成META-INF/services中的javax.annotation.processing.Processor接口实现类
@AutoService(Processor.class)
public class HelloProcessor extends AbstractProcessor {
/*
* 用来在编译期打log用的
*/
private Messager messager;
/**
* 提供了待处理的抽象语法树
*/
private JavacTrees trees;
/**
* 封装了创建AST节点的一些方法
*/
private TreeMaker treeMaker;
/**
* 提供了创建标识符的方法
*/
private Names names;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
System.err.println("-- Start init custom annotation processor --");
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) {
System.err.println("-- Start run custom annotation processor --");
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Hello.class);
if (CollectionUtil.isNotEmpty(elements)) {
for (Element element : elements) {
JCTree jcTree = trees.getTree(element);
Hello annotation = element.getAnnotation(Hello.class);
System.err.println("annotation value : " + annotation.name());
jcTree.accept(new TreeTranslator() {
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(annotation));
super.visitClassDef(jcClassDecl);
}
});
}
}
return true;
}
/**
* 为成员遍历构造 getter 方法
*/
private JCTree.JCMethodDecl makeGetterMethodDecl(Hello annotation) {
System.err.println("annotation value method decl : " + annotation.name());
// 构造方法体
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
// 生成表达式 return name;
statements.append(treeMaker.Return(treeMaker.Literal(annotation.name())));
// 加上大括号 { return name; }
JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
// 构造方法
JCTree.JCMethodDecl jcMethodDecl = treeMaker.MethodDef(
// 访问标志
treeMaker.Modifiers(Flags.PUBLIC),
// 方法名
names.fromString("hello"),
// 返回类型
treeMaker.Ident(names.fromString("String")),
// 泛型形参列表
List.nil(),
// 参数列表
List.nil(),
// 异常列表
List.nil(),
// 方法体
body,
// 默认方法
null
);
// 组装方法
return jcMethodDecl;
}
}
4.引用自定义注解
在另一项目或者包中引用上述注解项目生成的jar包文件,具体引入如下pom.xml;
<dependency>
<groupId>com.jsyn</groupId>
<artifactId>custom-processor</artifactId>
<version>1.0</version>
</dependency>
5.注解使用
虽然新的方法引用会在idea中爆红,但是不影响执行,还需要重写IDE的注解解释器
@Hello(name = "hello world")
public class TestLombokSpi {
public static void main(String[] args) {
TestLombokSpi testLombokSpi = new TestLombokSpi();
System.out.println(testLombokSpi.hello());
}
}
总结
在工作中会遇到一些很方便的组件,给我们的开发省了不少事;很清楚的是,这是前人栽树后人乘凉的工作,在这里简单记录下自己看lombok源码然后搜索各种帖子实现javac的注解,踩了很多坑,这里跟大家分享下。