Android 教你如何用 AST 语法树对代码 “动手脚”

HongJack · 2017年05月15日 · 最后由 jasl 回复于 2017年05月16日 · 6039 次阅读

作者 | 刘斌 个推安卓工程师,负责公司移动端项目的架构和开发,主导移动端日志管理平台系统架构和开发工作,熟悉前后端的技术线,参与个推 SDK 主要业务研发工作,善于解决项目中遇到的痛点问题。

作为程序猿,每天都在写代码,但是有没有想过通过代码对写好的代码” 动点手脚” 呢?今天就与大家分享——如何通过用 AST 语法树改写 Java 代码。

先抛一个问题:如何将图一代码改写为图二?

void someMethod(){ String rst=callAnotherMethod(); LogUtil.log(TAG,” 这里是一条非常非常长,比唐僧还啰嗦的日志信息描述,但是我短一点还不方便进行错误日志分析,调用 callSomeMethod 返回的结果是:”+rst); …… } 图一

void someMethod(){ String rst=callAnotherMethod(); LogUtil.log(TAG,”<-(1)->”+rst); …… } 图二

此题需要把代码中和程序逻辑无关的字符串提取出来,替换为 id。比如个推日志输出类,缩短日志描述信息后,输出的日志就随之变短,根据映射表可以恢复真实原始日志。

通过何种方案改写?

你可能会想通过万能的 “正则表达式” 匹配替换,但当代码较为复杂时(如下图所示),使用 “正则表达法” 则会将问题复杂化,难以确保所有代码的完美覆盖并匹配。若通过 AST 语法树,可以很好地解决此问题。

import static Log.log; … log(“i am also the log”); String aa=“i am variable string”; log(“i am the part of log”+ aa +String.format(“current time is %d”,System.currentTimeMillis()));

什么是 AST 语法树?

AST(Abstract syntax tree)即为 “抽象语法树”,简称语法树,指代码在计算机内存的一种树状数据结构,便于计算机理解和阅读。

一般只有语言的编译器开发人员或者从事语言设计的人员才涉及到语法树的提取和处理,所以很多人会对这个概念比较陌生。

上图即为语法树,左边树的节点对应右边相同颜色覆盖的代码块。

众所周知,Java 编译流程(上图)中也有对 AST 语法树的提取处理,那是否可以在此环节操作语法树呢?由于编译链代码栈太深,鲜有对外的接口和文档,使得其可操作性不强。不过,如果采用迂回战术如下图所示,可以对其进行操作。

个推 log-rewrite 项目改写日志,就是用 AST 语法树进行的,流程图如下图所示。

先把所有源码解析为 AST 语法树,遍历每一个编译单元与单元的类声明,在类声明里根据日志方法的签名找到所有的方法调用,然后遍历每个方法调用,将方法调用的第二个参数表达式放入递归方法,对字符串字面值进行改写。

对应的代码较为简短, 使用 github 的 Netflix-Skunkworks/rewrite 开源库与 kotlin 语言,能读懂 Java 的你也一定能读明白。

val JavaSources:List //Java source file path list OracleJdkParser().parse(JavaSources) .forEach { unit -> unit.refactor(Consumer { tx -> unit.classes.forEach { clazz -> clazz.findMethodCalls("demo.LogUtillog(String,String)").forEach{ mc -> val args = mc.args.args val expression = args[1] logMapping.refactor(clazz, expression, tx) } } val fix = tx.fix() val newFile = ...//dist Source File ... newFile.writeText(fix.print()) }) } fun refactor(clazz: Tr.ClassDecl, target: Expression, refactor: Refactor, originSb: StringBuilder): Unit { when(target) { is Tr.Literal -> { refactor.changeLiteral(target) { t -> val id = pushMapping(clazz, t) //pushLiteral to mapping and return id originSb.append("$PREFIX$t$POSTFIX") return@changeLiteral rewriteNormal(id) } } } is Tr.Binary -> { refactor(clazz, target.left, refactor, originSb) refactor(clazz, target.right, refactor, originSb) } } }

如果想将日志恢复原样,可根据前缀、后缀定制正则表达式,逐行匹配替换。如下图所示。

val normalPattern = Pattern.compile("(<!--\[([^|]+)\|(\d+)_(\d+):(\d+)]-->)") logFiles.forEach { file -> file.bufferedReader().use { reader -> File(distDir, file.name).bufferedWriter().use { writer -> var line: String while(true){ line = reader.readLine() if (line == null) break val matcher = normalPattern.matcher(line) var newLine: String = line + "" while (matcher.find()) { //normal recover val token = matcher.group(1) val projectName = matcher.group(2) val appVersion = matcher.group(3).toInt() val targetVersion = matcher.group(4).toInt() val id = matcher.group(5).toLong() val replaceMent = findReplacement(projectName,appVersion, targetVersion, id) newLine = newLine.replace(token, replaceMent) } writer.write(newLine) writer.newLine() } } }

AST 有哪些应用场景?

1、 编译工具从 ant 到 gradle 的切换

the ant env SDK_VERSION=2.0.0.2 // #expand public static final Stringsdk_conf_version = "%SDK_VERSION%"; publicstaticfinalString sdk_conf_version = "1.0.0.1";

publicstaticfinalString sdk_conf_version = “2.0.0.2"; //public static final String sdk_conf_version= "1.0.0.1";

此项目起步于 ant 主流时期,随着技术日渐成熟,gradle 逐渐取代了 ant 的位置,演变成官方的编译打包方式。因为历史原因,若直接将上图类似预编译的代码切换到 gradle 较为棘手,通过 AST 语法树重写,再用 gradle 编译,就可以解决此问题。

try{ value = Boolean.parseBoolean(str); } catch (Throwable e) { // #debug e.printStackTrace(); }

try{ value = Boolean.parseBoolean(str); } catch (Throwable e) {

}

void m(){ relaseCall(); //#mdebug String info="some debug infomation"; LogUtil.log(info); //#enddebug }

void m(){ relaseCall(); }

上图的 #debug 和 #mdebug 指令,也可以通过 AST 改写之后再进行编译。

2、 自动静态埋点

void onClick(View v){ doSomeThing() }

void onClick(View v){ RUtil.recordClick(v); doSomeThing(); }

代码中需要运营统计、数据分析等,需要通过代码埋点进行用户行为数据收集。传统的做法是手动在代码中添加埋点代码,但此过程较为繁琐,可能会对业务代码造成干扰,倘若通过改写 AST 语法树,在编译打包期添加这种类似的埋点代码,就可减少不必要的繁琐过程,使其更加高效。

最后附推荐操作 AST 类库链接&完整项目源码地址,希望可以帮助大家打开脑洞,设想更多的应用场景。

推荐操作 AST 类库链接 https://github.com/Netflix-Skunkworks/rewrite
https://github.com/Javaparser/Javaparser https://github.com/antlr/antlr4

完整项目源码地址如下,欢迎 fork&start https://github.com/foxundermoon/log-rewrite

共收到 2 条回复

以前遇到过一个通过改写 impala 的 ast 来做类似分片分表的事情的产品。 怎么说呢,如果你不是对 ast 和编译器了如指掌,还是尽量不要用这种方法做事。 那个产品 bug 多到不能忍受,稍微复杂一点的查询就错误一大堆,简直毫无可用性

用 MD 排版一下嘛。。。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册