Java方法完整調(diào)用鏈生成工具
1. 前言
在很多場(chǎng)景下,如果能夠生成Java代碼中方法之間的調(diào)用鏈,是很有幫助的,在代碼審計(jì)及漏洞分析等場(chǎng)景中也是。
IDEA提供了顯示調(diào)用指定Java方法向上的完整調(diào)用鏈的功能,可以通過“Navigate -> Call Hierarchy”菜單(快捷鍵:Ctrl+Alt+H)使用;Eclipse也提供了相同的功能。但以上都需要針對(duì)每個(gè)方法進(jìn)行手工處理,拷貝出來的文本無法展示調(diào)用層級(jí),且不支持生成指定Java方法向下的完整調(diào)用鏈。
以下實(shí)現(xiàn)了一個(gè)工具,能夠批量生成指定Java方法向下的完整調(diào)用鏈,對(duì)于關(guān)注的Java方法,能夠生成其向下調(diào)用的方法信息,及被調(diào)用方法再向下調(diào)用的方法,直到最下層被調(diào)用的方法。
也可以生成調(diào)用指定Java類向上的完整調(diào)用鏈,對(duì)于關(guān)注的Java類的方法,能夠生成調(diào)用對(duì)應(yīng)方法的方法信息,及調(diào)用上述方法的信息,直到最上層未被其他方法調(diào)用的方法(通常是對(duì)外提供的服務(wù),或定時(shí)任務(wù)等)。
2. 輸出結(jié)果示例
2.1. 調(diào)用指定類向上的完整調(diào)用鏈?zhǔn)纠?/h3>
調(diào)用指定類向上的完整調(diào)用鏈輸出結(jié)果格式類似一棵樹,每行代表一個(gè)Java方法,與實(shí)際的代碼執(zhí)行順序無關(guān),前面的數(shù)字越大代表調(diào)用層級(jí)越靠上,0代表指定類中的方法。
對(duì)于不被其他方法調(diào)用的方法,認(rèn)為是入口方法,在對(duì)應(yīng)行的最后會(huì)顯示“!entry!”。
當(dāng)存在上述調(diào)用關(guān)系時(shí),生成的調(diào)用指定類向上的完整調(diào)用鏈如下所示:
- [0]#DestClass.destfunc()
- [1]# ClassA3.funcA3()
- [2]# ClassA2.funcA2()
- [3]# ClassA1.funcA1() !entry!
- [1]# ClassB1.funcB1() !entry!
- [1]# ClassC2.funcC2()
- [2]# ClassC1.funcC1() !entry!
以下為使用該工具生成的調(diào)用Mybatis的SqlSessionUtils類的部分方法向上完整調(diào)用鏈(方法參數(shù)太長,已省略):
2.2. 指定方法向下完整調(diào)用鏈?zhǔn)纠?/h3>
指定方法向下完整調(diào)用鏈輸出結(jié)果類似一棵樹,每行代表一個(gè)Java方法,與實(shí)際的代碼執(zhí)行順序一致,前面的數(shù)字越大代表調(diào)用層級(jí)越靠下,0代表指定方法。
當(dāng)存在上述調(diào)用關(guān)系時(shí),生成的指定方法向下完整調(diào)用鏈如下所示:
- [0]#DestClass.destfunc()
- [1]# ClassA1.funcA1()
- [2]# ClassA2a.funcA2a()
- [2]# ClassA2b.funcA2b()
- [3]# ClassA3.funcA3()
- [1]# ClassB1.funcB1()
- [1]# ClassC1.funcC1()
- [2]# ClassC2.funcC2()
以下為使用該工具生成的Mybatis的SqlSessionFactoryBean:scanClasses()方法向下的完整調(diào)用鏈:
除此之外,當(dāng)方法指定了注解時(shí),也可以顯示在結(jié)果中;當(dāng)出現(xiàn)方法循環(huán)調(diào)用時(shí),會(huì)顯示出現(xiàn)循環(huán)調(diào)用的方法。
3. 適用場(chǎng)景
3.1. 分析代碼執(zhí)行流程
使用該工具生成指定方法向下調(diào)用鏈的功能,可以將代碼中復(fù)雜的方法調(diào)用轉(zhuǎn)換為相對(duì)簡單的方法調(diào)用鏈形式展示。
人工查看生成的調(diào)用鏈時(shí),能夠通過類名及方法名識(shí)別出對(duì)應(yīng)含義。
支持將不關(guān)注的方法調(diào)用忽略,僅展示重要的方法調(diào)用。
對(duì)于分析代碼執(zhí)行流程有一定幫助,適合進(jìn)行代碼審計(jì)時(shí)梳理交易流程、查找敏感API調(diào)用等場(chǎng)景。
3.2. 確認(rèn)被修改代碼的影響范圍
使用該工具生成指定方法向上調(diào)用鏈的功能,可以生成調(diào)用指定類的所有方法的調(diào)用鏈。
能識(shí)別入口方法,減少人工逐層確認(rèn)入口方法的工作量。
可用于快速確認(rèn)被修改代碼的影響范圍。
3.3. 應(yīng)用功能拆分
在進(jìn)行應(yīng)用功能拆分時(shí),需要準(zhǔn)確定位指定功能涉及的數(shù)據(jù)庫表,及使用了對(duì)應(yīng)數(shù)據(jù)庫表的相關(guān)入口方法。
使用該工具生成指定方法向下調(diào)用鏈的功能,生成指定入口方法向下的調(diào)用鏈,能夠根據(jù)類的包名快速找到Mapper接口(使用Mybatis的場(chǎng)景),即可找到相關(guān)的數(shù)據(jù)庫表。
使用該工具生成指定方法向上調(diào)用鏈的功能,生成調(diào)用指定Mapper接口向上的調(diào)用鏈,能夠根據(jù)“!entry!”找到入口方法。
重復(fù)執(zhí)行以上過程,直到?jīng)]有再找到新的Mapper接口(即數(shù)據(jù)庫表)和入口方法,即可確認(rèn)指定功能涉及的數(shù)據(jù)庫表及相關(guān)入口方法。
4. 使用說明
4.1. 依賴環(huán)境
該工具將Java方法調(diào)用關(guān)系寫入文件之后,會(huì)將數(shù)據(jù)保存在數(shù)據(jù)庫中,需要訪問MySQL數(shù)據(jù)庫(理論上支持其他數(shù)據(jù)庫,但需要對(duì)SQL語句進(jìn)行調(diào)整)。
所使用的數(shù)據(jù)庫用戶需要有DML讀寫權(quán)限,及DDL權(quán)限(需要執(zhí)行CREATE TABLE、TRUNCATE TABLE操作)。
4.2. 引入組件
在使用該工具前,首先需要在對(duì)應(yīng)的項(xiàng)目引入該工具組件的依賴,將其引入到test模塊或使用provided類型,可以避免發(fā)布到服務(wù)器中。
Gradle
- testImplementation 'com.github.adrninistrator:java-all-call-graph:0.0.8'
Maven
- <dependency>
- <groupId>com.github.adrninistrator</groupId>
- <artifactId>java-all-call-graph</artifactId>
- <version>0.0.8</version>
- <type>provided</type>
- </dependency>
最新版本號(hào)可查看 https://search.maven.org/artifact/com.github.adrninistrator/java-all-call-graph 。
對(duì)應(yīng)代碼地址為 https://github.com/Adrninistrator/java-all-call-graph 。
建議在需要生成方法調(diào)用鏈的項(xiàng)目中分別引入依賴,可以使每個(gè)項(xiàng)目使用單獨(dú)的配置,不會(huì)相互影響。
該工具僅引入了log4j-over-slf4j組件,在引入該工具組件的項(xiàng)目中,還需要引入log4j2、logback等日志組件,且保證配置正確,能夠在本地正常運(yùn)行。
4.3. 執(zhí)行步驟
4.3.1. 總體步驟
該工具的總體使用步驟如下:
a. 將后續(xù)步驟使用的幾個(gè)啟動(dòng)類對(duì)應(yīng)的Java文件,及配置文件解壓到當(dāng)前Java項(xiàng)目的test模塊的對(duì)應(yīng)目錄中,該步驟只需要執(zhí)行一次;
b. 調(diào)用增強(qiáng)后的java-callgraph.jar(詳細(xì)內(nèi)容見后續(xù)“原理說明”部分),解析指定jar包中的class文件,將Java方法調(diào)用關(guān)系寫入文件;從該文件讀取Java方法調(diào)用關(guān)系,再寫入MySQL數(shù)據(jù)庫;
c.1 需要生成調(diào)用指定類的向上完整方法調(diào)用鏈時(shí),從數(shù)據(jù)庫讀取方法調(diào)用關(guān)系,再將完整的方法調(diào)用鏈寫入文件;
c.2 需要生成指定方法的向下完整方法調(diào)用鏈時(shí),從數(shù)據(jù)庫讀取方法調(diào)用關(guān)系,再將完整的方法調(diào)用鏈寫入文件;
如下圖所示:
4.3.2. 釋放啟動(dòng)類及配置文件
當(dāng)前步驟在每個(gè)Java項(xiàng)目只需要執(zhí)行一次。
執(zhí)行當(dāng)前步驟時(shí),需要執(zhí)行main()方法的類名如下:
- com.adrninistrator.jacg.unzip.UnzipFile
需要選擇classpath對(duì)應(yīng)模塊為test。
執(zhí)行以上類后,會(huì)將java-all-callgraph.jar中保存配置文件的~jacg_config、~jacg_sql目錄,保存啟動(dòng)類的“test/jacg”目錄,分別釋放到當(dāng)前Java項(xiàng)目的test模塊的resources、java目錄中(僅在本地生效,避免發(fā)布到服務(wù)器中)。
若當(dāng)前Java項(xiàng)目存在“src/test”或“src/unit.test”目錄,則將配置文件與Java文件分別釋放在該目錄的resources、java目錄中;
若當(dāng)前Java項(xiàng)目不存在以上目錄,則將上述文件釋放在“~jacg-[當(dāng)前時(shí)間戳]”目錄中,之后需要手工處理,將對(duì)應(yīng)目錄拷貝至test模塊對(duì)應(yīng)目錄中。
4.3.3. Java方法調(diào)用關(guān)系入庫
在生成Java方法調(diào)用關(guān)系并寫入數(shù)據(jù)庫之前,需要確保需要分析的jar包或war包已存在,對(duì)于通過源碼使用構(gòu)建工具生成的jar/war包,或者M(jìn)aven倉庫中的jar包(需要是包含.class文件的jar包),均可支持。
當(dāng)需要解析的jar/war包中的class文件內(nèi)容發(fā)生變化時(shí),需要重新執(zhí)行當(dāng)前步驟,以重新獲取對(duì)應(yīng)jar/war包中的Java方法調(diào)用關(guān)系,寫入文件及數(shù)據(jù)庫;若需要解析的jar/war包文件未發(fā)生變化,則不需要重新執(zhí)行當(dāng)前步驟。
執(zhí)行當(dāng)前步驟時(shí),需要執(zhí)行main()方法的類名如下:
- test.jacg.TestRunnerWriteDb
需要選擇classpath對(duì)應(yīng)模塊為test。
當(dāng)前步驟執(zhí)行的操作及使用的相關(guān)參數(shù)如下圖所示:
b.1 調(diào)用增強(qiáng)后的java-callgraph.jar中的類的方法
TestRunnerWriteDb類讀取配置文件 config.properties
中的參數(shù):
call.graph.jar.list
:等待解析的jar包路徑列表,各jar包路徑之間使用空格分隔(若路徑中包含空格,則需要使用""包含對(duì)應(yīng)的路徑)
將第1個(gè)jar包路徑后面加上“.txt”作為本次保存Java方法調(diào)用關(guān)系文件路徑;
設(shè)置JVM參數(shù)“output.file”值為本次保存Java方法調(diào)用關(guān)系文件的路徑,調(diào)用增強(qiáng)后的java-callgraph.jar中的類的方法,通過方法的參數(shù)傳遞上述jar包路徑列表;
b.2 解析指定jar包
增強(qiáng)后的java-callgraph.jar中的類的方法開始解析指定的jar包;
b.3 將Java方法調(diào)用關(guān)系寫入文件
增強(qiáng)后的java-callgraph.jar中的類的方法將解析出的Java方法調(diào)用關(guān)系寫入指定的文件中;
b.4 讀取Java方法調(diào)用關(guān)系文件
TestRunnerWriteDb類讀取保存Java方法調(diào)用關(guān)系的文件,文件路徑即第1個(gè)jar包路徑加“.txt”;
b.5 將Java方法調(diào)用關(guān)系寫入數(shù)據(jù)庫
TestRunnerWriteDb類讀取配置文件 i_allowed_class_prefix.properties
,該文件中指定了需要處理的類名前綴,可指定包名,或包名+類名,示例如下:
- com.test
- com.test.Test1
讀取配置文件 config.properties
中的參數(shù):
app.name
:當(dāng)前應(yīng)用名稱,對(duì)應(yīng)數(shù)據(jù)庫表名后綴,該參數(shù)值中的分隔符不能使用-,需要使用_
thread.num
:寫入數(shù)據(jù)庫時(shí)并發(fā)處理的線程數(shù)量,也是數(shù)據(jù)源連接池?cái)?shù)量
db.driver.name
:數(shù)據(jù)庫驅(qū)動(dòng)類名
db.url
:數(shù)據(jù)庫URL,使用MySQL時(shí),url需要指定rewriteBatchedStatements=true,開啟批量插入,提高效率
db.username
:數(shù)據(jù)庫用戶名
db.password
:數(shù)據(jù)庫密碼
input.ignore.other.package
:忽略其他包的開關(guān),值為true/false;當(dāng)開關(guān)為開時(shí),僅將 i_allowed_class_prefix.properties
中指定的類名前綴相符的類調(diào)用關(guān)系寫入數(shù)據(jù)庫;當(dāng)開關(guān)為關(guān)時(shí),所有的類調(diào)用關(guān)系都寫入數(shù)據(jù)庫
向數(shù)據(jù)庫寫入數(shù)據(jù)庫前,會(huì)判斷對(duì)應(yīng)數(shù)據(jù)庫表是否存在,若不存在則創(chuàng)建,之后會(huì)執(zhí)行“TRUNCATE TABLE”操作清空表中的數(shù)據(jù);
根據(jù)配置文件 config.properties
中的 input.ignore.other.package
參數(shù)值及配置文件 i_allowed_class_prefix.properties
,將Java方法調(diào)用關(guān)系逐條寫入數(shù)據(jù)庫中;
增強(qiáng)后的java-callgraph.jar除了會(huì)將Java方法調(diào)用關(guān)系寫入文件外,還會(huì)將各個(gè)方法上的注解信息寫入文件(文件名為保存方法調(diào)用關(guān)系的文件名加上“-annotation.txt”);TestRunnerWriteDb類也會(huì)讀取對(duì)應(yīng)文件,將各方法上的注解信息寫入數(shù)據(jù)庫中。
4.3.4. 生成調(diào)用指定類向上的完整調(diào)用鏈
執(zhí)行當(dāng)前步驟之前,需要確認(rèn)Java方法調(diào)用關(guān)系已成功寫入數(shù)據(jù)庫中。
執(zhí)行當(dāng)前步驟時(shí),需要執(zhí)行main()方法的類名如下:
- test.jacg.TestRunnerGenAllGraph4Callee
需要選擇classpath對(duì)應(yīng)模塊為test。
當(dāng)前步驟執(zhí)行的操作及使用的相關(guān)參數(shù)如下圖所示:
c.1.1 從數(shù)據(jù)庫讀取Java方法調(diào)用關(guān)系
TestRunnerGenAllGraph4Callee類讀取配置文件 o_g4callee_class_name.properties
,該文件中指定了需要生成向上完整調(diào)用鏈的類名;若存在同名類,則類名需要指定完整類名;若不存在同名類,則類名需要指定簡單類名;示例如下:
- Test1
- com.test.Test1
讀取配置文件 config.properties
中的參數(shù):
thread.num
:從數(shù)據(jù)庫并發(fā)讀取數(shù)據(jù)的線程數(shù)量,也是數(shù)據(jù)源連接池?cái)?shù)量;若 o_g4callee_class_name.properties
配置文件中的記錄數(shù)比該值小,則會(huì)使用記錄數(shù)覆蓋該參數(shù)值
以下參數(shù)說明略:app.name、db.driver.name、db.url、db.username、db.password
c.1.2 將方法完整調(diào)用鏈(向上)寫入文件
對(duì)于配置文件 o_g4callee_class_name.properties
中指定的類,對(duì)每個(gè)類生成一個(gè)對(duì)應(yīng)的文件,文件名為“[類名].txt”,在某個(gè)類對(duì)應(yīng)的文件中,會(huì)為對(duì)應(yīng)類的每個(gè)方法生成向上完整調(diào)用鏈;
以上文件名示例為“TestClass1.txt”;
每次執(zhí)行時(shí)會(huì)生成一個(gè)新的目錄,用于保存輸出文件,目錄名格式為“~jacg_output_for_callee/[yyyyMMdd-HHmmss.SSS]”;
讀取配置文件 config.properties
中的參數(shù):
call.graph.output.detail
:輸出文件中調(diào)用關(guān)系的詳細(xì)程度,1: 最詳細(xì),包含完整類名+方法名+方法參數(shù),2: 中等,包含完整類名+方法名,3: 最簡單,包含簡單類名(對(duì)于同名類展示完整類名)+方法名,示例如下
call.graph.output.detail參數(shù)值 | 顯示示例 |
---|---|
1 | com.test.Test1.func1(java.lang.String) |
2 | com.test.Test1.func1 |
3 | Test1.func1 |
show.method.annotation
:調(diào)用鏈中是否顯示方法上的注解開關(guān),值為true/false;當(dāng)開關(guān)為開時(shí),會(huì)顯示當(dāng)前方法上的全部注解的完整類名,格式為“[方法信息]@注解1@注解2...”
gen.combined.output
:是否生成調(diào)用鏈的合并文件開關(guān),值為true/false;當(dāng)開關(guān)為開時(shí),在為各個(gè)類生成了對(duì)應(yīng)的調(diào)用鏈文件后,會(huì)生成一個(gè)將全部文件合并的文件,文件名為“~all-4callee.txt”
gen.upwards.methods.file
:生成向上的調(diào)用鏈時(shí),是否需要為每個(gè)方法生成單獨(dú)的文件開關(guān),值為true/false;當(dāng)開關(guān)為開時(shí),會(huì)為o_g4callee_class_name.properties中指定的每個(gè)類的每個(gè)方法單獨(dú)生成一個(gè)文件,保存在“~jacg_output_for_callee/[yyyyMMdd-HHmmss.SSS]/methods”
4.3.5. 生成指定方法向下完整調(diào)用鏈
執(zhí)行當(dāng)前步驟之前,需要確認(rèn)Java方法調(diào)用關(guān)系已成功寫入數(shù)據(jù)庫中。
4.3.5.1. 生成所有的調(diào)用鏈
執(zhí)行當(dāng)前步驟時(shí),需要執(zhí)行main()方法的類名如下:
- test.jacg.TestRunnerGenAllGraph4Caller
需要選擇classpath對(duì)應(yīng)模塊為test。
當(dāng)前步驟執(zhí)行的操作及使用的相關(guān)參數(shù)如下圖所示:
c.2.1 從數(shù)據(jù)庫讀取Java方法調(diào)用關(guān)系
TestRunnerGenAllGraph4Caller類讀取配置文件 o_g4caller_entry_method.properties
,該文件中指定了需要生成向下完整調(diào)用鏈的類名與方法名前綴,格式為[類名]:[方法名],或[類名]:[方法名]+參數(shù);
若存在同名類,則類名需要指定完整類名;若不存在同名類,則類名需要指定簡單類名;
示例如下:
- Test1:func1
- Test1:func1(
- Test1:func1(java.lang.String)
- com.test.Test1:func1
- com.test.Test1:func1(
- com.test.Test1:func1(java.lang.String)
若 o_g4caller_entry_method.properties
配置文件中指定的方法前綴對(duì)應(yīng)多個(gè)方法,則可在 o_g4caller_entry_method_ignore_prefix.properties
配置文件中指定需要忽略的方法前綴;
o_g4caller_entry_method_ignore_prefix.properties
配置文件的格式為方法名,或方法名+參數(shù),示例如下:
- func1
- func1(
- func1(java.lang.String)
例如指定生成Class1.test方法的向下完整調(diào)用鏈,存在方法Class1.test1,則可指定忽略test1方法;指定生成Class1.test方法的向下完整調(diào)用鏈,所關(guān)注的test方法為test(java.lang.String),存在不關(guān)注的方法test(java.lang.Integer),則可指定忽略test(java.lang.Integer)方法;
讀取配置文件 config.properties
中的參數(shù):
thread.num
:從數(shù)據(jù)庫并發(fā)讀取數(shù)據(jù)的線程數(shù)量,也是數(shù)據(jù)源連接池?cái)?shù)量;若 o_g4caller_entry_method.properties
配置文件中的記錄數(shù)比該值小,則會(huì)使用記錄數(shù)覆蓋該參數(shù)值
以下參數(shù)說明略:app.name、db.driver.name、db.url、db.username、db.password
c.2.2 將方法完整調(diào)用鏈(向下)寫入文件
對(duì)于配置文件 o_g4caller_entry_method.properties
中指定的方法,對(duì)每個(gè)方法生成一個(gè)對(duì)應(yīng)的文件,文件名為“[類名]@[方法名]@[完整方法名HASH+長度].txt”;
以上文件名示例為“TestClass1@func1@qDb0chxHzmPj1F26S7kzhw#048.txt”;
每次執(zhí)行時(shí)會(huì)生成一個(gè)新的目錄,用于保存輸出文件,目錄名格式為“~jacg_output_for_caller/[yyyyMMdd-HHmmss.SSS]”;
讀取配置文件 config.properties
中的參數(shù):
gen.combined.output
:是否生成調(diào)用鏈的合并文件開關(guān),值為true/false;當(dāng)開關(guān)為開時(shí),在為各個(gè)類生成了對(duì)應(yīng)的調(diào)用鏈文件后,會(huì)生成一個(gè)將全部文件合并的文件,文件名為“~all-4caller.txt”
以下參數(shù)說明略:call.graph.output.detail、show.method.annotation。
4.3.5.2. 忽略特定的調(diào)用關(guān)系
以上生成指定方法向下的完整調(diào)用鏈中,包含了所有的方法調(diào)用鏈,可用于查找指定方法直接調(diào)用及間接調(diào)用的方法,例如通過調(diào)用的Mybatis的Mapper接口確認(rèn)該方法相關(guān)的數(shù)據(jù)庫表操作;
當(dāng)生成指定方法向下的完整調(diào)用鏈?zhǔn)菫榱巳斯し治龃a結(jié)構(gòu)時(shí),若包含了所有的方法調(diào)用鏈,則會(huì)有很多不重要的代碼產(chǎn)生干擾,例如對(duì)dto、entity等對(duì)象的讀取及賦值操作、通信數(shù)據(jù)序列化/反序列化操作(JSON等格式)、日期操作、流水號(hào)生成、請(qǐng)求字段格式檢查、注解/枚舉/常量/異常/日期相關(guān)類操作、Java對(duì)象默認(rèn)方法調(diào)用等;
調(diào)用以下類,支持將不關(guān)注的方法調(diào)用關(guān)系忽略:
- test.jacg.TestRunnerGenAllGraph4CallerSupportIgnore
在配置文件 o_g4caller_ignore_class_keyword.properties
中可以指定需要忽略的類名關(guān)鍵字,可為包名中的關(guān)鍵字,或類名中的關(guān)鍵字,示例如下:
- .dto.
- .entity.
- Enum
- Constant
在配置文件 o_g4caller_ignore_full_method_prefix.properties
中可以指定需要忽略的完整方法前綴,可指定包名,或包名+類名,或包名+類名+方法名,或包名+類名+方法名+參數(shù),示例如下:
- com.test
- com.test.Test1
- com.test.Test1:func1
- com.test.Test1:func1(
- com.test.Test1:func1(java.lang.String)
在配置文件 o_g4caller_ignore_method_prefix.properties
中可以指定需要忽略的方法名前綴,如Java對(duì)象中的默認(rèn)方法“toString()、hashCode()、equals(java.lang.Object)、<init>(、<clinit>(”等,示例如下:
- func1
- func1(
- func1()
- func1(java.lang.String)
5. 原理說明
5.1. Java方法調(diào)用關(guān)系獲取
在獲取Java方法調(diào)用關(guān)系時(shí),使用了 https://github.com/gousiosg/java-callgraph 項(xiàng)目,并對(duì)其進(jìn)行了增強(qiáng),java-callgraph使用Apache Commons BCEL(Byte Code Engineering Library)解析Java方法調(diào)用關(guān)系,Matthieu Vergne( https://www.matthieu-vergne.fr/ )為該項(xiàng)目增加了解析動(dòng)態(tài)調(diào)用的能力(lambda表達(dá)式等)。
原始java-callgraph在多數(shù)場(chǎng)景下能夠獲取到Java方法調(diào)用關(guān)系,但以下場(chǎng)景的調(diào)用關(guān)系會(huì)缺失:
接口與實(shí)現(xiàn)類方法
假如存在接口Interface1,及其實(shí)現(xiàn)類Impl1,若在某個(gè)類Class1中引入了接口Interface1,實(shí)際為實(shí)現(xiàn)類Impl1的實(shí)例(使用Spring時(shí)的常見場(chǎng)景),在其方法Class1.func1()中調(diào)用了Interface1.fi()方法;
原始java-callgraph生成的方法調(diào)用關(guān)系中,只包含Class1.func1()調(diào)用Interface1.fi()的關(guān)系,Class1.func1()調(diào)用Impl1.fi(),及Impl1.fi()向下調(diào)用的關(guān)系會(huì)缺失。
Runnable實(shí)現(xiàn)類線程調(diào)用
假如f1()方法中使用內(nèi)部匿名類形式的Runnable實(shí)現(xiàn)類在線程中執(zhí)行操作,在線程中執(zhí)行了f2()方法,如下所示:
- private void f1() {
- new Thread(new Runnable() {
- @Override
- public void run() {
- f2();
- }
- }).start();
- }
原始java-callgraph生成的方法調(diào)用關(guān)系中,f1()調(diào)用f2(),及f2()向下調(diào)用的關(guān)系會(huì)缺失;
對(duì)于使用命名類形式的Runnable實(shí)現(xiàn)類在線程中執(zhí)行操作的情況,存在相同的問題,原方法調(diào)用線程中執(zhí)行的方法,及繼續(xù)向下的調(diào)用關(guān)系會(huì)缺失。
Thread子類線程調(diào)用
與Runnable實(shí)現(xiàn)類線程調(diào)用情況類似,略。
lambda表達(dá)式(含線程調(diào)用等)
假如f1()方法中使用lambda表達(dá)式的形式在線程中執(zhí)行操作,在線程中執(zhí)行了f2()方法,如下所示:
- private void f1() {
- new Thread(() -> f2()).start();
- }
原始java-callgraph生成的方法調(diào)用關(guān)系中,f1()調(diào)用f2(),及f2()向下調(diào)用的關(guān)系會(huì)缺失;
對(duì)于其他使用lambda表達(dá)式的情況,存在相同的問題,原方法調(diào)用lambda表達(dá)式中執(zhí)行的方法,及繼續(xù)向下的調(diào)用關(guān)系會(huì)缺失。
父類調(diào)用子類的實(shí)現(xiàn)方法
假如存在抽象父類Abstract1,及其非抽象子類ChildImpl1,若在某個(gè)類Class1中引入了抽象父類Abstract1,實(shí)際為子類ChildImpl1的實(shí)例(使用Spring時(shí)的常見場(chǎng)景),在其方法Class1.func1()中調(diào)用了Abstract1.fa()方法;
原始java-callgraph生成的方法調(diào)用關(guān)系中,只包含Class1.func1()調(diào)用Abstract1.fa()的關(guān)系,Class1.func1()調(diào)用ChildImpl1.fa()的關(guān)系會(huì)缺失。
子類調(diào)用父類的實(shí)現(xiàn)方法
假如存在抽象父類Abstract1,及其非抽象子類ChildImpl1,若在ChildImpl1.fc1()方法中調(diào)用了父類Abstract1實(shí)現(xiàn)的方法fi();
原始java-callgraph生成的方法調(diào)用關(guān)系中,ChildImpl1.fc1()調(diào)用Abstract1.fi()的關(guān)系會(huì)缺失。
針對(duì)以上問題,增強(qiáng)后的java-callgraph都進(jìn)行了優(yōu)化,能夠生成缺失的調(diào)用關(guān)系。
增強(qiáng)后的java-callgraph地址為 https://github.com/Adrninistrator/java-callgraph 。
對(duì)于更復(fù)雜的情況,例如存在接口Interface1,及其抽象實(shí)現(xiàn)類Abstract1,及其子類ChildImpl1,若在某個(gè)類中引入了抽象實(shí)現(xiàn)類Abstract1并調(diào)用其方法的情況,生成的方法調(diào)用關(guān)系中也不會(huì)出現(xiàn)缺失。
5.2. Java方法完整調(diào)用鏈生成
在獲取了Java方法調(diào)用關(guān)系之后,將其保存在數(shù)據(jù)庫中,涉及到3個(gè)數(shù)據(jù)庫表,可查看java-all-callgraph.jar釋放的~jacg_sql目錄中的.sql文件,相關(guān)數(shù)據(jù)庫表如下所示:
表名前綴 | 注釋 | 作用 |
---|---|---|
class_name_ | 類名信息表 | 保存相關(guān)類的完整類名及簡單類名 |
method_annotation_ | 方法注解表 | 保存方法及方法上的注解信息 |
method_call_ | 方法調(diào)用關(guān)系表 | 保存各方法之間調(diào)用信息 |
上述數(shù)據(jù)庫表在創(chuàng)建時(shí)使用表名前綴加上配置文件 config.properties
中的 app.name
參數(shù)值。
該工具會(huì)主要從方法調(diào)用關(guān)系表中逐級(jí)查詢數(shù)據(jù),生成完整的方法調(diào)用鏈。
6. 其他功能
6.1. 處理循環(huán)方法調(diào)用
在生成向上或向下的Java方法完整調(diào)用鏈時(shí),若出現(xiàn)了循環(huán)方法調(diào)用,該工具會(huì)從循環(huán)調(diào)用中跳出,并在生成的方法調(diào)用鏈中對(duì)出現(xiàn)循環(huán)調(diào)用的方法增加標(biāo)記“!cycle[n]!”,其中n代表被循環(huán)調(diào)用的方法對(duì)應(yīng)層級(jí)。
生成向上的Java方法完整調(diào)用鏈時(shí),出現(xiàn)循環(huán)方法調(diào)用的示例如下:
- [0]#org.springframework.transaction.TransactionDefinition:getIsolationLevel
- [1]# org.springframework.transaction.support.DelegatingTransactionDefinition:getIsolationLevel
- [2]# org.springframework.transaction.TransactionDefinition:getIsolationLevel !cycle[0]!
生成向下的Java方法完整調(diào)用鏈時(shí),出現(xiàn)循環(huán)方法調(diào)用的示例如下:
- [0]#org.springframework.transaction.support.TransactionTemplate:execute
- [1]# org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager:execute
- [2]# org.springframework.transaction.jta.WebSphereUowTransactionManager:execute
- [3]# org.springframework.transaction.TransactionDefinition:getTimeout
- [4]# org.springframework.transaction.support.DefaultTransactionDefinition:getTimeout
- [4]# org.springframework.transaction.support.DelegatingTransactionDefinition:getTimeout
- [5]# org.springframework.transaction.TransactionDefinition:getTimeout !cycle[3]!
6.2. 生成兩個(gè)方法之間的調(diào)用鏈
該工具生成的向上或向下的Java方法完整調(diào)用鏈通常會(huì)比較大,如果只關(guān)注某個(gè)方法到起始方法之間的調(diào)用鏈時(shí),可以按照以下步驟生成:
執(zhí)行以下java類:
- com.adrninistrator.jacg.other.GenSingleCallGraph
需要選擇classpath對(duì)應(yīng)模塊為test。
在程序參數(shù)(即main()方法處理的參數(shù))中指定對(duì)應(yīng)的向上或向下的Java方法完整調(diào)用鏈文件路徑,及關(guān)注的方法所在行數(shù),格式為“[完整調(diào)用鏈文件路徑] [關(guān)注方法所在行數(shù)]”。
當(dāng)文件路徑包含空格時(shí),需要使用""包含;關(guān)注方法所在行數(shù)從1開始。
例如完整調(diào)用鏈文件“dir/a.txt”內(nèi)容如下:
- [0]#DestClass.destfunc()
- [1]# ClassA3.funcA3()
- [2]# ClassA2.funcA2()
- [3]# ClassA1.funcA1() !entry!
- [1]# ClassB1.funcB1() !entry!
- [1]# ClassC2.funcC2()
- [2]# ClassC1.funcC1() !entry!
假如希望知道第7行“[2]# ClassC1.funcC1() !entry!”方法與起始方法“[0]#DestClass.destfunc()”之間的調(diào)用關(guān)系,可在執(zhí)行以上類時(shí)指定程序參數(shù)為“dir/a.txt 7”,則生成調(diào)用關(guān)系如下:
- [0]#DestClass.destfunc()
- [1]# ClassC2.funcC2()
- [2]# ClassC1.funcC1() !entry!
7. 分析腳本
在 https://github.com/Adrninistrator/java-all-call-graph 的“shell腳本”、“SQL語句”目錄中,保存了以下腳本,可以用于對(duì)代碼進(jìn)行一些分析操作。
7.1. shell腳本
根據(jù)Mybatis的Mapper查找對(duì)應(yīng)數(shù)據(jù)庫表名
根據(jù)數(shù)據(jù)庫表名查找Mybatis的對(duì)應(yīng)Mapper
根據(jù)向上完整調(diào)用鏈查找入口方法完整類名
根據(jù)向上完整調(diào)用鏈查找入口方法簡單類名
根據(jù)向下完整調(diào)用鏈查找被使用的Mapper完整類名
根據(jù)向下完整調(diào)用鏈查找被使用的Mapper方法
根據(jù)向下完整調(diào)用鏈查找被使用的Mapper簡單類名
7.2. SQL語句
針對(duì)該工具使用的數(shù)據(jù)庫表進(jìn)行分析的SQL語句。
8. 無法正確處理的情況
以下情況,對(duì)應(yīng)的方法找不到被調(diào)用關(guān)系,可能會(huì)被誤識(shí)別為入口方法:
不是直接通過Java方法進(jìn)行調(diào)用的情況(例如在XML文件中配置代碼執(zhí)行流程、通過注解配置代碼執(zhí)行流程、使用AOP處理等);
未被調(diào)用的方法;
方法作為流式處理的參數(shù),如“xxx.stream().filter(this::func)”。
9. 使用建議
可能存在以下問題:
當(dāng)一個(gè)接口對(duì)應(yīng)多個(gè)實(shí)現(xiàn)類時(shí),若在某個(gè)類中引入了接口,并調(diào)用其方法,生成的完整調(diào)用鏈中,可能將當(dāng)前類未使用的其他實(shí)現(xiàn)類相關(guān)的調(diào)用關(guān)系也包含進(jìn)來;
當(dāng)一個(gè)抽象父類對(duì)應(yīng)多個(gè)非抽象子類時(shí),若在某個(gè)類中引入了抽象父類,并調(diào)用其方法,生成的完整調(diào)用鏈中,可能將當(dāng)前類未使用的其他非抽象子類相關(guān)的調(diào)用關(guān)系也包含進(jìn)來。
對(duì)于以上問題,可以臨時(shí)修改代碼但不提交,將引入的接口使用實(shí)現(xiàn)類替代,或抽象父類使用非抽象子類替代,生成jar包/war包后生成調(diào)用關(guān)系,再重新生成完整調(diào)用鏈。