大家好,我是不才陈某~

刺进式注解处理器在《深入理解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完成的插件在功用上有很大的发挥空间。