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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM