浅析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方法增加了返回值,所以不匹配无法拷贝属性。


赞赏(Donation)
微信(Wechat Pay)

donation-wechatpay