由lombok说起,浅析JSR-269原理及应用

lombok简介

在Java语言的项目开发中,存在着大量的样板代码。如实体类中大量的setter,getter,equals,HashCode,toString方法,即使idea可以自动快捷帮我们生成这些方法,但在增减字段时仍然需要重新去维护这些方法;又比如各种IO流等资源的关闭,try…catch…finally模式如此经典以至于成为了effective java中的编程法则,但也导致代码变得冗长,同时在业务代码中混杂着这些样板代码也降低了代码简洁性。这些冗长的样板代码也是Java语言长期被诟病的原因之一。

语言特性不够,第三方插件来凑,lombok插件正是为了减少这些样板代码而诞生的。简单来说,lombok提供了一组注解,在编译时帮我们生成这些样板代码,从而避免了手工维护这些代码的麻烦,同时lombok是非侵入式的,对于特殊情况如果我们需要自己实现equals,toString方法,那么lombok将自动跳过而不会覆盖我们自己的实现。

lombok的使用非常简单,以最常用的@Data注解为例,只需在实体类添加@Data注解,在代码编译时lombok将自动帮我们生成getter,setter,toString,equals hashCode方法;只需在实体类上添加@Builder注解,lombok将自动帮我们生成Builder构造器类;对于其他很多样板代码,lombok都提供了相应的注解用于自动生成,具体可参考官方文档: https://projectlombok.org/features/all.

@Data
@Builder
public class Role {
     
    private Long id;
    private String name;
    private String title;
    private String remark;
    private Date createDate;
    private Date updateDate;
}

JSR-269原理浅析

初次使用lombok时,都需要在idea安装lombok插件,这让我们怀疑lombok的实现是通过提供自己的编译器实现的,然而实际情况并非如此,在脱离idea使用javac编译时,只要类路径有lombok的jar包,项目也可以正常编译通过。其原理在于JSR-269规范。

Java6开始纳入了JSR-269规范:Pluggable Annotation Processing API(插件式注解处理器)。JSR-269提供一套标准API来处理Annotations,具体来说,我们只需要继承AbstractProcessor类,重写process方法实现自己的注解处理逻辑,并且在META-INF/services目录下创建javax.annotation.processing.Processor文件注册自己实现的Annotation Processor,在javac编译过程中编译器便会调用我们实现的Annotation Processor,从而使得我们有机会对java编译过程中生产的抽象语法树进行修改。

javac的编译过程,大致可以分为3个过程:

  1. 解析与填充符号表过程
  2. 插入式注解处理器的注解处理过程
  3. 分析与字节码生成过程

解析与填充符号表过程会将源码转换为一棵抽象语法树(Abstract Syntax Tree,AST),AST是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。在插入式注解处理器的注解处理过程中,lombok对第一步骤得到的AST进行处理,找到类似@Data注解所在类对应的语法树(AST),然后修改该语法树(AST),增加getter和setter等方法的相应树节点,javac使用修改后的抽象语法树(AST)生成字节码文件,因此最终生成的class文件中包含了这些自动生成的getter,setter方法。

实现自己的Annotation Processor

JSR-269允许我们修改编译过程,在编译期融入我们自己的处理逻辑,这给我们提供了很灵活的运用空间,如利用JSR-269我们可以实现自己的代码规范检查插件,在编辑时检查代码规范,不符合规范则编译不通过;利用JSR-269我们可以实现自己的代码生成器,对于常见的模板化代码通过配置即可自动生成。

public interface AdNotifyV2Mapper {
     

    int addNotify(@Param("adNotify") AdNotify adNotify);

    AdNotify getLatestAndroidNotify(@Param("adApp") AdApp adApp,
                                    @Param("imeiMd5") String imeiMd5,
                                    @Param("oaidMd5") String oaidMd5,
                                    @Param("androidIdMd5") String androidIdMd5);

    AdNotify getLatestIosNotify(@Param("adApp") AdApp adApp, 
                                @Param("idfaMd5") String idfaMd5);

    int updateCallback(@Param("id") long id,
                       @Param("matchTime") Date matchTime,
                       @Param("isMatch") long isMatch,
                       @Param("isCallback") long isCallback);
}

上边的DAO类是我们日常开发中很常见的一个mybatis DAO类,可以看到对于每一个方法参数,我们都必须机械式地在每个参数前边加上@Param(“xxx”)注解,这使得方法参数列表变得又丑又长,这是因为javac在编译过程中默认不会保留方法参数名,因此mybatis在运行时无法获取到我们定义的参数名称,只能用注解在前边把方法参数名又声明了一遍。

