小言_互联网的博客

JAVA注解处理API实战

330人阅读  评论(0)

简介

​ 插件化注解处理(Pluggable Annotation Processing)API JSR 269提供一套标准API来处理Annotations( JSR 175),实际上JSR 269不仅仅用来处理Annotation,它建立了Java 语言本身的一个模型,它把method、package、constructor、type、variable、enum、annotation等Java语言元素映射为Types和Elements,从而将Java语言的语义映射成为对象,我们可以在javax.lang.model包下面可以看到这些类。所以我们可以利用JSR 269提供的API来构建一个功能丰富的元编程(metaprogramming)环境。

JSR 269Annotation Processor编译期间而不是运行期间处理Annotation, Annotation Processor相当于编译器的一个插件,所以称为插入式注解处理.如果Annotation Processor处理Annotation时(执行process方法)产生了新的Java代码,编译器会再调用一次Annotation Processor,如果第二次处理还有新代码产生,就会接着调用Annotation Processor,直到没有新代码产生为止。每执行一次process()方法被称为一个"round",这样整个Annotation processing过程可以看作是一个round的序列。JSR 269主要被设计成为针对Tools或者容器的API。这个特性在JavaSE 6已经存在。

lombok就是使用这个特性实现编译期的代码插入的。

Pluggable Annotation Processing API的核心是Annotation Processor即注解处理器,一般需要继承抽象类javax.annotation.processing.AbstractProcessor

注意,与运行时注解RetentionPolicy.RUNTIME不同,注解处理器只会处理编译期注解,也就是RetentionPolicy.SOURCE的注解类型,处理的阶段位于Java代码编译期间。

使用步骤

​ 插件化注解处理API的使用步骤大概如下:

  1. 自定义一个注解,注解的元注解需要指定@Retention(RetentionPolicy.SOURCE)

  2. 自定义一个Annotation Processor,需要继承javax.annotation.processing.AbstractProcessor,并覆写process方法。

  3. 需要在声明的自定义Annotation Processor中使用javax.annotation.processing.SupportedAnnotationTypes指定在第2步创建的注解类型的名称(注意需要全类名,“包名.注解类型名称”,否则会不生效)。

    支持* 号,参考:@SupportedAnnotationTypes("lombok.*")

  4. 需要在声明的自定义Annotation Processor中使用javax.annotation.processing.SupportedSourceVersion指定编译版本。

  5. 可选操作,可以通在声明的自定义Annotation Processor中使用javax.annotation.processing.SupportedOptions指定编译参数。

  6. 编写代码,使用第1步定义的注解。

  7. 启用 自定义Processor。

  8. 验证

第3、4、5中的注解,可以通过接口方法指定

Set<String> getSupportedOptions();
Set<String> getSupportedAnnotationTypes();
SourceVersion getSupportedSourceVersion();

示例

目标

为注释了 VersionFill 的类加一个方法,返回编译时的时间戳

第1步:定义注解

@Target({
   ElementType.TYPE,ElementType.METHOD})
@Retention(value = RetentionPolicy.SOURCE)
public @interface VersionFill {
   
}

第2、3、4步:定义processor

package demon.research;

import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Symbol;
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.TypeElement;
import javax.tools.Diagnostic;
import java.time.LocalDateTime;
import java.util.Set;

@SupportedAnnotationTypes({
   "demon.research.VersionFill"})
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
public class VersionFillProcessor extends AbstractProcessor {
   
    /**
     * 用于在编译器打印消息的组件
     */
    Messager messager;

    /**
     * 语法树
     */
    JavacTrees trees;

    /**
     * 用来构造语法树节点
     */
    TreeMaker treeMaker;

    /**
     * 用于创建标识符的对象
     */
    Names names;

    static final String VERSION_METHOD_NAME = "version";

    String versionStr = "";

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

