随便唠唠 编译时注解 这个技术

随便唠唠 编译时注解 这个技术

1 => 缘起

公司部门负责人突然有一天聊起了 编译时注解这么个名词,说实话,编译时注解这两个名词非常的耳熟,连在一起对我就有点陌生了。使我不得不坐下来研究一番。

经常写过注解,配合着反射,AOP等操作,能写出各种方便、通用的程序。但是这里用的注解说我们常说的 运行时注解,它会跟随着程序在运行时对程序进行处理。如下就是我们常用的一个运行时注解 @RestController

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}
@RestController
@RequestMapping({"/user"})
public class UserController {
}

它会伴随程序直到运行期,也就是随程序一起进入JVM,在运行时可以获取他们。而 编译时注解不一样的地方在于他们只伴随程序直到编译期,不会被编入class文件,当然,更不会随程序进入JVM,那么,它有什么作用呢?

2 => 缘及Lombok

这不得不提起大名鼎鼎的 Lombok,我不知道是否全世界的Java程序员都在用它,至少我目前我待过的公司和我所写过的程序都在用它。毋庸置疑,它最大的好处就是帮我们生成setter、getter、全参构造、hashCode()、toString()等方法函数,使我们的实体类看上去非常的清爽、简洁。它的使用方法如下:

@Setter
public class User {

 	/**
     * 名称
     */
    private String name;
}

可是为什么使用它 @Setter这么一个小小的注解就不用再为字段写 set 方法了呢?我们可以康康生成的 class 文件长什么样,用此来验证它的工作方式。class如下:

public class User {
    private String name;

    public User() {
    }

    public void setName(final String name) {
        this.name = name;
    }
}

我们可以发现,生成的 User 没有了 @Setter 注解,多了无参构造函数和 name 的 Setter 方法。无参构造是编译期给类默认生成的,和 @Setter 注解无关。@Setter 只是为类生成了 setName() 方法。

所以,我们可以粗糙的认为,Lombok 通过识别 @Setter 注解在编译的过程中为我们实体类程序加入了 setter 方法的代码。而@Setter 注解本身并不会随程序到JVM去执行。它符合编译时注解的特点,因此 @Setter是一个合格的 编译时注解

了解了它大致的工作内容后,我们便可以开始了解它的实现方式了。

3 => 缘深 APT

在我的多方打听下(也就是百度,bing,搜狗等),找到了一个叫 APT的家伙,他的全称是 AbstractProcessor,他是在javax.annotation.processing包下,也就是 JDK 包含的拓展包 javax 下。他是一个抽象类,结构如下:

public abstract class AbstractProcessor implements Processor {
    /**
     * Processing environment providing by the tool framework.
     */
    protected ProcessingEnvironment processingEnv;
    private boolean initialized = false;

    /**
     * Constructor for subclasses to call.
     */
    protected AbstractProcessor() {}
    
	....
}

先说说他的接口 Process,它被官方定义为注解处理器,在编译期会 有一个单独且完整的JVM来运行所有被实现的APT对象,这个JVM和你的程序最后运行的在的JVM区别开,他只是用于编译阶段的。

也就是说,我们可以借助于这个技术来自定义我们自己的编译过程,比如为我们的实体类生成Settter、Getter等。那么我们如何自己实现这个抽象类来生成我们自己需要的代码呢?

4 => 缘生@Setter

这里我们先通过实现生成Setter的方式来做例子,方便引申出更多的玩法。

新建一个 Springboot项目吧,在 pom.xml中导入如下依赖:

    <dependencies>
        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0-rc2</version>
        </dependency>

        <dependency>
            <groupId>com.squareup</groupId>
            <artifactId>javapoet</artifactId>
            <version>1.9.0</version>
        </dependency>

		<!-- ${java.home} 为 JRE 的安装目录 -->
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>
    </dependencies>

接下来我们先定义一个 @Setter的注解,注意哦,我们将它定义为编译时的注解。它大概先长这个样子吧。

@Retention(RetentionPolicy.SOURCE)  // SOURCE 表示该注解只在源码中保留
@Target(ElementType.TYPE)
public @interface Setter {

    String value() default "";

}

接下里我们定义一个 SetterProcessor来实现 AbstractProcessor

@AutoService(Processor.class)
public class SetterProcessor extends AbstractProcessor {