有没有办法不加上这个@Param呢,一种方法是SQL语句中不使用实际参数名,而是使用#{param1}, #{param2}这种名称,如下所示,这是因为mybatis在解析参数过程中自动将参数绑定到param1,param2,param3…这样的名称,这种做法的缺点是一定程度上降低了SQL语句的可读性。

@Select({
     "select * from sys_agent_info where agent_name=#{param1} and status=#{param2}"})
AgentInfo findByNameAndStatus(String name, Integer status);

第二种方法是利用Java8的一项新的特性——在class文件中保留参数名。只需要在javac编译时加上 -parameters参数,在生成的字节码将保留原始的方法参数名,同时配合使用将mybatis中useActualParamName参数设置为true,即可不加@Param注解直接使用实际方法参数名。这种方法的缺点是需要修改javac编译参数,如果一些编译环境忘记修改编译参数将导致运行时报错。

有没有其他更好的解决方法呢,由前文可知利用JSR-269我们可以修改在编译过程中生成的抽象语法树,因此我们可以编写自己的Annotation Processor,在编译过程中自动在方法参数前边添加@Param注解。下边以此为例子展示一个Annotation Processor的开发流程

  1. 创建maven项目,定义自己的注解类UseActualParam,注解的RetentionPolicy指定为SOURCE;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface UseActualParam {
     
}
  1. 开发Annotation Processor,继续AbstractProcessor类,实现process方法
@SupportedAnnotationTypes("com.kugou.jsr269.UseActualParam")
public class MybatisParamProcessor extends AbstractProcessor {
     

    /**
     * JavacTrees提供了待处理的抽象语法树
     * TreeMaker中了一些操作抽象语法树节点的方法
     * Names提供了创建标识符的方法
     */
    private JavacTrees trees;
    private TreeMaker treeMaker;
    private Names names;

    @Override
    public SourceVersion getSupportedSourceVersion() {
     
        return SourceVersion.RELEASE_8;
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
     
        super.init(processingEnv);

        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) {
     
        roundEnv.getElementsAnnotatedWith(UseActualParam.class).stream()
                .map(element -> trees.getTree(element))
                .forEach(tree -> tree.accept(new TreeTranslator() {
     
                    @Override
                    public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
     
                        prependParamAnnotation(jcClassDecl);
                        super.visitClassDef(jcClassDecl);
                    }
                }));
        return true;
    }

    /**
     * 在DAO方法参数前边追加@Param注解
     */
    private void prependParamAnnotation(JCTree.JCClassDecl jcClassDecl) {
     
        jcClassDecl.defs.stream()
                .filter(element -> element.getKind().equals(Tree.Kind.METHOD))
                .map(methodTree -> (JCTree.JCMethodDecl) methodTree)
                .forEach(methodTree -> {
     
                    methodTree.getParameters().forEach(parameter -> {
     
                        JCTree.JCAnnotation paramAnnotation = createParamAnnotation(parameter);
                        parameter.getModifiers().annotations.append(paramAnnotation);
                    });
                });
    }

    /**
     * 创建@Param注解对应的语法树对象
     */
    private JCTree.JCAnnotation createParamAnnotation(JCTree.JCVariableDecl parameter) {
     
        return treeMaker.Annotation(
                treeMaker.Ident(names.fromString("Param")),
                List.of(treeMaker.Assign(treeMaker.Ident(names.fromString("value")), treeMaker.Literal(parameter.name.toString()))));
    }

}

  1. 开发完Annotation Processor后,在resources/META-INF/services创建名为javax.annotation.processing.Processor的文本文件,内容填写自己实现的Annotation Processor的全限定类名
  2. 编译项目并deploy jar包到maven仓库,在其他项目引入该jar包,在项目编译时即可自动帮我们在DAO方法参数前边生成@Param注解。如下图所示,DAO源文件没有加@Param注解,将编译后的DAO类文件反编译,可以看到方法参数前边已经自动加上@Param注解

由lombok说起,浅析JSR-269原理及应用_第1张图片
上边的示例只是JSR-269规范的一个简单应用,在实际应用中可以做其他更复杂的事情,比如在DAO类中通常存在大量insert,batchInsert,updateById,findByIds等简单CRUD样板代码,这些代码开发麻烦又容易出错,完全可以实现自己的注解处理器,在编译时自动生成SQL语句,DAO类中只需编写符合命名规范的方法名即可。

你可能感兴趣的:(java,mybatis,jdk)