服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

项目终于用上了插入式注解,真香!

日期: 来源:石杉的架构笔记收集编辑:不才陈某
「 关注“石杉的架构笔记”,大厂架构经验倾囊相授 


文章来源:【公众号:码猿技术专栏】




前言


插入式注解处理器在《深入理解Java虚拟机》一书中有一些介绍(前端编译篇有提到),但一直没有机会使用,直到碰到这个需求,觉得再合适不过了,就简单用了一下,这里做个记录。

了解过lombok底层原理的都知道其使用的就是的插入式注解,那么今天笔者就以真实场景演示一下插入式注解的使用。


需求





我们为公司提供了一套通用的JAVA基础组件包,组件包内有不同的模块,比如熔断模块、负载均模块、rpc模块等等,这些模块均会被打成jar包,然后发布到公司的内部代码仓库中,供其他人引入使用。

这份代码会不断的迭代,我们希望可以通过promethus来监控现在公司内使用各版本代码库的比例,希望达到的效果图如下:

我们希望看到每一个版本的使用率,这有利于我们做版本兼容,必要的时候可以对古早版本使用者溯源。


问题





需求似乎很简单,但真要获取自身的jar版本号还是挺麻烦的,有个比较简单但阴间的办法,就是给每一个组件都加上当前的jar版本号,写到配置文件里或者直接设置成常量,这样上报promethus时就可以直接获取到jar包版本号了,这个方法虽然可以解决问题,但每次迭代版本都要跟着改一遍所有组件包的版本号数据,过于麻烦。

有没有更好的解决办法呢?比如我们可不可以在gradle打包构建时拿到jar包的版本号,然后注入到每个组件中去呢?就像lombok那样,不需要写get、set方法,只需要加个注解标记就可以自动注入get、set方法。

比如我们可以给每个组件定义一个空常量,加上自定义的注解:

@TrisceliVersion
public static final String version = "";

然后像lombok生成set/get方法那样注入真正的版本号:

@TrisceliVersion
public static final String version = "1.0.31-SNAPSHOT";

参考lombok的实现,这其实是可以做到的,下面来看解决方案。


解决





java中解析一个注解的方式主要有两种:编译期扫描、运行期反射,这是lombok @Setter的实现:

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

可以看到@SetterRetentionSOURCE类型的,也就是说这个注解只在编译期有效,它甚至不会被编入class文件,所以lombok无疑是第一种解析方式,那用什么方式可以在编译期就让注解被解析到并执行我们的解析代码呢?答案就是定义插入式注解处理器(通过JSR-269提案定义的Pluggable Annotation Processing API实现)

插入式注解处理器的触发点如下图所示:

也就是说插入式注解处理器可以帮助我们在编译期修改抽象语法树(AST)!所以现在我们只需要自定义一个这样的处理器,然后其内部拿到jar版本信息(因为是编译期,可以找到源码的path,源码里随便搞个文件存放版本号,然后用java io读取进来即可),再将注解对应语法树上的常量值设置成jar包版本号,语法树变了,最终生成的字节码也会跟着变,这样就实现了我们想在编译期给常量version注入值的愿望。

自定义一个插入式注解处理器也很简单,首先要将自己的注解定义出来:

@Documented
@Retention(RetentionPolicy.SOURCE) //只在编译期有效,最终不会打进class文件中
@Target({ElementType.FIELD}) //仅允许作用于类属性之上
public @interface TrisceliVersion {
}

然后定义一个继承了AbstractProcessor的处理器:

/**
 * {@link AbstractProcessor} 就属于 Pluggable Annotation Processing API
 */
public class TrisceliVersionProcessor extends AbstractProcessor {

    private JavacTrees javacTrees;
    private TreeMaker treeMaker;
    private ProcessingEnvironment processingEnv;

    /**
     * 初始化处理器
     *
     * @param processingEnv 提供了一系列的实用工具
     */
    @SneakyThrows
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.processingEnv = processingEnv;
        this.javacTrees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
    }


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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> set = new HashSet<>();
        set.add(TrisceliVersion.class.getName()); // 支持解析的注解
        return set;
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement t : annotations) {
            for (Element e : roundEnv.getElementsAnnotatedWith(t)) { // 获取到给定注解的element(element可以是一个类、方法、包等)
                // JCVariableDecl为字段/变量定义语法树节点
                JCTree.JCVariableDecl jcv = (JCTree.JCVariableDecl) javacTrees.getTree(e);
                String varType = jcv.vartype.type.toString();
                if (!"java.lang.String".equals(varType)) { // 限定变量类型必须是String类型,否则抛异常
                    printErrorMessage(e, "Type '" + varType + "'" + " is not support.");
                }
                jcv.init = treeMaker.Literal(getVersion()); // 给这个字段赋值,也就是getVersion的返回值
            }
        }
        return true;
    }

    /**
     * 利用processingEnv内的Messager对象输出一些日志
     *
     * @param e element
     * @param m error message
     */
    private void printErrorMessage(Element e, String m) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, m, e);
    }

    private String getVersion() {
        /**
         * 获取version,这里省略掉复杂的代码,直接返回固定值
         */
        return "v1.0.1";
    }

