基于注解处理器开发自动生成getter和setter方法的插件

简介: 基于注解处理器开发自动生成getter和setter方法的插件

昨天无意中,逛到了lombok的网站,并看到了首页的5分钟视频,视频中的作者只是在实体类中写了几个字段,就可以自动编译为含setter、getter、toString()等方法的class文件。看着挺新奇的,于是自己研究了一下原理,整理下发出来。


何处下手


(1) 编写Java文件,在类上写@Data注解


@Data
public class Demo {
    private String name;
    private double abc;
}


(2) javac编译,lombok.jar是lombok的jar包。


javac -cp lombok.jar Demo.java


(3) javap查看Demo.class类文件


javap Demo


Demo.class:


public class Demo {
  public Demo();
  public java.lang.String getName();
  public void setName(java.lang.String);
  public double getAbc();
  public void setAbc(double);
}


可以看到Demo.class内部竟然多了很多未定义的setter、getter方法,而视频作者主要使用的就是注解+编译,那么我们就从这方面入手。


必备知识


注解


注解,相信大部分人都用过,不少人还会自定义注解,并会利用反射等搞点小东西。但本文所讲的并非是利用注解加反射在运行期自定义行为,而是在编译期。

自定义注解离不开四大元注解。


@Retention:注解保留时期


保留类型 说明
SOURCE 只保留到源码中,编译出来的class不存在
CLASS 保留到class文件中,但是JVM不会加载
RUNTIME 一直存在,JVM会加载,可用反射获取


@Target:用于标记可以应用于哪些类型上

元素类型 适用场合
ANOTATION_TYPE 注解类型声明
PACKAGE
TYPE 类,枚举,接口,注解
METHOD 方法
CONSTRUCTOR 构造方法
FIELD 成员域,枚举常量
PARAMETER 方法或构造器参数
LOCAL_VARIABLE 局部变量
TYPE_PARAMETER 类型参数
TYPE_USE 类型用法


@Documented:作用是能够将注解中的元素包含到 Javadoc 中


@Inherited:继承。假设注解A使用了此注解,那么类B使用了注解A,类C继承了类B,那么类C也使用了注解A。(这里的使用是为了区分易理解,实际为被注解


注解处理器


注解处理器就是 javac 包中专门用来处理注解的工具。所有的注解处理器都必须继承抽象类AbstractProcessor然后重写它的几个方法。


注解处理器是运行在它自己的JVM中。javac 启动一个完整Java虚拟机来运行注解处理器,这意味着你可以使用任何你在其他java应用中使用的的东西。其中抽象方法process是必须要重写的,再该方法中注解处理器可以遍历所有的源文件,然后通过RoundEnvironment类获取我们需要处理的注解所标注的所有的元素,这里的元素可以代表包,类,接口,方法,属性等。


再处理的过程中可以利用特定的工具类自动生成特定的.java文件或者.class文件,来帮助我们处理自定义注解。


一个普通的注解处理器文件如下:


package com.example;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
public class MyProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annoations,
            RoundEnvironment env) {
        return false;
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add("com.example.MyAnnotation");
        return annotataions;
    }
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }
}


  • init(ProcessingEnvironment processingEnv)
    所有的注解处理器类都必须有一个无参构造函数。然而,有一个特殊的方法init(),它会被注解处理工具调用,以ProcessingEnvironment作为参数。ProcessingEnvironment 提供了一些实用的工具类Elements, TypesFiler

  • process(Set annoations, RoundEnvironment env)
    这类似于每个处理器的main()方法。你可以在这个方法里面编码实现扫描,处理注解,生成 java 文件。使用RoundEnvironment 参数,你可以查询被特定注解标注的元素。

  • getSupportedAnnotationTypes()
    在这个方法里面你必须指定哪些注解应该被注解处理器注册。注意,它的返回值是一个String集合,包含了你的注解处理器想要处理的注解类型的全称。换句话说,你在这里定义你的注解处理器要处理哪些注解。

  • getSupportedSourceVersion()
    用来指定你使用的 java 版本。通常你应该返回SourceVersion.latestSupported() 。不过,如果你有足够的理由坚持用 java 6 的话,你也可以返回SourceVersion.RELEASE_6


