利用Java手写一个简易的lombok的示例代码

发布时间:

Lombok是一款Java开发插件,使得Java开发者可以通过其定义的一系列注解来消除业务工程中冗长和繁琐的代码,尤其对于简单的Java模型对象。本文就来手写一个简易的lombok,需要的可以参考一下!

1.概述

在面向对象编程中,必不可少的需要在代码中定义对象模型,而在基于Java的业务平台开发实践中尤其如此。相信大家在平时开发中也深有感触,本来是没有多少代码开发量的,但是因为定义的业务模型对象比较多,而需要重复写Getter/Setter、构造器方法、字符串输出的ToString方法、Equals/HashCode方法等。我们都知道Lombok能够替大家完成这些繁琐的操作,但是其背后的原理很少有人会关注或者说得清,本文会带着大家了解这一开发神器内部的运行机制与原理!

Lombok是一款Java开发插件,使得Java开发者可以通过其定义的一系列注解来消除业务工程中冗长和繁琐的代码,尤其对于简单的Java模型对象(POJO)。在开发环境中使用Lombok插件后,Java开发人员可以节省出重复构建,诸如HashCode和Equals这样的方法以及各种业务对象模型的accessor和ToString等方法的大量时间。对于这些方法,它能够在编译源代码期间自动帮我们生成这些方法,且并不会如反射那样降低程序的性能。主要是这样比较灵活,即使你在实体类中新增了属性,也不用重新回过头来维护该实体的set和get方法等。

2.lombok使用方法

安装插件,在编译类路径中加入lombok.jar包(具体安装方法可自己百度);

在需要简化的类或方法上,加上要使用的注解;

使用支持lombok的编译工具编译源代码(关于支持lombok的编译工具,见4.支持lombok的编译工具);

编译得到的字节码文件中自动生成Lombok注解对应的方法或代码;

3.lombok原理解析

接下来,我们进行lombok的原理分析,以Oracle的javac编译工具为例。自Java 6起,javac开始支持JSR 269 Pluggable Annotation Processing API规范,只要程序实现了该API,就能在java源码编译时调用定义的注解。举例来说,现在有一个实现了"JSR 269 API"的程序A,那么使用javac编译源码的时候具体流程如下:

javac对源代码进行分析,生成一棵抽象语法树(AST);

运行过程中调用实现了"JSR 269 API"的A程序;

此时A程序就可以完成它自己的逻辑,包括修改第一步骤得到的抽象语法树(AST);

javac使用修改后的抽象语法树(AST)生成字节码文件;

详细的流程图如下:

利用Java手写一个简易的lombok的示例代码

从上面的Lombok执行的流程图中可以看出,在Javac 解析成AST抽象语法树之后, Lombok 根据自己编写的注解处理器,动态地修改 AST,增加新的节点(即Lombok自定义注解所需要生成的代码),最终通过分析生成JVM可执行的字节码Class文件。使用Annotation Processing自定义注解是在编译阶段进行修改,而jdk的反射技术是在运行时动态修改,两者相比,反射虽然更加灵活一些但是带来的性能损耗更加大。

Lombok本质上就是一个实现了JSR 269 API的程序,在使用javac的命令过程中,它生效的具体流程如下:

  • javac对源代码进行分析,生成一棵抽象语法树(AST);
  • 运行过程中调用实现了JSR 269 API的lombok程序;
  • 编译机会调用lombok程序对第一步得到的AST进行处理,找到其注解所在类对应的语法树(AST),然后修改该语法树,增加注解对应的方法或代码片段到定义的相应树节点;
  • javac使用修改后的抽象语法树生成最终的java字节码文件;

4.手写简易lombok

使用的是idea工具进行开发,使用的jdk版本为1.8,因为我们是自己手写的idea提示会报错,但是能正常运行,因为lombok是idea针对于他有插件提示,我们的没有,但是也不影响正常使用。

1.我们需要使用到jdk安装路径下lib包下的tools.jar,我们可以收到加入到项目依赖,也可以在maven中直接引入。我们直接使用idea新建一个普通的maven项目,然后配置如下,最后将这个项目打包一下,在别的项目中引入即可。

maven配置如下:

<?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>lombok</groupId>
<artifactId>com.compass.lombok</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>C:/Program Files/Java/jdk1.8.0_251/lib/tools.jar</systemPath>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0-rc5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

还有一个是 com.google.auto.service 这个是使用SPI机制的一个依赖,关于spi可以自行百度了解,这里就不再进行展开。

