从实践 APT 到深入理解 Lombok

13 篇文章 0 订阅
3 篇文章 0 订阅

一、概述

  博客内所有文章均为 原创,所有示意图均为 原创,若转载请附原文链接。

1.1 起因

  在开始正文的分析之前我想先大概谈一下自己为什么要写这篇博文。首先是因为自己正在学习 Java 虚拟机比较底层的一些东西,然后也是在对虚拟机的各个部分进行探索,在这个探索的过程中我接触到了 APT(Annotation Processing Tool),也就是我们通常意义上的 注解处理器 ,对于 Java 中的注解大家一定都不会感到陌生,相信大家在准备面试的时候也背过很多关于注解的作用。

  在做开发的过程中我们最常使用到的比如 @Override 这样提供给编译器进行规范检查的标记注解,还有一种就是在 Spring 开发中经常用到的比如 @Bean 或 @Import 等等这样提供给框架自身使用的功能性注解(对于 Spring 当中注解的源码分析可以回看我之前的博文)。但是我想你也应该接触过一款叫做 Lombok 的插件,对于这款插件的功能我就不赘述了,网上的介绍一大堆,但是你有没有思考过 Lombok 的底层到底是怎么实现的。

  当我在学习 APT 的过程中通过资料的阅读发现了 Lombok 的底层也是使用 APT 来实现的,但是对于具体是怎么实现的,为什么要这么实现,以及这么实现有什么优点和缺点等等一些列问题我查询了很多的中文资料都是一无所获,当我们在搜索 Lombok 的时候得到的结果更多的重复的 API 罗列,把官网中的话反复地复制,相同的 Demo 展示,甚至还有一些博文直接凭自己的臆想来推测 Lombok 底层实现,却不并没有给出任何有力的证明(显然这种臆想是错的)。

  因此,我希望通过自己对资料的收集整理,并在 源码求证代码验证 的基础上对这个问题给出一个相对比较正确的答案。所以,本篇博文我们会从下面的提出的几个疑问入手来分析 Lombok 底层和 Java APT 的相关原理。如果,你是一个小萌新或者仅仅想了解一下 Lombok 的使用方法,那这篇博文可能不太适合你。

1.2 疑问

  • Lombok 中的注解和 Spring 中的注解有什么区别?
  • Lombok 到底是生成新的 Java 源文件还是修改已有的 Java 源文件?
  • 如果是修改已有的 Java 源文件那 Lombok 又是怎么实现的呢?
  • 如何从源码的角度来理解 Lombok 的实现方式?
  • 为什么会有很多人反对使用 Lombok ?
  • 我们是否可以利用 Lombok 提供的 API 来实现自己需要的功能?

二、前提

2.1 Java 编译器的工作流程

pic

  对于这个 Java 编译器的工作流程网上的博文数不胜数,且其中都引用到了这张著名的示意图,所以我这里再把它放上来,因为网上相关的博文已经很多了,所以我不做赘述,这里就直接引用官网的总结:

  • Parse and Enter: 所有在命令行中指定的源文件都被读取,解析成语法树,然后所有外部可见的定义都被输入到编译器的符号表中。
  • Annotation Processing: 调用所有适当的注释处理器。如果任何注释处理程序生成任何新的源文件或类文件,则重新开始编译,直到没有创建任何新文件为止。
  • Analyse and Generate: 最后,解析器创建的语法树将被分析并转换为类文件。在分析过程中,可能会发现对其他类的引用。编译器将检查这些类的源和类路径,如果在源路径上找到它们,也会编译这些文件,尽管它们不需要进行注释处理。

2.2 关于 Lombok

2.3 关于 Javac 的源码


三、求证

