文章目录

  • 写在前面
  • 1、编译时注解原理
  • 2、AbstractProcessor 解析
  • 2.1、RoundEnvironment
  • 2.2、获取Messager、JavacTrees、TreeMaker、Names、elementUtils
  • 2.3、JCTree
  • 2.3.1、pos和defs
  • 2.3.2、JCTree访问者模式
  • 2.4、TreeMaker
  • 3、实例一:@NoArgsConstructor 添加无参构造函数
  • 3.1、pom
  • 3.2、代码
  • 4、实例二:@AllArgsConstructor 添加全参构造函数
  • 4.1、pom
  • 4.2、代码


写在前面

接上一篇文章:java AbstractProcessor 编译时注解(入手体验)

1、编译时注解原理

在java文件编译成class文件之前,对java文件进行操作,生成代码。
AbstractProcessor 完成的就是对java文件进行操作的相关功能。

相关知识点:

1、java文件操作相关的两个类: JCTree 树节点、TreeMaker 树节点构建器
2、JCTree 的一个子类就是java语法中的一个节点,类、方法、字段等这些都被封装成了一个JCTree子类。
3、TreeMaker 里的一个方法用于构建JCTree某个子类,例:treeMaker.MethodDef(…)返回值是JCTree的子类JCMethodDecl(方法节点)

2、AbstractProcessor 解析

一个编译时注解,最后对java文件进行的操作,都在 AbstractProcessor 子类中编写。

java自定义注解值不能为空 java自定义编译时注解_访问者模式

  1. @SupportedAnnotationTypes(“cc.HelloWorld”) ,“cc.HelloWorld” 是HelloWorld注解的类全名
  2. @SupportedSourceVersion(SourceVersion.RELEASE_7) ,annotation支持版本,向前兼容到JDK7
  3. @AutoService(Processor.class),让AbstractProcessor子类被编译器发现
  4. annotations 传进来的注解列表
  5. roundEnv 环境,可以从它获取被注解标记的元素,然后获取元素的JCTree对象。
  6. process 方法返回值,涉及到编译器的处理逻辑,返回true表示已经处理,返回false表示未被处理

注意AbstractProcessor每一次有修改,就要重新编译该类。或者有修改就删除target,让编译器重新生成class文件

2.1、RoundEnvironment

process 方法的第二个参数RoundEnvironment roundEnv,可以从里面获取java文件的树结构,并往里面加入代码。
RoundEnvironment 的方法:

方法名

说明

errorRaised()

如果在前一轮处理中引发错误,则返回true ; 否则返回false 。

processingOver()

如果此轮生成的类型不受后续轮注释处理的影响,则返回true ; 否则返回false 。

getRootElements()

返回前一轮生成的注释处理的root elements 。

getElementsAnnotatedWith()

返回被某个注解标记的元素。

getElementsAnnotatedWithAny()

返回多个注解标记的元素。

2.2、获取Messager、JavacTrees、TreeMaker、Names、elementUtils

这些对象中,JavacTrees和TreeMaker是一定会用到的

@SupportedAnnotationTypes("这里填注解类的全名")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    Messager messager;                 // 用于在编译器打印消息的组件
    Names names;                       // 用于创建标识符的对象
    JavacTrees trees;                  // 语法树
    JavacElements elementUtils;        // trees 和 elementUtils都可以获取 元素的JCTree对象
    TreeMaker treeMaker;               // 用来构造语法树节点

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        Context context = ((JavacProcessingEnvironment) this.processingEnv).getContext();
        this.elementUtils = (JavacElements) this.processingEnv.getElementUtils();
        this.messager = this.processingEnv.getMessager();
        this.names = Names.instance(this.context);
        this.trees = JavacTrees.instance(this.processingEnv);
        this.treeMaker = TreeMaker.instance(this.context);
    }
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
		...
        return true;
    }
这里有个坑,获取Messager、JavacTrees、TreeMaker这些对象只能在init()方法里面或者init()方法执行之后,否则获取不到这些对象,如下错误示例。

java自定义注解值不能为空 java自定义编译时注解_开发语言_02