	// 语法树操作工具
    private JavacTrees javacTrees;
    private TreeMaker treeMaker;
    private Names names;
    private Messager messager;

	// 初始化,获取一些工具
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        this.javacTrees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

	// 处理方法
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    	// 获取被添加了 @Setter 的元素(可能是类、也可能是方法函数)
        Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Setter.class);
        
        elementsAnnotatedWith.forEach(e -> {
        	// 从元素中获取语法树
            JCTree tree = javacTrees.getTree(e);
            // 语法树追加生成的Setter代码
            tree.accept(new TreeTranslator() {
            
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                	// 生成空变量链表
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();

                    // 在抽象树中找出所有待生成Setter的变量
                    for (JCTree jcTree : jcClassDecl.defs) {
                        if (jcTree.getKind().equals(Tree.Kind.VARIABLE)) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }

                    // 对于变量进行生成Setter方法的操作
                    jcVariableDeclList.forEach(jcVariableDecl -> {
                    	// 控制台打印信息
                        messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " 已生成Setter。");
                        
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));
                    });
                    
                    super.visitClassDef(jcClassDecl);
                }
                
            });
            
        });

        return true;
    }

	// 为变量生成Setter
    private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {

        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        // 生成表达式 例如 this.a = a;
        JCTree.JCExpressionStatement aThis = makeAssignment(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()));
        statements.append(aThis);
        JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

        // 生成入参
        JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), jcVariableDecl.getName(), jcVariableDecl.vartype, null);
        List<JCTree.JCVariableDecl> parameters = List.of(param);

        // 生成返回对象
        JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());

        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewMethodName(jcVariableDecl.getName()), methodType, List.nil(), parameters, List.nil(), block, null);

    }

    private Name getNewMethodName(Name name) {
        String s = name.toString();
        return names.fromString("set" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
    }

	// 表达式赋值,获取语句流
    private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
        return treeMaker.Exec(
                treeMaker.Assign(
                        lhs,
                        rhs
                )
        );
    }

	// 支持的注解类型,这里把我们自己定义的 @Setter 给他
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> typeSet = new LinkedHashSet<>();
        typeSet.add(Setter.class.getCanonicalName());
        return typeSet;
    }

	// JDK 版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_8;
    }
}

这里突然出现的大段代码,大家大可不必惊慌。主要包含了四个 重载的 @Override 方法:
<1> synchronized void init(processingEnv):初始化方法,可以获取一些工具类
<2> boolean process(annotations, roundEnv):处理方法
<3> Set<String> getSupportedAnnotationTypes():支持的注解类型
<4> SourceVersion getSupportedSourceVersion():编译的JDK版本

这里核心的就是 init()process()这两个方法。先说 init(),它在初始化时获取了几个奇怪的对象:
JavacTrees
TreeMaker
Names
Messager

Messager :最简单的 Messager 是用于编译时控制台信息打印的。

JavacTrees :而其他几个在说之前大家需要先参照以前的知识明白一个概念,对于前端的 html代码,在浏览器运行时会有 DOM树的概念,而Java的代码,对于编译器来说,一层一层的,有循环嵌套和方法调用,也就像树一样,它被称为 语法树,因其抽象性,也被称为 抽象语法树。编译器在操作语法树时就需要用到语法树操作工具,也就是 JavacTrees

TreeMaker :对于语法树上的每个节点,比如代码块 {}、IF 判断、变量定义、变量赋值、Switch、Return等如果都用 new 的方法不免有些麻烦,于是 TreeMaker提供了静态方法,方便快速创建和拼接这些语法节点。

Names :加入我们需要创建一个 List 变量时,不能直接写 List list;而需要指定命令空间,因为执行 process 这已经是在编译时了,不允许出现编译不通过的语法了,所以除了写成 java.util.List list,还必须确保 java.util.List 的的存在,通过 Names 工具类如果能将 java.util.List转为一个 Name对象,则证明这个类是存在的,这个命名空间是可用的,否则编译会失败。你在编程时可能会想为啥不直接传个 "java.util.List"字符串呢,哈哈,强大的 javac编译器会告诉你他不是好忽悠的!不信你试试呢。

