插入式注解处理器

在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 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 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 methodGenericParams = List.nil();
        List parameters = List.nil();
        List 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 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 methodGenericParams = List.nil();
            //定义入参
            JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER, List.nil()), jcVariableDecl.name, jcVariableDecl.vartype, null);
            //设置入参
            List parameters = List.of(param);
            List 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包。

        
            
                src/main/resources
                
                    META-INF/**/*
                
            
        
        
            
                org.apache.maven.plugins
                maven-compiler-plugin
                3.1
                
                    1.8
                    1.8
                
            
            
                org.apache.maven.plugins
                maven-resources-plugin
                2.6
                
                    
                        process-META
                        prepare-package
                        
                            copy-resources
                        
                        
                            target/classes
                            
                                
                                    ${basedir}/src/main/resources/
                                    
                                        **/*
                                    
                                
                            
                        
                    
                
            

        
    

四. 使用

客户端使用如下

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原理分析与功能实现

你可能感兴趣的:(插入式注解处理器)