关键核心接口:AbstractProcessor,这个就是在编译期处理注解的一个接口,然后我们可以通过实现这个接口通过修改字节码文件,最终在字节码文件中生成get和set方法。

首先我们定义一个DATA注解:

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.RUNTIME)
public @interface Data {
}

然后写一个 DataAnnotationProcessor 继承AbstractProcessor即可

import com.compass.lombok.annotation.Data;
import com.google.auto.service.AutoService;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.*;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.util.Set;
/**
* @author compass
* @date 2022-10-13
* @since 1.0
**/
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.compass.lombok.annotation.Data")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class DataAnnotationProcessor extends AbstractProcessor {
private JavacTrees javacTrees;
private TreeMaker treeMaker;
private Names names;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
javacTrees = JavacTrees.instance(context);
treeMaker = TreeMaker.instance(context);
names = Names.instance(context);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Data.class);
for (Element element : set) {
javacTrees.getTree(element).accept(new TreeTranslator(){
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
jcClassDecl.defs.stream()
.filter(it->it.getKind().equals(Tree.Kind.VARIABLE))
.map(it->(JCTree.JCVariableDecl) it).forEach(it->{
jcClassDecl.defs = jcClassDecl.defs.prepend(genGetterMethod(it));
jcClassDecl.defs = jcClassDecl.defs.prepend(genSetterMethod(it));
});
super.visitClassDef(jcClassDecl);
}
});
}
return true;
}
private JCTree.JCMethodDecl genGetterMethod(JCTree.JCVariableDecl jcVariableDecl){
JCTree.JCIdent _this = treeMaker.Ident(names.fromString("this"));
Name name = jcVariableDecl.getName();
JCTree.JCFieldAccess select = treeMaker.Select(_this, name);
JCTree.JCReturn returnStatement = treeMaker.Return(select);
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
statements.append(returnStatement);
JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
Name getMethodName = getGetMethodName(jcVariableDecl.getName());
JCTree.JCExpression returnMethodType = jcVariableDecl.vartype;
JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
List<JCTree.JCTypeParameter> methodGenericParamList = List.nil();
List<JCTree.JCVariableDecl> parameterList = List.nil();
List<JCTree.JCExpression> throwList = List.nil();
return treeMaker.MethodDef(modifiers, getMethodName, returnMethodType, methodGenericParamList, parameterList, throwList, body, null);
}
public  JCTree.JCMethodDecl genSetterMethod(JCTree.JCVariableDecl jcVariableDecl){
JCTree.JCIdent _this = treeMaker.Ident(names.fromString("this"));
Name name = jcVariableDecl.getName();
JCTree.JCFieldAccess select = treeMaker.Select(_this, name);
JCTree.JCAssign statementAssign = treeMaker.Assign(select, treeMaker.Ident(jcVariableDecl.getName()));
JCTree.JCExpressionStatement statement = treeMaker.Exec(statementAssign);
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
statements.append(statement);
JCTree.JCVariableDecl params = treeMaker.VarDef(
treeMaker.Modifiers(Flags.PARAMETER, List.nil()),
jcVariableDecl.name,
jcVariableDecl.vartype,
null
);
JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
Name setMethodName = getSetMethodName(jcVariableDecl.getName());
JCTree.JCExpression returnMethodType = treeMaker.Type(new Type.JCVoidType());
JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
List<JCTree.JCTypeParameter> methodGenericParamList = List.nil();
List<JCTree.JCVariableDecl> parameterList = List.of(params);
List<JCTree.JCExpression> throwList = List.nil();
return treeMaker.MethodDef(modifiers, setMethodName, returnMethodType, methodGenericParamList, parameterList, throwList, body, null);
}
private Name getGetMethodName(Name name){
String filedName = name.toString();
return names.fromString("get"+filedName.substring(0,1).toUpperCase()+filedName.substring(1));
}
private Name getSetMethodName(Name name){
String filedName = name.toString();
return names.fromString("set"+filedName.substring(0,1).toUpperCase()+filedName.substring(1));
}
}

其实到这里就编写完毕了,这里去动态修改字节码,然后生成了get和set方法,至于其他的方法那就后面再说,此案例参照于《深入jvm字节码》进行编写。

最后在maven项目中打包

利用Java手写一个简易的lombok的示例代码

在别的项目直接使用即可,直接在别的项目的实体类上加上@Data注解即可生成get和set方法,但是没有方法提升,但是能正常运行,这里是idea的一个代码提示的问题,因为我们这个没有对应的idea插件,所以idea会提示报错,但是能正常运行。