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

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


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

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

需求

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

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

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

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

问题

需求似乎很简单,但真要获取自身的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的实现,这其实是可以做到的,下面来看解决方案。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

解决

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

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

可以看到@Setter的Retention是SOURCE类型的,也就是说这个注解只在编译期有效,它甚至不会被编入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实现的插件在功能上有很大的发挥空间。



相关文章
|
16天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
8220 18
|
14天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
2512 14
|
14天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
1869 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
13天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
8天前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
9天前
|
人工智能 JSON 编解码
【2026最新版】ComfyUI本地部署教程,新手也能看懂!
ComfyUI是本地运行的AI绘画工具,采用节点式工作流设计:通过拖拽连接“加载模型”“提示词编码”“采样”“解码”等模块,实现高度可控的文生图。新手推荐使用秋叶整合包,一键启动、内置模型管理与插件安装器,轻松上手。(239字)
|
22天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
2450 1

热门文章

最新文章