Lombok中,打个注解就能自动生成Getter,Setter方法,Builder类;而且还能通过注解在JDK8中使用var、val关键字,这是如何实现的?
基础原理,就是通过JDK提供的Annotation Processor实现:自定义Processor,继承AbstractProcessor
,并在META-INF/services/javax.annotation.processing.Processor
中,指定自定义的Processor的全限定类名(也可直接使用google的AutoService工具)。这样打包后,其它程序引入此包打包时,在编译期,就会由SPI机制加载自定义的Processor,对程序进行语法处理。
由于Java9中引入了模块化,并修改了众多API,并将javac相关的部分内部API设为了不可见,需要额外进行配置。Java8中的代码生成方式不再适。因此Lombok的核心层,在Java8与Java9不尽相同。如下图:
对Lombok原理的解读与模仿实现的博客很多,不再拾人牙慧,本文简单介绍如何通过拓展Lombok的方式,享受Lombok项目对抽象语法树(AST)的基建,更加方便的拓展自定义注解。
本文所提到的代码已托管至github LombokExtensions
下载并在IDE中配置原生Lombok
参考文章:
Lombok Execution Path
Contributing to Project Lombok’s development
- 克隆Lombok官方git仓库 本博客基于1.18.33版本的Lombok
- 下载ant
Lombok使用ant构建,如果使用mac,可以直接使用brew install ant
。Linux可换成apt或yum等。Windows自行下载并配置环境变量。 - 根据你使用的IDE,在Lombok工程根目录下执行
ant eclipse
或ant intellij
- 使用IDE打开Lombok项目
- (可选)执行
ant dist
构建Lombok,构建完成后,项目根目录/dist/lombok-1.18.xx.jar
即我们平时通过Maven引入的Lombok的jar包。可cd到此目录下,使用jar -xf lombok-1.18.xx.jar
查看jar包内容,与平时通过Maven引入的Lombok jar包内容一致。 - (可选)新建一个项目,引入本地打包的lombok jar包
项目根目录下新建lib目录,将打包好的lombok jar包放进去,再在pom.xml里添加如下依赖即可。<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>system</scope> <systemPath>${project.basedir}/lib/lombok-1.18.xx.jar</systemPath> </dependency>
Lombok项目结构
Lombok定义了一套AnnocationProcessor,我们并不需要关注其AnnocationProcessor如何实现,需要明白的是,它会将打上自定义注解的AST节点 分派到对应到Handler上,自定义Annocation需要实现的就是这个Handler
拓展自定义注解及Handler
实现目标
- @Nullable 捕获长链调用中的NullPointerException,并返回null或基础类型的默认值
before:
// 获取学生的班级名称(可能引发NullPointerException的写法) String getStuClassName(String stuId) { String classId = stuService.findById(stuId).getClassId(); return classService.findById(classId).getName(); } // 获取学生的班级名称(不引发NullPointerException的写法) String getStuClassName(String stuId) { try { String classId = stuService.findById(stuId).getClassId(); return classService.findById(classId).getName(); } catch (NullPointerException e) { return null; } }
after:
非常遗憾的是,Java的Annocation,不支持Statement级别的注解,否则我们可以写成这样,更加直观,并且也不必在赋值时才能使用此注解。String getStuClassName(String stuId) { @Nullable String classId = stuService.findById(stuId).getClassId(); @Nullable String className = classService.findById(classId).getName(); return className; } // 或者也可以这样写 @Nullable String getStuClassName(String stuId) { String classId = stuService.findById(stuId).getClassId(); String className = classService.findById(classId).getName(); return className; }
String getStuClassName(String stuId) { String classId = @Nullable stuService.findById(stuId).getClassId(); return @Nullable classService.findById(classId).getName(); }
实现过程
首先明确,我们只实现Javac的编译,不对eclipse的编译时做修改。
要实现@Nullable
,需要对Java的Statement级别的AST进行修改,Javac的官方文档并未介绍AST相关的模块,Lombok也没有类似的“新手教程”文档。所以需要一个参考。
好在,Lombok中存在可参考的实现。
@PrintAST
(Lombok内部注解)可参考此注解的实现,方便在实现自定义注解时,打印AST,以Debug@Cleanup
可参考此注解的实现,明白如何替换、删除原本的语句,与在定义局部变量时,在AST中插入try-catch
新节点(JavacTreeMaker::Try)@EqualsAndHashCode
可参考此注解的实现,明白如何获取当前定义的局部变量是基本类型还是对象类型@SneakyThrows
可参考此注解的实现,明白如何在定义方法时,在AST中插入try-catch
新节点,并知道如何定义&引入一个错误类型。@Getter
可参考此注解的实现,了解如何定义一个赋值语句。(JavacTreeMaker::Assign)
package lombok.javac.handlers;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.*;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
import lombok.Lombok;
import lombok.Nullable;
import lombok.core.AnnotationValues;
import lombok.core.HandlerPriority;
import lombok.javac.*;
import lombok.spi.Provides;
import java.io.PrintStream;
import static lombok.javac.Javac.*;
import static lombok.javac.handlers.HandleDelegate.HANDLE_DELEGATE_PRIORITY;
import static lombok.javac.handlers.JavacHandlerUtil.*;
@Provides
@HandlerPriority(HANDLE_DELEGATE_PRIORITY + 99)
public class HandleNullable extends JavacAnnotationHandler<Nullable> {
private static boolean eq(String typeTreeToString, String key) {
return typeTreeToString.equals(key) || typeTreeToString.equals("lombok." + key) || typeTreeToString.equals("lombok.experimental." + key);
}
@Override
public void handle(final AnnotationValues<Nullable> annotation, final JCAnnotation ast, final JavacNode annotationNode) {
if (inNetbeansEditor(annotationNode)) return;
switch (annotationNode.up().getKind()) {
case METHOD:
handleMethodNullable(annotation, ast, annotationNode);
break;
case LOCAL:
handleLocalNullable(annotation, ast, annotationNode);
break;
default:
annotationNode.addError("@Nullable is only supported on method parameters and local variables.");
return;
}
}
private void handleMethodNullable(final AnnotationValues<Nullable> annotation, final JCAnnotation ast, final JavacNode annotationNode) {
// TODO 整个方法可空的处理
}
private void handleLocalNullable(final AnnotationValues<Nullable> annotation, final JCAnnotation ast, final JavacNode annotationNode) {
JCVariableDecl decl = (JCVariableDecl) annotationNode.up().get();
if (decl.init == null) {
annotationNode.addError("@Nullable variable declarations need to be initialized.");
return;
}
// 获取被Nullable修饰的语句的上级语句块
JavacNode ancestor = annotationNode.up().directUp();
JCTree blockNode = ancestor.get();
final List<JCStatement> statements;
if (blockNode instanceof JCBlock) {
statements = ((JCBlock) blockNode).stats;
} else if (blockNode instanceof JCCase) {
statements = ((JCCase) blockNode).stats;
} else if (blockNode instanceof JCMethodDecl) {
statements = ((JCMethodDecl) blockNode).body.stats;
} else {
annotationNode.addError("@Nullable is legal only on a local variable declaration inside a block.");
return;
}
// 获取被 @Nullable 修饰的语句前后的语句
boolean seenDeclaration = false;
ListBuffer<JCStatement> beforeStatements = new ListBuffer<JCStatement>();
ListBuffer<JCStatement> afterStatements = new ListBuffer<JCStatement>();
for (JCStatement statement : statements) {
if (statement == decl){
seenDeclaration = true;
continue;
}
if (!seenDeclaration) {
beforeStatements.append(statement);
} else {
afterStatements.append(statement);
}
}
JavacTreeMaker maker = annotationNode.getTreeMaker();
// 构建 try-catch 语句
ListBuffer<JCAnnotation> newAnnotations = new ListBuffer<JCAnnotation>();
for (JCAnnotation anno : decl.mods.annotations) {
if (anno.getAnnotationType().toString().endsWith("Nullable")) {
continue;
}
newAnnotations.append(anno);
}
decl.mods.annotations = newAnnotations.toList();
JCVariableDecl defDecl = maker.VarDef(decl.mods, decl.name, decl.vartype, null);
JCStatement tryAssign = maker.Exec(maker.Assign(maker.Ident(decl.name), decl.getInitializer()));
JCStatement catchAssign = maker.Exec(maker.Assign(maker.Ident(decl.name), createDefaultInitializer(defDecl, maker)));
JCVariableDecl exceptionDef = maker.VarDef(
maker.Modifiers(Flags.FINAL | Flags.PARAMETER),
annotationNode.toName("e"),
chainDots(annotationNode, "java.lang.NullPointerException".split("\\.")),
null
);
JCTry tryStatement = maker.Try(
maker.Block(0, List.of(tryAssign)),
List.of(maker.Catch(exceptionDef, maker.Block(0, List.of(catchAssign)))),
maker.Block(0, List.<JCStatement>nil())
);
// 构建新代码块
ListBuffer<JCStatement> newStatements = new ListBuffer<JCStatement>();
newStatements.appendList(beforeStatements);
newStatements.append(defDecl);
newStatements.append(tryStatement);
newStatements.appendList(afterStatements);
if (blockNode instanceof JCBlock) {
((JCBlock)blockNode).stats = newStatements.toList();
} else if (blockNode instanceof JCCase) {
((JCCase)blockNode).stats = newStatements.toList();
} else if (blockNode instanceof JCMethodDecl) {
((JCMethodDecl)blockNode).body.stats = newStatements.toList();
} else throw new AssertionError("Should not get here");
System.out.println(annotationNode.up().up().get());
ancestor.rebuild();
}
private JCExpression createDefaultInitializer(JCVariableDecl defDecl, JavacTreeMaker maker) {
if (defDecl.vartype instanceof JCPrimitiveTypeTree) {
switch (((JCPrimitiveTypeTree) defDecl.vartype).getPrimitiveTypeKind()) {
case BOOLEAN:
return maker.Literal(CTC_BOOLEAN, 0);
case CHAR:
return maker.Literal(CTC_CHAR, 0);
default:
case BYTE:
case SHORT:
case INT:
return maker.Literal(CTC_INT, 0);
case LONG:
return maker.Literal(CTC_LONG, 0L);
case FLOAT:
return maker.Literal(CTC_FLOAT, 0F);
case DOUBLE:
return maker.Literal(CTC_DOUBLE, 0D);
}
}
return maker.Literal(CTC_BOT, null);
}
private void printAST(final AnnotationValues<Nullable> annotation, final JCAnnotation ast, final JavacNode annotationNode) {
PrintStream stream = System.out;
try {
annotationNode.up().traverse(new JavacASTVisitor.Printer(true, stream));
} finally {
if (stream != System.out) {
try {
stream.close();
} catch (Exception e) {
throw Lombok.sneakyThrow(e);
}
}
}
}
}
重新编译并引入自己的项目中
在Lombok的项目中运行
ant dist` 编译- 在其它项目中引入编译后的jar包
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.33</version> <scope>system</scope> <systemPath>/Users/xxx/project/lombok/dist/lombok-1.18.33.jar</systemPath> </dependency>
- 使用自定义的@Nullable注解即可