很多同学都知道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实现的编译时注解相关推荐

  1. java实现浏览器ui中的收藏夹_基于Selenium2+Java的UI自动化(2) - 启动浏览器

    一.准备工作 我们常用的浏览器主要有三个:chrome.Firefox.IE:其中chrome 和 IE 需要下载驱动程序,才能启动浏览器,注意驱动程序有32位和64位两种. 另外:如何查看本机的浏览 ...

  2. 在一个java源文件中只能定义_10在Java的一个源文件中可以定义多个类。

    [单选题]一组常量和抽象方法的集合可以定义成一个 ( ) [判断题]3.当定义一个类时没定义构造方法,则系统自动产生一个构方法. [填空题]JAVA源程序中,跨越多行的注释只需在开始和结尾处用____ ...

  3. java sql范围查询语句,java类中写sql语句,查询条件包含换行

    java类中写sql语句,查询条件包含换行 detachedCriteria.add(Restrictions.or( Restrictions.like("chengBanDanWeiId ...

  4. java中构造方法只能有一个_对Java中类的构造方法描述正确的是()A.如果在类中没有定义,Java就提供一个默认的构造方法B.只能...

    对Java中类的构造方法描述正确的是()A.如果在类中没有定义,Java就提供一个默认的构造方法B.只能 更多相关问题 猛虎噬人卣是_______时期的陶塑代表作品. 静态网页是指网页的内容是固定的, ...

  5. java定义一个点_JAVA 定义一个Point类 它的对象是指一个平面上的点(x,y),在定义Point类中要定义它的三个构造函数...

    JAVA 定义一个Point类 它的对象是指一个平面上的点(x,y),在定义Point类中要定义它的三个构造函数 JAVA 定义一个Point类 它的对象是指一个平面上的点(x,y),在定义Point ...

  6. java mysql查询字段换行,java类中写sql语句,查询条件包含换行

    java类中写sql语句,查询条件包含换行 detachedCriteria.add(Restrictions.or( Restrictions.like("chengBanDanWeiId ...

  7. java编译时注解_简单介绍 Java 中的编译时注解

    1. 前言 上一篇 主要介绍了什么是 注解 (Annotation) 以及如何读取 运行时注解 中的数据, 同时用注解实现了简单的 ORM 功能. 这次介绍另一部分: 如何读取 编译时注解 ( Ret ...

  8. java中unknown source,java - 对于 提示信息为 unknown source的解决办法: jdk 替换jre, 编译时加上debug=true...

    java - 对于 提示信息为 unknown source的解决办法: jdk 替换jre, 编译时加上debug=true 2017-08-09 10:43 访问量: 4466 分类: 技术 昨天 ...

  9. 3. 自定义Java编译时注解处理器

    1. 絮絮叨叨 要么是注解跟我有仇,要么是公司配发的笔记本跟我有仇,要么是因为心急吃不了热豆腐 痛定思痛:从头开始,新建一个Java项目,实现一个超级简单的注解@Hello 通过获取被标识类的类名(原 ...

最新文章

  1. kalilinux安装搜狗输入法
  2. 用VSCode写python的正确姿势
  3. 继续不务正业,今天来弄弄R
  4. (0048)iOS开发之内存管理探究
  5. XShell技巧收集
  6. 作为开发者发布小程序_如何建立个人品牌作为新开发者
  7. 获取当前jvm的进程号
  8. Mac 使用Navicat连接Oracle提示:ORA-21561: OID generation failed
  9. css的9个常用选择器
  10. Linux设备驱动模型-Bus
  11. matlab 图像方差,Matlab方差解析var--实例说明matlab求方差
  12. 美团外卖前端可视化界面组装平台 —— 乐高
  13. 网站采集器-免费任意网页数据采集器
  14. 写论文 参考文献引用 谷歌学术 规范格式 一键生成
  15. 费曼技巧:学习任何东西的最佳方法
  16. 家居家装行业人群洞察白皮书.pdf
  17. (转)create-react-app入门教程
  18. 在线编辑PDF:GcPDF|PDF在线预览GrapeCity Documen PDF
  19. 动画和漫画里ed、op、OVA、ost、bl、gl是什么意思?
  20. 小冰与51CTO的前世今生

热门文章

  1. oracle REPLACE函数语法
  2. 基于python的文件传输程序_GitHub - orange0cat/python-ft: 基于socket的文件传输程序,能传输整个文件夹...
  3. 服务器不知别内存_程序优化浅谈服务器实现高并发的原理
  4. dedecms友情链接plus/flink.php页面出错,dedecms友情链接flink的调用方法
  5. 这些规范你需要上点心
  6. j-link “the connected j-link is defective“问题的解决
  7. 刘金藏:3.24黄金晚间如何操作3.25黄金原油最新操作策略
  8. vba listbox 内容输出到文本_利用剪贴板提取工作表的文本内容
  9. 安全中心开启小米云服务器,小米云服务使用手册
  10. 珠宝VIP客户该怎么管理?