当前位置: 首页 > 后端技术 > Java

项目终于用上了插件注解,太棒了!

时间:2023-04-01 14:17:06 Java

插件annotationprocessor在本书《深入理解Java虚拟机》中有一些介绍(在前端编译文章中有提到),但一直没用过,在这里做一个记录。了解过lombok底层原理的人都知道,它使用的是插件注解,所以今天我将在真实场景中演示插件注解的使用。需求我们为公司提供一套通用的JAVA基础组件包。组件包中有不同的模块,比如fuse模块、loadaveraging模块、rpc模块等,这些模块会被打包成jar包,然后发布到公司内部代码库中,供其他人导入使用。这段代码会不断迭代。我们希望可以使用promethus来监控公司中使用各种版本的代码库比例。想要的效果如下:我们希望看到各个版本的使用率,有利于我们做版本兼容,必要时可以追溯早期版本的用户来源。问题需求看似很简单,但是真正拿到自己的jar版本号还是挺麻烦的。有一种比较简单但是地狱般的方式,就是给每个组件加上当前的jar版本号,写在配置文件中或者直接设置为常量,这样就可以直接获取到jar包的版本号了向普罗米修斯报告。这种方法虽然可以解决问题,但是每次迭代都要更改所有组件包的版本号数据,太麻烦了。有更好的解决方案吗?比如我们能否在gradle打包构建的时候获取到jar包的版本号,然后注入到各个组件中呢?就像lombok一样,不需要写get和set方法,只需要添加注释标记就可以自动注入get和set方法。例如,我们可以为每个组件定义一个空常量,并添加自定义注解:@TrisceliVersionpublicstaticfinalStringversion="";复制代码并注入真实的版本号,就像lombok生成set/get方法:@TrisceliVersionpublicstaticfinalStringversion="1.0.31-SNAPSHOT";参考lombok的实现复制代码。这实际上是可能的。让我们看看下面的解决方案。java中解析注解的方式主要有两种:编译时扫描和运行时反射。这是lombok@Setter的实现:@Target({ElementType.FIELD,ElementType.TYPE})@Retention(RetentionPolicy.SOURCE)public@interfaceSetter{//略...}复制代码可以看到@Setter保留的是SOURCE类型,也就是说这个注解只在编译时有效,甚至不会被编译到class文件中,所以lombok无疑是第一个解析的方法,我们可以用什么方法来制作注释被解析并在编译时执行我们的解析代码?答案是定义插件注释处理器(由JSR-269提案定义的PluggableAnnotationProcessingAPI实现)。插件注解处理器的触发点如下图所示:也就是说插件注解处理器可以帮我们修改抽象语法树(AST)!所以现在我们只需要自定义这样一个处理器,然后获取里面的jar版本信息即可(因为是编译期,可以找到源码的路径,在源码中创建一个文件存放版本即可number,然后在That'sit)中使用javaio读取即可,然后将注解对应的语法树上的常量值设置为jar包的版本号。当语法树发生变化时,最终生成的字节码也会随之变化。这实现了我们想要在编译时注入常量版本的愿望。定制一个插件注解处理器也很简单。首先定义自己的注解:@Documented@Retention(RetentionPolicy.SOURCE)//只在编译时有效,最后不会进入class文件@Target({ElementType.FIELD})//只允许作用于类属性public@interfaceTrisceliVersion{}复制代码,定义一个继承AbstractProcessor的处理器:/**{@linkAbstractProcessor}属于PluggableAnnotationProcessingAPI*/publicclassTrisceliVersionProcessorextendsAbstractProcessor{privateJavacTreesjavacTrees;privateTreeMakertreeMaker;privateProcessingEnvironmentprocessingEnv;/***初始化处理器**@paramprocessingEnv提供一系列实用工具*/@SneakyThrows@OverridepublicEnchronizedEnchronizedcproviit{super.init(processingEnv);this.processingEnv=processingEnv;this.javacTrees=JavacTrees.instance(processingEnv);上下文context=((JavacProcessingEnvironment)processingEnv).getContext();this.treeMaker=TreeMaker.instance(context);}@OverridepublicSourceVersiongetSupportedSourceVersion(){returnSourceVersion.latest();}@OverridepublicSetgetSupportedAnnotationTypes(){HashSetset=newHashSet<>();set.add(TrisceliVersion.class.getName());//支持解析的注解returnset;}@Overridepublicbooleanprocess(Setannotations,RoundEnvironmentroundEnv){for(TypeElementt:annotations){for(Elemente:roundEnv.getElementsAnnotatedWith(t)){//获取具有给定注解的元素(元素可以是类、方法、包等)//JCVariableDecl为字段/变量定义语法树节点StringvarType=jcv.vartype.type.toString();if(!"java.lang.String".equals(varType)){//限定的变量类型必须是String类型,否则会抛出异常printErrorMessage(e,"Type'"+varType+"'"+"不支持。");}jcv。init=treeMaker.Literal(getVersion());//给这个字段赋值,就是getVersion的返回值}}returntrue;}/***使用processingEnv中的Messager对象输出一些日志**@parameelement*@paramm错误信息*/privatevoidprintErrorMessage(Elemente,Stringm){processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,m,e);}privateStringgetVersion(){/***获取版本,这里省略复杂的代码,直接返回固定值*/return"v1.0.1";}复制定义的处理器需要SPI机制被发现,所以需要定义META.services:Test新建一个测试模块,导入刚才写的代码包:这是Test类:现在我们只需要让gradle构建它,在新获取的字段中字节码会有价值:这只是插件注解处理器功能的冰山一角,既然可以通过修改抽象语法树来控制生成的字节码,那么自然有人可以充分利用它的特性来实现一些很酷的插件,比如lombok,我们不用再写set/get之类的模板代码,只要我们足够有创意,就可以让基于这套API的插件有很大的发挥空间