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

作为程序猿,每天都在写代码,但是有没有想过通过代码对写好的代码”动点手脚”呢?今天就与大家分享——如何通过用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<Path> //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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏网络

编码在网络安全中的应用和原理

前言:现在的网站架构复杂,大多都有多个应用互相配合,不同应用之间往往需要数据交互,应用之间的编码不统一,编码自身的特性等都很有可能会被利用来绕过或配合一些策略,...

2986
来自专栏WeTest质量开放平台团队的专栏

Unity3d底层数据传递分析

这篇文章主要分析了在Mono框架下,非托管堆、运行时、托管堆如何关联,以及通过哪些方式调用。内存方面,介绍了什么是封送,以及类和结构体的关系和区别。

5972
来自专栏C语言及其他语言

【编程经验】C语言程序真正的启动函数(文末有福利,第二弹)

又到了小编和大家说到的时间了,闲话少说,直接上正题,今天就讲讲“C语言程序真正的启动函数”。 为什么要用”真正”这个词?因为我们从...

31712
来自专栏华仔的技术笔记

浅谈Objective-C设计模式(Design Pattern)import "Singleton.h"

3287
来自专栏ImportSource

设计模式-抽象类,只是想为你做更多

如果说面向对象中的接口是把所有的事情扔给你的话,那么抽象类显然是想要为你做一些事情,如果实在有一部分做不了再扔给你。 相信对于大部分业务开发的场景下都不太会需要...

3447
来自专栏大愚Talk

Redis的数据类型——探究竟

接上篇 为什么要用Redis,今天来聊聊具体的Redis数据类型与命令。本篇是深入理解Redis的一个重要基础,请坐稳,前方 长文预警。

971
来自专栏西枫里博客

Ajax处理success回调函数返回的json数据。

站长最近在项目中用调用一个分类的子数据,由于表单要填写的数据较多,为了实现无刷新的选择操作,就使用ajax做了异步查询。查询的结果因为是多条数据,一直以来动用a...

1292
来自专栏Golang语言社区

十条有用的 Golang语言 技术

十条有用的 Go 技术 这里是我过去几年中编写的大量 Go 代码的经验总结而来的自己的最佳实践。我相信它们具有弹性的。这里的弹性是指: 某个应用需要适配一个灵...

4159
来自专栏Golang语言社区

十条有用的 Golang语言 技术

十条有用的 Go 技术 这里是我过去几年中编写的大量 Go 代码的经验总结而来的自己的最佳实践。我相信它们具有弹性的。这里的弹性是指: 某个应用需要适配一个灵...

3676
来自专栏PHP在线

十个 PHP 开发者最容易犯的错误

PHP 语言让 WEB 端程序设计变得简单,这也是它能流行起来的原因。但也是因为它的简单,PHP 也慢慢发展成一个相对复杂的语言,层出不穷的框架,各种语言特性和...

3745

扫码关注云+社区