2.3、JCTree

JCTree 的一个子类就是java语法中的一个节点,类、方法、字段等这些都被封装成了一个JCTree子类。
JCTree 从JavacTrees中获取,如下例:

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elementSet = roundEnv.getElementsAnnotatedWith(HelloWorld.class);

		elementSet.forEach(element -> {
            //获取元素的JCTree对象
            JCTree jcTree = trees.getTree(element);               // JCTree也可以使用elementUtils获取: JCTree jcTree = elementUtils.getTree(element);
            ...
            });
        return true;
    }

JCTree 是语法树的基类,包含子类:

父类

子类

JCTree

声明节点 JCStatement、方法定义节点 JCMethodDecl、访问标志节点 JCModifiers、表达式节点 JCExpression

JCStatement 声明节点

语块JCBlock、返回语句JCReturn、类定义JCClassDecl、 字段定义JCVariableDecl

JCExpression 表达式节点

赋值语句语JCAssign、变量,类型,关键字JCIdent

JCMethodDecl 方法节点


JCModifiers 访问标识节点


java自定义注解值不能为空 java自定义编译时注解_开发语言_03

2.3.1、pos和defs

jcTree.pos 用于指明当前节点在语法树中的位置,treeMaker也有这个字段。
jcTree.defs 类定义的详细语句,包括字段,方法定义等,treeMaker也有这个字段。

2.3.2、JCTree访问者模式

如果你对访问者模式没有了解,建议先阅读这篇文章:访问者模式一篇就够了

JCTree jcTree = trees.getTree(element); 
// JCTree利用的是访问者模式,将数据与数据的处理进行解耦,TreeTranslator是访问者,这里可以重写访问类时的逻辑
jcTree.accept(new TreeTranslator(){...});

2.4、TreeMaker

TreeMaker 里的一个方法用于构建JCTree某个子类,例:treeMaker.MethodDef(…)返回值是JCTree的子类JCMethodDecl(方法节点)。

参考官方文档:TreeMaker文档

TreeMaker创建各种节点的方法如下:

方法名

说明

Modifiers()

创建访问标志

ClassDef()

类定义

MethodDef()

方法定义

VarDef()

创建字段/变量

Ident()

创建标识符

Return()

返回语句

Select()

创建域访问/方法访问

NewClass()

创建new语句

Apply()

创建方法调用

Assign()

创建赋值语句

Exec()

创建可执行语句

Block()

创建组合语句

示例:

// public访问修饰符
JCTree.JCModifiers publicDot = treeMaker.Modifiers(Flags.PUBLIC);
// void关键字
JCTree.JCPrimitiveTypeTree voidDot = treeMaker.TypeIdent(TypeTag.VOID);
// 空语句
JCTree.JCBlock emptyDot = treeMaker.Block(0, List.nil());
// 一个无参构造方法
JCTree.JCMethodDecl noArgsMethod = treeMaker.MethodDef(
                    publicDot,
                    names.fromString("<init>"),
                    voidDot ,
                    List.nil(),
                    List.nil(),
                    List.nil(),
                    emptyDot,
                    null);



// this关键字
JCTree.JCIdent thisDot = treeMaker.Ident(names.fromString("this"));
// userPassword                           (Name类型可以是方法名、字段名、变量名、类名)
Name userPasswordName = names.fromString("userPassword");
// this.userPassword                      (访问当前方法中的字段userPassword)
JCTree.JCFieldAccess thisUserPassword = treeMaker.Select(thisDot , userPasswordName);
// userPassword 变量
JCTree.JCIdent userPassword = treeMaker.Ident(userPasswordName);
// this.userPassword = userPassword       (treeMaker.Apply()以及treeMaker.Assign()需要外面包一层treeMaker.Exec())
treeMaker.Exec(treeMaker.Assign(thisUserPassword, userPassword));

3、实例一:@NoArgsConstructor 添加无参构造函数

本实例未使用JCTree访问者模式,直接添加一个无参构造函数到jcTree.defs中

新建项目a(使用注解)、b(写编译时注解功能)

3.1、pom

