教你如何用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

相關文章
相關標籤/搜索