一、概要说明
本次程序使用Lombok(Project Lombok)类似的工作方式,通过注解声明,在Java程序编译时,修改AST(抽象语法树),为函数增加debug输出。
二、实现效果
为使用了自定义Annotation(TraceMethodInfo)的类App的方法中,加入Slf4j的debug语句:“log.debug( <函数入参> )”。
Java类App原代码:
package com.xxyyzz;
import com.xyz.annotation.TraceMethodInfo;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class App
{
public static void main( String[] args )
{
log.debug( "" );
System.out.println( "Hello World!" );
App app = new App();
app.testNoParams();
app.testOneParams( "10", 10 );
}
@TraceMethodInfo
public void testNoParams() {
}
@TraceMethodInfo
public int testOneParams( String input, int x ) {
int l = input.length();
System.out.println( "length: " + l + ", int value: " + input );
if( l == 0 ) {
return 100;
}
return l;
}
private int notAnnotationProcess() {
return 0;
}
}
编译后代码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.xxyyzz;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class App {
private static final Logger log = LoggerFactory.getLogger(App.class);//此变量为Slf4j添加
public App() {
}
public static void main(String[] args) {
log.debug("");
System.out.println("Hello World!");
App app = new App();
app.testNoParams();
app.testOneParams("10", 10);
}
public void testNoParams() {
log.debug("testNoParams()");//此行为本次程序添加
}
public int testOneParams(String input, int x) {
log.debug("testOneParams(): input=" + input + ",x=" + x);// 此行为本次程序添加
int l = input.length();
System.out.println("length: " + l + ", int value: " + input);
return l == 0 ? 100 : l;
}
private int notAnnotationProcess() {
return 0;
}
}
三、JSR269介绍
Java6开始纳入了JSR-269规范:Pluggable Annotation Processing API(插件式注解处理器)。JSR-269提供一套标准API来处理Annotations,具体来说,我们只需要继承AbstractProcessor类,重写process方法实现自己的注解处理逻辑,并且在META-INF/services目录下创建javax.annotation.processing.Processor文件注册自己实现的Annotation Processor,在javac编译过程中编译器便会调用我们实现的Annotation Processor,从而使得我们有机会对java编译过程中生产的抽象语法树进行修改。
原文链接:https://blog.csdn.net/weixin_43983762/article/details/105867398
参考链接:https://juejin.cn/post/6960544470635347999
四、Lombok工作方式介绍
Project Lombok 是一个 java 库,可自动插入您的编辑器和构建工具,为您的 java 增添趣味。
永远不需要再编写另一个 getter 或 equals 方法,使用一个注释,您的类就有一套功能齐全的构造函数。
自动化log您的日志记录变量等功能。
参考链接:https://projectlombok.org/
五、本次程序实现
为了实现在编译期,修改Java方法,添加debug语句,需要编程实现两项:
- 声明Annotation
- 实现Annotation处理类,该类继承自javax.annotation.processing.AbstractProcessor
1. 定义Annotation
定义Annotation,类似于一个锚定(Anchor),告诉处理程序,凡是使用了此Annotation的方法,都需要被处理。
package com.xyz.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface TraceMethodInfo {
}
2. 方法实现类TracePathAnnotationProcessor
该类由两个主要的方法组成:
初始化系统变量:init(ProcessingEnvironment processingEnv)
处理相应的Annotation:process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
此方法首先找到使用了@TraceMethodInfo的方法,之后循环访问他们,添加log.debug()语句。添加语句的动作使用到了抽象语法树(AST),Java的源代码在编译的过程中,都会被转换为一颗语法树。通过修改语法树的结点,就可以将自己需要的功能添加上去。
3. AST编程说明
基本思想是:通过TreeMaker修改AST上面的结点。 比如下面源代码中的方法testOneParams:
@TraceMethodInfo
public int testOneParams( String input, int x ) {
int l = input.length();
System.out.println( "length: " + l + ", int value: " + input );
if( l == 0 ) {
return 100;
}
return l;
}
经过编译,会变成根为JCMethodDecl如下图的一棵树:
方法体(body),类型为JCBlock,是JCMethodDecl的一个属性,其也是一棵树,如下图:
想要给方法加入下面这句话:
log.debug("testOneParams(): input=" + input + ",x=" + x);
可以参考debug出来的程序中的语句:
System.out.println("length: " + l + ", int value: " + input);
如图:
每一个可以执行的语句要变成JCTree.JCStatement类型才能被执行。上面System.out这句话是类型JCExpressionStatement,该对象包括属性meth(System.out.println)和类型为com.sun.tools.javac.util.List的args。
注意这里参数args是a+b+c+d的样式,此样式的类型是JCBinary(图中4),opcode(操作符)是JCTree.Tag.PLUS(+),lhs(左子树)为a+b+c,rhs(右子树)为d(图中3),这里lhs同样是JCBinary,它可以再次被分解为lhs和rhs(图中2)。逐级分解到不能再分解时(图中1),lhs(“length:”)为JCLiteral类型,rhs(l)为JCIdent类型。
JCLiteral类型是字串,JCIndent类型是变量。
4. 程序实现说明
有了这些基础知识后,让我们看一下如何组织我们希望得到的这句话:
log.debug("testOneParams(): input=" + input + ",x=" + x);
源程序:
private JCTree.JCStatement buildArgumentsLog(Name name, List<JCTree.JCVariableDecl> params) {
JCTree.JCFieldAccess debugMethod = getDebugMethod();
// only one parameter, then set array size = 1
JCTree.JCExpression[] jcExpressions = new JCTree.JCExpression[ 1 ];
// no parameters for the method
if( params.isEmpty() ) {
// parameter of calling method: log.debug();
jcExpressions[0] = treeMaker.Literal( name.toString() + "()" );
} else {
jcExpressions[0] = generateParametersString( name, params );
}
// each steps with return value type
List<JCTree.JCExpression> debugParams = List.from(jcExpressions);
JCTree.JCMethodInvocation debugMethodInvocation = treeMaker.App( debugMethod, debugParams);
JCTree.JCExpressionStatement exec = treeMaker.Exec(debugMethodInvocation);
showLog( "Result: " + exec );
return exec;
}
程序说明
该方法使用目标方法名称(Name name)和目标方法参数列表(List<JCTree.JCVariableDecl> params)作为参数,通过以下两步实现。
- 方法getDebugMethod()得到“log.debug()”
private JCTree.JCFieldAccess getDebugMethod() {
JCTree.JCIdent ident = treeMaker.Ident(names.fromString("log"));
JCTree.JCFieldAccess debugMethod = treeMaker.Select(ident, names.fromString("debug"));
debugMethod.setType( new Type.JCVoidType() );
return debugMethod;
}
- 组织debug方法的String输入参数,样式:"method(): param[0]=" + param[0] + ",param[1]=" + param[1]......
方法:
private JCTree.JCBinary generateParametersString(Name name, List<JCTree.JCVariableDecl> params ) {
JCTree.JCBinary ret = null;
int pos = 0;
for( int i = 0; i < params.length()*2-1; i ++ ) {
if( i == 0 ) {
ret = treeMaker.Binary(JCTree.Tag.PLUS, treeMaker.Literal( name.toString() + "(): " + params.get( 0 ).getName() + "=" ), treeMaker.Ident( params.get( 0 ) ) );
continue;
}
// i is even, then pos = i/2
pos = (i%2) == 0? i/2: (i+1)/2;
// i is even, then return Indent ( b ), else Literal( "b" )
ret = treeMaker.Binary( JCTree.Tag.PLUS, ret, ( (i%2) == 0? treeMaker.Ident( params.get( pos ) ): treeMaker.Literal( "," + params.get( pos ).getName() + "=" ) ) );
}
return ret;
}
此方法循环访问所有的参数,循环次数是参数个数*2-1,将其组装成 "method(): param[0]=" + param[0] + ",param[1]=" + param[1]......,返回类型JCTree.JCBinary。
至此,语句
log.debug("testOneParams(): input=" + input + ",x=" + x);
已经组织完成。
下一步就是将其加入到method的body中。
- 修改body树
想要修改body树,不能通过改变method的body树中的stats属性值的方式实现。需要将新的statement和旧的statement List混合成新的List,依据此List创建新的body替换掉method的旧body。
// move old body to new
ArrayList<JCTree.JCStatement> jsta = new ArrayList<>();
jsta.add( logPart );
for(JCTree.JCStatement stm : methodBody.getStatements() ) {
// 方法返回
if( stm instanceof JCTree.JCReturn ) {
System.out.println( "X" );
}
jsta.add( stm );
}
methodBody = treeMaker.Block( 0, List.from(jsta) );
jcMethodDecl.body = methodBody;
整个的处理过程到这里就结束了。
Annotation处理类TracePathAnnotationProcessor 的完整程序如下:
package com.xyz.handler;
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 java.util.ArrayList;
import java.util.Set;
import javax.tools.Diagnostic;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.code.Type;
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.List;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;
import com.xyz.annotation.TraceMethodInfo;
/**
* Class to adding trace information on the call
* Use lombok.log to trace out (TRACE level) method parameters and return values
* and total execute time.
*/
@SupportedAnnotationTypes( "com.xyz.annotation.TraceMethodInfo" )
public class TracePathAnnotationProcessor extends AbstractProcessor {
// 编译时期输入日志的
private Messager messager;
// 将Element转换为JCTree的工具,提供了待处理的抽象语法树
private JavacTrees trees;
// 封装了创建AST节点的一些方法
private TreeMaker treeMaker;
// 提供了创建标识符的方法
private Names names;
/**
* if not provide this method, then output:
* 警告: No SupportedSourceVersion annotation found on com.xyz.handler.TracePathAnnotationProcessor, returning RELEASE_6.
* 警告: 来自注释处理程序 'com.xyz.handler.TracePathAnnotationProcessor' 的受支持 source 版本 'RELEASE_6' 低于 -source '1.8'
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_8;
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init( processingEnv );
this.messager = processingEnv.getMessager();
trees = JavacTrees.instance( processingEnv );
treeMaker = TreeMaker.instance( ((JavacProcessingEnvironment) processingEnv).getContext() );
names = Names.instance( ((JavacProcessingEnvironment) processingEnv).getContext() );
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(TraceMethodInfo.class);
annotatedElements.forEach( element -> {
showLog(" processing [" + element.toString() + "] " );
if( element.getKind().equals( ElementKind.METHOD ) ) {
showLog(" processing method: " + element.getSimpleName() );
JCTree tree = trees.getTree(element);
Symbol.MethodSymbol methodSymbol = (Symbol.MethodSymbol) element;
// 重写 accept 方法,extend TreeTranslator类
tree.accept(new TreeTranslator() {
@Override
public void visitMethodDef(JCTree.JCMethodDecl jcMethodDecl) {
super.visitMethodDef(jcMethodDecl);
showLog( " process " + jcMethodDecl.getName() + " via visitMethodDef " );
JCTree.JCBlock methodBody = jcMethodDecl.getBody();
if( methodBody == null ) {
showError( "Method " + jcMethodDecl.name + " dont have body!" );
return;
}
List<JCTree.JCVariableDecl> params = jcMethodDecl.params;
JCTree.JCStatement logPart = buildArgumentsLog( jcMethodDecl.name, params );
// move old body to new
ArrayList<JCTree.JCStatement> jsta = new ArrayList<>();
jsta.add( logPart );
for(JCTree.JCStatement stm : methodBody.getStatements() ) {
// 方法返回
if( stm instanceof JCTree.JCReturn ) {
System.out.println( "X" );
}
jsta.add( stm );
}
methodBody = treeMaker.Block( 0, List.from(jsta) );
jcMethodDecl.body = methodBody;
// showLog( methodBody.stats.toString() );
}
});
}
} );
return false;
}
/**
* build a new method statement to replace the old one.
* @param name
* @param params
* @return new method statement with log code added.
* format: "<method-name> ( <params(0)-name>=<params(0)-value>, <params(1)-name>=<params(1)-value>,
* ..., <params(n)-name>=<params(n)-value> )"
* in case no parameter, format: "<method-name> ()"
*/
private JCTree.JCStatement buildArgumentsLog(Name name, List<JCTree.JCVariableDecl> params) {
JCTree.JCFieldAccess debugMethod = getDebugMethod();
// only one parameter, then set array size = 1
JCTree.JCExpression[] jcExpressions = new JCTree.JCExpression[ 1 ];
// no parameters for the method
if( params.isEmpty() ) {
// parameter of calling method: log.debug();
jcExpressions[0] = treeMaker.Literal( name.toString() + "()" );
} else {
jcExpressions[0] = generateParametersString( name, params );
}
// each steps with return value type
List<JCTree.JCExpression> debugParams = List.from(jcExpressions);
JCTree.JCMethodInvocation debugMethodInvocation = treeMaker.App( debugMethod, debugParams);
JCTree.JCExpressionStatement exec = treeMaker.Exec(debugMethodInvocation);
showLog( "Result: " + exec );
return exec;
// return treeMaker.Exec( treeMaker.App( debugMethod, List.from( jcExpressions ) ) );
}
// Loop to get the JCBinary
private JCTree.JCBinary generateParametersString(Name name, List<JCTree.JCVariableDecl> params ) {
JCTree.JCBinary ret = null;
int pos = 0;
for( int i = 0; i < params.length()*2-1; i ++ ) {
if( i == 0 ) {
ret = treeMaker.Binary(JCTree.Tag.PLUS, treeMaker.Literal( name.toString() + "(): " + params.get( 0 ).getName() + "=" ), treeMaker.Ident( params.get( 0 ) ) );
continue;
}
// i is even, then pos = i/2
pos = (i%2) == 0? i/2: (i+1)/2;
// i is even, then return Indent ( b ), else Literal( "b" )
ret = treeMaker.Binary( JCTree.Tag.PLUS, ret, ( (i%2) == 0? treeMaker.Ident( params.get( pos ) ): treeMaker.Literal( "," + params.get( pos ).getName() + "=" ) ) );
}
return ret;
}
/**
* Get lombok created "log" field in the class
* @return “log.debug()", here log is created by Slf4j
*/
private JCTree.JCFieldAccess getDebugMethod() {
JCTree.JCIdent ident = treeMaker.Ident(names.fromString("log"));
JCTree.JCFieldAccess debugMethod = treeMaker.Select(ident, names.fromString("debug"));
debugMethod.setType( new Type.JCVoidType() );
return debugMethod;
}
/**
* Print log
* @param msg
*/
void showLog( String msg ) {
show( Diagnostic.Kind.NOTE, msg );
}
void showWarn( String msg ) {
show( Diagnostic.Kind.WARNING, msg );
}
void showError( String msg ) {
show( Diagnostic.Kind.ERROR, msg );
}
void show( Diagnostic.Kind type, String msg ) {
messager.printMessage( type, msg );
}
}
5. META-INF 配置文件
文件路径:resources\META-INF\services\javax.annotation.processing.Processor
文件内容:
com.xyz.handler.TracePathAnnotationProcessor
此文件告诉Java编译器,去哪里能找Annotation Processor类。
6. 如何调试Annotation Processor 程序
在IDEA中定义一个项目,两个模块。第一个模块是Processor处理器程序,第二个模块是测试用程序。
调试Processor模块的方法,请参考链接:# IDEA 设置编译方式为MAVEN编译, 并且在编译JAVA源文件时DEBUG(ANNOTATION PROCESSOR