需要重点关注的是,这些语法树节点都源于一个叫 JCTree的家伙,他在 pom.xml 中依赖的第三个包中,也就是 tools.jar 中,提供他的 API 文档,可以学习他提供的节点用法:API文档,这个网页有点小卡,忍忍就好了。

接下来,需要说说 @AutoService(Processor.class)这个玩意,在我们的 SetterProcessor 类的头上,他有什么作用呢?

我们不妨思考下,只定义一个类,编译器怎么会知道要执行他呢,让我们看看 Lombok是怎么告诉编译器的:

在这里插入图片描述

原来他是在项目的 Resources 下的 META-INF下的 services下生成了一个 javax.annotation.processing.Processor文件,在里面告诉编译器需要执行的 Process ,而 @AutoService(Processor.class)这个小玩意正是帮我们干了这件事。他是在 pom.xml文件引入的第一个依赖包中,如果您想自己写这个文件也可以不需要依赖它哦!

那么,最后的问题来了,为什么没起作用呢?

在这里插入图片描述

这里的方式都是正常操作的,为什么没有对 User 的 name 参数生成 Setter 呢?这里就是需要注意的了,实现了 Process 的类不能和使用它的类一起编译,那样就无法使用 Process 了,只有把 实现有 Process 的项目编译成 jar 放进 maven 仓库,作为依赖使用,才可以起作用。

新建一个项目,导入依赖,使用@Setter,如下成功生成了:

在这里插入图片描述

这两个小demo就没传gitee , 需要参考的可以下载哈 下载

4 => 缘远

其实,APT这个技术还是很厉害的,只是大家平时用的少,都比较喜欢运行时注解,都不太喜欢去看接近底层、JVM、编译等东西,包括我自己,对这些晦涩的东西有一种敬而远之的主动,主要是有点怕,对未知的东西的害怕,但这足以说明自己的浅薄。

编译时注解本质可以做到和运行时注解一样的事儿,甚至可以做到更多更好,但是运行的效率比运行时注解更快,因为他不需要在运行时为我们的代码生成代理类然后编译成class等操作,这无疑是略微提高了效率的,虽然大部分时候对这点效率无足轻重。本文并非建议您一定使用编译时注解,只是说明了有另外一种方式而已,并且这种方式有他的局限性,一则是学习成本较大,二来使用它需要对每种情况具有预见性,稍有不慎,便会使得程序无法通过编译,这在作为框架使用时是不被喜欢的。

比如缓存这件事,如果您用了 redis ,不免要在 service 的方法最前面中加入从 redis 获取缓存和方法最后从 redis 存入缓存的逻辑,您当然想使自己的代码更加优美,于是使用 AOP 切面 + 运行时注解来搞定,这样不至于使业务代码看起来臃肿。而使用编译时注解则避免了代理类的使用,将直接在源码中生成缓存逻辑,一个简简单单的注解确实充满了魔力。

如下,是我依照自己的想法写了一版自动生成缓存的,不足以称为框架的东西吧!

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Cache {

    /**
     * 是否开启缓存
     */
    boolean enable() default true;

    /**
     * 缓存的 key | you'd better to set this param
     */
    String key() default "";

    /**
     * 失效时间【默认30分钟】
     */
    long expire() default 30 * 60l;

    /**
     * 失效时间单位【默认为秒】
     */
    TimeUnit unit() default TimeUnit.SECONDS;

    /**
     * 缓存类型【默认是 REDIS 】
     */
    CacheType cacheType() default CacheType.REDIS;
}
@AutoService(Processor.class)
// 生成 Java 8 的源码
@SupportedSourceVersion(SourceVersion.RELEASE_8)
// 支持的注解
@SupportedAnnotationTypes({
        "com.mabang.cache.annotation.Cache"
})
@SuppressWarnings("all")
public class CacheProcessor extends AbstractProcessor {

    // 获取语法树的工具
    private JavacTrees javacTrees;

    // 创建语法树节点的工具
    private TreeMaker treeMaker;

    // 名字工具
    private Names names;

    // 打印信息的工具类
    private Messager messager;
    