关于getSupportedAnnotationTypes()getSupportedSourceVersion()这两个方法,你也可以使用相应注解进行代替。代码如下:


@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.MyAnnotation")
public class MyProcessor extends AbstractProcessor {
....


不过为了兼容Java6,最好是重载这俩方法。


开始编码


知识我们已经学会,现在开始实战。


自定义注解


@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Data {
}


自定义注解处理器


public class DataAnnotationProcessor extends AbstractProcessor {
    private Messager messager; //用于打印日志
    private Elements elementUtils; //用于处理元素
    private Filer filer;  //用来创建java文件或者class文件
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
    }
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    @Override
    public Set<String> getSupportedAnnotationTypes(){
        Set<String> set = new HashSet<>();
        set.add(Data.class.getCanonicalName());
        return Collections.unmodifiableSet(set);
    }
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        messager.printMessage(Diagnostic.Kind.NOTE,"-----开始自动生成源代码");
        try {
            // 标识符
            boolean isClass = false;
            // 类的全限定名
            String classAllName = null;
            // 返回被注释的节点
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Data.class);
            Element element = null;
            for (Element e : elements) {
                // 如果注释在类上
                if (e.getKind() == ElementKind.CLASS && e instanceof TypeElement) {
                    TypeElement t = (TypeElement) e;
                    isClass = true;
                    classAllName = t.getQualifiedName().toString();
                    element = t;
                    break;
                }
            }
            // 未在类上使用注释则直接返回,返回false停止编译
            if (!isClass) {
                return true;
            }
            // 返回类内的所有节点
            List<? extends Element> enclosedElements = element.getEnclosedElements();
            // 保存字段的集合
            Map<TypeMirror, Name> fieldMap = new HashMap<>();
            for (Element ele : enclosedElements) {
                if (ele.getKind() == ElementKind.FIELD) {
                    //字段的类型
                    TypeMirror typeMirror = ele.asType();
                    //字段的名称
                    Name simpleName = ele.getSimpleName();
                    fieldMap.put(typeMirror, simpleName);
                }
            }
            // 生成一个Java源文件
            JavaFileObject sourceFile = filer.createSourceFile(getClassName(classAllName));
            // 写入代码
            createSourceFile(classAllName, fieldMap, sourceFile.openWriter());
            // 手动编译
            compile(sourceFile.toUri().getPath());
        } catch (IOException e) {
            messager.printMessage(Diagnostic.Kind.ERROR,e.getMessage());
        }
        messager.printMessage(Diagnostic.Kind.NOTE,"-----完成自动生成源代码");
        return true;
    }
    private void createSourceFile(String className, Map<TypeMirror, Name> fieldMap, Writer writer) throws IOException {
        // 生成源代码
        JavaWriter jw = new JavaWriter(writer);
        jw.emitPackage(getPackage(className));
        jw.beginType(getClassName(className), "class", EnumSet.of(Modifier.PUBLIC));
        for (Map.Entry<TypeMirror, Name> map : fieldMap.entrySet()) {
            String type = map.getKey().toString();
            String name = map.getValue().toString();
            //字段
            jw.emitField(type, name, EnumSet.of(Modifier.PRIVATE));
        }
        for (Map.Entry<TypeMirror, Name> map : fieldMap.entrySet()) {
            String type = map.getKey().toString();
            String name = map.getValue().toString();
            //getter
            jw.beginMethod(type, "get" + humpString(name), EnumSet.of(Modifier.PUBLIC))
                    .emitStatement("return " + name)
                    .endMethod();
            //setter
            jw.beginMethod("void", "set" + humpString(name), EnumSet.of(Modifier.PUBLIC), type, "arg")
                    .emitStatement("this." + name + " = arg")
                    .endMethod();
        }
        jw.endType().close();
    }
    /**
     * 编译文件
     * @param path
     * @throws IOException
     */
    private void compile(String path) throws IOException {
        //拿到编译器
        JavaCompiler complier = ToolProvider.getSystemJavaCompiler();
        //文件管理者
        StandardJavaFileManager fileMgr =
                complier.getStandardFileManager(null, null, null);
        //获取文件
        Iterable units = fileMgr.getJavaFileObjects(path);
        //编译任务
        JavaCompiler.CompilationTask t = complier.getTask(null, fileMgr, null, null, null, units);
        //进行编译
        t.call();
        fileMgr.close();
    }
    /**
     * 驼峰命名
     *
     * @param name
     * @return
     */
    private String humpString(String name) {
        String result = name;
        if (name.length() == 1) {
            result = name.toUpperCase();
        }
        if (name.length() > 1) {
            result = name.substring(0, 1).toUpperCase() + name.substring(1);
        }
        return result;
    }
    /**
     * 读取类名
     * @param name
     * @return
     */
    private String getClassName(String name) {
        String result = name;
        if (name.contains(".")) {
            result = name.substring(name.lastIndexOf(".") + 1);
        }
        return result;
    }
    /**
     * 读取包名
     * @param name
     * @return
     */
    private String getPackage(String name) {
        String result = name;
        if (name.contains(".")) {
            result = name.substring(0, name.lastIndexOf("."));
        }else {
            result = "";
        }
        return result;
    }
}


