Java 插入式注解处理器

178

这是什么?

Java 中提供了 Annotation 注解机制. 注解就像是一个标识符, 等待一个处理器扫瞄代码中的注解并执行对应的逻辑, 例如 @Setter 注解在被注解处理器扫瞄到之后就会在源码中插入对应的 setter 方法

Java 的注解处理分为: 运行时处理 和 编译时处理, 运行时注解处理无需多说, 都是一个思路, 只是运行时你必须得自己决定处理时机

这篇文章讲源码注解处理 (Java 插入式注解处理器), 也就是 lombok 的部分原理

注解介绍:

在这篇文章中有详细说明: https://erzbir.com/archives/java---annotation

注解的用途:

其实就是一个目的: 方便编程

注解就是一个标签, 在方法, 类等等地方标注之后, 让你可以找到这些被标记的元素 (AnnotatedElement). 找到之后我们就可以做一些特殊处理, 比如扫瞄所有带有 @Component 注解的类并将其实例化到一个容器 (Bean 注册)

详细作用有以下几点:

  • 编译预处理
  • 减少代码量
  • 增加可读性
  • 用作注释
  • 生成文档
  • 代码审查

如何处理注解:

在 Java 中如果想处理源码级别的注解 (ClASS, SOURCE), 需要编写一个注解处理器 (AnnotationProcessor) 继承 Java 提供的抽象类 AbstractProcessor 实现其中的抽象方法 boolean process(*, *), 在抽象方法中写自己的逻辑即可

Java 使用 spi 机制来加载用户自定义的注解处理器, 所以在编写完注解处理器之后还需要配置 spi, 还需要手动指定处理器才会生效, 在下文会详细介绍

AbstractProcessor 详解:

process()方法签名: boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)

处理流程:

注解处理流程由多轮完成。每一轮都从编译器在源文件中搜索注解并选择适合这些注解的 注解处理器(AbstractProcessor) 开始。。

Java 的注解处理机制由多个 round 组成, 每个注解处理器依次在相应的源上被调用, 每一轮从编译器在源文件中搜索注解并选择适合这些注解的注解处理器开始, 每一轮中如果有任何文件发生了变化就将变化后的所有文件作为输入开启新的一轮, 直到没有变化

如何判断结束:

process() 方法最后会返回一个布尔值, 而这个布尔值就是决定下一轮处理时这个注解处理器参不参与处理, 注解处理器最终对返回 true 后, 就表示不需要这个注解处理器了, 相对的如果为 false, 则在后续处理中还需要此注解处理器

在每个注解处理器都返回 true 之后以及文件无变化后, 处理结束

方法详解:

getSupportedOptions():

Set<String> getSupportedOptions();

获取一个 Set, 注解处理器处理的元素的名称只有和 Set 中的字符串匹配才可处理

默认实现中获取的是注解处理器上 @SupportedOptions 注解的值

@SupportedOptions({"name", "phone"})
public class TestAnnotationProcessor extends AbstractProcessor {

}

这个可以用作一种规定, 比如你想处理 name 字段, 那么字段名就必须写成 name, 否则注解上了也没有效果

getSupportedAnnotationTypes():

Set<String> getSupportedAnnotationTypes();

获取一个 Set, 用于规定注解处理器处理哪些注解

支持通配符

默认实现中是获取 @SupportedAnnotationTypes 注解的值

@SupportedAnnotationTypes("com.erzbir.annotations.toString")
public class ToStringAnnotationProcessor extends AbstractProcessor {

}

getSupportedSourceVersion(:

SourceVersion getSupportedSourceVersion();

获取源码的版本信息, 用于规定注解处理器处理可以处理哪个版本的源码

默认实现是获取 @SupportedSourceVersion 注解的值

@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class ToStringAnnotationProcessor extends AbstractProcessor {

}

init():

void init(ProcessingEnvironment processingEnv);

初始化注解处理器, 将 processingEnv 传入处理器

默认实现是 synchronized

ProcessingEnvironment:

方法描述
Map<String,String> getOptions()返回指定的参数选项
Elements getElementUtils()返回实现 Elements 接口的对象, 用于操作元素的工具类
Messager getMessager()返回实现 Messager 接口的对象,用于报告错误信息、警告提醒
Filer getFiler()返回实现 Filer 接口的对象,用于创建文件、类和辅助文件
Types getTypeUtils()返回实现 Types 接口的对象,用于操作类型的工具类
SourceVersion getSourceVersion()返回源码版本
Locale getLocale()返回当前语言环境, 可用于 本地化
boolean isPreviewEnabled()返回是否开启预览功能

process():

boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);