项目b的pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
         
    <artifactId>b</artifactId>
    <groupId>org.example</groupId>
    <version>1.0</version>
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>

        <!-- 提供@AutoService注解,让AbstractProcessor子类被编译器发现 -->
        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0-rc5</version>
        </dependency>

        <!-- 提供JCTree等一些功能api -->
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8</version>
            <scope>system</scope>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>commons-validator</groupId>
            <artifactId>commons-validator</artifactId>
            <version>1.3.1</version>
        </dependency>

    </dependencies>

</project>

项目a的poml.xml(引入项目b)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    <artifactId>a</artifactId>
    <groupId>org.example</groupId>
    <version>1.0</version>
    <modelVersion>4.0.0</modelVersion>
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.example</groupId>
            <artifactId>b</artifactId>
            <version>1.0</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>

3.2、代码

@NoArgsConstructor

package cc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

NoArgsConstructorProcessor.java

package cc;

import com.google.auto.service.AutoService;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.TypeTag;
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.TypeElement;
import java.util.Set;

@SupportedAnnotationTypes("cc.NoArgsConstructor")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class NoArgsConstructorProcessor extends AbstractProcessor {
    JavacTrees trees;
    TreeMaker treeMaker;
    Names names;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        Context context = ((JavacProcessingEnvironment) this.processingEnv).getContext();
        this.trees = JavacTrees.instance(this.processingEnv);
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elementsSet = roundEnv.getElementsAnnotatedWith(NoArgsConstructor.class);
        elementsSet.forEach(element -> {
            JCTree.JCClassDecl jcTree = (JCTree.JCClassDecl) trees.getTree(element);
            jcTree.defs = jcTree.defs.append(
                    treeMaker.MethodDef(
                        treeMaker.Modifiers(Flags.PUBLIC),
                        names.fromString("<init>"),
                        treeMaker.TypeIdent(TypeTag.VOID),
                        List.nil(),
                        List.nil(),
                        List.nil(),
                        treeMaker.Block(0, List.nil()),
                        null)
            );
        });
        return true;
    }
}

项目a中使用@NoArgsConstructor

import cc.NoArgsConstructor;

@NoArgsConstructor
public class Test {

    public Test(String s){}
    
    public static void main(String[] args) {}
}

Test编译后的class

java自定义注解值不能为空 java自定义编译时注解_访问者模式_04

4、实例二:@AllArgsConstructor 添加全参构造函数

本实例使用JCTree访问者模式。

新建项目a(使用注解)、b(写编译时注解功能)

4.1、pom

同 3.1、pom

4.2、代码

@AllArgsConstructor

package cc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

AllArgsConstructorProcessor.java

package cc;

import com.google.auto.service.AutoService;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.TypeTag;
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.tree.TreeTranslator;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
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.Modifier;
import javax.lang.model.element.TypeElement;
import java.util.Set;

