JAVA 插入注解处理器
JDK1.5 后,Java 语言提供了对注解(Annotation)的支持
JDK1.6 中提供一组插件式注解处理器的标准 API,可以实现 API 自定义注解处理器,干涉编译器的行为。
在这里, 注解处理器可以看作编译器的插件,在编译期间对注解进行处理,可以对语法树进行读取、修改、添加任意元素;但如果有注解处理器修改了语法树,编译器将返回解析及填充符号表的过程,重新处理,直到没有注解处理器修改为止,每一次重新处理循环称为一个 Round。
平时工作中,使用的注解, 除了框架 等自带的注解,还有一些自定义的注解,而一般情况下使用自定义注解,主要是用来使用 AOP 对其
进行增强的。
而这篇博客所说的插件式注解处理器, 是直接干预生成的字节码的文件的。
Java 常用的 Lombok , Android 常用的 ButterKnife 就属于此类,通过干预 Java 的编译过程来达到代码增强的著名类库。
先简单描述一下 Java 文件的编译:
Java 前端编译(Java 三种编译方式:前端编译 JIT 编译 AOT 编译):Java 源代码编译成 Class 文件的过程
javac 编译器是官方 JDK 中提供的前端编译器,JDK/bin 目录下的 javac 只是一个与平台相关的调用入口,具体实现在 JDK/lib 目录下的 tools.jar。此外,JDK6 开始提供在运行时进行前端编译,默认也是调用到 javac
javac 是由 Java 语言编写的,而 HotSpot 虚拟机则是由 C++ 语言编写;标准 JDK 中并没有提供 javac 的源码,而在 OpenJDK 中的提供
javac 编译器程序入口:com.sun.tools.javac.Main 类中的 main() 方法
我们先来了解下 javac 的编译过程,大致可以分为 3 个过程,分别是:
- 解析与填充符号表过程
- 插入式注解处理器的注解处理过程(jsr269 规范)
- 分析与字节码生成过程
解析与填充符号表过程会将源码转换为一棵抽象语法树(Abstract Syntax Tree,AST),AST 是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构(Construct),例如包、类型、修饰符、运算符、接口、返回值甚至代码注释等都可以是一个语法结构。
因此可以利用 Java 的插入式注解处理器提供的 API,读取、修改、添加抽象语法树中的任意元素。如果因为这些注解对语法树进行了修改,编译器会重新进行词法、语法的分析处理,直到所有的插入式注解没有对语法树进行修改为止。
那么,就以一个例子来说说这个插入式注解处理器:
平时工作中有时候需要查看一个方法的执行耗时, 那么通常的做法是方法体前后通过两个时间戳变量来计算,这种情况对于单个方法使用,但是多个方法就显得比较麻烦了。当然也可以通过一些设计模式来解决这个问题
如果恰好使用 Spring,那么使用 AOP 也可以解决这个问题!
但是说来说去,我就是想看一下方法的执行时间,上述的方法都太过麻烦!怎么才能方便的解决这个问题呢?
这里我先自定义一个注解
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) @Documented public @interface TakeTime {</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 标记前缀,无实质作用,只是为了方便查找 * </span><span style="color: rgba(128, 128, 128, 1)">@return</span> <span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> String tag() </span><span style="color: rgba(0, 0, 255, 1)">default</span> ""<span style="color: rgba(0, 0, 0, 1)">;
}
这是一个非常简单的自定义注解, 注解只能标注在方法上, 同时注解的有效期只在 @Retention(RetentionPolicy.SOURCE) 源码期
这里要说明的是, 这个有效期在我这个 demo 中可以是任意值,因为本博客说的插入式注解处理器, 不管注解的生命周期是什么值, 插入式注解处理器都会进行处理。
如果你的需求是, 不但在编译时你需要这个注解, 其他时候也需要该注解, 比如我想在运行期拦截这个注解 等等!那么请更改注解的生命周期, 已符合对应业务。
而如果仅仅只需要编译代码, 那么设置为源码期就足够了
那么有了注解了, 接下来就是注解处理器
package cn.kanyun.annotation_processor.taketime;import com.google.auto.service.AutoService;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.model.JavacElements;
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.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import java.util.Set;/**
- @TakeTime 注解的注解处理器
- @SupportedSourceVersion 表示对应的版本
- @SupportedAnnotationTypes 表示处理哪种类型的注解 (这是一个集合, 其值注解的全限定名)
- @AutoService @AutoService(Processor.class) : 向 javac 注册我们这个自定义的注解处理器,
- 这样,在 javac 编译时,才会调用到我们这个自定义的注解处理器方法。@AutoService 这里主要是用来生成
- META-INF/services/javax.annotation.processing.Processor 文件的。如果不加上这个注解,那么,你需要自己进行手动配置进行注册
- <p>
- AbstractProcessor 是注解处理器的抽象类,我们通过继承 AbstractProcessor 类然后实现 process 方法来创建我们自己的注解处理器,
- 所有处理注解的代码放在 process 方法里面
*/@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
@SupportedAnnotationTypes(value = {"cn.kanyun.annotation_processor.taketime.TakeTime"})
@AutoService(Processor.class)
public class TakeTimeProcessor extends AbstractProcessor {</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * Messager接口提供注解处理器用来报告错误消息、警告和其他通知的方式 * 它不是注解处理器开发者的日志工具,而是用来写一些信息给使用此注解器的第三方开发者的 * 注意:我们应该对在处理过程中可能发生的异常进行捕获,通过Messager接口提供的方法通知用户(在官方文档中描述了消息的不同级别。非常重要的是Kind.ERROR)。 * 此外,使用带有Element参数的方法连接到出错的元素, * 用户可以直接点击错误信息跳到出错源文件的相应行。 * 如果你在process()中抛出一个异常,那么运行注解处理器的JVM将会崩溃(就像其他Java应用一样), * 这样用户会从javac中得到一个非常难懂出错信息 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Messager messager; </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 实现Filer接口的对象,用于创建文件、类和辅助文件。 * 使用Filer你可以创建文件 * Filer中提供了一系列方法,可以用来创建class、java、resources文件 * filer.createClassFile()[创建一个新的类文件,并返回一个对象以允许写入它] * filer.createResource() [创建一个新的源文件,并返回一个对象以允许写入它] * filer.createSourceFile() [创建一个用于写入操作的新辅助资源文件,并为它返回一个文件对象] </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Filer filer; </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 用来处理Element的工具类 * Elements接口的对象,用于操作元素的工具类。 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> JavacElements elementUtils; </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 用来处理TypeMirror的工具类 * 实现Types接口的对象,用于操作类型的工具类。 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Types typeUtils; </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 这个依赖需要将${JAVA_HOME}/lib/tools.jar 添加到项目的classpath,IDE默认不加载这个依赖 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> JavacTrees trees; </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 这个依赖需要将${JAVA_HOME}/lib/tools.jar 添加到项目的classpath,IDE默认不加载这个依赖 * TreeMaker创建语法树节点的所有方法,创建时会为创建出来的JCTree设置pos字段, * 所以必须用上下文相关的TreeMaker对象来创建语法树节点,而不能直接new语法树节点。 </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> TreeMaker treeMaker; </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Names names; @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">synchronized</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> init(ProcessingEnvironment processingEnv) { </span><span style="color: rgba(0, 0, 255, 1)">super</span><span style="color: rgba(0, 0, 0, 1)">.init(processingEnv); messager </span>=<span style="color: rgba(0, 0, 0, 1)"> processingEnv.getMessager(); filer </span>=<span style="color: rgba(0, 0, 0, 1)"> processingEnv.getFiler(); elementUtils </span>=<span style="color: rgba(0, 0, 0, 1)"> (JavacElements) processingEnv.getElementUtils(); typeUtils </span>=<span style="color: rgba(0, 0, 0, 1)"> processingEnv.getTypeUtils(); </span><span style="color: rgba(0, 0, 255, 1)">this</span>.trees =<span style="color: rgba(0, 0, 0, 1)"> JavacTrees.instance(processingEnv); Context context </span>=<span style="color: rgba(0, 0, 0, 1)"> ((JavacProcessingEnvironment) processingEnv).getContext(); </span><span style="color: rgba(0, 0, 255, 1)">this</span>.treeMaker =<span style="color: rgba(0, 0, 0, 1)"> TreeMaker.instance(context); </span><span style="color: rgba(0, 0, 255, 1)">this</span>.names =<span style="color: rgba(0, 0, 0, 1)"> Names.instance(context); } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 该方法将一轮一轮的遍历源代码 * 处理注解前需要先获取两个重要信息, * 第一是注解本身的信息,具体来说就是获取注解对象,有了注解对象以后就可以获取注解的值。 * 第二是被注解元素的信息,具体来说就是获取被注解的字段、方法、类等元素的信息 * * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> annotations 该方法需要处理的注解类型 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> roundEnv 关于一轮遍历中提供给我们调用的信息. * </span><span style="color: rgba(128, 128, 128, 1)">@return</span><span style="color: rgba(0, 128, 0, 1)"> 该轮注解是否处理完成 true 下轮或者其他的注解处理器将不会接收到次类型的注解.用处不大. </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)"> @Override </span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> process(Set<? <span style="color: rgba(0, 0, 255, 1)">extends</span> TypeElement><span style="color: rgba(0, 0, 0, 1)"> annotations, RoundEnvironment roundEnv) {
// roundEnv.getRootElements() 会返回工程中所有的 Class, 在实际应用中需要对各个 Class 先做过滤以提高效率,避免对每个 Class 的内容都进行扫描
roundEnv.getRootElements();
messager.printMessage(Diagnostic.Kind.NOTE, "TakeTimeProcessor 注解处理器处理中");
TypeElement currentAnnotation = null;
// 遍历注解集合, 也即 @SupportedAnnotationTypes 中标注的类型
for (TypeElement annotation : annotations) {
messager.printMessage(Diagnostic.Kind.NOTE, "遍历本注解处理器处理的所有注解, 当前遍历到的注解是:" + annotation.getSimpleName());
currentAnnotation = annotation;
}
// 获取所有包含 TakeTime 注解的元素 (roundEnv.getElementsAnnotatedWith(TakeTime.class)) 返回所有被注解了 @Factory 的元素的列表。你可能已经注意到,我们并没有说“所有被注解了 @TakeTime 的方法的列表”,因为它真的是返回 Element 的列表。请记住:Element 可以是类、方法、变量等。所以,接下来,我们必须检查这些 Element 是否是一个方法)
Set<? extends Element> elementSet = roundEnv.getElementsAnnotatedWith(TakeTime.class);
messager.printMessage(Diagnostic.Kind.NOTE, "TakeTimeProcessor 注解处理器处理 @TakeTime 注解");
for (Element element : elementSet) {
//获取注解
TakeTime TakeTimeAnnotation = element.getAnnotation(TakeTime.class);
//获取注解中配置的值
String tag = TakeTimeAnnotation.tag();
messager.printMessage(Diagnostic.Kind.NOTE, currentAnnotation.getSimpleName() + "注解上设置的值为:" + tag);// TypeSpec typeSpec = generateCodeByPoet(typeElement, null);
// 方法名 (这里之所以是方法名, 是因为这个注解是标注在方法上的)
String methodName = element.getSimpleName().toString();// 类名 [全限定名]
// element.getEnclosingElement() 返回封装此元素(非严格意义上)的最里层元素, 由于我们在上面判断了 element 是 method 类型, 所以直接封装 method 的的就是类了
// http://www.169it.com/article/3400309390285698450.html
String className = element.getEnclosingElement().toString();messager.printMessage(Diagnostic.Kind.NOTE, </span>"当前被标注注解的方法所在的类是:" +<span style="color: rgba(0, 0, 0, 1)"> className); messager.printMessage(Diagnostic.Kind.NOTE, currentAnnotation.getSimpleName() </span>+ "当前被标注注解的方法是:" +<span style="color: rgba(0, 0, 0, 1)"> methodName);
// JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
enhanceMethodDecl(elementUtils.getTree(element), tag, className + "." + methodName);</span><span style="color: rgba(0, 0, 255, 1)">if</span> (element.getKind() ==<span style="color: rgba(0, 0, 0, 1)"> ElementKind.FIELD) {
// 当前 element 是字段类型
VariableElement variableElement = (VariableElement) element;
messager.printMessage(Diagnostic.Kind.ERROR, "字段不能使用 @TakeTime 注解", element);
}</span><span style="color: rgba(0, 0, 255, 1)">if</span> (element.getKind() ==<span style="color: rgba(0, 0, 0, 1)"> ElementKind.CONSTRUCTOR) {
// 当前 element 是构造方法类型
}
}</span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">; } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 方法增强 * * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> jcTree * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> methodName 方法的全限定名 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> tag 标识 * </span><span style="color: rgba(128, 128, 128, 1)">@return</span> <span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> JCTree.JCMethodDecl enhanceMethodDecl(JCTree jcTree, String tag, String methodName) { JCTree.JCMethodDecl jcMethodDecl </span>=<span style="color: rgba(0, 0, 0, 1)"> (JCTree.JCMethodDecl) jcTree;
// 生成表达式 System.currentTimeMillis()
JCTree.JCExpressionStatement time = treeMaker.Exec(treeMaker.Apply(
//参数类型 (传入方法的参数的类型) 如果是无参的不能设置为 null 使用 List.nil()
List.nil(),
memberAccess("java.lang.System.currentTimeMillis"),
//因为不需要传递参数, 所以直接设置为 List.nil() 不能设置为 null
List.nil()
//参数集合 [集合中每一项的类型需要跟第一个参数对照]
// List.of(treeMaker.Literal())
)
);// 编译后该方法会存在一个 startTime 的变量, 其值为编译时的时间
JCTree.JCVariableDecl startTime = createVarDef(treeMaker.Modifiers(0), "startTime", memberAccess("java.lang.Long"), treeMaker.Literal(System.currentTimeMillis()));// 耗时计算表示式
JCTree.JCExpressionStatement timeoutStatement = treeMaker.Exec(
treeMaker.Apply(
List.of(memberAccess("java.lang.Long"), memberAccess("java.lang.Long")),
memberAccess("java.lang.Math.subtractExact"),
List.of(time.expr, treeMaker.Ident(startTime.name))
));
//
messager.printMessage(Diagnostic.Kind.NOTE, "::::::::::::::::::::");
messager.printMessage(Diagnostic.Kind.NOTE, timeoutStatement.expr.toString());// 生成表达式 System.out.println()
JCTree.JCExpressionStatement TakeTime = treeMaker.Exec(treeMaker.Apply(
//参数类型 (传入方法的参数的类型) 如果是无参的不能设置为 null 使用 List.nil()
List.of(memberAccess("java.lang.String"), memberAccess("java.lang.String"), memberAccess("java.lang.Long")),
// 因为这里要传多个参数, 所以此处应使用 printf, 而不是 println
memberAccess("java.lang.System.out.printf"),
//取到前面定义的 startTime 的变量
// List.of(treeMaker.Ident(startTime.name))
// 取得结果
List.of(treeMaker.Literal(">>>>>>>>TAG:%s -> 方法 %s 执行用时:%d<<<<<<<"), treeMaker.Literal(tag), treeMaker.Literal(methodName), timeoutStatement.getExpression())
)
);// catch 中的代码块
JCTree.JCBlock catchBlock = treeMaker.Block(0, List.of(
treeMaker.Throw(
// e 这个字符是 catch 块中定义的变量
treeMaker.Ident(getNameFromString("e"))
)
));
// finally 代码块中的代码
JCTree.JCBlock finallyBlock = treeMaker.Block(0, List.of(TakeTime));List</span><JCTree.JCStatement> statements =<span style="color: rgba(0, 0, 0, 1)"> jcMethodDecl.body.getStatements();
// 遍历方法体中每一行 (断句符【分号 / 大括号】) 代码
for (JCTree.JCStatement statement : statements) {
messager.printMessage(Diagnostic.Kind.NOTE, "遍历方法体中的 statement:" + statement);
messager.printMessage(Diagnostic.Kind.NOTE, "该 statement 的类型:" + statement.getKind());
if (statement.getKind() == Tree.Kind.RETURN) {
messager.printMessage(Diagnostic.Kind.NOTE, "该 statement 是 Return 语句");
break;
}}
// jcMethodDecl.body 即为方法体,利用 treeMaker 的 Block 方法获取到一个新方法体,将原来的替换掉
jcMethodDecl.body = treeMaker.Block(0, List.of(
// 定义开始时间, 并附上初始值 , 初始值为编译时的时间
startTime,
treeMaker.Exec(
// 这一步 将 startTime 变量进行赋值 其值 为 (表达式也即运行时时间) startTime = System.currentTimeMillis()
treeMaker.Assign(
treeMaker.Ident(getNameFromString("startTime")),
time.getExpression()
)
),
// 添加 TryCatch
treeMaker.Try(jcMethodDecl.body,
List.of(treeMaker.Catch(createVarDef(treeMaker.Modifiers(0), "e", memberAccess("java.lang.Exception"),
null), catchBlock)), finallyBlock)// 下面这段是 IF 代码, 是我想在 try catch finally 后添加 return 代码 (如果有需要的话), 结果发现 如果不写下面的代码的话
// Javac 会进行判断, 如果这个方法有返回值的话, 那么 Javac 会自动在 try 块外定义一个变量, 同时找到要上一个 return 的变量并赋值
// 然后返回, 具体可以查看编译后的字节码的反编译文件, 如果该方法没有返回值, 那么什么也不做// 根据返回值类型, 判断是否在方法末尾添加 return 语句 判断返回类型的 Kind 是否等于 TypeKind.VOID
// treeMaker.If(treeMaker.Parens(
// treeMaker.Binary(
// JCTree.Tag.EQ,
// treeMaker.Literal(returnType.getKind().toString()),
// treeMaker.Literal(TypeKind.VOID.toString()))
// ),
//
// //符合 IF 判断的 Statement
// treeMaker.Exec(treeMaker.Literal("返回类型是 Void, 不需要 return")),
//// 不符合 IF 判断的 Statement
// null
// )
)); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> jcMethodDecl; } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 创建变量语句 * * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> modifiers * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> name 变量名 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> varType 变量类型 * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> init 变量初始化语句 * </span><span style="color: rgba(128, 128, 128, 1)">@return</span> <span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> JCTree.JCVariableDecl createVarDef(JCTree.JCModifiers modifiers, String name, JCTree.JCExpression varType, JCTree.JCExpression init) { </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> treeMaker.VarDef( modifiers, </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">名字</span>
getNameFromString(name),
//类型
varType,
//初始化语句
init
);
}</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 根据字符串获取Name,(利用Names的fromString静态方法) * * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> s * </span><span style="color: rgba(128, 128, 128, 1)">@return</span> <span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> com.sun.tools.javac.util.Name getNameFromString(String s) { </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> names.fromString(s); } </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * 创建 域/方法 的多级访问, 方法的标识只能是最后一个 * * </span><span style="color: rgba(128, 128, 128, 1)">@param</span><span style="color: rgba(0, 128, 0, 1)"> components * </span><span style="color: rgba(128, 128, 128, 1)">@return</span> <span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> JCTree.JCExpression memberAccess(String components) { String[] componentArray </span>= components.split("\\."<span style="color: rgba(0, 0, 0, 1)">); JCTree.JCExpression expr </span>= treeMaker.Ident(getNameFromString(componentArray[0<span style="color: rgba(0, 0, 0, 1)">])); </span><span style="color: rgba(0, 0, 255, 1)">for</span> (<span style="color: rgba(0, 0, 255, 1)">int</span> i = 1; i < componentArray.length; i++<span style="color: rgba(0, 0, 0, 1)">) { expr </span>=<span style="color: rgba(0, 0, 0, 1)"> treeMaker.Select(expr, getNameFromString(componentArray[i])); } </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> expr; }
}
这里我自定义的注解处理器, 主要操作 Javac 在编译被注解标注的方法时, 在生成字节码时添加自己的逻辑
首先在 方法体的开头 插入一条当前时间的变量 , 并赋值为 System.currentTimeMillis()
然后将整个方法体包括在 try 块中, 添加 catch 即 finally
catch 块中直接定义异常并跑出,finally 块中打印用时语句!
之所以添加 try catch finally 语句, 主要是考虑有的方法是在 if 判断中返回, 所以如果在每个 return 前插入打印用时代码, 就十分麻烦
所以直接使用 try 块包裹方法体来实现!
另外为什么 catch 块中直接要出异常?因为如果原来的方法体捕获了异常, 那么自然不会走自己创建的 catch 块, 如果没有捕获, 那么自定义的 catch 块
会把这个异常原封不动的抛出去, 这样并不影响原来的业务了!
直接说并不直观, 放两张图片说明问题
这张图是源码
这是编译后的源码, 编译后的源码是.class 文件,我用 IDEA 直接打开 class 文件 就是反编译后的文件
可以看到反编译后的代码, 添加了时间变量, 和 try 块代码, 需要注意的是,startTime 被赋了初始值, 这个值其实是这个 class 被编译的时间, 这个初始值也可以设置为其他值,当然必须是 Long 类型的, 或者设置为 null, 我这里设置这个时间主要是用来测试!建议设置为 null
最后看一下 原方法的打印结果:
因为源码中 sleep 了 3 秒中, 所以最后直接时间是 3001 毫秒
同时 mmm 是 注解中的自定义的 tag 的值, 之所以设置这个, 是因为如果方法较多时, 方便进行查找等!(其实是参照的 Android 的 Logger)
关于注解处理器的文章有很多, 我这里只写一些自己认为比较重要的!
1. 关于 @AutoServier 注解 :如果使用的是 maven 话, 那么直接引入 google 的 auto-service 依赖即可,如果使用的是 gradle 的话且版本在 5(包含) 之后
需要添加两条依赖:
// 声明注解处理器的注解, 用于代替手动编辑 resources/META-INF/services 的文件 compile group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc6' // 这行配置也需要添加,gradle 升级到 5 之后, 不加此配置, 不会生成 META-INF/services/javax.annotation.processing.Processor 文件 annotationProcessor group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc6'
如果当注解处理器打完包后, 被其他项目 (gradle) 引用, 也要 使用 compile / annotationProcessor 来引入两次
2. 理解几个常见的类 :
声明变量
JCTree.JCVariableDecl |
定义变量 | long a = 1 |
JCTree.JCExpressionStatement |
生成表达式 |
a = System.currentTimeMillis() |
JCTree.JCBlock |
代码块 (主要是用来放其他代码块, 或者 JCExpressionStatement 的) |
关于 更多 API 参照:https://blog.csdn.net/a_zhenzhen/article/details/86065063
简单用法参照:https://blog.csdn.net/dap769815768/article/details/90448451