浅析Lombok原理并动手编写@Getter与@Setter的简单实现
1、lombok使用及其原理
Lombok是一个 Java 库,能够以极其简单的注解方式解决工程中的繁琐重复的代码,提高研发人员的工作效率。例如java bean中的大量getter setter tostring方法,常用的注解有@Getter @Setter @Slf4j等。 官网https://projectlombok.org/
- 官方的lombok使用,如果是maven项目,只需要引入包的依赖即可,IDEA中可选择安装对应的lombok插件
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Student{
private String name;
private int age;
}
使用反编译工具打开Student.class后可发现,lombok自动生成了get set方法,并且在生成的class类中没有lombok相关的类;
- lombok生成代码原理
Lombok实际上是通过jdk实现的JSR 269: Pluggable Annotation Processing API
(编译期的注解处理器) ,在编译期时把Lombok的注解转换成java代码,其是在编译期进行工作的,相当于在编译期对代码进行了修改。javac编译的的过程大概有以下几个步骤
1、词法分析 2、语法分析 3、填充符号表 4、插入式注解处理器处理 5、语义分析 6、解语法糖 7、生成字节码
lombok就是实现了插入式注解处理器,通过插入式注解处理器可以读取、修改、添加抽象语法树中的任意元素;
2、动手实现lombok加深理解
基本了解了原理后,可以动手写一个简单的案例理解一下Pluggable Annotation Processing
,lombok本身实现比较复杂完整,不过都是基于这个原理;
新建两个项目lombok和lombok-test,使用maven编译构建(windows环境下);
- lombok(用于实现自己的lombok)
编写pom.xml,注意需要配置好JAVA_HOME环境变量,编译时需要依赖tools.jar rt.jar
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mixfate</groupId>
<artifactId>lombok</artifactId>
<version>0.0.1</version>
<properties>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
<compilerArguments>
<verbose />
<bootclasspath>${JAVA_HOME}\lib\tools.jar;${JAVA_HOME}\jre\lib\rt.jar</bootclasspath>
</compilerArguments>
</configuration>
</plugin>
</plugins>
</build>
</project>
编写Getter注解类
package com.mixfate;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface Getter {
}
编写GetterProcessor处理类
package com.mixfate;
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 javax.tools.Diagnostic;
import java.util.Set;
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.mixfate.Getter")
public class GetterProcessor extends AbstractProcessor {
private 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);
}
@Override
public synchronized boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Getter.class);
set.forEach(element -> {
JCTree jcTree = trees.getTree(element);
jcTree.accept(new TreeTranslator() {
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
for (JCTree tree : jcClassDecl.defs) {
if (tree.getKind().equals(Tree.Kind.VARIABLE)) {
JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree;
jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
}
}
jcVariableDeclList.forEach(jcVariableDecl -> {
messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " has been processed");
jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
});
super.visitClassDef(jcClassDecl);
}
});
});
return true;
}
private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));
JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewMethodName(jcVariableDecl.getName()), jcVariableDecl.vartype, List.nil(), List.nil(), List.nil(), body, null);
}
private Name getNewMethodName(Name name) {
String s = name.toString();
return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
}
}
编译构建并安装jar包到本地(供lombok-test使用)
mvn clean package install
- lombok-test(用于测试自己实现的lombok)
编写 pom.xml,引入自己实现的lombok
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mixfate</groupId>
<artifactId>lombok-test</artifactId>
<version>0.0.1</version>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.mixfate</groupId>
<artifactId>lombok</artifactId>
<version>0.0.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
<annotationProcessors>
<annotationProcessor>com.mixfate.GetterProcessor</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>
</project>
编写一个测试类Person
package com.mixfate;
import com.mixfate.Getter;
@Getter
public class Person {
public String name = "michael";
public static void main(String[] args){
Person p = new Person();
System.out.println(p.getName());
}
}
使用maven构建并运行测试代码
mvn clean package exec:exec -Dexec.executable="java" -Dexec.args="-cp %classpath com.mixfate.Person"
代码可见 https://gitee.com/viturefree/lombok.git 案例中简单实现了getter setter
3、lombok使用总结
lombok的优点显而易见,它可以减少很多代码,让代码非常优雅;但也有一些缺点,因为是编译期生成的没有办法调试代码,同时一定程度破坏了封装性,不过在实践中大多数场景下应该是利大于弊。
生产故障案例:使用了lombok的@Accessors(chain = true)注解,导致原来使用了org.apache.commons.beanutils.BeanUtils.copyProperties的功能中对象字段拷贝失效。跟踪代码可发现是由于改变setter方法的返回值,而copyProperties中的
getPropertyUtils().isWriteable(dest, name)
判断无writeable的方法,继续跟踪此判断方法可以找到以下关联方法
PropertyUtilsBean.fetchIntrospectionData
DefaultBeanIntrospector.introspect
Introspector.getBeanInfo
Introspector.getTargetPropertyInfo
其中关键代码如下
else if (argCount == 1) {
if (int.class.equals(argTypes[0]) && name.startsWith(GET_PREFIX)) {
pd = new IndexedPropertyDescriptor(this.beanClass, name.substring(3), null, null, method, null);
} else if (void.class.equals(resultType) && name.startsWith(SET_PREFIX)) {
// Simple setter
pd = new PropertyDescriptor(this.beanClass, name.substring(3), null, method);
if (throwsException(method, PropertyVetoException.class)) {
pd.setConstrained(true);
}
}
}
可以看到当参数为1个时,以set开头的方法并且返回值为void时才处理,当使用
@Accessors(chain = true)
修饰时,生成的类中setter方法增加了返回值,所以不匹配无法拷贝属性。