【学习总结】自定义lombok注解

【学习总结】自定义lombok注解

说明

​ 业务开发中我们项目常用lombok来简化JavaBean的一些样板代码,虽然大家对lombok的褒贬不一,我认为只要公司约定好一些基础的依赖库,就没有多大的问题,适当取舍;不是工具不好用,而是要有合适的使用方法。最近,想要学习一下lombok中对于注解在编译时期就能生效的原理,通常我们都是使用Spring的AOP编程定义一些运行时注解,对于编译时注解还是知之甚少,所以想要通过这样一个机会来了解下lombok的注解原理,在以后的开发中可以适当使用,内部维护lombok版本。

使用场景

​ 公司内部业务有太多的样板代码,每次开发都得去做重复性的处理,这个时候自定义lombok注解就会简化一些重复性的劳动。

原理说明
概述

​ 官网对他的功能的描述:ProjectLombok是一个java库,可以自动插入编辑器和构建工具,为您的java增添色彩。

永远不要再编写另一个getter或equals方法,只要有一个注释,您的类就有一个功能齐全的构建器,自动化您的日志变量,等等。

​ 根据官网的解释我们可以把lombok定位为一个构建工具插件。

编译期优化
概述

​ Java语言的”编译期“其实是一段”不确定“的操作过程,因为他可能是指一个前端编译器(其实叫”编译器的前端“更准确一些)把 *.java文件转变成 *.class文件;也可能指虚拟机的后端运行期编译器(JIT编译器,Just In Time Compiler)把字节码转变成机器码的过程;还可能是指使用静态提前编译器(AOT编译器,Ahead Of Time Compiler)直接把 *.java文件编译成本地机器代码的过程。下面列举了3类编译过程中比较有代表性的编译器。

​ (1)前端编译器:Sun的Javac、Eclipse JDT 中的增量式编译器(ECJ)。

​ (2)JIT编译器:HotSpot VM的C1、C2编译器。

​ (3)AOT编译器:GNU Compiler for the Java(GCJ)、Excelsior JET。

​ —— 节选自《深入理解Java虚拟机(第二版)》

​ 我们常用的就是Javac编译器,我们也是基于这个编译器做的自定义注解。

Javac编译器

​ Javac编译器不像HotSpot虚拟机那样使用C/C++语言实现,它本身是用Java语言实现的,这为纯Java开发程序员提供了便利。编译过程大致分为3个过程:

​ (1)解析与填充符号表过程。

​ (2)插入式注解处理器的注解处理过程。

​ (3)分析与字节码生成过程。

​ 这三个过程如图所示。

在这里插入图片描述

​ 我们利用的就是注解处理器。

注解处理器

​ 在JDK1.5之后,Java语言提供了对注解(Annotation)的支持,这些注解与普通代码一样在运行时执行。在JDK1.6中实现了JSR-269规范,提供了一组插入式注解处理器的标准API在编译期间对注解进行处理,我们把他们看做是编译器插件,在这些插件里,可以读取、修改、添加抽象语法树种的任意元素。有了编译器注解处理的标准API后,我们的代码才有可能干涉编译器的行为,由于语法树的任意元素,甚至包括注释代码都可以在插件中访问到,所以通过插入式注解处理器实现的插件在功能上有很大的发挥空间。

Java注解

​ 以@interface定义声明的Java类型,一般会修饰@Target和@Retention注解指定目标和保留策略,目标只该注解修饰的Java语法结构,分为。

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

​ 保留策略分为。

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}
  • SOURCE:表示注解的信息会被编译器抛弃,不会留在 class 文件中,注解的信息只会留在源文件中。
  • CLASS:表示注解的信息被保留在 class 文件中。当程序编译时,但不会被虚拟机读取在运行的时候。
  • RUNTIME:表示注解的信息被保留在 class 文件中。当程序编译时,会被虚拟机保留在运行时。
Lombok自定义注解
1.下载lombok源码

​ GitHub地址:https://github.com/rzwitserloot/lombok.git

​ Gitee镜像:https://gitee.com/mirrors_projectlombok/lombok_1.git

2.编译环境

​ 需要JDK9以上版本编译(这里下载的JDK11),配置IDEAjava运行环境

3.源码解读

​ 以@Setter注解为例(定义在src/core/lombok下),在src/core/lombok/javac下定义了HandleSetter类继承抽象类JavacAnnotationHandler,这个是lombok自定以的中间注解处理器,且HandleSetter用注解@Provides 修饰(这个在我们另一篇文章中分享过),会写入SPI文件;

