Java 9裏開始支持模塊化,以一個獨立的開源項目jigsaw而來, 具體可以參考鏈接,
https://openjdk.java.net/projects/jigsaw/ 同時也可以參考JSR376標準
1. module-info 分析
在模塊化的時候,需要構建module_info.java來聲明模塊之間的關係,
在Module聲明裏面定義了requires ,exports, opens, uses, provides 5種類型,關於這幾種類型的作用就不在這裏詳細介紹了。
在字節碼種並沒有單獨的opcode,只是用一種固定的順序進行定義,以requires ,exports, opens, uses, provides這個順序來排序
我們來看一下以下的一段bytecode
2 // requires #8,0 // pointto #0 #10,8000 // "java.base" ACC_MANDATED #0 0 // exports 0 // opens 1 // uses #12 // testRequire/service 1 // provides #12 // testRequire/service with ... 1 #14 // ... with testRequire/ServiceImpl |
第一個2表示requires,同時表示有幾個requires,下面的縮進行代表着有哪2個,#10,8000表示的是8000表示的是16進制的ACC_Flag 修飾符
其中requires的ACC獨有的修飾符(TRANSITIVE,STATIC)
SYNTHETIC,MANDATED是共用的ACC修飾符
每個Flag定義的值爲
TRANSITIVE |
0x20 |
STATIC |
0x40 |
SYNTHETIC |
0x1000 |
MANDATED |
0x8000 |
其本質上是通過bit位來設置不同的值的合集
第二個0代表着exports,第三個0代表着opens第四個1代表着uses,第五個1代表着provides,當值爲非0的時候下一行就會縮進,代表着共有幾個這樣的類型,格式比較簡單。但是我們從這種結構看出只能增加新的類型,同時類型順序無法修改和刪除,否則很難向下兼容。
這裏簡單的提一下這幾個關鍵字
uses, provides,這個主要是對Java的SPI機制
Opens 主要是對Java的類反射機制,都是很有針對性的對Java提供的原生態的能力進行了管控。
2.Soot模塊支持
Soot 的原來主版本目前並不支持模塊分析,在Soot的Java9的branch孵化,目前已經合到主版本
2.1 Soot設置模塊
Soot 需要顯示的設置模塊的相關信息
Options.v().set_soot_modulepath(path); |
Soot會通過設置的模塊的路徑去加載類
需要注意的是:基礎模塊java.base
你不需要設置基礎模塊的地址,Soot裏面會去自己尋找基礎模塊,如何尋找請參考下面的章節。
Soot 本身是使用運行的jdk 加載基礎模塊的,那就是說Soot的加載基礎模塊是綁定你的運行的jdk的,你無法通過設置模塊相關參數來設置基礎模塊,通常基礎模塊都被打包成了jmod文件,Soot解析jmod文件是通過運行soot的jdk來解析的,jmod基礎類的讀取就的依賴運行jdk的能力,如果你想用一個運行jdk的版本來分析不同的版本jdk基礎類,就會變的困難,不過好在java的基礎類是向下兼容的,確保升級到最新的版本,可以確保能分析低版本的基礎類。Soot內部會添加VIRTUAL_FS_FOR_JDK這個值到modulepath中去,用來標示基礎類的路徑。
如果你運行的Soot的jdk是低版本的,或許還有條路可以試試,解開jmod文件,設置到你的模塊路徑下,這種方式適合在不高於jdk8的版本方式上,同時你還的設置:
Options.v().set_prepend_classpath(false); |
5.2 Soot 加載模塊
ModulePathSourceLocator是查找模塊路徑和類文件的核心模塊:
- 模塊名字和模塊的路徑
- 獲取模塊下的類文件
通過ModulePathSourceLocator.
FoundFile lookUpInModulePath(String filename)方法去查找你想要獲取的模塊裏的class
- Soot首先會去查找模塊所綁定的路徑,在soot開始分析的時候,初始化參數裏並沒有設置模塊的路徑,只能通過順序查找你定義的modulepath下的模塊路徑
- 順序解析模塊路徑下的模塊:簡單解析路徑下的所有模塊的module-info文件,只是爲了獲取模塊的定義,並分析該路徑下的模塊以及該模塊下的所有的類,構建模塊和class的關係,(當然這個時候並不會去解析class),而是構建className和模塊的關係,但是soot在這裏並沒有緩存這個關係,而是構建和緩存了模塊名字和路徑的關係,直到找到模塊的路徑
3. 深度解析該模塊所在路徑下的module-info 文件,分析requires的部分,構建模塊間的依賴,分析依賴模塊下的module-info,細節可參考5.2.5注:在Java虛擬機編譯中,會對自定義的module-info的類,在編譯過程中自動添加java.base的模塊依賴,默認依賴java.base的基本模塊。在這種場景下,soot會分析基礎模塊java.base的module-info, 通過查找基礎模塊的路徑(重複步驟a,b), 找到java.base的module-info進行解析
4. 找到模塊所對應的class文件,將FoundFile返回,如何找到所對應的模塊請參考下面
5.3 模塊的依賴關係
Java使用模塊的方式進行類的管控,Soot裏在LoadClass的時候需要準確的找到模塊裏的類,以確定該類是否能被模塊加載,核心代碼在ModuleUtil裏findModuleThatExports方法,其原則如下:
Java裏的module管控是基於Package的,只需要鑑權Package就足夠。
- 首先分析要加載的模塊的Module-info,該模塊下的包是否包含該Package
- 如果不包含,讀取該模塊的Requires的模塊,對依賴的模塊進行module-info的分析
- 如果該module-info的export裏包含該package,一種是export to 指定到該模塊,如果沒有to代表所有模塊都可以使用。
- 鏈式訪問requires transitive模塊,對模塊的module-info進行分析,分析邏輯和步驟3一致,直到鏈路的最後一層模塊
5.4 如何獲取模塊下的class文件
通過ModulePathSourceLocator可以獲取模塊下的class文件,這裏涉及到模塊的兩種表示方式
1. 自定義的模塊:打包成jar或者直接class,這和原來獲取jar下的class一樣,區別主要是需要解析module-info.class類,獲取類的目的是爲了獲取模塊的依賴關係,同時也需要加載require的模塊。
2. Java的基礎類:清楚Java 模塊的知道Java9將原來的rt.jar 替換成了模塊的方式,在java_home下會有一個jmods的目錄,裏面保存着將rt.jar 拆解成多個不同的jmod文件,在Java下面我們可以使用jmod的命令來查看jmod文件的內容。Soot 並沒有使用jmod去解析默認的jdk下的基礎的這些jmod文件,而是通過jrt的方式去加載jmod,也就是
jrt:/ |
的方式通過NIO File 去解析文件,其本質是通過lib下的jrt-fs.jar去解析,所以Soot會先去判斷一下該文件是否存在。如果存在,Soot會將jrt:/加到需要分析的模塊邏輯中。
注意:如果是jar的方式,分析module只需要直接設置路徑就可以了,而如果是jmod 的方式,需要jrt:/的方式去分析
jrt的訪問方式默認的起始路徑是以modules,也就是對應的jmods目錄
jrt:/modules/ |
訪問JRT的方式可以使用NIO的Path,Path是支持jrt的方式,而傳統的File是不支持jrt的路徑訪問方式,這裏要特別注意。
5.5 加載分析類
在前面的段落裏提到過Soot的核心函數Scene,對模塊的解析,Soot提供了新的ModuleScene 來支持,當加載class的時候,在模塊的方式下需要使用ModuleScene來加載類,代碼如下:
public static SootClass loadClass(String name, boolean main, String module) { SootClass c = ModuleScene.v().loadClassAndSupport(name, Optional.of(module)); c.setApplicationClass(); if (main) Scene.v().setMainClass(c); return c; } |
需要使用name和模塊名字來加載class,而原來的方式只需要提供class的名字
private static SootClass loadClass(String name, boolean main) { SootClass c = Scene.v().loadClassAndSupport(name); c.setApplicationClass(); if (main) Scene.v().setMainClass(c); return c; } |
5.6 加載模塊特有的module-info類
解析模塊下的類,Soot 沿用原來的框架思路通過Scene來進行加載,本質上使用SootResolver來進行class解析,而不同的是定義了新ModuleScene類來進行加載,使用SootModuleResolver 對模塊下的class解析,思路類似:先構建class的引用,然後加到工作隊列中,最後獲取工作隊列進行類的解析(ResolveClass),具體流程可以參考下面的圖:
在分析Module-info的類的bytecode的時候,Soot繼續沿用了objweb的asm解析框架,使用opcode v7.1的版本,可以支持最高java13的版本。和解析普通的類不一樣的是Soot在SootClassBuilder通過構建SootModuleInfoBuilder 去實現asm提供的ModuleVisitor的接口。對Module-info.class裏單獨定義了SootModuleInfo類,而不是常見的SootClass,當然SootModuleInfo繼承SootClass。
可見下圖:
在上圖可以看到SootModuleInfo裏只是封裝了export,open,require 這3種類型,具體的實現是在SootModuleInfoBuilder的代碼中,Soot目前不支持uses, provides這2種類型,就是SPI的模式。
5.6.1 關鍵字requires
關鍵字requires裏,是支持version有多個版本,在代碼中是無法聲明的,但是在編譯的字節碼裏是聲明的。
Module: #5,0 // "com.example.hello" #0 1 // requires #6,8000 // "java.base" ACC_MANDATED #7 // 11.0.5 1 // exports #8,0 // com/example/hello 0 // opens 0 // uses 0 // provides |
上面的例子裏#7 11.0.5 代表的就是requires的版本,雖然require是有版本的,但是java裏並不是必須的,我們可以從openjdk的項目聲明看出:
http://openjdk.java.net/projects/jigsaw/spec/reqs/02#version-selection
雖然module裏requires有明確的版本,但並沒有希望在module裏去嚴格執行版本管控,而是依賴於構建工具。模塊系統只需要校驗所選的模塊集合滿足每個模塊的依賴即可,無需關注版本。在這個思路上Soot是一致的,Soot並沒有對module-info中提供的版本構建版本的依賴分析,也就是並沒有對依賴進行版本管控。
5.6.2 關鍵字exports
export 代表着允許被外部模塊訪問的package,同時exports 也和to搭檔進行外部模塊的指定
在上圖中,在soot裏exportPackage是個列表,每個列表裏的key是packageName,而value是指定的多個模塊名字。當沒有沒有指定to的時候,value裏面soot保存的是EVERYONE_MODULE代表着任何模塊都可以使用。