    // 每个方法中缓存对象的名称
    private static final String cacheObjectName = "cacheObject$$";
    
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);

        messager = processingEnv.getMessager();
        javacTrees = JavacTrees.instance(processingEnv);

        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        treeMaker = TreeMaker.instance(context);
        names = Names.instance(context);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Cache.class);

        elements.stream().forEach(element -> {
            if (element.getKind().equals(ElementKind.METHOD)) {
                // 如果是方法
                handleMethodCache(element, element.getAnnotation(Cache.class));
            } else if (element.getKind().equals(ElementKind.CLASS)) {
                // 如果是类
                handleClassCache(element);
            } else {
                messager.printMessage(Diagnostic.Kind.WARNING,
                        new StringBuilder()
                                .append("对于 ")
                                .append(element.getEnclosingElement().getSimpleName()).append(".")
                                .append(element.getSimpleName())
                                .append(" 该元素无法完成缓存,暂时只支持对于方法的缓存。"));
            }
        });

        return true;
    }

    /**
     * 处理方法的缓存
     *
     * @param methodElement
     * @param cache
     */
    private void handleMethodCache(Element methodElement, Cache cache) {

        // 获取语法树
        JCTree.JCMethodDecl methodDecl = (JCTree.JCMethodDecl) javacTrees.getTree(methodElement);

        // 获取返回类型,如果是 void 则不做缓存
        JCTree.JCExpression resType = methodDecl.restype;

        if (resType.type instanceof Type.JCVoidType) {
            waringMessage(
                    new StringBuilder()
                            .append(methodElement.getEnclosingElement().getSimpleName()).append(".")
                            .append(methodElement.getSimpleName())
                            .append(" 返回类型为 void , 跳过缓存织入。")
                            .toString());
            return;
        }

        // 非 void 方法
        if (!cache.enable()) return;

        // 获取具体的缓存处理实现
        CacheHandler cacheHandler$$ = CacheLoadService.get(cache.cacheType());

        if (cacheHandler$$ == null) {
            if (CacheType.REDIS == cache.cacheType()) {
                waringMessage(
                        new StringBuilder()
                                .append(methodElement.getEnclosingElement().getSimpleName()).append(".")
                                .append(methodElement.getSimpleName())
                                .append(" 缓存织入失败,原因可能是")
                                .append(cache.cacheType().desc())
                                .append(" 未导入相应的依赖!")
                                .toString());
            } else {
                waringMessage(
                        new StringBuilder()
                                .append(methodElement.getEnclosingElement().getSimpleName()).append(".")
                                .append(methodElement.getSimpleName())
                                .append(" 缓存织入失败,原因可能是")
                                .append(cache.cacheType().desc())
                                .append(" 暂不被支持!")
                                .toString());
            }
            return;
        }

        // 织入逻辑
        weavingCacheToMethod(cacheHandler$$, methodDecl, methodElement, cache);
    }

    /**
     * 对方法织入缓存
     *
     * @param cacheHandler$$ 缓存处理器
     * @param methodDecl     需要缓存的方法
     * @param methodElement  需要缓存的方法元素
     * @param cache          缓存注解
     */
    private void weavingCacheToMethod(CacheHandler cacheHandler$$,
                                      JCTree.JCMethodDecl methodDecl,
                                      Element methodElement,
                                      Cache cache) {

        // 给他的类引入相应的 cacheHandler$$
        processImportCacheHandler(cacheHandler$$, methodElement.getEnclosingElement());

        // 创建相应的 cacheHandler$$ 属性
        processCreateCacheHandlerField(cacheHandler$$, methodElement.getEnclosingElement());

        // 1、生成缓存变量
        JCTree.JCVariableDecl cacheObject$$ = makeCacheObject(methodDecl);

        // 2、查缓存
        JCTree.JCIf getFromCacheIf = makeGetFromCacheIf(cacheHandler$$, methodElement, methodDecl, cache);

        // 3、对方法主体的改造和存储缓存
        JCTree.JCBlock body = makeBodyTryCacheAndSetCache(cacheHandler$$, methodElement, methodDecl, cache);

        // 设置新 body
        methodDecl.body = treeMaker.Block(0, com.sun.tools.javac.util.List.of(
                cacheObject$$,          // 定义 cacheObject$$ 变量
                getFromCacheIf,         // 判断缓存是否存在,存在则返回
                // 判断是否值得缓存
                body
        ));

        infoMessage("asjdfh");
    }

	.......
}

这里只提供了一部分,需要看全部源码的请移驾 cache-processor,如果您有一些 idea 的话,也欢迎一起探讨呀。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值