        versionStr = LocalDateTime.now().toString();
    }

    /**
     * {@inheritDoc}
     *
     * @param annotations
     * @param roundEnv
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
   
        System.out.println("Log in AnnotationProcessor.process");
        for (TypeElement typeElement : annotations) {
   
            Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(typeElement);

            annotatedElements.forEach(element -> {
   

                if (element instanceof TypeElement) {
   
                    //获取当前元素的JCTree对象
                    JCTree jcTree = trees.getTree(element);

                    //JCTree利用的是访问者模式,将数据与数据的处理进行解耦,TreeTranslator就是访问者,这里我们重写访问类时的逻辑
                    jcTree.accept(new TreeTranslator() {
   

                        @Override
                        public void visitClassDef(JCTree.JCClassDecl jcClass) {
   
                            messager.printMessage(Diagnostic.Kind.NOTE, "");
                            JCTree.JCMethodDecl methodDecl = createVersion();
                            jcClass.defs = jcClass.defs.append(methodDecl);

                            super.visitClassDef(jcClass);
                        }
                    });
                }

                if (element instanceof Symbol.MethodSymbol) {
   
                    //获取当前元素的JCTree对象
                    JCTree jcTree = trees.getTree(element);

                    //JCTree利用的是访问者模式,将数据与数据的处理进行解耦,TreeTranslator就是访问者,这里我们重写访问类时的逻辑
                    jcTree.accept(new TreeTranslator() {
   

                        @Override
                        public void visitMethodDef(JCTree.JCMethodDecl jcMethodDecl) {
   
                            super.visitMethodDef(jcMethodDecl);
                            updateVersion(jcMethodDecl);
                        }
                    });
                }
            });

            System.out.println(annotatedElements);
        }

        System.out.println(roundEnv);
        return true;
    }

    /**
     * 创建全参数构造方法
     *
     * @return 全参构造方法语法树节点
     */
    private JCTree.JCMethodDecl createVersion() {
   

        ListBuffer<JCTree.JCStatement> jcStatements = new ListBuffer<>();
        jcStatements.append(treeMaker.Return(
                treeMaker.Literal(versionStr)
        ))
        ;

        JCTree.JCBlock jcBlock = treeMaker.Block(
                0 //访问标志
                , jcStatements.toList() //所有的语句
        );

        return treeMaker.MethodDef(
                treeMaker.Modifiers(Flags.PUBLIC), //访问标志
                names.fromString(VERSION_METHOD_NAME), //名字
                treeMaker.Ident(names.fromString("String")), //返回类型
                List.nil(), //泛型形参列表
                List.nil(), //参数列表
                List.nil(), //异常列表
                jcBlock, //方法体
                null //默认方法(可能是interface中的那个default)
        );
    }

    private void updateVersion(JCTree.JCMethodDecl jcMethodDecl) {
   
        ListBuffer<JCTree.JCStatement> jcStatements = new ListBuffer<>();
        jcStatements.append(treeMaker.Return(
                treeMaker.Literal(versionStr)
        ))
        ;
        JCTree.JCBlock body = treeMaker.Block(0, jcStatements.toList());
        jcMethodDecl.body = body;
    }
}


 

如果使用JCMaker, maven项目需要引入:

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8</version>
    <scope>system</scope>
    <systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

第6步:编写需要处理的代码

package demon.research;

@VersionFill
public class VersionController {
   


    @VersionFill
    public String version2() {
   
        return "default2";
    }
}



public class MainClass {
   
    public static void main(String[] args) {
   
        VersionController controller = new VersionController();
        //此处调用的是自动生成的方法
        System.out.println(controller.version());
        int a;
    }
}

 

第7步:启用自定义Processor

注意:如果使用IDEA的话,Compiler->Annotation Processors中的Enable annotation processing必须勾选

使用mvn配置

pom.xml中配置 build 元素,添加 annotationProcessor


    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                    <encoding>UTF-8</encoding>
                    <annotationProcessors>
                        <!-- 添加自定义的Processor -->
                        <annotationProcessor>
                            demon.research.VersionFillProcessor
                        </annotationProcessor>
                    </annotationProcessors>

                </configuration>
            </plugin>
        </plugins>
    </build>

 