3.1 Lombok 中的注解和 Spring 中的注解有什么区别?

  这个问题其实是比较有趣的一个问题,当我第一次近距离接触注解的时候还是在准备面试的过程中,当时知道注解根据运行机制可以分为三类即源码注解、编译时注解和运行时注解,但是当时并不是很理解这样划分的作用,以及不同运行机制对注解产生的影响。后来在阅读过 Spring 的源码后,发现其实 Spring 当中的大多数注解都是 运行时注解(RUNTIME) ,比如常用的 @Autowire 等都是在编译完成后且程序启动时或者运行过程中发挥作用,Spring 可以通过反射动态的获取到每个字段或方法注解,并对被不同注解注释的采用不同的处理方式,所以这里的注解其实更像是一种标志和约定,能够让系统的动态的识别出它所需要的元素。

  另一种注解就是 编译时注解(CLASS) ,这种注解一般都会存在于源码和字节码文件中,比如 @Override、@Deprecated 和 @SuppressWarnings ,这种类型的注解都是仅在编译中发挥作用,而在类加载的时候就会被丢弃,可以理解为仅对编译器有效,用来进行一些语法规范的检查。

  还有一种是我们平时很少接触到的 源码注解(SOURCE),这种注解一般仅存在于源文件中,当文件被编译为字节码时就会被丢弃,通过这种类型的注解我们可以 定义新的编译规则 ,并检查被编译的源文件、可以修改已有源代码,还可以生成新的源代码。而我们所说的 Lombok 使用的就是这种类型注解,也就是说 Lombok 实际的作用时间是在源码编译为字节码的过程中。

  所以概括来说,Spring 中的注解和 Lombok 中的注解最大的区别之处就在于运行机制的不同,且作用也是大不相同,Spring 中的注解主要是起一个标注性的作用,而 Lombok 中的注解更多的是对 Java 编译器的工作流程进行干预。

3.2 Lombok 到底是生成新的 Java 源文件还是修改已有的 Java 源文件?

  这个也是一个很有趣的问题,关于这个问题的起因还要从我接触了 APT 后开始尝试自定义 APT 讲起,我当时已经实现了一个比较简单,用于检查类中字段是否都存在 getter 方法的检测注解,在这之后我联想到了之前用过的 Lombok 插件,因为这个插件的功能也是基于注解对类进行一些字段和方法的检测和代码插入,所以我就想尝试着自己去实现一个低配版的 Lombok 插件。

  但是在开始的时候我就遇到了问题,因为我发现当我们把 @Data 注解在类上的时候,我们虽然可以获取到该类中的字段和方法,也可以通过 PrintWriter 把需要生成的代码写到新的文件中,但是我们好像没有办法将需要添加的代码直接插入到当前 Java 源文件中。这个时候我就比较困惑了,如果我们不能够将新生成的 getter 和 setter 等方法直接写入到源 Java 文件,那么我们就只能将当前 Java 原文件中的代码先全部写到另一个新文件中,然后再在新的文件中添加我们需要插入的代码,但其实这也是不可行的,因为对于每个方法你没办法获取到源 Java 文件中该方法的方法体,那这是不是意味着我们就穷途末路了。

  而当我去网上查看别人的实现思路时,发现他们的实现思路仅仅是将当前被 @Data 注释的类中的字段提取出来,先生成一个新的 Java 文件,然后根据刚刚提取到的字段信息,将字段以及其 getter 和 setter 一同写入到新文件中,然后就完事大吉。看完这种实现方式之后的我是黑人问号,这么做难道是源 java 文件中的方法全都舍弃不要了么。因为自己之前也没有做过特殊的测试,所以特意用 Lombok 测试了一下,测试的结果很清晰即 Lombok 的 @Data 注解标注的类经 Lombok 的 processor 处理后其源 Java 文件中的方法仍然是可被调用的。

  因此通过上面的测试,我们可以首先明确的一点是 Lombok 并不是通过生成新的 Java 源文件来完成 getter 和 setter 等方法的插入的。那 Lombok 底层的实现到底是怎样的呢,我又继续开始了探索。

3.3 如果是修改已有的 Java 源文件那 Lombok 又是怎么实现的呢(源码解析)?

3.3.1 概述

  首先,通过上面的分析我们可以大致确定 Lombok 底层的实现应该是通过修改已有的 Java 源文件(准确来说修改是 AST)来完成的,那他到底是怎么实现的呢,好奇心驱使我从 GitHub 上找到了 Lombok 的源码,并对比较基础的 @Getter 进行了剖析。这里需要注意的一点,因为 Eclipse 是使用自己的内置编译器,而不是 Javac 所以 Lombok 提供了插件的两套实现,这里我们主要分析 javac 的实现版本,并对应下面这个示意图进行分析(图源)。

pic1

3.3.2 Processor

  我们都知道当我们在自定义一个 APT 的时候需要继承 AbstractProcessor ,并实现其最核心的 process 方法来对当前轮编译的结果进行处理,在 Lombok 中也不例外,Lombok 也是通过一个顶层的 Processor 来接收当前轮的编译结果,而这个 Processor 就是 LombokProcessor ,所以我们第一步直接进入到这个类的 process 方法中。