@SupportedAnnotationTypes("cc.AllArgsConstructor")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class AllArgsConstructorProcessor extends AbstractProcessor {
    JavacTrees trees;
    TreeMaker treeMaker;
    Names names;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        Context context = ((JavacProcessingEnvironment) this.processingEnv).getContext();
        this.trees = JavacTrees.instance(this.processingEnv);
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

    /**
     * 字段的语法树节点的集合
     */
    private List<JCTree.JCVariableDecl> fieldJcVariables;


    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(AllArgsConstructor.class);
        set.forEach(element -> {
            JCTree jcTree = trees.getTree(element);
            jcTree.accept(new TreeTranslator() {

                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClass) {
                    before(jcClass);

                    //添加全参构造方法
                    jcClass.defs = jcClass.defs.append(
                                createAllArgsConstructor()
                    );
                    after();
                }
            });
        });

        return true;
    }

    /**
     * 判断是否是合法的字段
     *
     * @param jcTree 语法树节点
     * @return 是否是合法字段
     */
    private static boolean isValidField(JCTree jcTree) {
        if (jcTree.getKind().equals(JCTree.Kind.VARIABLE)) {
            JCTree.JCVariableDecl jcVariable = (JCTree.JCVariableDecl) jcTree;

            Set<Modifier> flagSets = jcVariable.mods.getFlags();
            return (!flagSets.contains(Modifier.STATIC)
                    && !flagSets.contains(Modifier.FINAL));
        }

        return false;
    }

    /**
     * 进行一些初始化工作
     *
     * @param jcClass 类的语法树节点
     */
    private void before(JCTree.JCClassDecl jcClass) {
        ListBuffer<JCTree.JCVariableDecl> jcVariables = new ListBuffer<>();

        //遍历jcClass的所有内部节点,可能是字段,方法等等
        for (JCTree jcTree : jcClass.defs) {
            //找出所有set方法节点,并添加
            if (isValidField(jcTree)) {
                //注意这个com.sun.tools.javac.util.List的用法,不支持链式操作,更改后必须赋值
                jcVariables.append((JCTree.JCVariableDecl) jcTree);
            }
        }
        this.fieldJcVariables = jcVariables.toList();
    }

    /**
     * 清理
     */
    private void after() {
        this.fieldJcVariables = null;
    }

    /**
     * 创建全参数构造方法
     *
     * @return 全参构造方法语法树节点
     */
    private JCTree.JCMethodDecl createAllArgsConstructor() {
        ListBuffer<JCTree.JCStatement> jcStatements = new ListBuffer<>();
        for (JCTree.JCVariableDecl jcVariable : fieldJcVariables) {
            Name name = names.fromString(jcVariable.name.toString());
            JCTree.JCIdent thisIdent = treeMaker.Ident(names.fromString("this"));

            JCTree.JCFieldAccess select = treeMaker.Select(thisIdent, name);
            JCTree.JCIdent ident = treeMaker.Ident(names.fromString(jcVariable.name.toString()));

            //添加构造方法的赋值语句 " this.xxx = xxx; "
            jcStatements.append(
                    treeMaker.Exec(treeMaker.Assign(select, ident))
            );
        }

        return treeMaker.MethodDef(
                treeMaker.Modifiers(Flags.PUBLIC),
                names.fromString("<init>"),
                treeMaker.TypeIdent(TypeTag.VOID),
                List.nil(),
                cloneJCVariablesAsParams(treeMaker, fieldJcVariables),
                List.nil(),
                treeMaker.Block(0 , jcStatements.toList()),
                null
        );
    }

    /**
     * 克隆一个字段的语法树节点集合,作为方法的参数列表
     *
     * @param treeMaker            语法树节点构造器
     * @param prototypeJCVariables 字段的语法树节点集合
     * @return 方法参数的语法树节点集合
     */
    static List<JCTree.JCVariableDecl> cloneJCVariablesAsParams(TreeMaker treeMaker, List<JCTree.JCVariableDecl> prototypeJCVariables) {
        ListBuffer<JCTree.JCVariableDecl> jcVariables = new ListBuffer<>();
        for (JCTree.JCVariableDecl jcVariable : prototypeJCVariables) {
            jcVariables.append(cloneJCVariableAsParam(treeMaker, jcVariable));
        }
        return jcVariables.toList();
    }

    /**
     * 克隆一个字段的语法树节点,该节点作为方法的参数
     * 具有位置信息的语法树节点是不能复用的!
     *
     * @param treeMaker           语法树节点构造器
     * @param prototypeJCVariable 字段的语法树节点
     * @return 方法参数的语法树节点
     */
    static JCTree.JCVariableDecl cloneJCVariableAsParam(TreeMaker treeMaker, JCTree.JCVariableDecl prototypeJCVariable) {
        return treeMaker.VarDef(
                treeMaker.Modifiers(Flags.PARAMETER), //访问标志。极其坑爹!!!
                prototypeJCVariable.name, //名字
                prototypeJCVariable.vartype, //类型
                null //初始化语句
        );
    }
}

项目a中使用@AllArgsConstructor

import cc.AllArgsConstructor;

@AllArgsConstructor
public class Test {
    private String a;
    private String b;

    public static void main(String[] args) {}
}

编译后的class文件

java自定义注解值不能为空 java自定义编译时注解_java自定义注解值不能为空_05