在自定义注解处理器中,注释非常详细的说明了每一步的思路,首先是读取被注释的节点,判断是否是类节点,然后生成Java源文件,并使用javawriter框架写入Java代码,最后手动编译该java源文件。


javawriter框架引用如下:


compile 'com.squareup:javawriter:2.5.1'


注解处理器的注册


编码结束后,还需要把注解处理器注册到javac编译器,所以需要提供一个 .jar 文件。就像其他 .jar 文件一样,你将你已经编译好的注解处理器打包到此文件中。并且,在你的 .jar 文件中,你必须打包一个特殊的文件javax.annotation.processing.Processor到META-INF/services目录下。因此你的 .jar 文件目录结构看起来就你这样:


MyProcess.jar
    -com
        -example
            -MyProcess.class
    -META-INF
        -services
            -javax.annotation.processing.Processor


javax.annotation.processing.Processor 文件的内容是一个列表,每一行是一个注解处理器的全称。例如:


com.example.MyProcess


在IDE中,只需在resources目录下新建META-INF/services/javax.annotation.processing.Processor文件即可。


其它注册方式


前面的注册方式很底层,个人推荐使用。当处理的注解处理器过多时,这种方式不免过于繁琐,所以另一种方式就是使用自动注册注解处理器的框架。


添加对谷歌自动注册注解库的引用


implementation ‘com.google.auto.service:auto-service:1.0-rc4’


在注解处理器类前面声明


@AutoService(Processor.class)

打包使用


此时我们把项目打包为jar包即可使用,下面演示下使用过程。



(1) 写个Demo.java


import cn.zyzpp.annotation.Data;
@Data
public class Demo {
    private String name;
    private double abc;
}


(2) 编译java文件,在该Demo.java文件夹下打开控制台窗口,记得把打包的jar包一起放在此目录。


javac -cp annotation-1.0-SNAPSHOT.jar Demo.java


(3) 使用javap查看编译后的Demo.class


Compiled from "Demo.java"
public class Demo {
  public Demo();
  public double getAbc();
  public void setAbc(double);
  public java.lang.String getName();
  public void setName(java.lang.String);
}


再看此时的Demo.java代码


public class Demo {
  private double abc;
  private String name;
  public double getAbc() {
    return abc;
  }
  public void setAbc(double arg) {
    this.abc = arg;
  }
  public String getName() {
    return name;
  }
  public void setName(String arg) {
    this.name = arg;
  }
}


到此,我们正式开发出了自动生成getter、setter方法的插件。有的人可能觉得这个也很没多大用,用IDE快捷键就能办到,对此我只能说你的举一反三能力需要提高。知识已经教会你了,能做出什么多姿多彩的框架就靠小伙伴们的智慧了。比如,我们一般都会新建Entity类,然后基于此新建Dao层,Service层代码,用本文锁所述知识足可以打造一款适合自己的代码生成器,节约时间,提高开发效率。

