之前写java代码,如果要增加一个拥有很多个属性的实体类时,每一个属性都需要写setter方法和getter方法,无疑是平白增加工作量,后来发现可以使用lombok库,通过在类前加注解省去这个费时的步骤。那这么好用的功能到底怎么实现的呢?同样使用场景很多的单例,是不是也可以用同样的原理实现呢?

lombok

简单使用


这里放一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 等级
*
* generated by tool
* @since: 2019-12-16
*/
@Setter
@Getter
@ToString
@NoArgsConstructor
public class JLevelInfo
{
/**
*
*/
long id;

/**
*
*/
long userId;

/**
*
*/
int type;

/**
*
*/
int value;

/**
*
*/
int level;
}

这样就可以不需要手动或自动去写一堆的getter,setter,或是构造函数。具体的用法不多啰嗦,可以随便找一篇文章, 或者进入官网查看。

实现原理


可以猜测,这个lombok其实是在写完代码,到运行字节码中间的某一个时机,坐了某一个编译器编译之外的工作,将字节码变成了增加了代码后的字节码。

通过一番搜索了解到:

jdk5时引用注解的同时提供了两种解析方式:

  • 运行时解析
  • 编译时解析

这里使用了编译时解析的JSP 269 Pluggable Annotation Processing API机制,javac 在执行时会调用实现了该api的程序,通过这个程序,我们就可以实现编译器之外的自定义功能。

lombok 本质上就是这样一个程序,它触发生效的具体流程为:

1.javac对源代码进行分析,生成了一棵抽象语法树(AST)
2.运行过程中调用实现了“JSR 269 API”的Lombok程序
3.此时Lombok就对第一步骤得到的AST进行处理,找到@Data注解所在类对应的语法树(AST),然后修改该语法树(AST),增加getter和setter方法定义的相应树节点
4.javac使用修改后的抽象语法树(AST)生成字节码文件,即给class增加新的节点(代码块)

实现单例注解

需要实现的目标


我们想要的注解处理后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RpcComponent
{
private ClassPathXmlApplicationContext context;

public static RpcComponent getInstance()
{
return RpcComponentHolder.instance;
}

private static class RpcComponentHolder
{
private static final RpcComponent instance = new RpcComponent();
}
}

在编辑器里实际写的代码:

1
2
3
4
5
6
@SingleInstance
public class RpcComponent
{
private ClassPathXmlApplicationContext context;
}

开始操作


HelloAnnotation 体验编译调用调试

创建注解类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.sictiy.processor.single;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* @author sictiy.xu
* @version 2019/10/11 15:17
**/
@Target(ElementType.TYPE) // 注解的使用范围,只能用于类
@Retention(RetentionPolicy.SOURCE) // 指该注解只存在源代码范围内,编译后不存在
public @interface HelloAnnotation
{
}

创建编译器处理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import com.google.auto.service.AutoService;

/**
* @author sictiy.xu
* @version 2020/03/09 11:11
**/
@SupportedAnnotationTypes("com.sictiy.processor.single.SingleInstance")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
@AutoService(Processor.class)
public class HelloProcessor extends AbstractProcessor
{
Messager messager;

@Override
public synchronized void init(ProcessingEnvironment processingEnv)
{
super.init(processingEnv);
this.messager = processingEnv.getMessager(); //编译的时候用于输出
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(HelloAnnotation.class);
set.forEach(element -> {
note("hello world");
});
return true;
}

//简单封装的函数
private void note(String message)
{
this.messager.printMessage(Diagnostic.Kind.NOTE, message);
}
}

auto-service 需要添加maven依赖:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0-rc4</version>
</dependency>
</dependencies>

使用maven编译打包 在jar里面可以看到这个文件:
processorJar

文件内容为:

1
com.sictiy.processor.single.HelloProcessor

创建另一个项目引用上面第一个项目,新建以下测试类:

1
2
3
4
5
6
7
8
9
10
11
import com.sictiy.processor.single.HelloAnnotation;

