1. 程式人生 > 其它 >教你如何用AST語法樹對程式碼“動手腳”

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