// lombok/src/core/lombok/javac/apt/LombokProcessor.java
@Override 
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
	while(true){
		for (long prio : priorityLevels) {
			// ...
			// 类型转换
			transformer.transform(prio, javacProcessingEnv.getContext(), cusForThisRound, cleanup);
		}
	}
}

  在 process 方法中最重要的一段代码就是上面这段,我们先不要管这里面的参数都是什么意思,但是根据这个方法名我们可以猜测一下这个方法应该是将当前轮中的相关信息进行了封装转换,所以我们先继续跟进这个方法。

// lombok/src/core/lombok/javac/JavacTransformer.java
public void transform(long priority, Context context, java.util.List<JCCompilationUnit> compilationUnitsRaw, CleanupRegistry cleanup) {
	// ...	

	java.util.List<JavacAST> asts = new ArrayList<JavacAST>();
		
	for (JCCompilationUnit unit : compilationUnits) {
		if (!Boolean.TRUE.equals(LombokConfiguration.read(ConfigurationKeys.LOMBOK_DISABLE, JavacAST.getAbsoluteFileLocation(unit)))) {
			// 将当前轮上下文封装后添加到 asts 集合中
			asts.add(new JavacAST(messager, context, unit, cleanup));
		}
	}
		
	for (JavacAST ast : asts) {
		ast.traverse(new AnnotationVisitor(priority));
		// 从当前编译单元开始深度优先遍历 AST 并调用每个节点的 visit 方法
		handlers.callASTVisitors(ast, priority);
	}
}

// lombok/src/core/lombok/javac/HandlerLibrary.java
public void callASTVisitors(JavacAST ast, long priority) {
	for (VisitorContainer container : visitorHandlers) {
		try {
			if (container.getPriority() == priority) 
				ast.traverse(container.visitor);
		}
	}
}

// lombok/src/core/lombok/javac/JavacAST.java
public void traverse(JavacASTVisitor visitor) {
	// 从当前编译单元开始深度优先遍历 AST 并调用每个节点的 visit 方法
	// 其中 top 方法返回类型为 JavacNode
	top().traverse(visitor);
}

// lombok/src/core/lombok/javac/JavacNode.java
public void traverse(JavacASTVisitor visitor) {
	switch (this.getKind()) { 
			//...
			case TYPE:
			visitor.visitType(this, (JCClassDecl) get());
			ast.traverseChildren(visitor, this);
			visitor.endVisitType(this, (JCClassDecl) get());
			break;
		case FIELD:
			visitor.visitField(this, (JCVariableDecl) get());
			ast.traverseChildren(visitor, this);
			visitor.endVisitField(this, (JCVariableDecl) get());
			break;
		case METHOD:
			visitor.visitMethod(this, (JCMethodDecl) get());
			ast.traverseChildren(visitor, this);
			visitor.endVisitMethod(this, (JCMethodDecl) get());
			break;
			//...
	}
}

  上面代码的总体逻辑还算比较清晰,但是因为开始的时候的处理逻辑还是蛮多的,但这里比较关键的其实就是最后的 traverse 方法,通过该方法我们完成了对整棵 AST 树的深度优先遍历,并且对每个节点都调用了其对应的 visit 方法。其实从这个位置开始我们已经大概能猜到 Lombok 的操作方式了,通过上面的代码 Lombok 已经获取到了 javac 的 AST ,并对其进行了遍历,其实在这里开始 Lombok 已经越界了,因为正常来说 javac 中的 JavacAST 和 JavacNode 等这些 Javac 的类是属于内部类,对外部不可见的,Lombok 在这里相当于走了后门,并且使用这样的方法可以让整棵 AST 对代码可见,并可以追溯每个节点的父节点。

  下面的代码逻辑就比较好理解了,就是运用了适配器设计模式,对 handlers 变量调用了 handleAnnotation 方法,这里需要注意的是变量 handlers 的类型就是 HandlerLibrary 。

// lombok/src/core/lombok/javac/JavacASTVisitor.java
public interface JavacASTVisitor {
	void setTrees(Trees trees);
	//...
}

// lombok/src/core/lombok/javac/JavacASTAdapter.java
public class JavacASTAdapter implements JavacASTVisitor {
	// JavacASTVisitor 接口的标准适配器,接口上的每个方法以空方法体实现,仅需覆盖需要使用的方法
	@Override 
	public void setTrees(Trees trees) {}
	//...
}

