乐趣区

关于后端:项目终于用上了插入式注解真香

插入式注解处理器在《深刻了解 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 {

  // 略...

}
复制代码
能够看到 @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 实现的插件在性能上有很大的施展空间。

退出移动版