zoukankan      html  css  js  c++  java
  • 啰嗦的 java,简洁的 lombok —— lombok 的使用及简单实现单例模式注解

    lombok 是什么?

    lombok 是一个非常神奇的 java 类库,会利用注解自动生成 java Bean 中烦人的 Getter、Setting,还能自动生成 logger、ToString、HashCode、Builder 等 java
    特色的函数或是符合设计模式的函数,能够让你 java Bean 更简洁,更美观。

    来先看下使用 lombok 后的 java bean 看起来是怎样的

    @Data
    @AllArgsConstructor
    public class User {
        private Long id;
        private String name;
        private Integer age;
    
        public static void main(String[] args) {
            User user = new User(1L,"张三",18);
            System.out.println("toString:"+user);
            System.out.println("name:"+user.getName());
        }
    }
    

    输出如下

    toString():User(id=1, name=张三, age=18)
    getName:张三
    

    看到了吗,仅仅在类中添加 @Data 注解就能做到自动生成 GetterToString 函数了! 仅仅添加了 @AllArgsConstructor 注解就再也不用写那些让你心烦的构造函数了!

    我第一次使用 lombok 的时候就很喜欢它了。觉得它的思想非常好的,即是不应该花时间去写重复的代码,应该让之自动化

    当然自动化的手段是什么也很重要,我可以通过 ide 的生成器功能、或者是自己写的代码生成器,自动生成 Getter、Setting、ToString,就算不用 lombok 也是可以做到的。而我喜欢 lombok 最主要原因就在于它会让代码更简洁、阅读起来更清晰。

    可能会有人说这玩意只适合个人项目,不适合大型合作。。。但知乎有位大神说过亚马逊(重度使用 java 的公司) 内部的项目都在用。不管如何还是先了解再决定吧,我个人还是很喜欢用这种方式。

    上面的介绍了一下 lombok 有点简陋,所以下面会介绍得更详细的一点。当然也许会有读者对 lombok 这种像是魔法的写法感到好奇,究竟它是怎么自动生成代码的呢?所以文章的后面会对 lombok 进行简单的探讨,希望读者会喜欢。

    lombok 的安装

    安装

    得现代的依赖管理 ,引入 lombok 依赖及其简单

    meavn

    使用 meavn 的朋友在 pom.xml 文件中添加依赖即可

    <dependencies>
    	<dependency>
    		<groupId>org.projectlombok</groupId>
    		<artifactId>lombok</artifactId>
    		<version>1.18.6</version>
    		<scope>provided</scope>
    	</dependency>
    </dependencies>
    

    gradle

    用 gradle 的朋友 在 build.gradle 在添加依赖即可

    repositories {
    	mavenCentral()
    }
    
    dependencies {
    	compileOnly 'org.projectlombok:lombok:1.18.6'
    	annotationProcessor 'org.projectlombok:lombok:1.18.6'
    }
    

    idea 的插件

    你看 pom.xml 的依赖的作用域(scope) 是 provided,也就是说 lombok 是在编译的时候才起作用的。因此 idea 在正确使用 lombok 的时候也会报错的

    1409313-20190331165746794-47856150.md.png
    所以你要安装 lombok 插件才能正常使用。

    1409313-20190331170210429-178360424.md.png

    使用

    注解的类型

    类型 解释
    val,var 神奇的类型推到,可以代表任意类型
    @Getter and @Setter
    @ToString
    @EqualsAndHashCode
    @NonNull
    @AllArgsConstructor、@RequiredArgsConstructor、@NoArgsConstructor 构造函数部分,针对不同情况的构造函数
    @Data 相当于 @Getter + @Setter + @ToString + @EqualsAndHashCode + RequiredArgsConstructor
    @Value 类变成只读模式
    @Builder builder 模式,会创建内 Builder
    @Singular 要配合 builder 使用,会对(List、Set)等生成更方便函数
    @Cleanup 告别烦人的释放的资源
    @Log, @Log4j, @Log4j2, @Slf4j, @XSlf4j
    @CommonsLog, @JBossLog, @Flogger
    不同框架的日志注解
    @SneakyThrows 偷偷摸摸地抛出异常
    @Delegate 带实验性质的,能非常方便实现代理模式
    @Accessors 带实验性质的存取器
    @Wither 带实验性质的,根据被修饰的成员变量创建类

    val,var

    可以表示任何类型!
    var 可以用来表示变量,类似其他语言中的 let
    val 可以用来表示常量(final),类似其他语言中的 const

    var str = "hello world";
    val list = Arrays.asList(1,2,3,4);
    System.out.println(str);
    for(val item : list){
        System.out.printf("%d	",item);
    }
    

    等价于

    String str = "hello world";
    final List<Integer> list = Arrays.asList(1,2,3,4);
    System.out.println(str);
    for(final Integer item : list){
        System.out.printf("%d	",item);
    }
    

    @Getter、@Setter

    添加了注解后会根据字段生成对应的 get、set 函数,可以修饰成员变量或者类

    @Getter
    @Setter
    public class User {
        private Long id;
        private String name;
        private Integer age;
    }
    

    灵活的 lombok 可以通过,下面的方式指定访问级别(PUBLIC、PROTECTED、PACKAGE、PRIVATE)

    @Getter 
    @Setter
    public class User {   
        private Long id;
        
        private String name;
        
        @Setter(AccessLevel.PROTECTED)
        private Integer age;
    }
    

    @ToString

    @ToString
    public class User {
        private Long id;
        private String name;
        private Integer age;
    }
    

    ToString 生成后代码大概如下

     public String toString() {
        return "User(id=" + this.id + ", name=" + this.name + ", age=" + this.age + ")";
    }
    

    选项:

    1、@ToString(includeFieldNames=false) 不显示变量名,会直接输出值

    public String toString() {
        return "User(" + this.id + ", " + this.name + ", " + this.age + ")";
    }
    

    2、@ToString(exclude = {"age"}) 生成的结果会排出 age 变量

    public String toString(){
        return "User(id=" + this.id + ", name=" + this.name + ")";
    }
    

    3、@ToString(of = {"id","name"}) 生成的结果包括

    public String toString(){
        return "User(id=" + this.id + ", name=" + this.name + ")";
    }
    

    @EqualsAndHashCode

    只能用于修饰类。

    @EqualsAndHashCode
    public class User {
        //...
    }
    

    和 ToString 类似,可以用 of 以及 exclude 来排出成员变量

    @NonNull

    可以用于成员变量、本地变量、参数、方法

    @Setter
    public class User {
    
        private Long id;
    
        @NonNull
        private String name;
    
        private Integer age;
        
    }
    

    setName 函数实际上会变成这样

    public void setName(@NonNull String name) {
        if (name == null) {
            throw new NullPointerException("name is marked @NonNull but is null");
        } else {
            this.name = name;
        }
    }
    

    构造函数 @AllArgsConstructor、@RequiredArgsConstructor、@NoArgsConstructor

    这三者都是处理构造函数的注解,都只能修饰类,都能通过staticName 创建静态工厂方法,使用access控制访问级别。
    不同之处在于 @AllArgsConstructor 会把所有的成员变量都纳入到构造函数中, @RequiredArgsConstructor 只会把 final@NonNull 修饰的成员变量纳入、@NoArgsConstructor 所有的成员变量都不会纳入到构造函数。

    @AllArgsConstructor

    构造函数会包含所有字段

    @AllArgsConstructor
    public class User {
        private Long id;
        private String name;
        private Integer age;
    }
    

    会自动生成

    public User(Long id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
    

    关于 staticNameaccess 的选项,可以看下面的例子

    @AllArgsConstructor(staticName = "of",access = AccessLevel.PRIVATE)
    public class User {
        private Long id;
        private String name;
        private Integer age;
    }
    

    会看到构造函数和静态工厂函数的访问级别都变成 private

    public class User {
        private Long id;
        private String name;
        private Integer age;
    
        private User(Long id, String name, Integer age) {
            this.id = id;
            this.name = name;
            this.age = age;
        }
    
        private static User of(Long id, String name, Integer age) {
            return new User(id, name, age);
        }
    }
    

    @RequiredArgsConstructor

    final 修饰和 @NonNull 修饰的参数才会加入构造函数

    @RequiredArgsConstructor
    public class User {
        @NonNull
        private Long id;
        private final String name;
        private Integer age;
    }
    

    生成的结果大概是这样

    public class User {
        @NonNull
        private Long id;
        private final String name;
        private Integer age;
    
        public User(@NonNull Long id, String name) {
            if (id == null) {
                throw new NullPointerException("id is marked @NonNull but is null");
            } else {
                this.id = id;
                this.name = name;
            }
        }
    }
    

    @NoArgsConstructor

    顾名思义,使用 @NoArgsConstructor 会生成没有参数的构造函数

    但如果是用 final 修饰的成员函数呢?

    答:这样会编译出错的,除非是用 @NoArgsConstructor(force=true),那么所有的 final 字段会被定义为0,false,null等。

    如果使用使用的是 @NonNull 修饰的成员字段呢?那么使用无参数的构造函数构造出来的实例成员变量不就是 null 了吗?不就矛盾了吗?

    答:是的。。。

    比如

    @NoArgsConstructor
    @Getter
    public class User {
        private  Long id ;
        private @NonNull String name;
        private Integer age;
    
        public static void main(String[] args) {
            System.out.println(new User().getName());
        }
    }
    

    输出结果是 null

    因此如果有 @NonNull 修饰的成员的变量就不要用 @NoArgsConstructor 修饰类

    @Data

    @Data = @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor 就是那么的方便

    选项,可以通过 staticConstructor 创建静态工厂函数

    @Data(staticConstructor = "of")
    public class User {
        private  Long id ;
        private @NonNull String name;
        private Integer age;
    }
    

    @Value

    将类变成只读模式。会让所有类成员变量都变成 final,然后 + @RequiredArgsConstructor + @ToString + @EqualsAndHashCode

    @Builder

    面对复杂的数据结构,使用 builder 模式可以抽离复杂的构造方式,能保证线程安全,我在这篇文章中也有对 Builder 的进行粗略的探讨。

    使用 Builder 模式很爽,比如是这样的

    User user = User.builder().id(1L)
                            .name("张三")
                            .age(12).build();
    

    这样虽然好爽,但

    代价是什么呢?

    就是好多冗余的东西要写。比如是这样

    public class User {
        private Long id;
        private String name;
        private Integer age;
    
        User(Long id, String name, Integer age) {
            this.id = id;
            this.name = name;
            this.age = age;
        }
    
        public static User.UserBuilder builder() {
            return new User.UserBuilder();
        }
    
        public static class UserBuilder {
            private Long id;
            private String name;
            private Integer age;
    
            UserBuilder() {
            }
    
            public User.UserBuilder id(Long id) {
                this.id = id;
                return this;
            }
    
            public User.UserBuilder name(String name) {
                this.name = name;
                return this;
            }
    
            public User.UserBuilder age(Integer age) {
                this.age = age;
                return this;
            }
    
            public User build() {
                return new User(this.id, this.name, this.age);
            }
    
            public String toString() {
                return "User.UserBuilder(id=" + this.id + ", name=" + this.name + ", age=" + this.age + ")";
            }
        }
    }
    

    用 lombok 后

    @Builder
    public class User {
        private Long id;
        private String name;
        private Integer age;
    }
    

    清晰明了,爽!

    但是这里有个很严重的问题,就是不符合 java bean 的规范,java bean 要求有一个无参数的构造函数的。不符号 java bean 要求会有什么后果能?比如:json 字符不能反序列化成 java 对象。
    解决方式是写成这样,要同时写上 @AllArgsConstructor 和 @NoArgsConstructor 才行

    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private Long id;
        private String name;
        private Integer age;
    }
    

    @Singular

    Singular 要和 Builder 一起使用的,会对 List、Set 等集合类生出、处理 addOne、addAll、clear 方法
    比如源码是这样的

    @Builder
    public class User {
        private Long id;
        private String name;
        private Integer age;
        private @Singular Set<String> Girlfriends;
    }
    

    生成的代码 User 的静态内部类 UserBuilder 会在增添

    public User.UserBuilder Girlfriend(String Girlfriend) {
        if (this.Girlfriends == null) {
            this.Girlfriends = new ArrayList();
        }
        this.Girlfriends.add(Girlfriend);
            return this;
        }
    
    public User.UserBuilder Girlfriends(Collection<? extends String> Girlfriends) {
        if (this.Girlfriends == null) {
            this.Girlfriends = new ArrayList();
        }
    
        this.Girlfriends.addAll(Girlfriends);
        return this;
    }
    
    public User.UserBuilder clearGirlfriends() {
        if (this.Girlfriends != null) {
            this.Girlfriends.clear();
        }
    
        return this;
    }
    

    @Cleanup

    处理烦人的资源释放的神奇手段

    在 java 7 之前的资源释放是使用 try-catch-finally 的方式处理的,繁琐而容易出错

    FileInputStream  fis = null;
    FileOutputStream fos = null;
    try {
        fis = new FileInputStream(new File("a.txt"));
        fos = new FileOutputStream("b.txt");
        byte[] buffer = new byte[1024];
        int len;
        while ( (len = fis.read(buffer)) != -1){
            fos.write(buffer,0,len);
        }
    }catch (IOException e){
        e.printStackTrace();
    }finally {
        if(fis!=null){
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        if(fos != null){
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    当然 java 7 之后是使用简化了一下,实现了 Closeable 的类可以用 try-with-resource 自动关闭连接

    public static void main(String[] args) {
        try (
            FileInputStream fis = new FileInputStream(new File("a.txt"));
            FileOutputStream fos = new FileOutputStream("b.txt");){
            byte[] buffer = new byte[1024];
            int len;
            while ( (len = fis.read(buffer)) != -1){
                fos.write(buffer,0,len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

    而 lombok 则只需添加 @Cleanup 则可以完成释放资源,但同样需要类本身实现了 Closeable 接口

    try {
        @Cleanup FileInputStream fis = new FileInputStream(new File("a.txt"));
        @Cleanup FileOutputStream fos = new FileOutputStream("b.txt");
        byte[] buffer = new byte[1024];
        int len;
        while ((len = fis.read(buffer)) != -1) {
            fos.write(buffer, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    

    日志 @Log4j2

    java 有很多日志的框架,这里用就只以 Log4j2 为例了

    @Log4j2
    public class User {
    }
    

    会生成

    public class User {
        private static final Logger log = LogManager.getLogger(User.class);
    }
    

    @SneakyThrows

    算是一个比较有争议的,意思是近悄悄地抛出异常,要谨慎使用。check exception 会转成 unchecked 的。

    @SneakyThrows(FileNotFoundException.class)
    public static void read() {
        FileReader reader = new FileReader("sdf.txt");
    }
    

    调用的使用也不用再声明throws FileNotFoundException

    public static void main(String[] args)  {      
        read();
    }
    

    @Delegate

    利用 @Delegate 能非常方便地实现代理模式。下面用个场景介绍一下,比如:有台代理服务器,有台文件服务器,而我们只能通过代理服务去访问文件服务器。

    最终调用大概如此

    public class Main  {
        public static void main(String[] args) throws IOException {
            ProxyServer proxyServer = new ProxyServer();
            proxyServer.loadFile("avatar.png");
        }
    }
    

    假设获取接口是这样的

    public interface Server {
        byte[] loadFile(String fileName) throws IOException;
    }
    

    文件服务器的简单实现是如此

    public class FileServer implements Server {
    
        @Override
        public byte[] loadFile(String fileName) throws IOException {
            return Files.readAllBytes(new File(fileName).toPath());
        }
    }
    

    不用 lombok 时要这样

    public class ProxyServer implements Server{    
        FileServer realServer = new FileServer();
    
        @Override
        public byte[] loadFile(String fileName) throws IOException {
            return realServer.loadFile(fileName);
        }
    }
    

    代理服务器利用 @Delegate 即可,简单快捷

    public class ProxyServer implements Server{
        @Delegate
        FileServer realServer = new FileServer();
    }
    

    @Accessors

    使用 @Accessors 生成 Setter 会是链式的

    @Accessors(fluent = true)
    @Setter
    public class User {
        private Long id;
        private String name;
        private Integer age;
    
        public User() {
        }
    
        public User id(Long id) {
            this.id = id;
            return this;
        }
    
        public User name(String name) {
            this.name = name;
            return this;
        }
    
        public User age(Integer age) {
            this.age = age;
            return this;
        }
    }
    

    它有三个选项:(以 name 成员变量为例)

    • fluent: 默认值是 false。如果是 false ,setter 生成的函数是 String setName(String name); 如果是 true, setter 是生成的是 User name(String name) ,返回的是 this,是种链式 Setter,同时 chain 会是 true。
    • chain: 默认值是 false。如果是 true,setter 生成的函数是 User setName(String name)。否则的是void setName(String name)
    • prefix: 比如成员变量是 gk_name,加上注解 @Accessors(prefix = "gk_"),生成的 Getter 和 Setter 中则没有 gk_ 前缀

    @Wither

    用于修饰成员变量,可以根据用于被修饰的成员变量创建一个对象。

    @AllArgsConstructor
    public class User {
        private Long id;
        @Wither private String name;
        private Integer age;
    }
    

    生成后的代码

    public class User {
        private Long id;
        private String name;
        private Integer age;
    
        public User(Long id, String name, Integer age) {
            this.id = id;
            this.name = name;
            this.age = age;
        }
    
        public User withName(String name) {
            return this.name == name ? this : new User(this.id, name, this.age);
        }
    }
    

    原理

    或者你会好奇,这么神奇的魔法原理是什么呢?函数是如何生成的?

    想想编译原理 ,想想 java 文件编译成 class 文件的过程,
    Java 源码编译由以下三个过程组成:

    1. 词法分析、语法分,输出结果是 符号表 和 AST 语法树
    2. 注解处理
    3. 语义分析和生成 class 文件

    那么 lombok 是在那里添加要插入的代码呢?估计是注解处理处理部分吧。

    要如何生成呢?猜一下,估计可能是在注解处理时期 javac 能调用一个借口处理注解,我们并可以从中获取当 AST 树,然后就可以根据我们的想法,直接修改语法树了

    在网上搜索的时候找到这样一篇文章《Project Lombok: Creating Custom Transformations》stackoverflow 的 问题正好能解决我们的疑惑

    lombok 的执行过程如下图

    和我猜想的差不多

    上面一个答案说根据 JSR269 提案的 process 的 javax.annotation.processing.AbstractProcessor api 可能弄出来,还有一个回答说 lombok 做的东西比 process 多,还有针对 eclipse、javac 处理的 handle。找了一下文章看,确实如此,不仅设计的接口好,而且有丰富的例子可以给你参考。有关 java ast 的 jcTree 是有文档,但我只找到 api 文档,没什么例子。但我下面还是会用 a

    目标:单例模式

    关于单例模式,我在之前的博客中也探讨过了。
    我选取其中的一个例子吧。希望生成的结果是这样的

    public class SingletonRegistry {
        private SingletonRegistry() {}
         
        private static class SingletonRegistryHolder {
            private static SingletonRegistry instance = new SingletonRegistry();
        }
         
        public static SingletonRegistry getInstance() {
            return SingletonRegistryHolder.instance;
        }
         
        // other methods
    }
    

    而源码只需这样就可以了,

    @Singleton
    public class SingletonRegistry{}
    

    创建注解

    在 com.jojo.annotation.processor 目录下创建注解,其中

    • @Target(ElementType.TYPE) 意思是说注解只能修饰类,不能修饰方法、变量等,
    • @Retention(RetentionPolicy.SOURCE) 意思是说,注解保留范围是源代码,也就是在编译之后就看不到了,在 class 文件看不到,运行的时候用反射拿也就拿不到了
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Singleton {
    }
    

    HelloWorld

    万物起头 Hello World。我们要确认下,实现 AbstractProcessor 接口后。是如何编译、调用、调试的。
    在 com.jojo.annotation.processor 创建 SingletonProcessor 类

    @SupportedAnnotationTypes("com.jojo.annotation.processor.Singleton")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class SingletonProcessor extends AbstractProcessor {
    
        Messager messager;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            this.messager = processingEnv.getMessager();     //编译的时候用于输出
        }
    
         @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Singleton.class);
            set.forEach(element -> {
                note("hello world");
            });
            return true ;
        }
    
        //简单封装的函数
        private void note(String message){
            this.messager.printMessage(Diagnostic.Kind.NOTE, message);
        }
    }
    

    编译运行 命令行方式

    在 com.jojo 中创建用来测试的类 SingletonRegistry

    @Singleton
    public class SingletonRegistry {
    }
    

    编译是个问题,怎么编译呢?编译 SingletonRegistry.java 的时候就要编译器(javac)看到 SingletonProcessor 才能处理啊。。。也就是说编译 SingletonRegistry.java 要 SingletonProcessor 已经编译好了。

    先用命令行处理一下
    在 maven 项目的根目录下 创建 compile.sh

    #!/usr/bin/env bash
    if [ -d target ]; then
        rm -rf target;
    fi
    
    mkdir target
    
    source=src/main/java/com/jojo
    javac -cp $JAVA_HOME/lib/tools.jar  ${source}/annotation/*.java ${source}/processor/*.java -d target
    javac -cp target -processor com.jojo.processor.SingletonProcessor ${source}/SingletonRegistry.java -d target
    

    在命令行中执行 sh compile.sh 即可看到输出 Note: hello world

    debug

    因为真的不知道该如何 debug 。之前一直用上面的方式看输出进行调试。后面在 stackoverflower 上找到一篇文章了发现自己傻逼了

    我简单描述一下吧。在 meavn 上添加 google 的 auto-service 依赖

    1. 添加依赖

    <dependencies>
        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0-rc4</version>
        </dependency>
    </dependencies>
    

    这个东东能够在打包成 jar 时候自动生成 META-INF/services/javax.annotation.processing.Processor

    文件内容如下

    com.jojo.processor.SingletonProcessor
    

    javac 会检测 jar 中 javax.annotation.processing.Processor 文件,并将文件对应的类注册为 processor。编译的时候就会调用到。

    2. 添加 auto-service 注解

    在 SingletonProcessor 下添加注解 @AutoService(Processor.class)

    @SupportedAnnotationTypes("com.jojo.annotation.processor.Singleton")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    @AutoService(Processor.class)
    public class SingletonProcessor extends AbstractProcessor {
        //....
    }
    

    添加后会在 javax.annotation.processing.Processor 文件中写入被注解的类路径。

    3. maven 编译配置

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.3</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    

    4. 配置调试器

    配置参数结果如下

    5. 创建 src/test/java 下创建测试类

    类是以 Test 结尾,并添加上要用的注解

    @Singleton
    public class SingletonRegistryTest {
    }
    

    6. 在要调试的地方打上断点

    7. 在终端中输入 mvnDebug clean install

    这样会进入 meavn debug 模式。运行后会看到输出

    Preparing to execute Maven in debug mode
    Listening for transport dt_socket at address: 8000

    8. 在 idea 中点击运行按钮

    看到输出 Connected to the target VM, address: 'localhost:8000', transport: 'socket'
    也就连接成功,这样会停在你打断点的地方了

    SingletonProcessor

    回归正传,要生成一个上面所言的单例模式要怎样做呢?

    1. 获取注解对应的类的 AST 树
    2. 添加一个私有的无参数构造器
    3. 添加一个静态内联类,内联类里面要添加一个成员 instance 并完成初始化
    4. 添加一个成员函数,然后 instance

    看起来不难,但实际上还真的有点烦的,下面是代码的实现

    1. 添加依赖

    获取 AST 树要用到 sdk 中的tools.jar,所以要进入依赖

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

    2. 配置工具类

    public class SingletonProcessor 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); //AST 树
            Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
            this.treeMaker = TreeMaker.instance(context);    //封装了定义方法、变量、类等等的方法
            this.names = Names.instance(context);            //用于创建标识符
        }
    
        //还有更多的函数
    }
    

    3. 获取语法树

    public class SingletonProcessor extends AbstractProcessor {
        //...省略上面的
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            //获取被 Singleton 注解标注的类的集合
            Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Singleton.class);
    
            set.forEach(element -> {
                //获取到对应的 AST 树
                JCTree jcTree = trees.getTree(element);
            });
            return true;
        }
    }
    

    我打了个断点,你可以看到 jcTree 的定义是怎样的。我关注的地方是 defs, 现在可以 defs 只定义了一个构造函数、名字也独特叫 

    4. 创建构造函数

    这个函数的目的是去掉默认的公有的无参数的构造函数、添加一个私有的无参数构造函数

    private void createPrivateConstructor(JCTree.JCClassDecl singletonClass) {
        JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PRIVATE);
        JCTree.JCBlock block = treeMaker.Block(0L, nil());
        JCTree.JCMethodDecl constructor = treeMaker
                .MethodDef(
                        modifiers,                   //修饰符
                        names.fromString("<init>"),  //函数名
                        null,                        //方法返回的类型
                        nil(),                       //泛型参数
                        nil(),                       //参数
                        nil(),                       //throw
                        block,                       //函数代码块,这里是空代码块
                        null);                       //默认值
    
        //去掉默认的构造函数
        ListBuffer<JCTree> out = new ListBuffer<>();
        for (JCTree tree : singletonClass.defs) {
            if (isPublicDefaultConstructor(tree)) {//是否公有无参数的构造函数
                continue;
            }
            out.add(tree);
        }
        out.add(constructor);
        singletonClass.defs = out.toList();
    }
    
    private boolean isPublicDefaultConstructor(JCTree jcTree) {
        if (jcTree.getKind() == Tree.Kind.METHOD) {
            JCTree.JCMethodDecl jcMethodDecl = (JCTree.JCMethodDecl) jcTree;
            if (isConstructor(jcMethodDecl)
                    && isNoArgsMethod(jcMethodDecl)
                    && isPublicMethod(jcMethodDecl)) {
                return true;
            }
        }
        return false;
    }
    
    private static boolean isConstructor(JCTree.JCMethodDecl jcMethodDecl) {
        String name = jcMethodDecl.name.toString();
        if ("<init>".equals(name)) {
            return true;
        }
        return false;
    }
    
    private static boolean isNoArgsMethod(JCTree.JCMethodDecl jcMethodDecl) {
        List<JCTree.JCVariableDecl> jcVariableDeclList = jcMethodDecl.getParameters();
        if (jcVariableDeclList == null
                || jcVariableDeclList.size() == 0) {
            return true;
        }
        return false;
    }
    
    private  boolean isPublicMethod(JCTree.JCMethodDecl jcMethodDecl) {
        JCTree.JCModifiers jcModifiers = jcMethodDecl.getModifiers();
        Set<Modifier> modifiers = jcModifiers.getFlags();
        if (modifiers.contains(Modifier.PUBLIC)) {
            return true;
        }
        return false;
    }
    

    在 process 函数中处理构造函数

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Singleton.class);
        set.forEach(element -> {
            JCTree jcTree = trees.getTree(element);
            //修改 jcTree 的方式,可以修改类的定义
            jcTree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    createPrivateConstructor(jcClassDecl);
                }
            });
        });
        return true ;
    }
    

    在终端中输入 mvn clean install,编译成功后,你可以在 target/test-classes/ 中看到编译后的 SingletonRegistryTest。
    javap 反编译看一下 javap -p SingletonRegistryTest.class

    Compiled from "SingletonRegistryTest.java"
    public class SingletonRegistryTest {
      private SingletonRegistryTest();
    }
    

    5. 创建静态内联类

    要达到的结果是这样的

    private static class SingletonRegistryHolder {
        private static SingletonRegistry instance = new SingletonRegistry();
    }
    
    private JCTree.JCClassDecl createInnerClass(JCTree.JCClassDecl singletonClass) {
        JCTree.JCClassDecl innerClass = treeMaker.ClassDef(
                treeMaker.Modifiers(Flags.PRIVATE | Flags.STATIC),
                names.fromString(singletonClass.name+"Holder"),  //类名
                nil(),                                           //泛型参数
                null,                                            //extending
                nil(),                                           //implementing
                nil()                                            //类定义的详细语句,包括字段,方法定义等
        );
        addInstanceVar(innerClass, singletonClass);              //给类添加添加 instance变量
        singletonClass.defs = singletonClass.defs.append(innerClass);
        return innerClass;
    }
    
    private void addInstanceVar(JCTree.JCClassDecl innerClass, JCTree.JCClassDecl singletonClass) {
        JCTree.JCIdent singletonClassType = treeMaker.Ident(singletonClass.name); //获取注解的类型
        //new SingletonRegistry() 的语句
        JCTree.JCNewClass newKeyword = treeMaker.NewClass( null,                 //encl,enclosingExpression lambda 箭头吗?不太清楚
                                                           nil(),                //参数类型列表
                                                           singletonClassType,   //待创建对象的类型
                                                           nil(),                //参数蕾西
                                                           null);                //类定义
    
        JCTree.JCModifiers fieldMod = treeMaker.Modifiers(Flags.PRIVATE | Flags.STATIC | Flags.FINAL);
        //定义变量
        JCTree.JCVariableDecl instanceVar = treeMaker.VarDef(
                fieldMod,                                                        //修饰符
                names.fromString("instance"),                                    //变量名
                singletonClassType,                                              //类型
                newKeyword);                                                     //赋值语句
        innerClass.defs = innerClass.defs.prepend(instanceVar);
    }
    

    6. 创建一个成员函数,返回内联类中的 instance 变量

    目标完成

    public static SingletonRegistry getInstance() {
        return SingletonRegistryHolder.instance;
    }
    
    private void createReturnInstance(JCTree.JCClassDecl singletonClass,JCTree.JCClassDecl innerClass){
    
        JCTree.JCModifiers fieldMod = treeMaker.Modifiers(Flags.PUBLIC | Flags.STATIC);
    
        JCTree.JCIdent singletonClassType = treeMaker.Ident(singletonClass.name);
        //获取 return 语句块
        JCTree.JCBlock body = addReturnBlock(innerClass);
        //创建方法
        JCTree.JCMethodDecl  methodDec = treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC|Flags.STATIC), 
                                                            this.names.fromString("getInstance"), 
                                                            singletonClassType, nil(), nil(), nil(), body , null);
    
        singletonClass.defs = singletonClass.defs.prepend(methodDec);
    }
    
    
    private JCTree.JCBlock addReturnBlock(JCTree.JCClassDecl holderInnerClass) {
    
        JCTree.JCIdent holderInnerClassType = treeMaker.Ident(holderInnerClass.name);
    
        JCTree.JCFieldAccess instanceVarAccess = treeMaker.Select(holderInnerClassType, names.fromString("instance")); //获取内联类中的静态变量
        JCTree.JCReturn returnValue = treeMaker.Return(instanceVarAccess);//创建 return 语句
    
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        statements.append(returnValue);
    
        return treeMaker.Block(0L, statements.toList());
    }
    

    最后的 processor 函数像是这样的

    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Singleton.class);
    
        set.forEach(element -> {
            JCTree jcTree = trees.getTree(element);
            jcTree.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    createPrivateConstructor(jcClassDecl);
                    JCTree.JCClassDecl innerClass= createInnerClass(jcClassDecl);
                    createReturnInstance(jcClassDecl,innerClass);
                }
            });
        });
    
        return true;
    }
    

    完成了,mvn clean install 一下,使用 idea 查看对应的 .class 文件,生成的结果就好漂亮了。

    public class SingletonRegistryTest {
        public static SingletonRegistryTest getInstance() {
            return SingletonRegistryTest.SingletonRegistryTestHolder.instance;
        }
    
        private SingletonRegistryTest() {
        }
    
        private static class SingletonRegistryTestHolder {
            private static final SingletonRegistryTest instance = new SingletonRegistryTest();
    
            private SingletonRegistryTestHolder() {
            }
        }
    }
    
    # 参考链接
    - [lombok-custom-annotation](https://www.baeldung.com/lombok-custom-annotation)
    - [java-annotation-processing-builder](https://www.baeldng.com/java-annotation-processing-builder)
    - [lombok 指南](https://zhuanlan.zhihu.com/p/30318534)
  • 相关阅读:
    华为鲲鹏服务器测试
    gcc反汇编测试
    信息安全系统设计与实现:第五章学习笔记
    C语言实现ls之myls改进
    C语言编程实现mystat
    基于openEuler的OpenSSL编译安装和编程实践
    团队作业(三):确定分工
    centos的网络配置及克隆操作要点
    Flink特点分析
    机器学习之线性回归模型
  • 原文地址:https://www.cnblogs.com/jojo-feed/p/10631057.html
Copyright © 2011-2022 走看看