核心方法, 需要我们自己实现

返回 true 则表示此注解处理器不需要参与到下一轮处理

TypeElement

此接口表示一个 Class 或者 Interface (只是表示, 并不是 ClassInterface 与他有关)

RoundEnvironment

此接口是一轮处理所需要的环境

方法描述
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a)返回被指定注解类型注解的元素集合
Set<? extends Element> getElementsAnnotatedWith(TypeElement a)返回被指定注解类型注解的元素集合
boolean processingOver()如果循环处理完成返回 true,否则 false
boolean errorRaised()如果捕获到错误返回 true, 否则 false
Set<? extends Element> getRootElements()返回前一轮处理的根元素
Set<? extends Element> getElementsAnnotatedWithAny(TypeElement... annotations)返回被一种或更多类型注解的元素集合

实现 ToString 注解:

这里给出一个 @ToString 的简易实现, 用于为类自动生成一个 toString() 方法, 打印自身的成员变量

为了方便, 这里使用 JDK 1.8 来举例实现, 当然其他版本也可以实现, 只是因为项目要用到 com.sun 模块, 在 1.8 以后比较难操作

因为用到了 com.sun 模块中的语法树, 可以先了解一下, 当然如果只是为了学习注解处理器也可不用了解

参考这篇文章: https://blog.csdn.net/A_zhenzhen/article/details/86065063

首先我们定义 ToString 注解:

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

为了方便字符串的格式化, 定义一个 ToStringHelper 类, 用于创建字符串:

public class ToStringHelper {
    
    /**
     * 这里是 static 的, 这样在构建语法树的时候可以直接调用此方法
     */
    public static String toString(Object o) {
        Class<?> aClass = o.getClass();
        Field[] fields = aClass.getDeclaredFields();
        StringBuilder builder = new StringBuilder();
        builder.append("{");
        Arrays.stream(fields)
                .forEach(field ->
                        {
                            field.setAccessible(true);
                            try {
                                builder.append("\"")
                                        .append(field.getName())
                                        .append("\"")
                                        .append(":")
                                        .append("\"")
                                        .append(field.get(o).toString())
                                        .append("\"")
                                        .append(",");
                            } catch (IllegalAccessException ignore) {

                            }
                        }
                );
        builder.replace(builder.length() - 1, builder.length(), "}");
        return builder.toString();
    }
}

接下来我们编写最重要的注解处理类 ToStringAnnotationProcessor:

package com.erzbir.processor;

import com.erzbir.annotations.ToString;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
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.*;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author Erzbir
 * @Date 2023/10/30
 */
