zoukankan      html  css  js  c++  java
  • JAVA 插入注解处理器

    JDK1.5后,Java语言提供了对注解(Annotation)的支持

    JDK1.6中提供一组插件式注解处理器的标准API,可以实现API自定义注解处理器,干涉编译器的行为。

    在这里,注解处理器可以看作编译器的插件,在编译期间对注解进行处理,可以对语法树进行读取、修改、添加任意元素;但如果有注解处理器修改了语法树,编译器将返回解析及填充符号表的过程,重新处理,直到没有注解处理器修改为止,每一次重新处理循环称为一个Round。

    平时工作中,使用的注解,除了框架 等自带的注解,还有一些自定义的注解,而一般情况下使用自定义注解,主要是用来使用AOP对其

    进行增强的。

    而这篇博客所说的插件式注解处理器,是直接干预生成的字节码的文件的。

    Java常用的Lombok , Android 常用的 ButterKnife 就属于此类,通过干预 Java的编译过程来达到代码增强的著名类库。

    先简单描述一下Java文件的编译:

    Java前端编译(Java三种编译方式:前端编译 JIT编译 AOT编译):Java源代码编译成Class文件的过程

    javac编译器是官方JDK中提供的前端编译器,JDK/bin目录下的javac只是一个与平台相关的调用入口,具体实现在JDK/lib目录下的tools.jar。此外,JDK6开始提供在运行时进行前端编译,默认也是调用到javac

    javac是由Java语言编写的,而HotSpot虚拟机则是由C++语言编写;标准JDK中并没有提供javac的源码,而在OpenJDK中的提供

    javac编译器程序入口:com.sun.tools.javac.Main类中的main()方法

    我们先来了解下javac的编译过程,大致可以分为3个过程,分别是:

    1. 解析与填充符号表过程
    2. 插入式注解处理器的注解处理过程(jsr269规范)
    3. 分析与字节码生成过程

    解析与填充符号表过程会将源码转换为一棵抽象语法树(Abstract Syntax Tree,AST),AST是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。

    因此可以利用Java的插入式注解处理器提供的API,读取、修改、添加抽象语法树中的任意元素。如果因为这些注解对语法树进行了修改,编译器会重新进行词法、语法的分析处理,直到所有的插入式注解没有对语法树进行修改为止。

    那么,就以一个例子来说说这个插入式注解处理器:

    平时工作中有时候需要查看一个方法的执行耗时,那么通常的做法是方法体前后通过两个时间戳变量来计算,这种情况对于单个方法使用,但是多个方法就显得比较麻烦了。当然也可以通过一些设计模式来解决这个问题

    如果恰好使用Spring,那么使用AOP也可以解决这个问题!

    但是说来说去,我就是想看一下方法的执行时间,上述的方法都太过麻烦!怎么才能方便的解决这个问题呢?

    这里我先自定义一个注解

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.SOURCE)
    @Documented
    public @interface TakeTime {
    
        /**
         * 标记前缀,无实质作用,只是为了方便查找
         * @return
         */
        String tag() default "";
    }

    这是一个非常简单的自定义注解,注解只能标注在方法上,同时注解的有效期只在@Retention(RetentionPolicy.SOURCE) 源码期

    这里要说明的是,这个有效期在我这个demo中可以是任意值,因为本博客说的插入式注解处理器,不管注解的生命周期是什么值,插入式注解处理器都会进行处理。

    如果你的需求是,不但在编译时你需要这个注解,其他时候也需要该注解,比如我想在运行期拦截这个注解 等等!那么请更改注解的生命周期,已符合对应业务。

    而如果仅仅只需要编译代码,那么设置为源码期就足够了

    那么有了注解了,接下来就是注解处理器

    package cn.kanyun.annotation_processor.taketime;
    
    import com.google.auto.service.AutoService;
    import com.sun.source.tree.Tree;
    import com.sun.tools.javac.api.JavacTrees;
    import com.sun.tools.javac.model.JavacElements;
    import com.sun.tools.javac.processing.JavacProcessingEnvironment;
    import com.sun.tools.javac.tree.JCTree;
    import com.sun.tools.javac.tree.TreeMaker;
    import com.sun.tools.javac.util.Context;
    import com.sun.tools.javac.util.List;
    import com.sun.tools.javac.util.Names;
    
    import javax.annotation.processing.*;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.ElementKind;
    import javax.lang.model.element.TypeElement;
    import javax.lang.model.element.VariableElement;
    import javax.lang.model.util.Types;
    import javax.tools.Diagnostic;
    import java.util.Set;
    
    /**
     * @TakeTime 注解的注解处理器
     * @SupportedSourceVersion 表示对应的版本
     * @SupportedAnnotationTypes 表示处理哪种类型的注解(这是一个集合, 其值注解的全限定名)
     * @AutoService @AutoService(Processor.class) :向javac注册我们这个自定义的注解处理器,
     * 这样,在javac编译时,才会调用到我们这个自定义的注解处理器方法。@AutoService这里主要是用来生成
     * META-INF/services/javax.annotation.processing.Processor文件的。如果不加上这个注解,那么,你需要自己进行手动配置进行注册
     * <p>
     * AbstractProcessor是注解处理器的抽象类,我们通过继承AbstractProcessor类然后实现process方法来创建我们自己的注解处理器,
     * 所有处理注解的代码放在process方法里面
     */
    
    @SupportedSourceVersion(value = SourceVersion.RELEASE_8)
    @SupportedAnnotationTypes(value = {"cn.kanyun.annotation_processor.taketime.TakeTime"})
    @AutoService(Processor.class)
    public class TakeTimeProcessor extends AbstractProcessor {
    
        /**
         * Messager接口提供注解处理器用来报告错误消息、警告和其他通知的方式
         * 它不是注解处理器开发者的日志工具,而是用来写一些信息给使用此注解器的第三方开发者的
         * 注意:我们应该对在处理过程中可能发生的异常进行捕获,通过Messager接口提供的方法通知用户(在官方文档中描述了消息的不同级别。非常重要的是Kind.ERROR)。
         * 此外,使用带有Element参数的方法连接到出错的元素,
         * 用户可以直接点击错误信息跳到出错源文件的相应行。
         * 如果你在process()中抛出一个异常,那么运行注解处理器的JVM将会崩溃(就像其他Java应用一样),
         * 这样用户会从javac中得到一个非常难懂出错信息
         */
        private Messager messager;
    
        /**
         * 实现Filer接口的对象,用于创建文件、类和辅助文件。
         * 使用Filer你可以创建文件
         * Filer中提供了一系列方法,可以用来创建class、java、resources文件
         * filer.createClassFile()[创建一个新的类文件,并返回一个对象以允许写入它]
         * filer.createResource() [创建一个新的源文件,并返回一个对象以允许写入它]
         * filer.createSourceFile() [创建一个用于写入操作的新辅助资源文件,并为它返回一个文件对象]
         */
        private Filer filer;
    
        /**
         * 用来处理Element的工具类
         * Elements接口的对象,用于操作元素的工具类。
         */
        private JavacElements elementUtils;
    
        /**
         * 用来处理TypeMirror的工具类
         * 实现Types接口的对象,用于操作类型的工具类。
         */
        private Types typeUtils;
    
        /**
         * 这个依赖需要将${JAVA_HOME}/lib/tools.jar 添加到项目的classpath,IDE默认不加载这个依赖
         */
        private JavacTrees trees;
    
        /**
         * 这个依赖需要将${JAVA_HOME}/lib/tools.jar 添加到项目的classpath,IDE默认不加载这个依赖
         * TreeMaker创建语法树节点的所有方法,创建时会为创建出来的JCTree设置pos字段,
         * 所以必须用上下文相关的TreeMaker对象来创建语法树节点,而不能直接new语法树节点。
         */
        private TreeMaker treeMaker;
    
        private Names names;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            messager = processingEnv.getMessager();
            filer = processingEnv.getFiler();
            elementUtils = (JavacElements) processingEnv.getElementUtils();
            typeUtils = processingEnv.getTypeUtils();
            this.trees = JavacTrees.instance(processingEnv);
            Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
            this.treeMaker = TreeMaker.instance(context);
            this.names = Names.instance(context);
        }
    
        /**
         * 该方法将一轮一轮的遍历源代码
         * 处理注解前需要先获取两个重要信息,
         * 第一是注解本身的信息,具体来说就是获取注解对象,有了注解对象以后就可以获取注解的值。
         * 第二是被注解元素的信息,具体来说就是获取被注解的字段、方法、类等元素的信息
         *
         * @param annotations 该方法需要处理的注解类型
         * @param roundEnv    关于一轮遍历中提供给我们调用的信息.
         * @return 该轮注解是否处理完成 true 下轮或者其他的注解处理器将不会接收到次类型的注解.用处不大.
         */
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    //        roundEnv.getRootElements()会返回工程中所有的Class,在实际应用中需要对各个Class先做过滤以提高效率,避免对每个Class的内容都进行扫描
            roundEnv.getRootElements();
            messager.printMessage(Diagnostic.Kind.NOTE, "TakeTimeProcessor注解处理器处理中");
            TypeElement currentAnnotation = null;
    //        遍历注解集合,也即@SupportedAnnotationTypes中标注的类型
            for (TypeElement annotation : annotations) {
                messager.printMessage(Diagnostic.Kind.NOTE, "遍历本注解处理器处理的所有注解,当前遍历到的注解是:" + annotation.getSimpleName());
                currentAnnotation = annotation;
            }
    //      获取所有包含 TakeTime 注解的元素(roundEnv.getElementsAnnotatedWith(TakeTime.class))返回所有被注解了@Factory的元素的列表。你可能已经注意到,我们并没有说“所有被注解了@TakeTime的方法的列表”,因为它真的是返回Element的列表。请记住:Element可以是类、方法、变量等。所以,接下来,我们必须检查这些Element是否是一个方法)
            Set<? extends Element> elementSet = roundEnv.getElementsAnnotatedWith(TakeTime.class);
            messager.printMessage(Diagnostic.Kind.NOTE, "TakeTimeProcessor注解处理器处理@TakeTime注解");
            for (Element element : elementSet) {
                    //获取注解
                    TakeTime TakeTimeAnnotation = element.getAnnotation(TakeTime.class);
                    //获取注解中配置的值
                    String tag = TakeTimeAnnotation.tag();
                    messager.printMessage(Diagnostic.Kind.NOTE, currentAnnotation.getSimpleName() + "注解上设置的值为:" + tag);
    
    //                TypeSpec typeSpec = generateCodeByPoet(typeElement, null);
    
    //                方法名(这里之所以是方法名,是因为这个注解是标注在方法上的)
                    String methodName = element.getSimpleName().toString();
    
    //                类名[全限定名]
    //                element.getEnclosingElement()返回封装此元素(非严格意义上)的最里层元素,由于我们在上面判断了element是method类型,所以直接封装method的的就是类了
    //                http://www.169it.com/article/3400309390285698450.html
                    String className = element.getEnclosingElement().toString();
    
                    messager.printMessage(Diagnostic.Kind.NOTE, "当前被标注注解的方法所在的类是:" + className);
                    messager.printMessage(Diagnostic.Kind.NOTE, currentAnnotation.getSimpleName() + "当前被标注注解的方法是:" + methodName);
    
    //                JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
                    enhanceMethodDecl(elementUtils.getTree(element), tag, className + "." + methodName);
    
    
    
                if (element.getKind() == ElementKind.FIELD) {
    //                当前element是字段类型
                    VariableElement variableElement = (VariableElement) element;
                    messager.printMessage(Diagnostic.Kind.ERROR, "字段不能使用@TakeTime注解", element);
                }
    
                if (element.getKind() == ElementKind.CONSTRUCTOR) {
    //                当前element是构造方法类型
    
                }
            }
    
    
            return false;
        }
    
    
        /**
         * 方法增强
         *
         * @param jcTree
         * @param methodName 方法的全限定名
         * @param tag        标识
         * @return
         */
        private JCTree.JCMethodDecl enhanceMethodDecl(JCTree jcTree, String tag, String methodName) {
            JCTree.JCMethodDecl jcMethodDecl = (JCTree.JCMethodDecl) jcTree;
    
    //        生成表达式System.currentTimeMillis()
            JCTree.JCExpressionStatement time = treeMaker.Exec(treeMaker.Apply(
                    //参数类型(传入方法的参数的类型) 如果是无参的不能设置为null 使用 List.nil()
                    List.nil(),
                    memberAccess("java.lang.System.currentTimeMillis"),
                    //因为不需要传递参数,所以直接设置为List.nil() 不能设置为null
                    List.nil()
                    //参数集合[集合中每一项的类型需要跟第一个参数对照]
    //                List.of(treeMaker.Literal())
                    )
            );
    
    
    //        编译后该方法会存在一个startTime的变量,其值为编译时的时间
            JCTree.JCVariableDecl startTime = createVarDef(treeMaker.Modifiers(0), "startTime", memberAccess("java.lang.Long"), treeMaker.Literal(System.currentTimeMillis()));
    
    //        耗时计算表示式
            JCTree.JCExpressionStatement timeoutStatement = treeMaker.Exec(
                    treeMaker.Apply(
                            List.of(memberAccess("java.lang.Long"), memberAccess("java.lang.Long")),
                            memberAccess("java.lang.Math.subtractExact"),
                            List.of(time.expr, treeMaker.Ident(startTime.name))
                    )
    
            );
    //
            messager.printMessage(Diagnostic.Kind.NOTE, "::::::::::::::::::::");
            messager.printMessage(Diagnostic.Kind.NOTE, timeoutStatement.expr.toString());
    
    //        生成表达式System.out.println()
            JCTree.JCExpressionStatement TakeTime = treeMaker.Exec(treeMaker.Apply(
                    //参数类型(传入方法的参数的类型) 如果是无参的不能设置为null 使用 List.nil()
                    List.of(memberAccess("java.lang.String"), memberAccess("java.lang.String"), memberAccess("java.lang.Long")),
    //                因为这里要传多个参数,所以此处应使用printf,而不是println
                    memberAccess("java.lang.System.out.printf"),
                    //取到前面定义的startTime的变量
    //                List.of(treeMaker.Ident(startTime.name))
    //                取得结果
                    List.of(treeMaker.Literal(">>>>>>>>TAG:%s -> 方法%s执行用时:%d<<<<<<<"), treeMaker.Literal(tag), treeMaker.Literal(methodName), timeoutStatement.getExpression())
                    )
            );
    
    //        catch中的代码块
            JCTree.JCBlock catchBlock = treeMaker.Block(0, List.of(
                    treeMaker.Throw(
    //                        e 这个字符是catch块中定义的变量
                            treeMaker.Ident(getNameFromString("e"))
                    )
            ));
    //        finally代码块中的代码
            JCTree.JCBlock finallyBlock = treeMaker.Block(0, List.of(TakeTime));
    
    
            List<JCTree.JCStatement> statements = jcMethodDecl.body.getStatements();
    //        遍历方法体中每一行(断句符【分号/大括号】)代码
            for (JCTree.JCStatement statement : statements) {
                messager.printMessage(Diagnostic.Kind.NOTE, "遍历方法体中的statement:" + statement);
                messager.printMessage(Diagnostic.Kind.NOTE, "该statement的类型:" + statement.getKind());
                if (statement.getKind() == Tree.Kind.RETURN) {
                    messager.printMessage(Diagnostic.Kind.NOTE, "该statement是Return语句");
                    break;
                }
    
            }
    
    //        jcMethodDecl.body即为方法体,利用treeMaker的Block方法获取到一个新方法体,将原来的替换掉
            jcMethodDecl.body = treeMaker.Block(0, List.of(
    //                定义开始时间,并附上初始值 ,初始值为编译时的时间
                    startTime,
                    treeMaker.Exec(
    //                        这一步 将startTime变量进行赋值 其值 为(表达式也即运行时时间) startTime = System.currentTimeMillis()
                            treeMaker.Assign(
                                    treeMaker.Ident(getNameFromString("startTime")),
                                    time.getExpression()
                            )
                    ),
    //                添加TryCatch
                    treeMaker.Try(jcMethodDecl.body,
                            List.of(treeMaker.Catch(createVarDef(treeMaker.Modifiers(0), "e", memberAccess("java.lang.Exception"),
                                    null), catchBlock)), finallyBlock)
    
    //                下面这段是IF代码,是我想在try catch finally后添加return代码(如果有需要的话),结果发现 如果不写下面的代码的话
    //                Javac会进行判断,如果这个方法有返回值的话,那么Javac会自动在try块外定义一个变量,同时找到要上一个return的变量并赋值
    //                然后返回,具体可以查看编译后的字节码的反编译文件,如果该方法没有返回值,那么什么也不做
    
    //                根据返回值类型,判断是否在方法末尾添加 return  语句  判断返回类型的Kind是否等于TypeKind.VOID
    //                treeMaker.If(treeMaker.Parens(
    //                        treeMaker.Binary(
    //                                JCTree.Tag.EQ,
    //                                treeMaker.Literal(returnType.getKind().toString()),
    //                                treeMaker.Literal(TypeKind.VOID.toString()))
    //                        ),
    //
    //                        //符合IF判断的Statement
    //                        treeMaker.Exec(treeMaker.Literal("返回类型是Void,不需要return")),
    ////                        不符合IF判断的Statement
    //                        null
    //                )
                    )
    
    
            );
    
    
            return jcMethodDecl;
        }
    
    
        /**
         * 创建变量语句
         *
         * @param modifiers
         * @param name      变量名
         * @param varType   变量类型
         * @param init      变量初始化语句
         * @return
         */
        private JCTree.JCVariableDecl createVarDef(JCTree.JCModifiers modifiers, String name, JCTree.JCExpression varType, JCTree.JCExpression init) {
            return treeMaker.VarDef(
                    modifiers,
                    //名字
                    getNameFromString(name),
                    //类型
                    varType,
                    //初始化语句
                    init
            );
        }
    
    
        /**
         * 根据字符串获取Name,(利用Names的fromString静态方法)
         *
         * @param s
         * @return
         */
        private com.sun.tools.javac.util.Name getNameFromString(String s) {
            return names.fromString(s);
        }
    
    
        /**
         * 创建 域/方法 的多级访问, 方法的标识只能是最后一个
         *
         * @param components
         * @return
         */
        private JCTree.JCExpression memberAccess(String components) {
            String[] componentArray = components.split("\.");
            JCTree.JCExpression expr = treeMaker.Ident(getNameFromString(componentArray[0]));
            for (int i = 1; i < componentArray.length; i++) {
                expr = treeMaker.Select(expr, getNameFromString(componentArray[i]));
            }
            return expr;
        }
    
    
    }

    这里我自定义的注解处理器,主要操作Javac在编译被注解标注的方法时,在生成字节码时添加自己的逻辑

    首先在 方法体的开头 插入一条当前时间的变量 ,并赋值为 System.currentTimeMillis()

    然后将整个方法体包括在try块中,添加catch 即finally 

    catch块中直接定义异常并跑出,finally块中打印用时语句!

    之所以添加try catch finally语句,主要是考虑有的方法是在if判断中返回,所以如果在每个return前插入打印用时代码,就十分麻烦

    所以直接使用try块包裹方法体来实现!

    另外为什么catch块中直接要出异常?因为如果原来的方法体捕获了异常,那么自然不会走自己创建的catch块,如果没有捕获,那么自定义的catch块

    会把这个异常原封不动的抛出去,这样并不影响原来的业务了!

    直接说并不直观,放两张图片说明问题

    这张图是源码

     这是编译后的源码,编译后的源码是.class文件,我用IDEA直接打开class文件 就是反编译后的文件

     可以看到反编译后的代码,添加了时间变量,和try块代码,需要注意的是,startTime被赋了初始值,这个值其实是这个class被编译的时间,这个初始值也可以设置为其他值,当然必须是Long类型的,或者设置为null,我这里设置这个时间主要是用来测试!建议设置为null

    最后看一下 原方法的打印结果:

    因为源码中 sleep了3秒中,所以最后直接时间是3001毫秒

    同时 mmm 是 注解中的自定义的tag的值,之所以设置这个,是因为如果方法较多时,方便进行查找等!(其实是参照的Android的Logger)

    关于注解处理器的文章有很多,我这里只写一些自己认为比较重要的!

    1.关于@AutoServier注解 :如果使用的是maven话,那么直接引入google的 auto-service依赖即可,如果使用的是gradle的话且版本在5(包含)之后

    需要添加两条依赖:

    //    声明注解处理器的注解,用于代替手动编辑resources/META-INF/services的文件
        compile group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc6'
    //    这行配置也需要添加,gradle升级到5之后,不加此配置,不会生成META-INF/services/javax.annotation.processing.Processor文件
        annotationProcessor group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc6'

    如果当注解处理器打完包后,被其他项目(gradle)引用,也要 使用 compile / annotationProcessor 来引入两次 

    2.理解几个常见的类 :

     声明变量 
     
    JCTree.JCVariableDecl
    定义变量 long a = 1
    JCTree.JCExpressionStatement
    生成表达式
    a = System.currentTimeMillis()
    JCTree.JCBlock
    代码块(主要是用来放其他代码块,或者JCExpressionStatement的)  

    关于 更多API参照:https://blog.csdn.net/a_zhenzhen/article/details/86065063

    简单用法参照:https://blog.csdn.net/dap769815768/article/details/90448451

     源码:https://github.com/chenwuwen/annotation_processor

  • 相关阅读:
    Shared Memory in Windows NT
    Layered Memory Management in Win32
    软件项目管理的75条建议
    Load pdbs when you need it
    Stray pointer 野指针
    About the Rebase and Bind operation in the production of software
    About "Serious Error: No RTTI Data"
    Realizing 4 GB of Address Space[MSDN]
    [bbk4397] 第1集 第一章 AMS介绍
    [bbk3204] 第67集 Chapter 17Monitoring and Detecting Lock Contention(00)
  • 原文地址:https://www.cnblogs.com/kanyun/p/11541826.html
Copyright © 2011-2022 走看看