/**
* @author sictiy.xu
* @version 2020/03/09 10:52
**/
@HelloAnnotation
public class TestHello
{
}

在终端第二个项目下运行 mvnDebug clean install

mvnDebug

在idea中添加远程调试:

ideaRemote

在HelloProcessor中断点后启动调试就可以在idea中断到点了。

在HelloProcessor的基础上修改实现将空类处理成一个单例

生成单例主要有下面四个步骤:

1·获取注解对应的类的 AST 树
2.添加一个私有的无参数构造器
3.添加一个静态内联类,内联类里面要添加一个成员 instance 并完成初始化
4.添加一个成员函数,然后 instance

获取ast树

获取工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@SupportedAnnotationTypes("com.sictiy.processor.single.HelloAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
@AutoService(Processor.class)
public class HelloProcessor extends AbstractProcessor
{
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); //AST 树
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context); //封装了定义方法、变量、类等等的方法
this.names = Names.instance(context); //用于创建标识符
}

获取带注解的类的语法树:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
// 获取带注解的类的集合
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(HelloAnnotation.class);
set.forEach(element -> {
//获取到对应的 AST 树
JCTree jcTree = trees.getTree(element);
note("hello world");
});
return true;
}

这里有一个问题,在jdk11 版本中移除了bin下面的tools.jar工具包,不再需要手动添加tools.jar的依赖。但是如果直接使用tools包编译时会报错:Error:(20,27) java: 程序包 com.sun.tools.javac.api 不可见

解决方法是在pom.xml中加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<compilerArgs>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>

创建私有构造函数,移除原公共构造函数

创建一个私有构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 描述 ,代表方法是私有的
JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PRIVATE);
// 返回值
JCTree.JCBlock block = treeMaker.Block(0L, nil());
// 创建这个方法
JCTree.JCMethodDecl constructor = treeMaker
.MethodDef(
modifiers, //修饰符
names.fromString("<init>"), //函数名
null, //方法返回的类型
nil(), //泛型参数
nil(), //参数
nil(), //throw
block, //函数代码块,这里是空代码块
null); //默认值

判断方法是否为无参数,公共,且是构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 判断是否为构造函数
private static boolean isConstructor(JCTree.JCMethodDecl jcMethodDecl)
{
String name = jcMethodDecl.name.toString();
return "<init>".equals(name);
}

// 判读方法无参数
private static boolean isNoArgsMethod(JCTree.JCMethodDecl jcMethodDecl)
{
List<JCTree.JCVariableDecl> jcVariableDeclList = jcMethodDecl.getParameters();
return jcVariableDeclList == null
|| jcVariableDeclList.size() == 0;
}

// 判断方法是公共的
private boolean isPublicMethod(JCTree.JCMethodDecl jcMethodDecl)
{
JCTree.JCModifiers jcModifiers = jcMethodDecl.getModifiers();
Set<Modifier> modifiers = jcModifiers.getFlags();
return modifiers.contains(Modifier.PUBLIC);
}

移除原有的构造函数,添加新构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
//去掉默认的构造函数
ListBuffer<JCTree> out = new ListBuffer<>();
for (JCTree tree : singletonClass.defs)
{
if (isPublicDefaultConstructor(tree))
{//是否公有无参数的构造函数
continue;
}
out.add(tree);
}
// constructor 就是上面创建的私有构造方法
out.add(constructor);
singletonClass.defs = out.toList();
创建内联类

