插入式注解处理器

作者: 数齐 | 来源:发表于2018-08-31 23:29 被阅读23次

    在JAVA 1.6后,JDK提供了一种方式,可以让我们修改编译过程,在编译期融入我们自己编译逻辑。它提供了一组编译器的插入式注解处理器的标准API再编译期间对注解进行处理,我们可以利用这些API,读取、修改、添加抽象语法树中的任意元素。如果因为这些注解对语法树进行了修改,编译器会重新进行词法、语法的分析处理,直到所有的插入式注解没有对语法树进行修改为止。

    下面我们通过一个实例来讲解下。我们大家都知道java中有个类库叫做lombok,他的作用就是帮助我们减少一些模板代码,例如属性中的set、get方法的生成等,他通过在加注解的方式,在编译期,动态的为属性添加了set、get方法,利用的就是上述所讲的功能做的。今天我们也实现以下这个功能。

    一.定义注解

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Data {
    }
    

    既然是插入式注解那么注解肯定是必不可少的。我们定义一个Data类型的注解,希望可以在编译期,给打上这个注解的类,生产set、get方法。

    二. 定义处理器类

    @SupportedAnnotationTypes("jsr.Data")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class DataProcessor extends AbstractProcessor {
        private JavacTrees trees;
        private TreeMaker treeMaker;
        private Names names;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            this.trees = 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) {
            roundEnv.getElementsAnnotatedWith(Data.class).stream().map(element -> trees.getTree(element)).forEach(t -> t.accept(new TreeTranslator() {
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    jcClassDecl.defs.stream().filter(k -> k.getKind().equals(Tree.Kind.VARIABLE)).map(tree -> (JCTree.JCVariableDecl) tree).forEach(jcVariableDecl -> {
                        //添加get方法
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
                        //添加set方法
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));
                    });
                    super.visitClassDef(jcClassDecl);
                }
            }));
            return true;
        }
    
        /**
         * 创建get方法
         *
         * @param jcVariableDecl
         * @return
         */
        private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
            //方法的访问级别
            JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
            //方法名称
            Name methodName = getMethodName(jcVariableDecl.getName());
            //设置返回值类型
            JCTree.JCExpression returnMethodType = jcVariableDecl.vartype;
            ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
            statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));
            //设置方法体
            JCTree.JCBlock methodBody = treeMaker.Block(0, statements.toList());
            List<JCTree.JCTypeParameter> methodGenericParams = List.nil();
            List<JCTree.JCVariableDecl> parameters = List.nil();
            List<JCTree.JCExpression> throwsClauses = List.nil();
            //构建方法
            return treeMaker.MethodDef(modifiers, methodName, returnMethodType, methodGenericParams, parameters, throwsClauses, methodBody, null);
        }
    
        /**
         * 创建set方法
         *
         * @param jcVariableDecl
         * @return
         */
        private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
            try {
                //方法的访问级别
                JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);
                //定义方法名
                Name methodName = setMethodName(jcVariableDecl.getName());
                //定义返回值类型
                JCTree.JCExpression returnMethodType = treeMaker.Type((Type) (Class.forName("com.sun.tools.javac.code.Type$JCVoidType").newInstance()));
                ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
                statements.append(treeMaker.Exec(treeMaker.Assign(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()))));
                //定义方法体
                JCTree.JCBlock methodBody = treeMaker.Block(0, statements.toList());
                List<JCTree.JCTypeParameter> methodGenericParams = List.nil();
                //定义入参
                JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER, List.nil()), jcVariableDecl.name, jcVariableDecl.vartype, null);
                //设置入参
                List<JCTree.JCVariableDecl> parameters = List.of(param);
                List<JCTree.JCExpression> throwsClauses = List.nil();
                //构建新方法
                return treeMaker.MethodDef(modifiers, methodName, returnMethodType, methodGenericParams, parameters, throwsClauses, methodBody, null);
            } catch (Exception e) {
                System.err.println(e);
            }
            return null;
    
        }
    
        private Name getMethodName(Name name) {
            String s = name.toString();
            return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
        }
    
    
        private Name setMethodName(Name name) {
            String s = name.toString();
            return names.fromString("set" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
        }
    }
    
    

    @SupportedAnnotationTypes("jsr.Data") 标示此处理器可以处理的注解。如果是“*”代表所有的注解都可以处理。@SupportedSourceVersion(SourceVersion.RELEASE_8)标示此处理器可以处理那些版本的JAVA代码。init方法是初始化的方法,可以在里面初始化javac运行期间的一些对象。例如JavacTrees是带处理的抽象语法树对象,TreeMaker中提供了一些操作抽象语法树节点的方法,Names提供了创建标识符的方法。process方法是真正处理抽象语法树的方法,这个方法的返回值是布尔类型。如果返回true,说明修改了语法树,那么就需要重新进行解析和填充符号步骤。如果返回false,说明没有进行过修改,例如只是检查等操作,这样就不需要重新解析了。process方法里面所做的事情,就是根据属性名称,动态的创建set方法(makeSetterMethodDecl)和get方法(makeGetterMethodDecl),然后将创建的方法添加到这个类中的方法中(jcClassDecl.defs)随后进行编译,至于动态创建方法的逻辑中的没一步基本上都有注释,相信大家都能看懂,就不详细介绍了。这样,就在编译期间生成了属性对应的方法,这也就是lombok的原理所在。

    三. 如何加载处理器

    1. 通过javac命令的“-processor”参数来执行编译时需要附带的注解处理器,如果有多个注解处理器的话,用逗号进行分割。
    2. 通过JAVA SPI加载。在resources目录下添加META-INF/services目录内添加文件名为javax.annotation.processing.Processor文件内容是jsr.DataProcessor也就是我们自定义的这个处理器。关于JAVA SPI的内容,可以参考我的另外一篇文章揭秘JDK SPI,通过pom中的下面配置,将此信息打入JAR包。
    <build>
            <resources>
                <resource>
                    <directory>src/main/resources</directory>
                    <excludes>
                        <exclude>META-INF/**/*</exclude>
                    </excludes>
                </resource>
            </resources>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.1</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>2.6</version>
                    <executions>
                        <execution>
                            <id>process-META</id>
                            <phase>prepare-package</phase>
                            <goals>
                                <goal>copy-resources</goal>
                            </goals>
                            <configuration>
                                <outputDirectory>target/classes</outputDirectory>
                                <resources>
                                    <resource>
                                        <directory>${basedir}/src/main/resources/</directory>
                                        <includes>
                                            <include>**/*</include>
                                        </includes>
                                    </resource>
                                </resources>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
    
            </plugins>
        </build>
    
    

    四. 使用

    客户端使用如下

    package jsr;
    
    @Data
    public class App {
        private String value;
    
        public static void main(String[] args) {
            App app = new App();
            app.setValue("hello world");
            System.out.println(app.getValue());
        }
    }
    
    

    编译后的文件

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by Fernflower decompiler)
    //
    
    package jsr;
    
    public class App {
        private String value;
    
        public void setValue(String value) {
            this.value = value;
        }
    
        public String getValue() {
            return this.value;
        }
    
        public App() {
        }
    
        public static void main(String[] args) {
            App app = new App();
            app.setValue("hello world");
            System.out.println(app.getValue());
        }
    }
    
    

    可见我们的编译时注解生效了。

    五.项目源码

    项目源码

    六. 参考资料

    深入理解Java虚拟机_JVM高级特性与最佳实践
    Lombok原理分析与功能实现

    相关文章

      网友评论

        本文标题:插入式注解处理器

        本文链接:https://www.haomeiwen.com/subject/nfqywftx.html