浅析Lombok原理并动手编写@Getter与@Setter的简单实现

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容