Java 工程的外部依賴顯示工具實現及使用
原文出處: IBM——developerworks
在 Java 工程開發過程中,一般情況下,軟件工程師以及項目管理人員都很清楚自己的工程項目都依賴于哪些外部組件接口,但是在某些情況,尤其是工程比較龐大時,一個工程分成多個組件由不同的項目組負責開發時,想要了解各個的工程依賴關系就變得有些困難。我們開發了一個簡單易用工具(Java 工程的外部依賴顯示工具),通過簡單的配置就能清晰地顯示 Java 工程的外部依賴關系。例如,一個項目都依賴于哪些接口,一個接口被哪些工程所引用以及引用的文件分別是什么,而且結果還可以生成網頁用于發布以供其他相關人員參考。本文將介紹這個工具實現方式及使用方法。
背景
在測試中發現,我們的一個類錯誤地引用了外部接口,這個接口有不同的實現方案,應該如何確保調用了正確的接口?整個項目中是否還有其他類似錯誤的引用?為此我們開發了這個工具,能夠清晰地列出接口引用關系,希望對其他碰到類似情況的開發人員有所幫助。
實現原理
分析源文件引用接口及 Jar 文件導出接口
分析源文件的引用接口可以直接通過逐行掃描源代碼,讀取導入的包,然后找出所有的符號,再進一步分析符號調用的 Jar 文件接口,或者可以通過 Java 編譯器編譯源代碼,再從編譯后的符號表中讀取符號信息。第一種方法要自己分析源代,實現復雜寫,但是運行效率要比 Java 編譯器編譯源代碼高很多,因為 Java 編譯器編譯過程對源碼所有的符號做了詳細的分析,而我們只了解源碼中引用了哪些外部接口。第二種方法雖然實現比較簡單,可以使用開源的通用 Java 編譯器 GJC(Generic Java Compiler),但是 GJC 本身還是比較復雜,需要了解 GJC 的接口,本章會在后面做介紹。當然并不局限這兩種方法,例如,可以只使用 GJC 或者其他 Java 編譯器提供的接口做源代碼解析,不進行完整的編譯,然后對解析后的符號表進行過濾只留下感興趣的引用外部引用接口的符號,最后再分析接口關系。
利用掃描源代碼的方法進行接口分析
清單 1. 讀取 Jar 文件獲取類名代碼
Java.util.jar.JarFile jarFile = new Java.util.jar.JarFile(file); Enumeration<JarEntry> entries = jarFile.entries();//讀取 jar 文件把類名保存到 map 中 public boolean append(File file) throws IOException{ //jar file String path = file.getAbsolutePath(); JarFile jarFile=new JarFile(file); Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()){ JarEntry ent = entries.nextElement(); String name = ent.getName(); if(name.endsWith(".class")){ name = name.substring(0,name.length()-6).replace('/', '.'); map.put(name, path); } } return true; }</pre>
Java 源文件中讀取 import 關鍵字可以獲取所有導入類或者靜態常量及方法名,因為我們只關心導入的類名,如果導入靜態常量或方法,可以認為導入了它們所引用的類名。另外外部依賴顯示工具并不關心所有的導入類,例如 JRE 中的大部分類我們并不關心哪些是否導入了,而只關心某些特殊的類,所以這個步驟中可以把那些不關心的類過濾掉。
上一步驟中我們已經分析了導入類名,但是這并不夠,導入類名在類定義中可能并沒有使用到,或者類定義中使用類有可能是寫了完整的包名,并不需要導入類名。因此這個步驟需要做兩件事情,一是要分析哪些導入類是真正使用的,二是要分析源碼中使用了哪些未申明導入的帶完整包名的類。
利用 OpenJDK 的 Langtools 工具進行代碼分析
環境安裝:
Langtools 工具langtools.zip 下載:下載 V7 版本,如果下載 V8 版本,需要安裝 JRE8
Langtools version7 需要 JRE 7 支持,JRE 7 可以從oracle 官網下載。
利用 Javac 編譯器來實現導出接口分析,我們的目的不是要讓它編譯成 class 文件,而是要讓它幫助我們分析外部接口引用關系,所以有必要對編譯器做寫修改,這可以通過兩種方式進行,一是直接修改編譯器,另外一種是通過接口繼承方式修改接口功能,第一種方式比較簡單,但是比較粗暴,繼承方式顯得比較優雅,因此我們采用后者來做說明。
修改 JavaCompiler 只調用 flow 接口,并把結果保存, 當然還要修改其他接口,這里只是列出修改的關鍵地方。
清單 2. 重載編譯器代碼
public class VmiJavaCompiler extends com.sun.tools.javac.main.JavaCompiler{ public boolean saveTree = false; public Queue<Env<AttrContext>> envs=null; private void compile2() {//編譯入口函數 try { if (saveTree) envs=flow(attribute(todo)); else flow(attribute(todo)); } catch (Abort ex) { if (devVerbose) ex.printStackTrace(System.err); } }編譯器源碼由遍歷器 com.sun.tools.javac.TreeScanner 繼承這個接口,便可遍歷所有符號。
清單 3. 遍歷符號代碼
public class SymbolVisitor extends TreeScanner{ SymbolVisitor(SymbolRecorder recorder){//符號遍歷 this.recorder = recorder; } SymbolRecorder recorder = null; public void analyzeTree(JCTree tree) { if(!(tree instanceof JCClassDecl)) return; try { scan (tree); } finally { // note that recursive invocations of this method fail hard if(tree!=null){ recorder.DebugPrint (); } } } public void visitSelect(JCFieldAccess tree) {//遍歷 JCFieldAccess //Todo:add code here recorder.add(tree); super.visitSelect (tree); }public void visitIdent(JCIdent tree) {//遍歷 JCIdent //Todo:add code here recorder.add(tree); super.visitIdent(tree); } }</pre>
導出 xml 接口與引用關系文件
為了方便顯示輸出 html 樹形表格,把數據結果導出為 xml 格式的文件。
表 1. 導出文件說明表
文件名 | 說明 | </tr> </tbody>|||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
jp.xml | Jar 文件與 Java 包關系 | </tr>|||||||||||||||||||||||||||||||||||
jrc.xml | Jar 文件導出了哪些接口,接口被哪些 Java 文件使用 | </tr>|||||||||||||||||||||||||||||||||||
pjr.xml | 源文件的一個包使用了哪些 Jar 文件,每個 Jar 文件用的接口是什么 | </tr>|||||||||||||||||||||||||||||||||||
rj.xml | 項目都使用了哪些接口,接口由哪個 Jar 文件提供 | </tr>|||||||||||||||||||||||||||||||||||
rp.xml | 項目都使用了哪些接口,該接口被哪些 Java 包使用 | </tr> </tbody> </table>
屬性 | 說明 | </tr> </tbody>|||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
jarPathes | 需要分析的 Jar 文件路徑或文件名,可以多個用分號 (;) 分割 | </tr>|||||||||||||||||||||||||
javaPathes | 需要分析的 Java 源文件路徑或文件名,可以多個用分號 (;) 分割 | </tr>|||||||||||||||||||||||||
components | 可以不設置,可以設置為導出包名 | </tr>|||||||||||||||||||||||||
outputPath | 輸出文件保存路徑 | </tr> </tbody> </table>