Java无可匹敌的变身装备,钢铁侠客的绝密味道

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

我讨厌写一些业务代码,不仅仅因为它们的原始意图不是我设计的,成功了是产品的功劳,失败了代码要背锅。一个重要的原因,就是重复的代码太多,一个复杂的业务逻辑要找到它的Bug,也要下一番“苦力”。这里说的真的是苦力,而不是脑力,说明了大部分是低劣的重复劳动。

所以随着在项目中有了话语权,我会特别善待这些可怜的同学们。一个问题,直到发现的时候,才发现它的低级,但中间的曲折,很少有人能看到,一个非技术出身的管理者对此就很难理解。职位越是高,就越关注整体的目标达成,对个体的感受却关注的很少,这不是一个好的现象。千里之堤,溃于蚁穴。普通研发的整体水平代表了公司的竞争力。哦哦哦,我竟然违背了精英主义论调。

扯远了。下面介绍几个开发中常用的工具包,可以在Java源文件、语法树、字节码之间进行转换。用好了它们,不仅仅能实现一些黑科技,还能大大提高我们的生产力。

有时候,我们要做一些代码生成工具,需要生成一些Java类源文件。如果使用字符串去拼接的话,很容易拼的乱七八糟。为了解放双手,就可以使用工具JavaPoet进行方法或者代码块的构建。它还提供了占位符等一系列方便的操作,使得你生成的代码优美可读,清脆爽口。

特别在写一些框架的时候,可以将一些非常脏、非常累的活儿交给它。

代码示例。

JavaCC是一个语法生成器和语法分析器,可以通过读取一个.jj(它是我是姐姐)描述文件来生成一个Java文件。听起来和JavaPoet一样,但它是语法层面的,过程是翻译而不是“构造”,和我们学习的编译原理是一个层面的东西。而且JavaCC生成的文件一般是不可读的,谁让它和yacc以及lex比较像呢。

如果你想要自定义一些表达式,或者做一个特殊格式的解析器,你可能会用到它。

与此类似的还有ANTLR。应用方面,Velocity和FreeMarker都使用JavaCC作为语法解释器;Hibernate则使用ANTLR作为HQL的语法解释器。

不过我还是更喜欢Ragel(不是Java的)多一些。对于这些场景来说,文件生成以后就不再依赖这些工具了,还是高效和好用更重要。

上面是通过语法树生成Java文件(或者其他文件),Javaparser是通过Java文件生成语法树(AST),然后基于这棵语法树进行Java代码的分析和修改。注意,它可以直接再根据语法树,反向生成Java文件哦。

根据这棵语法树,你可以直接分析一个独立的Java文件,即使这个Java文件乱七八糟,不能通过编译,不用在运行时使用反射等功能。配合JavaPoet可以去做一些非常有意思的功能。比如,Javaparser提取Java文件的注释或者注解,然后通过JavaPoet生成一些自动文档(Swagger),或者进行测试用例的自动填充。

除了这些,Javaparser可以规定十分严格的语法格式,所以使用它做一个代码审查工具,甚至是做一些代码依赖分析,也是可以的。

其实这只是个辅助开发工具,应该有很多研发用起来了,它可以显著的减少代码量。让我们摆脱恼人的get、set、hashCode、equals,甚至log等。

Lombok其实部分上和上面说到的Javaparser类似,不过它是JDK层面的。

在javac将Java文件解析成抽象语法树之后(AST),Lombok则根据自己的注解处理器,动态的修改AST,增加新的节点代码。然后,生成我们最终的字节码。

这个技术,是由javac的注解API来实现的(JSR-269),可以让javac在编译期去干一些事情。

JDK做的工作,远比我们表面上用到的那些多的多。

上面几个工具都是把一些其他方式的代码转化为Java源文件,这些Java源文件还需要经过编译这道工序,才能够被真正使用。

ByteBuddy可以更进一步,直接动态生成Java类。对,就是直接加载在元空间的那种,在程序运行期间就可以动态的对class进行更改。不需要曲径通幽,直达目的。

它的底层是ASM,所以ByteBuddy是可以直接修改字节码的,是一种字节码增强工具

于此类似的工具还有JAVASIST、CGLiB等。

如果你在做一些代理类的工作,或者做一些APM等,想要实现类似Aspectj之类的功能,再或者类似Arthas那样的故障排查工具,那这些工具正适合。

以上几个工具包,有些是比较偏门的,但它们完成的功能却非常酷炫。不仅酷炫,而且非常有用。在领导们频繁开会,使用各种方法论探讨怎么管理文档,怎么自动化,怎么代码审查的时候,你就已经把功能完成了。

别告诉他们!

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。

基于语法树的 Java 代码自动化插桩

自动化插桩 ,也就是在代码的特定位置,自动的插入我们需要的一行或几行代码。 通常我们会在编译后的代码上进行插桩,这样做好处就是避免了对源码的侵入,一定程度上屏蔽了开发者不同的代码风格。 这里,我们主要介绍另一种在源码上插桩的方式,如果不考虑对代码的侵入性,那么在源码上直接插桩会更加直观,也就更加容易把控和调试,具有更高的灵活性,而且本文将要介绍的这种方法也无须考虑不同的代码风格。