// lombok/src/core/lombok/javac/JavacTransformer.java
// AnnotationVisitor 为 JavacTransformer 的内部类
private class AnnotationVisitor extends JavacASTAdapter {
	// ...
	@Override 
	public void visitAnnotationOnType(JCClassDecl type, JavacNode annotationNode, JCAnnotation annotation) {
		JCCompilationUnit top = (JCCompilationUnit) annotationNode.top().get();
		handlers.handleAnnotation(top, annotationNode, annotation, priority);
	}
		
	@Override 
	public void visitAnnotationOnField(JCVariableDecl field, JavacNode annotationNode, JCAnnotation annotation) {
	JCCompilationUnit top = (JCCompilationUnit) annotationNode.top().get();
		handlers.handleAnnotation(top, annotationNode, annotation, priority);
	}
		
	@Override 
	public void visitAnnotationOnMethod(JCMethodDecl method, JavacNode annotationNode, JCAnnotation annotation) {
		JCCompilationUnit top = (JCCompilationUnit) annotationNode.top().get();
		handlers.handleAnnotation(top, annotationNode, annotation, priority);
	}
	// ...
}
3.3.3 Handler
// lombok/src/core/lombok/javac/HandlerLibrary.java
public void handleAnnotation(JCCompilationUnit unit, JavacNode node, JCAnnotation annotation, long priority) {
	for (AnnotationHandlerContainer<?> container : containers) {
		try {
			if (container.getPriority() == priority) {
				if (checkAndSetHandled(annotation)) {
					// 主要逻辑调用 Handler
					container.handle(node);
				} else 					
					if (container.isEvenIfAlreadyHandled()) 
						container.handle(node);
					}
			}
		}
	}
}

// lombok/src/core/lombok/javac/HandlerLibrary.java
// AnnotationHandlerContainer 为 HandlerLibrary 内部类
public void handle(final JavacNode node) {
	// 可以看到在这里就是直接调用了 handle 方法
	handler.handle(JavacHandlerUtil.createAnnotation(annotationClass, node), (JCAnnotation)node.get(), node);
}

// lombok/src/core/lombok/javac/JavacAnnotationHandler.java
public abstract class JavacAnnotationHandler<T extends Annotation> {
	protected Trees trees;	
	// JavacAnnotationHandler 为所有 Handler 的基类
	// 所有 Handler 应在 handle 方法中处理 AST
	public abstract void handle(AnnotationValues<T> annotation, JCAnnotation ast, JavacNode annotationNode);
	//...
}

  当我们对每个 JavacNode 节点调用 visit* 方法时,其实就已经将 AST 的相关信息传递到了 Handler ,在 HandlerLibrary 类中会接着进行一连串的验证和调用,但归根到底最终调用了抽象类 JavacAnnotationHandlerhandle 方法。

  接下来下面代码的逻辑就是根据不同的注解选择不同的 Handler 来进行处理,而抽象父类 JavacAnnotationHandler 中的泛型 T 就表示当前 Handler 所关注的注解类型,如 HandleGetter 所关注的就是 @Getter 注解。而通过下面的代码我们也可以看到,在 HandleGetter 类中会首先通过 handle 方法来接收相关的 JavacNode 和 AST 信息,然后会根据不同的逻辑来使用 TreeMaker 来构建方法体,并最终组装成 JCMethodDecl ,最后通过 JavacNode 的 add 方法将新组装好的方法添加到 AST 中,这样就完成了对 AST 的修改。

// lombok/src/core/lombok/javac/handlers/HandleGetter.java
public class HandleGetter extends JavacAnnotationHandler<Getter> {
	@Override 
	public void handle(AnnotationValues<Getter> annotation, JCAnnotation ast, JavacNode annotationNode) {
		JavacNode node = annotationNode.up();
		switch (node.getKind()) {
		case FIELD:
			createGetterForFields(level, fields, annotationNode, true, lazy, onMethod);
			break;
		case TYPE:
			if (lazy) annotationNode.addError("'lazy' is not supported for @Getter on a type.");
			generateGetterForType(node, annotationNode, level, false, onMethod);
			break;
		}
	}
}

// lombok/src/core/lombok/javac/handlers/HandleGetter.java
public void createGetterForFields(AccessLevel level, Collection<JavacNode> fieldNodes, JavacNode errorNode, boolean whineIfExists, boolean lazy, List<JCAnnotation> onMethod) {
	for (JavacNode fieldNode : fieldNodes) {
		createGetterForField(level, fieldNode, errorNode, whineIfExists, lazy, onMethod);
	}
}