分为三步走:
1.创建内联类
2.创建类型为单例类型的私有静态变量
3.将静态变量添加到内联类,将内联类添加到单例类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private JCTree.JCClassDecl createInnerClass(JCTree.JCClassDecl jcClassDecl)
{
// 创建内联类
JCTree.JCClassDecl innerClass = treeMaker.ClassDef(
treeMaker.Modifiers(Flags.PRIVATE | Flags.STATIC),
names.fromString(jcClassDecl.name + "Holder"), //类名
nil(), //泛型参数
null, //extending
nil(), //implementing
nil() //类定义的详细语句,包括字段,方法定义等
);

// 给内联类添加一个私有静态的单例的属性 instance
// instance 的类型为原静态类
JCTree.JCIdent singletonClassType = treeMaker.Ident(jcClassDecl.name); //获取注解的类型
// instance 的赋值语句
JCTree.JCNewClass newKeyword = treeMaker.NewClass(null, //encl,enclosingExpression lambda 箭头吗?不太清楚
nil(), //参数类型列表
singletonClassType, //待创建对象的类型
nil(), //参数类型
null); //类定义
// instance 是私有,静态,final的
JCTree.JCModifiers fieldMod = treeMaker.Modifiers(Flags.PRIVATE | Flags.STATIC | Flags.FINAL);
// 定义这个instance变量
JCTree.JCVariableDecl instanceVar = treeMaker.VarDef(
fieldMod, //修饰符
names.fromString("instance"), //变量名
singletonClassType, //类型
newKeyword); //赋值语句
// 将instance 添加到内联函数中
innerClass.defs = innerClass.defs.prepend(instanceVar);

// 将内联类添加到原单例类中
jcClassDecl.defs = jcClassDecl.defs.append(innerClass);
return innerClass;
}
创建获取单例的getInstance方法

先看返回语句块怎么构建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private JCTree.JCBlock createReturnBlock(JCTree.JCClassDecl innerClass)
{
// 先拿到内联类的类型
JCTree.JCIdent holderInnerClassType = treeMaker.Ident(innerClass.name);
// 再拿到内联类中的静态变量 这个静态变量就是需要返回的值instance
JCTree.JCFieldAccess instanceVarAccess = treeMaker.Select(holderInnerClassType, names.fromString("instance"));
//创建 return 语句
JCTree.JCReturn returnValue = treeMaker.Return(instanceVarAccess);

ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
statements.append(returnValue);

return treeMaker.Block(0L, statements.toList());
}

有了return语句以后就直接构造这个get方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void createReturnInstance(JCTree.JCClassDecl jcClassDecl, JCTree.JCClassDecl innerClass)
{
// 方法是静态的公共的
JCTree.JCModifiers fieldMod = treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC);
// 返回类型 是 单例类的类型
JCTree.JCIdent singletonClassType = treeMaker.Ident(jcClassDecl.name);
// 构造return 语句块
JCTree.JCBlock body = createReturnBlock(innerClass);
//创建方法
JCTree.JCMethodDecl methodDec = treeMaker.MethodDef(fieldMod,
this.names.fromString("getInstance"),
singletonClassType, nil(), nil(), nil(), body, null);
// 将get方法添加的单例类中
jcClassDecl.defs = jcClassDecl.defs.prepend(methodDec);
}
将四个步骤组合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
var set = roundEnv.getElementsAnnotatedWith(HelloAnnotation.class);
set.forEach(element -> {
JCTree jcTree = trees.getTree(element);
jcTree.accept(new TreeTranslator()
{
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl)
{
// 修改构造方法
createPrivateConstructor(jcClassDecl);
// 添加内联类
JCTree.JCClassDecl innerClass = createInnerClass(jcClassDecl);
// 添加getInstance方法
createReturnInstance(jcClassDecl, innerClass);
}
});
});
return true;
}

结果

来个简单的示例来试试:

1
2
3
4
@HelloAnnotation
public class TestHello
{
}

编译后的.class 用idea打开:
class

附:以上支持单例注解的实现源代码地址:github

lombok idea 插件修改

到这里为止给类加上@SingleInstance注解,别的类就可以通过getInstance() 愉快地把他当单例使用了。但问题来了,编译后运行没是啥问题,但是编辑器不认啊,不仅一直先报错标红,而且方法调用的搜索,类Struct里面也找不到这个方法,自动补全什么的更是不可能有了。

所有这部分的任务就是想办法让idea也能知道加注解的意思就是多了个getInstance()方法。这个功能显然需要使用idea插件开发来实现,之前在这里简单了解过一次。这里可以选择重头写一个,想一想这个工作量,emmm… 还好lombok是开源的,在别人的基础上改一改总比重头来简单。

… 未完待续