以一段结构比较简单的Java代码为例,假设我们有一个后缀名为\”.java\”的源码文件,需要在里面的每个方法的开头插入一行代码打印当前方法的名字,在代码执行时,让我们能够知道哪个方法被调用过,以便绘制整个工程的调用关系图。文件中的源码如下:

显然,我们不可能直接插桩,因为那样即便是你能准确的定位到每个方法的第一行,仍然不具有通用性,同样的代码换一个写法,或者增加一些复杂的代码结构,再或者换个书写习惯,不仅无法识别代码第一行,即便是再开发,也非常复杂。接下来我们换个思路进行插桩。

在这里我们需要探究的是Java代码是如何构成的,或者说一个后缀名为\”.java\”的文件里,都包含了怎样的语句和结构。例如在上述代码中,可以很直观的看到,它包含:

  • 包名
  • 类名
  • 类里面三个属性
  • 类里面一个构造方法及其包含的语句
  • 类里面三个普通方法及其包含的语句按照它们的包含关系,可以画出这样的一个树形关系图:

从根节点开始一层一层的将所有代码都包含进来。完整且清晰的表示了这部分代码和它们之间的关系。

上面这棵\”树\”是一种非常直观的方式,但也说明了代码是可以抽象成树的形式表示的。接下来我们以更细的粒度再绘制这棵树。

抽象语法树(AST)是源码语法结构的一种抽象表示,它以树状的形式表现代码的结构。实际上Eclipse已经提供了源码的AST表示,以帮助开发者更加完整、清晰的分析代码的结构和关系。

在这里我们要实现的是自动化插桩,也就是说我们需要实时分析代码结构,然后在正确的位置插入准备好的代码,并且保证插桩后的代码能够被编译、执行。

我们使用JavaParser来对源码进行处理,它是一个比较通用的代码分析工具。通过JavaParser的解析,我们将会得到一个\”.java\”文件的抽象语法树。参考如下步骤:

1)引用

创建项目工程后,引入JavaParser,Maven或Gradle均可:

  • Maven
  • Gradle

注意引用版本不要低于3.6.4,否则会出现各种疑难杂问题。

2)解析Java源码

依赖JavaParser工具,我们只需要传入\”.java\”文件的输入流,就可以完成对源码的解析。

JavaParser中的parse方法,会依据源码生成代码树,并以CompilationUnit类型返回。CompilationUnit类位于com.github.javaparser.ast包下面,正常引入JavaParser就可以使用。通过打断点的形式,我们观察一下该对象的构成:

对于树形结构,我们最容易理解的属性就是childNode了,在根节点compilationUnit上有两个子节点,一个是所属包名,一个是ASTDemo类,类节点下又包含七个子节点,除了类名节点外,另外6个节点分别代表了三个类属性变量,一个构造方法和三个普通方法。

具体到某一个方法,以methodDemo(String, String)为例,它有5个子节点:

分别是方法名,两个参数,返回值和方法体,我们都知道,该方法体内的代码为:

继续向内跟踪,方法体节点下有两个子节点,分别代表了两个if

以此类推,细化到某一语句时,仍然是类似结构,例如赋值语句 strData = strData + param1; 以该语句为一个根节点包含两个子节点,分别是赋值符的左边和右边:

\”=\”作为一种“赋值(ASSIGN)”操作,保存在根节点的operator属性中:

同理对于\”=\”右侧,有两个节点strData和param2,以及标识PLUS操作的oprator属性:

可以看出抽象语法树中包含了源码的全部信息,在这棵树上,我们能够准确的定位到任何我们需要识别的代码结构。

回忆一下我们的需求:在每个方法的第一行插入打印日志的语句。所以我们首先需要找到“每个方法”。对于抽象语法树来说也就是找到所有的方法节点,很自然的我们想到遍历这棵树。

VoidVisitorAdapter的visit方法对于每一种类型都有定义,我们这里需要识别的是构造方法和普通方法,所以实现ConstructorDeclaration和MethodDeclaration两种类型即可。

visit方法中分别是遇到对应类型节点时的处理逻辑。所以我们需要在这里进行插桩。

当前的代码是树形结构,要想添加一行代码,我们需要创建一个节点。为了尽量简单,我们插入就插入一条简单的执行语句 System.out.println(…) ,这种语句的类型是ExpressionStmt,所以我们先创建一个节点:

接下来,我们要把这个节点挂到正确的位置上去,要求是每个方法代码的第一行,转换到语法树上也就是要在方法体节点的第一个子节点前增加这个节点:

这里需要特别说明的是,如果是子类的构造方法,因为调用

super()需要在第一行进行,所以可以判断如果构造方法中第一行是super方法,那么这个节点要插在第二个位置,否则编译时会报错。

我们可以通过调用adapter的visit方法,传入之前生成的语法树对象,开始遍历:

经过遍历后,每一个方法节点下面都插入了预设的代码。

最后,调用CompilationUnit的toString()方法将代码树转成源码,打印到相同位置的\”.java\”文件中,并覆盖原有数据,自动化插桩完成。

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。