目录
相关文章
|
11月前
|
人工智能 自然语言处理 Java
对话即服务:Spring Boot整合MCP让你的CRUD系统秒变AI助手
本文介绍了如何通过Model Context Protocol (MCP) 协议将传统Spring Boot服务改造为支持AI交互的智能系统。MCP作为“万能适配器”,让AI以统一方式与多种服务和数据源交互,降低开发复杂度。文章以图书管理服务为例,详细说明了引入依赖、配置MCP服务器、改造服务方法(注解方式或函数Bean方式)及接口测试的全流程。最终实现用户通过自然语言查询数据库的功能,展示了MCP在简化AI集成、提升系统易用性方面的价值。未来,“对话即服务”有望成为主流开发范式。
7994 7
|
SQL Java 数据库连接
如何在 Java 代码中使用 JSqlParser 解析复杂的 SQL 语句?
大家好,我是 V 哥。JSqlParser 是一个用于解析 SQL 语句的 Java 库,可将 SQL 解析为 Java 对象树,支持多种 SQL 类型(如 `SELECT`、`INSERT` 等)。它适用于 SQL 分析、修改、生成和验证等场景。通过 Maven 或 Gradle 安装后,可以方便地在 Java 代码中使用。
4175 11
|
Java 编译器 Maven
一文解读|Java编译期注解处理器AbstractProcessor
本文围绕编译器注解都是如何运行的呢? 又是怎么自动生成代码的呢?做出了详细介绍。
|
Unix Linux 开发工具
git中有关old mode 100644、new mode 10075的问题解决小结
在 Git 中处理文件权限变更时,理解 `old mode 100644` 和 `new mode 100755` 的含义是解决问题的关键。通过确认变更的合理性、修改不必要的权限变更,以及配置 Git 忽略权限变更,可以有效管理文件权限,确保版本库的稳定性和一致性。
1536 3
|
域名解析 JavaScript 网络协议
技术心得记录:如何使用google地图的api(整理)
技术心得记录:如何使用google地图的api(整理)
1653 0
|
SQL 分布式计算 Hadoop
Hadoop-14-Hive HQL学习与测试 表连接查询 HDFS数据导入导出等操作 逻辑运算 函数查询 全表查询 WHERE GROUP BY ORDER BY(一)
Hadoop-14-Hive HQL学习与测试 表连接查询 HDFS数据导入导出等操作 逻辑运算 函数查询 全表查询 WHERE GROUP BY ORDER BY(一)
293 4
|
缓存 NoSQL Java
Springboot自定义注解+aop实现redis自动清除缓存功能
通过上述步骤,我们不仅实现了一个高度灵活的缓存管理机制,还保证了代码的整洁与可维护性。自定义注解与AOP的结合,让缓存清除逻辑与业务逻辑分离,便于未来的扩展和修改。这种设计模式非常适合需要频繁更新缓存的应用场景,大大提高了开发效率和系统的响应速度。
636 2
|
机器学习/深度学习 安全 API
爱回收平台技术揭秘:构建高效、安全、用户友好的二手物品回收生态系统
爱回收利用微服务架构打造高效安全的二手电子回收平台。系统通过API Gateway处理前端请求,各微服务独立处理业务逻辑,如商品评估、订单创建和支付结算,采用机器学习算法预估价格。安全策略包括OAuth2.0授权、数据加密、访问控制和DDoS防护。性能优化涉及缓存、负载均衡及数据库优化,提供便捷、透明的服务,促进可持续发展。
813 1
|
存储 算法 Java
Java-数据结构(二)-Map:HashMap、TreeMap、LinkedHashMap
Java-数据结构(二)-Map:HashMap、TreeMap、LinkedHashMap
468 1
|
安全 Java 测试技术
使用Spring Cloud Zuul实现过滤器或拦截器功能案例
使用Spring Cloud Zuul实现过滤器或拦截器功能案例
481 0

热门文章

最新文章