定义好的处理器需要SPI机制被发现,所以需要定义META.services


测试





新建测试模块,引入刚才写好的代码包:

这是Test类:

现在我们只需要让gradle build一下,新得到的字节码中该字段就有值了:

这只是插入式注解处理器功能的冰山一角,既然它可以通过修改抽象语法树来控制生成的字节码,那么自然就有人能充分利用其特性来实现一些很酷的插件,比如lombok,我们再也不用写诸如set/get这种模板式的代码了,只要我们足够有创意,就可以让基于这一套API实现的插件在功能上有很大的发挥空间。

欢迎扫码加入儒猿技术交流群,每天晚上20:00都有Java面试、Redis、MySQL、RocketMQ、SpringCloudAlibaba、Java架构等技术答疑分享,更能跟小伙伴们一起交流技术


另外推荐儒猿课堂的9.9元系列课程给您,欢迎加入一起学习~


互联网Java工程师面试突击课
(9.9元专享)

SpringCloudAlibaba零基础入门到项目实战
9.9元专享)

亿级流量下的电商详情页系统实战项目
9.9元专享)

Kafka消息中间件内核源码精讲
9.9元专享)

12个实战案例带你玩转Java并发编程
9.9元专享)

Elasticsearch零基础入门到精通
9.9元专享)

基于Java手写分布式中间件系统实战
9.9元专享)

基于ShardingSphere的分库分表实战课
9.9元专享)

相关阅读

  • 有些事,身体会替你记得

  • 附件1:《护理小贴士》附件2:《参考资料》[1] 张悦, 王献珍. 感染对伤口愈合的阶段和影响[J]. 临床医学进展, 2022, 12(7): 6039-6044. https://doi.org/10.12677/ACM.2022.1
  • Windows系统管家,中文绿色便携版~

  • “设为星标”第一时间接收推送,精彩内容不容错过!Windows 10/11 Manager专门用于微软 Windows ,集清理、优化、安全、网络等功能于一身的实用工具,可以让你的系统实现个性化,提升
  • LOOK | 别人家的大学 · 深圳北理莫斯科大学

  • 高悬的“深北莫之星”哥特式塔楼、俄罗斯典型建筑风格徜徉校园感受自成一派的俄式学院风情…本期,LOOK带你一起走进中俄首所合作大学「 深圳北理莫斯科大学 」01. SMBU「 这
  • 发条月亮 2023「天体引力」全国巡演正式启动!

  • 我们知道天空充满信号却从未真正了解他们从蛮荒之地爬上星梯与无尽夜空对话 发条月亮2023「天体引力」全国巡演正式启动!(海报)//////////在一切开始之前不如让我们把时间的轮
  • 到底哪款OCR软件最强?免费版居然表现最佳?

  • 浏览网页经常会遇到一些无法复制的文档,或者偶尔需要复制某些图片上的文字,你们一般会怎么解决呢?这种情况一般来说最简单,最直接的方式还是使用 OCR 软件比较好,毕竟 OCR 是一种

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 单吃水果有点无聊,试试它们

  • 我们办公室吃东西很花哨。前有@音子 在茶水间现场做Gelato,后有我自制清补凉。这周我又找几个水果的花样吃法。加甘草椒盐粉、夹芝士、夹梅子,有广西广东传统吃法、有一些创新
  • 项目终于用上了插入式注解,真香!

  • 「 关注“石杉的架构笔记”,大厂架构经验倾囊相授 」文章来源:【公众号:码猿技术专栏】前言插入式注解处理器在《深入理解Java虚拟机》一书中有一些介绍(前端编译篇有提到),但一直
  • 成都1h车程直达的森林营地,送你去吃耍过周末

  • 哈喽,到处给大家搜索好耍地的刀上线~要说这两年最火的周末耍事,必须提名露营了吧?放眼现在的营地,要么在城里没氛围感(还贵)、要么离得太远费时费力,但我这次给大家挖到了——成都
  • 1000+接口,真正实现了GPT自由!

  • 不星标可能收不到消息,记得星标公众号回复QQ群获取群号前言最近看到一个大佬做的网站非常不错,网站收集了1000+ chat GPT(后面简称GPT)站点,有了它真的实现了GPT使用自由;为此小棉