​ 在使用 javac 编译器时(netbeans,maven,gradle),Lombok 会以 annotation processor 方式运行。 Javac 会以 SPI 方式加载所有 jar 包中 META-INF/services/javax.annotation.processing.Processor 文件所列举的类,并以 annotation processor 的方式运行它。对于 Lombok,这个类是 lombok.launch.AnnotationProcessorHider$AnnotationProcessor ,当它被 javac 加载创建后,会执行 init方法,在这个方法中会启动一个特殊的类加载器 ShadowClassLoader ,加载同 jar 包下所有以 .SCL.lombok 结尾的类(Lombok 为了对 IDE 隐藏这些类,所以不是通常地以 .class 结尾)。其中就包含各式各样的 handler

​ —引自https://blog.csdn.net/weixin_30896825/article/details/101424667?spm=1001.2014.3001.5506

4.定义一个注解

​ 定义注解后可以自定义一个handle,实现handle方法。

5.编译自己的lombok注解

​ 由于clone的lombok项目突然不能编译了,就没有具体操作验证。

Javac自定义注解

​ 注解所在的项目不能与引用注解的项目在一起,因为引用注解需要依赖注解解释器的jar包文件,我们这里直接分开了,应该用maven分子项目也可以实现,这里没有验证。

1.搭建的Maven项目

​ pom.xml文件如下,

<?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>com.jsyn</groupId>
    <artifactId>custom-processor</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.15</version>
        </dependency>

        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>

<!--        引用的google的APT自动配置库,用了好几种其他方式都没有成功-->
        <!--APT自动添加配置库-->
        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0.1</version>
        </dependency>

        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service-annotations</artifactId>
            <version>1.0.1</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>
2.创建注解

​ 主要想要在一个类中插入成员方法,并返回 Hello注解中的name的值

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Hello {

    String name() default "World";
}
3.创建解释器

​ 解释器中使用了sun.java的编译器的相关方法,pom中必须引入sun的java工具包tools,其中还涉及一些APT的语法,现在只是简单了解,以后有机会会学习一下其他的用法。

@SupportedAnnotationTypes("com.jsyn.annotation.Hello")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
// google的APT处理注解,用于生成META-INF/services中的javax.annotation.processing.Processor接口实现类
@AutoService(Processor.class)
public class HelloProcessor extends AbstractProcessor {
    /*
     * 用来在编译期打log用的
     */
    private Messager messager;
    /**
     * 提供了待处理的抽象语法树
     */
    private JavacTrees trees;
    /**
     * 封装了创建AST节点的一些方法
     */
    private TreeMaker treeMaker;
    /**
     * 提供了创建标识符的方法
     */
    private Names names;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        System.err.println("-- Start init custom annotation processor --");
        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 boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.err.println("-- Start run custom annotation processor --");
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Hello.class);
        if (CollectionUtil.isNotEmpty(elements)) {
            for (Element element : elements) {
                JCTree jcTree = trees.getTree(element);
                Hello annotation = element.getAnnotation(Hello.class);
                System.err.println("annotation value : " + annotation.name());
                jcTree.accept(new TreeTranslator() {
                    @Override
                    public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(annotation));
                        super.visitClassDef(jcClassDecl);
                    }
                });
            }
        }
        return true;
    }

    /**
     * 为成员遍历构造 getter 方法
     */
    private JCTree.JCMethodDecl makeGetterMethodDecl(Hello annotation) {
        System.err.println("annotation value method decl : " + annotation.name());
        // 构造方法体
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        // 生成表达式 return name;
        statements.append(treeMaker.Return(treeMaker.Literal(annotation.name())));
        // 加上大括号 { return name; }
        JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
        // 构造方法
        JCTree.JCMethodDecl jcMethodDecl = treeMaker.MethodDef(
                // 访问标志
                treeMaker.Modifiers(Flags.PUBLIC),
                // 方法名
                names.fromString("hello"),
                // 返回类型
                treeMaker.Ident(names.fromString("String")),
                // 泛型形参列表
                List.nil(),
                // 参数列表
                List.nil(),
                // 异常列表
                List.nil(),
                // 方法体
                body,
                // 默认方法
                null
        );

        // 组装方法
        return jcMethodDecl;
    }
}
4.引用自定义注解

​ 在另一项目或者包中引用上述注解项目生成的jar包文件,具体引入如下pom.xml;

<dependency>
   <groupId>com.jsyn</groupId>
   <artifactId>custom-processor</artifactId>
   <version>1.0</version>
</dependency>
5.注解使用

​ 虽然新的方法引用会在idea中爆红,但是不影响执行,还需要重写IDE的注解解释器

@Hello(name = "hello world")
public class TestLombokSpi {

    public static void main(String[] args) {
        TestLombokSpi testLombokSpi = new TestLombokSpi();
        System.out.println(testLombokSpi.hello());
    }
}

在这里插入图片描述

总结

​ 在工作中会遇到一些很方便的组件,给我们的开发省了不少事;很清楚的是,这是前人栽树后人乘凉的工作,在这里简单记录下自己看lombok源码然后搜索各种帖子实现javac的注解,踩了很多坑,这里跟大家分享下。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值