直接使用编译参数指定

javac -processor demon.research.VersionFillProcessor MainClass.java。

通过服务注册指定

META-INF/services/javax.annotation.processing.Processor文件中添加demon.research.VersionFillProcessor

具体见SPI 。

问题 1

第一次编译的时候,出现以下错误:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project plugin-anno: Compilation failure
[ERROR] 找不到注释处理程序 'demon.research.VersionFillProcessor'
[ERROR]
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.

原因:Processor 生效的前提是自定义的Processor 是 已经被编译过,否则编译的时候就会报错。

解决方法

1、先单独编译Processor 类

2、把Processor类作为单独一个jar包,引入

正常输出:

[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ plugin-anno ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 4 source files to E:\projects_study\research\plugin-anno\target\classes
Log in AnnotationProcessor.process  
demon.research.VersionFill
[errorRaised=false, rootElements=[demon.research.VersionFill, demon.research.VersionFillProcessor, demon.research.VersionController, demon.research.MainCl
ass], processingOver=false]
Log in AnnotationProcessor.process   xxxxx
[errorRaised=false, rootElements=[], processingOver=true]
[INFO] ------------------------------------------------------------------------

问题2:生成的类,没有改变。

JCTree.JCClassDecldefs 属性是 com.sun.tools.javac.util.List。不是list的数据结构,添加元素的时候就把自己赋给自己的tail,新来的元素放进head。不过需要注意的是这个东西不支持链式调用,prepend或者 append之后还要将新值赋给自己。

// 注意:append之后,要把修改后的值赋值给 defs。
jcClass.defs = jcClass.defs.append(methodDecl);

编译输出

package demon.research;

public class VersionController {
   
    public VersionController() {
   
    }

    public String version2() {
   
        return "2022-12-30T13:31:26.816";
    }

    public String version() {
   
        return "2022-12-30T13:31:26.816";
    }
}

IDEA调试自定义Processor

1、设置构建过程的调试端口

选择菜单:Help -> Edit Custom VM Options,添加下面的内容,并重启 IDEA:

(端口与 mvnDebug的 端口一致)

-Dcompiler.process.debug.port=8889

2、 配置 Run/Debug Configurations。

3、启动一个编译过程。

E:\projects_study\research>mvnDebug compile
Listening for transport dt_socket at address: 8889

此时 编译过程会被挂起。等待绑定

4、启动调试。

在代码中加断点。

使用第2步配置的Configuration,启动调试。

参数 annotations 的值为 Processor 定义上注解 @SupportedAnnotationTypes 的value值

注意:如果 target 目录下 包含所有待编译的类,就有可能不再触发 process 过程。即有编译需要,才触发,没有要编译的java文件,则不触发。

附录

参考

https://www.cnblogs.com/flyingskya/p/10970350.html

element:https://nowjava.com/docs/java-api-11/java.compiler/javax/lang/model/element/package-summary.html

debug 端口被占用问题解决

mvnDebug 命令所在的文件为 %MAVEN_HOME%/bin/mvnDebug.cmd,设置了debug 端口,默认为8000

@setlocal
@set MAVEN_DEBUG_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000

@call "%~dp0"mvn.cmd %*

在启动 mvnDebug命令时,如果输出以下异常信息:

ERROR: transport error 202: bind failed: Address already in use
ERROR: JDWP Transport dt_socket failed to initialize, TRANSPORT_INIT(510)
JDWP exit error AGENT_ERROR_TRANSPORT_INIT(197): No transports initialized [debugInit.c:750]

则表示 debug 端口被占用。

在windows 下可以通过以下命令查看端口被占用的情况:

E:\projects_study\research\plugin-anno>netstat -ano |findstr 8000
  TCP    0.0.0.0:8000           0.0.0.0:0              LISTENING       8280
  TCP    0.0.0.0:18000          0.0.0.0:0              LISTENING       8280
  TCP    127.0.0.1:8000         127.0.0.1:61597        TIME_WAIT       0
  TCP    127.0.0.1:8000         127.0.0.1:61608        TIME_WAIT       0
  TCP    127.0.0.1:8000         127.0.0.1:61614        TIME_WAIT       0

8280 为 占用端口8000的进程的pid。通过任务管理器,查看进程。

可以结束进程,或者修改 mvnDebug的端口。

@setlocal
@set MAVEN_DEBUG_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8889

@call "%~dp0"mvn.cmd %*

Element

接口 描述
AnnotationMirror 表示注释。
AnnotationValue 表示注释类型元素的值。
AnnotationValueVisitor<R,P> 使用访问者设计模式的变体访问注释类型元素的值。
Element 表示程序元素,例如模块,包,类或方法。
ElementVisitor<R,P> 程序元素的访问者,以访问者设计模式的风格。
ExecutableElement 表示类或接口的方法,构造函数或初始化程序(静态或实例),包括注释类型元素。
ModuleElement 表示模块程序元素。
ModuleElement.Directive 表示此模块声明中的指令。
ModuleElement.DirectiveVisitor<R,P> 模块指令的访问者,以访问者设计模式的样式。
ModuleElement.ExportsDirective 导出的模块包。
ModuleElement.OpensDirective 一个打开的模块包。
ModuleElement.ProvidesDirective 模块提供的服务的实现。
ModuleElement.RequiresDirective 模块的依赖关系。
ModuleElement.UsesDirective 对模块使用的服务的引用。
Name 不可变的字符序列。
PackageElement 表示包程序元素。
Parameterizable 具有类型参数的元素的mixin接口。
QualifiedNameable 具有限定名称的元素的mixin接口。
TypeElement 表示类或接口程序元素。
TypeParameterElement 表示泛型类,接口,方法或构造函数元素的正式类型参数。
VariableElement 表示字段, 枚举常量,方法或构造函数参数,局部变量,资源变量或异常参数。

JCTree

参考:

Java 中的屠龙之术:如何修改语法树?

java AST JCTree简要分析 (已转载)

Java-JSR-269-插入式注解处理器 (已转载)

JCTree是语法树元素的基类,包含一个重要的字段pos,该字段用于指明当前语法树节点(JCTree)在语法树中的位置,因此们不能直接用new关键字来创建语法树节点,即使创建了也没有意义。

JCTree是一个抽象类,这里重点介绍几个JCTree的子类:

  • JCStatement:声明语法树节点,常见的子类如下

  • JCBlock:语句块语法树节点

  • JCReturn:return语句语法树节点

  • JCClassDecl:类定义语法树节点

  • JCVariableDecl:字段/变量定义语法树节点

  • JCMethodDecl:方法定义语法树节点

  • JCModifiers:访问标志语法树节点

  • JCExpression:表达式语法树节点,常见的子类如下

  • JCAssign:赋值语句语法树节点

  • JCIdent:标识符语法树节点,可以是变量,类型,关键字等等

TreeMaker

TreeMaker用于创建一系列的语法树节点,们上面说了创建JCTree不能直接使用new关键字来创建,所以Java为们提供了一个工具,就是TreeMaker,它会在创建时为们创建的JCTree对象设置pos字段,所以必须使用上下文相关的TreeMaker对象来创建语法树节点。

一些语句构造

//字面量 
treeMaker.Literal("this is a literal");
//获取String 类型
treeMaker.Ident(names.fromString("String"))
//返回的是原生类型:JCPrimitiveTypeTree
treeMaker.TypeIdent(TypeTag.BYTE)

参考: java AST JCTree简要分析

**只要是能修改,节点被修改了,就会生效。**例如,bodydefs等。


转载:https://blog.csdn.net/demon7552003/article/details/128500366
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场