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

做为程序猿,天天都在写代码,可是有没有想过经过代码对写好的代码”动点手脚”呢?今天就与你们分享——如何经过用AST语法树改写Java代码。git

 

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

 

void someMethod(){正则表达式

    String rst=callAnotherMethod();express

    LogUtil.log(TAG,”这里是一条很是很是长,比唐僧还啰嗦的日志信息描述,可是我短一点还不方便进行错误日志分析,调用callSomeMethod返回的结果是:”+rst);数据结构

……app

}工具

图一gradle

 

 

void someMethod(){ui

    String rst=callAnotherMethod();debug

    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有哪些应用场景?

 

一、    编译工具从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改写以后再进行编译。

 

二、   自动静态埋点

 

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

相关文章
相关标签/搜索