简介
插件化注解处理(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.mode
l包下面可以看到这些类。所以我们可以利用JSR 269提供的API来构建一个功能丰富的元编程(metaprogramming)环境。
JSR 269
用Annotation 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的使用步骤大概如下:
-
自定义一个注解,注解的元注解需要指定
@Retention(RetentionPolicy.SOURCE)
。 -
自定义一个
Annotation Processor
,需要继承javax.annotation.processing.AbstractProcessor
,并覆写process
方法。 -
需要在声明的自定义
Annotation Processor
中使用javax.annotation.processing.SupportedAnnotationTypes
指定在第2步创建的注解类型的名称(注意需要全类名,“包名.注解类型名称”,否则会不生效)。支持
*
号,参考:@SupportedAnnotationTypes("lombok.*")
-
需要在声明的自定义
Annotation Processor
中使用javax.annotation.processing.SupportedSourceVersion
指定编译版本。 -
可选操作,可以通在声明的自定义Annotation Processor中使用
javax.annotation.processing.SupportedOptions
指定编译参数。 -
编写代码,使用第1步定义的注解。
-
启用 自定义Processor。
-
验证
第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.JCClassDecl
的 defs
属性是 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简要分析
**只要是能修改,节点被修改了,就会生效。**例如,
body
,defs
等。