失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > java可以在类中直接定义语句_基于javac实现的编译时注解

java可以在类中直接定义语句_基于javac实现的编译时注解

时间:2022-07-27 12:29:54

相关推荐

java可以在类中直接定义语句_基于javac实现的编译时注解

很多同学都知道jdk中有一个很重要的jar : tools.jar,但是 很少有人知道这个包里面究竟有哪些好玩的东西。

javac入口及编译过程

在使用javac命令去编译源文件时,实际上是去执行com.sun.tools.javac.Main#main方法。而真正执行编译动作的,正是com.sun.tools.javac.main.JavaCompiler类。

javac的编译过程大致分如下几个阶段:

解析与填充符号表处理过程。

插入式注解处理的注解处理过程。

分析与字节码生成过程。

上面几个过程画成图的话,就是下面这张(来自openjdk):

对应到代码中,就是上面提到的JavaCompiler类中的complie方法。

/**

* Main method: compile a list of files, return all compiled classes

* ...

*/

public void compile(Collection sourceFileObjects,

Collection classnames,

Iterable extends Processor> processors,

Collection addModules)

{

...

//准备过程:初始化插入式注解处理器

initProcessAnnotations(processors, sourceFileObjects, classnames);

...

// These method calls must be chained to avoid memory leaks

processAnnotations(//过程2:执行注解处理

enterTrees(//过程1.2:输入到符号表

stopIfError(CompileState.PARSE,

initModules(stopIfError(CompileState.PARSE,

parseFiles(sourceFileObjects))))//过程1.1:词法分析、语法分析

),

classnames

);

...

switch (compilePolicy) {

...

case BY_TODO://过程3:分析及字节码生成

while (!todo.isEmpty())

generate(//过程3.4:生成字节码

desugar(//过程3.3:解语法糖

flow(//过程3.2:数据流分析

attribute(//过程3.1:标注

todo.remove()))));

break;

...

}

...

}

复制代码

今天我们关注的,正是javac中的注解处理器。

插入式注解处理器(JSR-269)

在jdk5时,java提供了对注解的支持,但当时,这些注解与普通的java代码相同,只能在运行时发挥作用。

而在jdk6中实现了JSR-269规范,提供了插入式注解处理器的标准API在编译期间对注解进行处理。所以,他们更像是编译器插件,让我们可以读取、修改、添加抽象语法树中的任意元素。

如果这些插件在处理注解注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有的插入式注解处理器都没有再对语法树进行修改为止。每一次循环称为一个Round。

AbstractProcessor

注解处理抽象类:javax.annotation.processing.AbstractProcessor。

如果要实现一个注解处理器,就必须要继承AbstractProcessor类,其中的process()方法,就是javac编译器在执行注解处理器时要调用的过程。

public abstract boolean process(Set extends TypeElement> annotations,

RoundEnvironment roundEnv);

复制代码

该方法的第一个参数:annotations,为此注解处理器所要处理的注解集合;第二个参数roundEnv,就是当前这个Round中的语法树节点。

具体的语法树节点可以参看枚举类:javax.lang.model.element.ElementKind,包括了java代码中最常用的元素。

PACKAGE

CLASS

LOCAL_VARIABLE

FIELD

...

另外,在init方法中,传入了实例变量processingEnv,他代表了注解处理器框架提供的上下文环境,在创建代码,输出信息,获取工具类是都需要用到该实例变量。

那么,如何让代码在编译时执行到我们自己的注解处理器呢?

请看javac -help

$ javac -help

用法: javac

其中, 可能的选项包括:

...

-processor [,,...] 要运行的注释处理程序的名称; 绕过默认的搜索进程

-processorpath 指定查找注释处理程序的位置

...

复制代码

当然,不用每次编译的时候都辛苦带上这个参数,我们可以使用maven-compiler-plugin插件:

maven-compiler-plugin

default-compile

compile

compile

1.8

1.8

xxx.xxx.xxx.xxx

复制代码

(com.google.auto.service.autoService的方式本文不再涉及,感兴趣的自行搜索)

具体实现

预期效果

使用编译时注解,实现在方法进入和方法退出时新增日志打印入参,出参功能。

预期效果入下图所示,左侧为java源码,右侧为编译后的class文件。

其中,红色箭头执行代码行即为编译时注解处理器生成代码。

使用方式

基于slf4j,在目标类中定义一个成员属性,创建出org.slf4j.Logger实例,属性名为:logger。

在需要新增打印日志的方法在加上自定义注解@AroundSlf4j。

实现思路

这里只放出关键思路及一些主要的代码。

自定义一个注解@AroundSlf4j

在注解处理器中,获取到所有被该注解标注的元素,并过滤出其中类型为METHOD的元素。

找到该元素的“属主”,遍历其成员变量,找到类型为org.slf4j.Logger,且名字为logger的符号引用。

获取当前处理的METHOD元素名,及所在类名,以及其参数列表,拼接成日志打印格式。

生成调用logger.info方法JCTree节点,将其加入至METHOD节点列表中。