// lombok/src/core/lombok/javac/handlers/HandleGetter.java
public void createGetterForField(AccessLevel level,
		JavacNode fieldNode, JavacNode source, boolean whineIfExists, boolean lazy, List<JCAnnotation> onMethod) {
		
	if (fieldNode.getKind() != Kind.FIELD) {
		source.addError("@Getter is only supported on a class or a field.");
		return;
	}
		
	JCVariableDecl fieldDecl = (JCVariableDecl)fieldNode.get();
		
	// 通过 injectMethod 方法将组装好的 JCMethodDecl 添加到当前字段的上层节点中(JCClassDecl)
	injectMethod(fieldNode.up(), createGetter(access, fieldNode, fieldNode.getTreeMaker(), source.get(), lazy, onMethod), List.<Type>nil(), getMirrorForFieldType(fieldNode));
}

// lombok/src/core/lombok/javac/handlers/HandleGetter.java
public JCMethodDecl createGetter(long access, JavacNode field, JavacTreeMaker treeMaker, JCTree source, boolean lazy, List<JCAnnotation> onMethod) {
	// ...
	
	// 构建方法体	
	List<JCStatement> statements;
	JCTree toClearOfMarkers = null;
	if (lazy && !inNetbeansEditor(field)) {
		toClearOfMarkers = fieldNode.init;
		statements = createLazyGetterBody(treeMaker, field, source);
	} else {
		statements = createSimpleGetterBody(treeMaker, field);
	}
	JCBlock methodBody = treeMaker.Block(0, statements);
		
	// 构建 JCMethodDecl
	if (isFieldDeprecated(field)) annsOnMethod = annsOnMethod.prepend(treeMaker.Annotation(genJavaLangTypeRef(field, "Deprecated"), List.<JCExpression>nil()));
		JCMethodDecl decl = recursiveSetGeneratedBy(treeMaker.MethodDef(treeMaker.Modifiers(access, annsOnMethod), methodName, methodType, methodGenericParams, parameters, throwsClauses, methodBody, annotationMethodDefaultValue), source, field.getContext());

	// 将组装好的 JCMethodDecl 返回
	return decl;
}

// lombok/src/core/lombok/javac/handlers/HandleGetter.java
// 使用 TreeMaker 构建方法体
public List<JCStatement> createSimpleGetterBody(JavacTreeMaker treeMaker, JavacNode field) {
	return List.<JCStatement>of(treeMaker.Return(createFieldAccessor(treeMaker, field, FieldAccess.ALWAYS_FIELD)));
}
// lombok/src/core/lombok/javac/handlers/JavacHandlerUtil.java
public static void injectMethod(JavacNode typeNode, JCMethodDecl method, List<Type> paramTypes, Type returnType) {
	// ...
	typeNode.add(method, Kind.METHOD);
}

3.4 怎样利用 Lombok 提供的 API 来实现自己需要的功能?

  这部分因为网上已经有比较好的示例了,所以这里我就不再赘述了。

3.5 为什么会有很多人反对使用 Lombok ?

  对于这个说法网上众说纷纭,但说来说去都是在说 Lombok 修改了程序的源码,且因为其跟 JDK 的关联较强(使用了未公开的内部 API),所以可能在每次 JDK 更新后都会出现问题,且可能因为 JDK 更新后导致 Lombok 不可用进而导致程序崩溃。对于这个问题的话这里也还是提供两篇文章,一篇是比较公正的看法,另一篇是 Lombok 团队自己的看法。


四、内容总结

  这篇博文中我们探讨了 Lombok 底层的实现方式,并大概梳理了 Lombok 对注解的解析流程以及其功能实现的底层原理,说白了就是 Lombok 使用了一些小的技巧,使用了 Javac 未公开的一些 API 来取巧实现了动态修改 AST 的目的,从而实现了代替程序员向 Java 源代码中 “添加” 代码的操作,从而大大提升我们代码的整洁性和可读性。

  但是另一方面也正是因为其使用了这样的方式,所以也饱受争议,因为使用未公开的 API 就意味着每次 JDK 的更新都可能对 Lombok 的内部实现产生影响,也就意味着每次使用 Lombok 都存在着一些不可控的风险,比如如果 Lombok 在某次的 JDK 更新后没有及时更上版本的适配,那么就可能出现注解无法解析进而导致程序崩溃的情况。


  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值