教你如何用AST语法树对代码“动手脚”
个推安卓工程师,负责公司移动端项目的架构和开发,主导移动端日志管理平台系统架构和开发工作,熟悉前后端的技术线,参与个推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<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
教你如何用AST语法树对代码“动手脚”的更多相关文章
- Atitit. 构造ast 语法树的总结attilax oao 1. Ast结构树形12. ast view (自是个160k的jar )22.1. 多条语句ast结构22.2. 变量定义 int b,c; 的ast结构22.3. 方法调用meth1(a=1,b=2,c=3); 的ast结构23. 误解的问题33.1. 语法书子能是个二叉树,实际上多叉树越好..33.2. 非要不个ast放到个s
Atitit. 构造ast 语法树的总结attilax oao 1. Ast结构树形1 2. ast view (自是个160k的jar )2 2.1. 多条语句ast结构2 2.2. 变量定义 in ...
- Atitit. 构造ast 语法树的总结attilax v2 q0f
Atitit. 构造ast 语法树的总结attilax v2 q0f 1. Ast结构树形1 2. ast view (自是个160k的jar )2 2.1. 多条语句ast结构2 2.2. 变量定义 ...
- Atitit. 构造ast 语法树的总结attilax oao
Atitit. 构造ast 语法树的总结attilax oao 1. 能那更加有意义的名字来命名ast节点... 1 2. 如何命名表达式名称..使用实际对象名称,而不是操作符号表达式更好 1 2.1 ...
- 使用RoslynSyntaxTool工具互相转换C#代码与语法树代码
项目地址 MatoApps/RoslynSyntaxTool: 此工具能将C#代码,转换成使用语法工厂构造器(SyntaxFactory)生成等效语法树代码 (github.com) 基础概念 S ...
- vue 的模板编译—ast(抽象语法树) 详解与实现
首先AST是什么? 在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言 ...
- Atitit.sql ast 表达式 语法树 语法 解析原理与实现 java php c#.net js python
Atitit.sql ast 表达式 语法树 语法 解析原理与实现 java php c#.net js python 1.1. Sql语法树 ast 如下图锁死1 2. SQL语句解析的思路和过程3 ...
- AST抽象语法树——最基础的javascript重点知识,99%的人根本不了解
AST抽象语法树——最基础的javascript重点知识,99%的人根本不了解 javascriptvue-clicommonjswebpackast 阅读约 27 分钟 抽象语法树(AST),是一 ...
- 一篇文章教你如何用R进行数据挖掘
一篇文章教你如何用R进行数据挖掘 引言 R是一种广泛用于数据分析和统计计算的强大语言,于上世纪90年代开始发展起来.得益于全世界众多 爱好者的无尽努力,大家继而开发出了一种基于R但优于R基本文本编辑器 ...
- [WebKit内核] JavaScript引擎深度解析--基础篇(一)字节码生成及语法树的构建详情分析
[WebKit内核] JavaScript引擎深度解析--基础篇(一)字节码生成及语法树的构建详情分析 标签: webkit内核JavaScriptCore 2015-03-26 23:26 2285 ...
随机推荐
- CentOS7.2下Nginx的使用
Nginx的启动 指定配置文件的方式启动 nginx -c /etc/nginx/nginx.conf 对于yum安装的nginx,使用systemctl命令启动 systemctl start ng ...
- 排序算法Java实现(快速排序)
算法描述:对于一组给定的记录,通过一趟排序后,将原序列分为两部分,其中前一部分的所有记录均比后一部分的所有记录小,然后再依次对前后两部分的记录进行快速排序,递归该过程,直到序列中的所有记录均有序为止. ...
- c++ --> 重载、重写(覆盖)和隐藏的区别
重载.重写(覆盖)和隐藏的区别 一.重载 重载从overload翻译过来,是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心 ...
- USB Audio设计与实现
1 前言 本文将基于STM32F4 Discovery板,从零开始设计并实现一个USB Audio的例子. 2 设计构思 所谓的USB AUDIO就是制作一个盒子,这个盒子可以通过USB连接到PC,P ...
- [poj3687]Labeling Balls_拓扑排序
Labeling Balls poj-3687 题目大意:给出一些球之间的大小关系,求在满足这样的关系下,编号小的尽量比编号大的球的方案. 注释:1<=N(球的个数)<=200,1< ...
- python(练习实例)
Python 练习实例1 题目:有四个数字:1.2.3.4,能组成多少个互不相同且无重复数字的三位数?各是多少? 我的代码:python 3+ #2017-7-20 list_h = [1,2,3,4 ...
- 韩天峰博客 php基础知识学习记录
http://rango.swoole.com 写好PHP代码真的不容易,给大家几个建议: 慎用全局变量,全局变量不好管理的,会导致你的代码依赖于全局变量,而耦合度太高. 一定不要复制粘贴代码,可重用 ...
- 2017-2018-1 20155306 《信息安全系统设计基础》Mybash的实现
2017-2018-1 20155306 <信息安全系统设计基础>Mybash的实现 要求: 使用fork,exec,wait实现mybash 写出伪代码,产品代码和测试代码 发表知识理解 ...
- 201621123050 《Java程序设计》第11周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多线程相关内容. 2. 书面作业 本次PTA作业题集多线程 1. 源代码阅读:多线程程序BounceThread 1.1 BallR ...
- 【详细】Lucene使用案例
Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引 ...