递归遍历当前方法所有执行路径,找出所有类型为RETURN的节点。

6.1 根据RETURN语句的形式,创建出对应的调用logger.info方法JCTree节点。

6.2 将节点加入至RETURN节点前一个位置。

结束

定义注解&注解处理器

注意,该注解仅存在于SOURCE级别,再往后放也没什么用。

@Target({ElementType.TYPE,ElementType.METHOD})

@Retention(RetentionPolicy.SOURCE)

public @interface AroundSlf4j {

}

复制代码

@SupportedAnnotationTypes("AroundSlf4j")

@SupportedSourceVersion(SourceVersion.RELEASE_8)

public class AroundSlf4jProcessor extends AbstractProcessor {

...

}

复制代码

获取目标METHOD元素

在注解处理器的process方法中,可以得到所有被@AroundSlf4j注解标记的JCTree节点。这里需要再过滤掉owner为interface类型的元素。

Set extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(AroundSlf4j.class);

elementsAnnotatedWith.forEach(ele->{

if(ele.getKind() == ElementKind.METHOD && !((Symbol.MethodSymbol) ele).owner.isInterface()){

...

}

复制代码

获取类型指定类型、名称的成员属性符号引用

很简单,就是遍历该METHOD节点属主的全部成员属性,根据名字/类型比较。

private Symbol.VarSymbol getAvailableFieldInMethod(JCTree.JCMethodDecl jcMethodDecl,Class target,String name){

Scope members = jcMethodDecl.sym.owner.members();

Iterator iterator = members.getElements().iterator();

while (iterator.hasNext()){

Symbol next = iterator.next();

if(ElementKind.FIELD.equals(next.getKind()) && next.getQualifiedName().toString().equals(name)

&& next.type.tsym.getQualifiedName().toString().equals(target.getName())){

return (Symbol.VarSymbol) next;

}

}

return null;

}

复制代码

针对目标节点应用增强Visitor

我这里创建了一个名为AroundSlf4jMethodVisitor的增量类,继承自com.sun.tools.javac.tree.TreeTranslator。

因为需要在该增强类中生成logger.info调用。所以,刚才找到的logger成员属性当然要传递进去咯。

tree.accept(new AroundSlf4jMethodVisitor(treeMaker,names,logger));

复制代码

生成打印入参调用语句并加入

根据方法名和类名拼装日志打印内容比较简单,这里就不说了。

直接看怎么生成方法调用:

使用AroundSlf4jProcessor中传入的工具类:treeMaker。

JCTree.JCExpressionStatement beforeState = treeMaker.Exec(treeMaker.Apply(

List.nil(),

//调用方法

treeMaker.Select(treeMaker.Ident(logger.name),names.fromString("info")),

//入参

loggerArgs.toList()

)

);

复制代码

然后,需要把刚才生成的语句加入到原方法节树中。

因为打印入参应该在方法的第一条代码中。所以,使用prepend方法加入到节点头。

jcMethodDecl.body.stats = jcMethodDecl.body.stats.prepend(beforeState);

复制代码

递归遍历方法执行路径,找出所有RETURN语句

对于java代码中的语句类型,我这里只继续递归了代码块BLOCK,if语句IF,以及FOR_LOOP三种类型,已经可以覆盖大多数执行分支了。

private void walkReturnExpression(List statement){

for(int i = 0 ;i< statement.size();i++){

JCTree.JCStatement jcStatement = statement.get(i);

if(jcStatement == null){

continue;

}

switch (jcStatement.getKind()){

case BLOCK:

walkReturnExpression(((JCTree.JCBlock)jcStatement).stats);

break;

case IF:

((JCTree.JCIf)jcStatement).getThenStatement().accept(new AroundSlf4jBlockVisitor(treeMaker,names,logger));

JCTree.JCStatement current = ((JCTree.JCIf)jcStatement).getElseStatement();

walkReturnExpression(List.of(current));

break;

case FOR_LOOP:

JCTree.JCBlock body = (JCTree.JCBlock) ((JCTree.JCForLoop) jcStatement).body;

walkReturnExpression(body.stats);

break;

default:

System.out.println(jcStatement);

}

}

}

复制代码

后面,再给RETURN前加入打印日志调用。

jcMethodDecl.body.stats.stream().filter( c-> Tree.Kind.RETURN == c.getKind() ).findFirst().ifPresent( r->{

StatementHelper statementHelper = new StatementHelper(treeMaker,names);

JCTree.JCExpressionStatement endLogging = statementHelper.createEndLoggingStatementByReturn(logger, (JCTree.JCReturn) r);

jcMethodDecl.body.stats = SunListUtils.prependBeforeItem(jcMethodDecl.body.stats.iterator(),endLogging,r);

});

复制代码

因为打印日志调用因在return语句前。所以,他应该是倒数第二条代码。

我这里写了一个工具方法:在List指定元素前新增元素SunListUtils.prependBeforeItem。

参考资料

《深入理解java虚拟机》

openJDK源码

如果觉得《java可以在类中直接定义语句_基于javac实现的编译时注解》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。