之前写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
|
@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;
@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;
@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里面可以看到这个文件:

文件内容为:
1
| com.sictiy.processor.single.HelloProcessor
|
创建另一个项目引用上面第一个项目,新建以下测试类:
1 2 3 4 5 6 7 8 9 10 11
| import com.sictiy.processor.single.HelloAnnotation;
@HelloAnnotation public class TestHello { }
|
在终端第二个项目下运行 mvnDebug clean install

在idea中添加远程调试:

在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); 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 -> { 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(), 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); }
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, nil(), nil() );
JCTree.JCIdent singletonClassType = treeMaker.Ident(jcClassDecl.name); JCTree.JCNewClass newKeyword = treeMaker.NewClass(null, nil(), singletonClassType, nil(), null); JCTree.JCModifiers fieldMod = treeMaker.Modifiers(Flags.PRIVATE | Flags.STATIC | Flags.FINAL); JCTree.JCVariableDecl instanceVar = treeMaker.VarDef( fieldMod, names.fromString("instance"), singletonClassType, newKeyword); 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); JCTree.JCFieldAccess instanceVarAccess = treeMaker.Select(holderInnerClassType, names.fromString("instance")); 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); JCTree.JCBlock body = createReturnBlock(innerClass); JCTree.JCMethodDecl methodDec = treeMaker.MethodDef(fieldMod, this.names.fromString("getInstance"), singletonClassType, nil(), nil(), nil(), body, null); 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); createReturnInstance(jcClassDecl, innerClass); } }); }); return true; }
|
结果
来个简单的示例来试试:
1 2 3 4
| @HelloAnnotation public class TestHello { }
|
编译后的.class 用idea打开:

附:以上支持单例注解的实现源代码地址:github
lombok idea 插件修改
到这里为止给类加上@SingleInstance注解,别的类就可以通过getInstance() 愉快地把他当单例使用了。但问题来了,编译后运行没是啥问题,但是编辑器不认啊,不仅一直先报错标红,而且方法调用的搜索,类Struct里面也找不到这个方法,自动补全什么的更是不可能有了。
所有这部分的任务就是想办法让idea也能知道加注解的意思就是多了个getInstance()方法。这个功能显然需要使用idea插件开发来实现,之前在这里简单了解过一次。这里可以选择重头写一个,想一想这个工作量,emmm… 还好lombok是开源的,在别人的基础上改一改总比重头来简单。
… 未完待续