@SupportedAnnotationTypes("com.erzbir.annotations.ToString")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ToStringAnnotationProcessor extends AbstractProcessor {
    private JavacTrees trees;
    private TreeMaker treeMaker;
    private Names names;
    private Messager messager;
    private ProcessingEnvironment processingEnv;
    private Elements elementUtils;
    private Types typeUtils;
    private Filer filer;


    /**
     * 初始化
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.processingEnv = processingEnv;
        this.elementUtils = processingEnv.getElementUtils();
        this.typeUtils = processingEnv.getTypeUtils();
        this.filer = processingEnv.getFiler();
        this.trees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
        this.messager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 获取所有被 ToString 注解的元素
        Set<? extends Element> annotationSet = roundEnv.getElementsAnnotatedWith(ToString.class);
        annotationSet.forEach(element -> trees.getTree(element).accept(new TreeTranslator() {

            @Override
            public void visitClassDef(JCTree.JCClassDecl jcClass) {
                //过滤属性
                Map<Name, JCTree.JCVariableDecl> treeMap =
                        jcClass.defs.stream().filter(k -> k.getKind().equals(Tree.Kind.VARIABLE))
                                .map(tree -> (JCTree.JCVariableDecl) tree)
                                .collect(Collectors.toMap(JCTree.JCVariableDecl::getName, Function.identity()));
                //处理变量
                treeMap.forEach((k, jcVariable) -> {
                    messager.printMessage(Diagnostic.Kind.NOTE, String.format("fields:%s", k));
                });
                //增加toString方法
                ToString annotation = element.getAnnotation(ToString.class);
                jcClass.defs = jcClass.defs.prepend(generateToStringBuilderMethod(annotation));
                super.visitClassDef(jcClass);
            }

            @Override
            public void visitMethodDef(JCTree.JCMethodDecl jcMethod) {
                //打印所有方法
                messager.printMessage(Diagnostic.Kind.NOTE, jcMethod.toString());
                super.visitMethodDef(jcMethod);
            }
        }));
        return true;
    }

    private JCTree.JCMethodDecl generateToStringBuilderMethod(ToString annotation) {

        //修改方法级别
        JCTree.JCModifiers modifiers = treeMaker.Modifiers(Flags.PUBLIC);

        //添加方法名称
        Name methodName = getNameFromString("toString");

        // 调用方法
        JCTree.JCExpressionStatement statement = treeMaker.Exec(
                // 创建方法调用语法树节点
                treeMaker.Apply(
                        // 设置调用的方法的参数的类型
                        List.of(memberAccess("java.lang.Object")),

                        // 设置调用的方法
                        memberAccess("com.erzbir.processor.ToStringHelper.toString"),

                        // 设置上面设置的方法需要传入的参数
                        List.of(
                                treeMaker.Ident(getNameFromString("this"))
                        )
                )
        );
        ListBuffer<JCTree.JCStatement> jcStatements = new ListBuffer<>();

        jcStatements.append(
                // 设置方法的返回值
                treeMaker.Return(statement.getExpression())
        );

        //设置方法体
        JCTree.JCBlock jcBlock = treeMaker.Block(0, jcStatements.toList());

        //添加返回值类型
        JCTree.JCExpression returnType = memberAccess("java.lang.String");

        //参数类型, 无参数
        List<JCTree.JCTypeParameter> typeParameters = List.nil();

        //参数变量, 无参数
        List<JCTree.JCVariableDecl> parameters = List.nil();

        //声明异常, 无异常抛出
        List<JCTree.JCExpression> throwsClauses = List.nil();

        // 定义方法
        return treeMaker
                .MethodDef(modifiers, methodName, returnType, typeParameters, parameters, throwsClauses, jcBlock, null);
    }

    /**
     * 将类似 java.lang.String 的字符串解析为表达式
     */
    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;
    }

    private Name getNameFromString(String s) {
        return names.fromString(s);
    }
}

使用:

@ToString
public class User {
    private String name;
    private String phone;

    public User(String name, String phone) {
        this.name = name;
        this.phone = phone;
    }
    
    
    public static void main(String[] args) {
        User user = new User("test", "123455623");
        System.out.println(user.toString());
    }
}

打印结果为:

{"name":"test","phone":"123455623"}

使用注解处理器:

这里使用的工具是 IntelliJ IDEA

配置 SPI:

Java 使用自身的 spi 机制来调用用户自定义的注解处理器

  1. 在 "resources" 目录下新建 META-INF/services/javax.annotation.processing.Processor

    文件名一定要为 "javax.annotation.processing.Processor" 且目录名也要为 "META-INF/services"

    resources
    └──META-INF
       └──services
          └──javax.annotation.processing.Processor
    
  2. 在 javax.annotation.processing.Processor 文件中写入你注解处理器的全限定名, 一行写一个

    com.erzbir.processor.ToStringAnnotationProcessor
    

开启注解处理器:

在 "Preferences | Build, Execution, Deployment | Compiler | Annotation Processors" 设置中, 选中需要使用注解处理器的模块, 勾选 "Enable annotation Processing"

问题解决:

如果你的编译出现:

java: Workaround: to make project compile with the current annotation processor implementation, start JPS with VM option: -Djps.track.ap.dependencies=false
  When run from IDE, the option can be set in "Compiler Settings | build process VM options"

根据提示在 "Compiler Settings | build process VM options" 中设置中添加: "-Djps.track.ap.dependencies=false"

或者你也可以在运行配置中添加

如果你的 maven 编译出现问题, 添加如下配置:

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
		<artifactId>maven-compiler-plugin</artifactId>
		<version>3.11.0</version>
		<configuration>
			<source>8</source>
			<target>8</target>
			<compilerArgument>
				-proc:none
			</compilerArgument>
    	</configuration>
</plugin>