試圖定位無法解析的外掛程式依賴性是件緊張而又耗時的事情。啟用每個外掛程式都要依賴於很多其他外掛程式,這些外掛程式又會依賴於其他更多外掛程式。如果 Eclipse 無法載入這個長長的鏈條中的某個外掛程式,那麼手工尋找出現問題的到底是哪個外掛程式可能會比原計劃所花費的時間和精力都要多。如果您希望有一種方法可以自動實現這種外掛程式依賴性的檢測,就請閱讀本文。
碰到的問題
假設我們希望在 Eclipse 中使用一個特定的外掛程式,並已經執行了所有必須的操作,將其包含到外掛程式的 manifest 檔案中,並將其聲明為一個依賴檔案。但是系統並沒有載入這個外掛程式,這樣我們就會被困在這裡了,軟體開發就無法繼續進展下去了。
聽起來非常熟悉嗎?如果是這樣,那麼您可能早已花費了很多時間和努力來查看很多 plugin.xml 檔案,從而查明 Eclipse 可能沒有載入哪個特定的外掛程式。還可能已經嘗試使用了 Eclipse PDE 項目提供的 Plug-in Dependencies 視圖,此時您會發現這個視圖的唯一工作不過是顯示已經成功載入的外掛程式而已。不幸的是,有問題的外掛程式很可能並不屬於成功載入的外掛程式。
要確定 Eclipse 沒有找到或載入哪個特定的外掛程式,我們應該做些什麼呢?我們不用手工遍曆每個 plugin.xml 檔案,而是考慮自動實現這種搜尋功能。要自動進行搜尋,我們需要瞭解 Eclipse 是如何儲存自己的外掛程式的,以及如何發現到儲存在磁碟上的其他外掛程式的連結。基於這些知識,我們可能會希望編寫自己的代碼來建立一個外掛程式依賴性遍曆程式,或者使用在本文中給出的這個通用的 Dependency Walker 外掛程式。本文的 “下載” 一節給出了這個例子的原始碼。
開始:理解外掛程式依賴性和 Eclipse 的外掛程式鏈
外掛程式依賴性
Eclipse 外掛程式是提供了其他外掛程式可以使用的功能的軟體模組。如果外掛程式 A 需要外掛程式 B 才能正常工作,那麼我們就說 A 依賴於 B。這種依賴性還意味著,除非外掛程式 B 已經成功載入了,否則外掛程式 A 就不能正常工作。有時候,外掛程式 B 可能還會依賴於外掛程式 C、D、E,令人更不爽的是,這些外掛程式每個都可能會依賴於其他外掛程式。這種依賴鏈很容易形成數百個外掛程式相互連結在一起。毫無疑問,如果這個鏈條中的任何一個外掛程式不能成功載入,那麼依賴它的外掛程式就可能會出現問題。
外掛程式 manifest 檔案 plugin.xml 描述了每個外掛程式。這個 XML 檔案中有一節聲明了對於其他外掛程式的依賴性或需求。在清單 1 中,plugin.xml 檔案中使用黑體表示的一節就聲明了這種依賴性。
清單 1. plugin.xml 檔案
<?xml version="1.0" encoding="UTF-8" ?> <?eclipse version="3.0"?> <plugin id="org.eclipse.draw2d" name="Draw2d" version="3.0.0" provider-name="Eclipse.org"> <runtime> <library name="draw2d.jar"> <export name="*" /> <packages prefixes="org.eclipse.draw2d" /> </library> </runtime> <requires> <import plugin="org.eclipse.swt" export="true" /> <import plugin="org.eclipse.core.runtime" /> </requires> </plugin>
注意嵌入在 <requires> </requires> 節中的 <import plugin="plugin id"/> 聲明。清單 1 的例子說明這個外掛程式 ID org.eclipse.draw2d 依賴於 ID 為 org.eclipse.swt 和 org.eclipse.core.runtime 的外掛程式。
外掛程式鏈
當我們在 Eclipse 中使用 Java? 技術平台來開發軟體時,系統實際上根據所選擇的目標平台對原始碼進行編譯。可以在
Window > Preferences > Plug-in Development > Target Platform 中指定目標平台的位置。這個目標平台在 <targetPlatform>\eclipse 中有自己的一個 Eclipse 副本。要為代碼解析這些依賴性,請從兩個地方尋找是否存在所需要的外掛程式:
- <targetPlatform>\eclipse\plugins 檔案夾中的 Eclipse 外掛程式
- <targetPlatform>\eclipse\links 檔案夾中 .link 檔案所指向的連結外掛程式
程式員通常會將第二個地方稱為 links 檔案夾。這個 links 檔案夾中包含 0 個或多個檔案,檔案名稱通常都是以 “.link” 副檔名結尾。這些檔案中包含了一些連結資訊,可以使用這些資訊定位在磁碟上哪些地方可以找到連結外掛程式。
每個 .link 檔案都有一個關鍵字-值對,其格式為 path=location。(例如,links 檔案夾 C:\eclipse\links 中就可能會有很多 .link 檔案,其中一個檔案的名字可能為 com.ibm.indiver.dependencywalker.link。這個檔案中唯一的一行可能類似於 path=c:\myPlugins\dependencyWalker)。這個 .link 檔案會將 Eclipse 引導到指定的位置,並在 \eclipse\plugins 檔案夾中尋找更多的可用外掛程式。
建立自己的 Eclipse 外掛程式依賴性遍曆程式
編寫一個依賴性遍曆程式基本上分為兩個步驟:首先羅列出所有外掛程式,其次羅列出使用者所選擇的外掛程式的依賴性。
第一個步驟要負責定位 Eclipse 系統中出現的每個外掛程式,並在一個簡單的使用者介面(UI)—— 例如表 —— 中為終端使用者提供所有外掛程式的清單。這個 UI 還應該為使用者提供一些方法來選擇希望解析其依賴性的外掛程式。
第二個步驟則要對使用者選擇的外掛程式的 plugin.xml 檔案進行分析,並尋找這個 plugin.xml 檔案中嵌入的 <import plugin="plugin id"/> 聲明。這種努力顯然需要對每個外掛程式的 manifest 檔案進行遞迴搜尋,從而查明依賴外掛程式的整個鏈條。對於描述這個外掛程式可能依賴於其他外掛程式的父-兄-子關係,樹狀檢視是最合適的一種 UI。我們還應該可以直觀地看出某個 Eclipse 外掛程式註冊項是否真正載入了一個物理存在的外掛程式。
步驟 1:羅列 Eclipse 系統中的所有外掛程式
在掌握了以下資訊之後,就可以編寫一些代碼來羅列磁碟上物理存在的所有外掛程式了:
- 外掛程式主要在 <targetPlatform>\eclipse\plugins 檔案夾中。
- 在其他幾個 <someLinkedPath>\eclipse\plugins 檔案夾中也可能會找到外掛程式。
- 從 <targetPlatform>\eclipse\links 檔案夾中的 .link 檔案中可以獲得到每個 <someLinkedPath> 的路徑。
下面是羅列 Eclipse 系統中所有外掛程式的詳細步驟:
- 找到目標平台的位置。
- 準備 links 檔案夾的路徑。links 檔案夾在 \eclipse 檔案夾中。
- 獲得 \eclipse\links 檔案夾中檔案的清單。請參考原始碼中的 Utilities.getLinkedPaths() 函數。
- 查看每個 .link 檔案,擷取連結 Eclipse 外掛程式的路徑。
- 準備一個所有外掛程式根資料夾的清單(即,<targetPlatform>\eclipse\plugins 檔案夾和所有可能的 <someLinkedPath>\eclipse\plugins 檔案夾)。
- 對於每個根資料夾,進入每個外掛程式目錄中,並擷取 plugin.xml 檔案的路徑。
- 對 plugin.xml 檔案進行分析,獲得外掛程式 ID 和外掛程式版本,並將這些資訊儲存到一個資料結構中。
- 回到步驟 6,繼續處理下一個外掛程式目錄。
清單 2. 準備在 Eclipse 系統下物理存在的所有外掛程式的清單
/** * * @return returns a Vector containing PluginData objects. * Each PluginData object represents a Plugin found under any of the following * plugin directories * a. the targetPlatformLocation\eclipse\plugins directory, * b. other plugin directories as specified by *.link files under * targetPlatform\eclipse\links directory **/ public static Vector getPluginsInTargetPlatform(){ /** //step1: Get path of target platform. //step2: Prepare path of links folder. //step3: Get list of files in links folder. //step4: Parse each link file and get the path of linked Eclipse folder. //step5: Prepare a list of all plugin root folders // (Eclipse plugins and linked Eclipse plugins). //step6: 6a. For each plugin root folder, // 6b. go to each plugin directory and get path of plugin.xml. //step7: Parse the plugin.xml file to get plugin id, plugin version, // and store in vectors, lists, etc. //step8: Go back to step 6 to continue with next plugin directory. **/ //step1: Get path of target platform. //Fall back to Eclipse install location if targetplatform in not set. URL platFormURL = Platform.getInstallLocation().getURL(); Location location = Platform.getInstallLocation(); IPath eclipsePath = null ; //Get path of target platform against which the users of this tool //will compile their code. IPath targetPlatFormLocation = new Path(getTargetPlatformPath(true)); if(_useTargetPlatform == false) eclipsePath = new Path(platFormURL.getPath()); else eclipsePath = targetPlatFormLocation; showMessage("Considering target platform to be: " + eclipsePath.toString()); //step2: Prepare path of links folder. //step3: Get list of files in links folder. //step4: Parse each link file and get the path of linked Eclipse folder. IPath linksPath = new Path( eclipsePath.toString() ).append("/links"); String linkedPaths[] = getLinkedPaths(linksPath.toString()); int linkedPathLength = 0; if(null != linkedPaths){ linkedPathLength = linkedPaths.length; } //step5: Prepare a list of all plugin root folders // (Eclipse plugins and linked Eclipse plugins). IPath eclipsePluginRootFolders[] = new IPath[linkedPathLength + 1]; eclipsePluginRootFolders[0] = new Path( eclipsePath.toString() ).append("/plugins"); if(null != linkedPaths){ for(int i=0; i<linkedPaths.length; i++){ eclipsePluginRootFolders[i+1] = new Path(linkedPaths[i]).append("/eclipse/plugins"); } } //step6: 6a. For each plugin root folder, // 6b. go to each plugin directory and get path of plugin.xml. //step7: Parse the plugin.xml file to get plugin id, plugin version, // and store in vectors, lists, etc. Vector vectorsInThisVector = new Vector(); for(int i=0; i<eclipsePluginRootFolders.length; i++){ System.out.println("\n========plugin IDs and Versions in " + eclipsePluginRootFolders[i] + "========"); Vector pluginDataObjs = getPluginDataForAllPlugins( eclipsePluginRootFolders[i].toString()); vectorsInThisVector.add(pluginDataObjs); System.out.println(pluginDataObjs); System.out.println("\n===========|||=== end ===|||==========="); } Vector pluginData = new Vector(); Iterator outerIterator = vectorsInThisVector.iterator(); while(outerIterator.hasNext()){ Vector pluginDataObjs = (Vector)outerIterator.next(); Iterator innerIterator = pluginDataObjs.iterator(); while(innerIterator.hasNext()){ PluginData pd = (PluginData)innerIterator.next(); String pluginIdKey = pd.getPluginID(); String versionValue = pd.getPluginVersion(); String pluginPath = pd.getPluginLocation(); pluginData.add(pd); } } int breakpoint=0; return pluginData; }
在掌握了所有的外掛程式之後,我們就可以顯示外掛程式的 ID、版本、位置以及更多資訊了,這些可以顯示在一個 Standard Widget Toolkit(SWT)表中,從而使這些資訊更加直觀。我們也可以編寫一些代碼根據外掛程式 ID 列進行排序,就像是我們的範例代碼一樣。還應該在一列中說明找到了多少個外掛程式。結果應該如下所示:
圖 1. Target-Platform 視圖中的所有外掛程式
步驟 2:對 plugin.xml 檔案進行遞迴搜尋,從而遍曆整個依賴鏈
當使用者選擇希望解析依賴鏈的外掛程式之後,我們就需要對使用者所選擇的外掛程式的 plugin.xml 檔案進行分析,從而查看它的依賴性。每個依賴性都會導致檢查另外一個 plugin.xml 檔案,後者又有自己的依賴性。從使用者選擇的外掛程式開始,這個依賴鏈可以迅速導致有很多個 plugin.xml 檔案需要進行分析。我們可以編寫一個遞迴函式來遍曆這些依賴性,它可以尋找某個外掛程式的最新版本(針對在相同的系統中有重複外掛程式的情況)以及它的所有依賴性。
編寫這種遞迴函式需要執行的步驟如下所示,清單 3 給出了這個函數的原始碼。遞迴函式有時對資源的消耗量很大,而且在使用者失去耐心之前可能還沒有返回結果。另外一種選擇是編寫一個函數,只擷取使用者選擇的外掛程式的直接依賴性清單。後一種方法請參看範例代碼中的 loadImmediateDependencies() 函數。
- 獲得使用者選擇的外掛程式的路徑。
- 檢查這個位置上是否存在 plugin.xml 或 fragment.xml 檔案。
- 對 plugin.xml 或 fragment.xml 檔案進行分析,從而獲得這個外掛程式所需要的所有外掛程式的清單。
- 對於這個清單中的每個外掛程式 ID,尋找對應的外掛程式。
- 如果多個外掛程式具有相同的 ID,就只向使用者報告一次,並自動確定使用版本較新的外掛程式。如何編程對外掛程式版本進行比較並尋找一個版本較新的外掛程式,請參看圖 4。
- 將(步驟 4 或 4a 中找到的)外掛程式添加到一個樹視圖中,並遞迴地調用相同的函數,再次從步驟 1 開始重新執行。不過這次使用者不用再選擇外掛程式了;步驟 4 或 4a 對應的代碼會負責選擇外掛程式。
清單 3. recursivePluginDependencyWalker() 函數
private Vector alreadyNotified = new Vector(); private boolean firstCall = true; private TreeParent root = null; private void recursivePluginDependencyWalker(PluginData pdObject, TreeParent parentNode){ try { String path = pdObject.getPluginLocation(); PluginParser pp = null; File pluginDotXmlFile = new File(path + "/plugin.xml"); if(pluginDotXmlFile.exists()){ pp = new PluginParser(pluginDotXmlFile); }else{ File fragmentDotXmlFile = new File(path + "/fragment.xml"); if(fragmentDotXmlFile.exists()){ pp = new PluginParser(fragmentDotXmlFile); }else{ return;//no plugin.xml or fragment.xml found } } String displayName = pdObject.getDisplayName(); System.out.println("\nPlugin ["+ displayName + "] requires" + "\n"); String requires[] = pp.getDependencyList(); if(0 != requires.length ){ for(int i=0; i<requires.length; i++){ System.out.println("\t" + requires[i] ); PluginData pd[] = getPluginDataObjectsFromPluginID(requires[i]); PluginData nextPlugin = null; switch(pd.length){ case 0: //great, we know there is //something missing nextPlugin = null; break; case 1: //best case, everything will be smooth nextPlugin = pd[0]; break; default: //worst case, there must be more //than 1 plugin with the same id //at different locations. String msgLine1 = "Plugin " + displayName + " requires " + requires[i] + "\n"; String msgLine2 = "Duplicate plug-ins found for ID: \ " " + requires[i] + "\"" + "\n Continuing with higher version... " ; //it is bad to give repeated //reminders, //so remind only once per plugin id. if(! alreadyNotified.contains( new String(requires[i]))){ MessageDialog.openInformation(null, "Dependency Walker", msgLine1 + msgLine2); alreadyNotified.add( new String(requires[i])); } //always take the better //version anyway nextPlugin = getBetterVersionPlugin(pd); break; }//end of switch if( null != nextPlugin ){ TreeParent nextinLine = new TreeParent( nextPlugin.getDisplayName(), nextPlugin.isPlugin LoadedInRegistry() ); parentNode.addChild(nextinLine); recursivePluginDependencyWalker( nextPlugin, nextinLine); }else{ TreeParent nextinLine = new TreeParent( requires[i] + " [This plug-in is missing] ", false); parentNode.addChild(nextinLine); //obviously we can't recurse //into a missing plugin... } }//end of for }else{ System.out.println("\t NOTHING: No further dependency \n" ); //no further dependency } } catch (Exception e) { e.printStackTrace(); } }
有時候,我們會碰到在不同位置存在具有相同 ID 的外掛程式的情況。例如,ID 為 org.eclipse.xsd 的外掛程式可能會在 <targetPlatform>\eclipse\plugins 檔案夾和 <someLinkedPath>\eclipse\plugins 檔案夾中同時出現。
在這種情況中,必須要確定從這兩個或更多個磁碟副本中選用哪個外掛程式。顯然,我們所感興趣的應該是最新的外掛程式,也就是說,版本較新的外掛程式。我們可以利用現有的一些函數來對 Eclipse 外掛程式的版本進行比較,或者可以基於清單 4 所示的範例代碼編寫一個簡單的函數來對外掛程式版本進行比較。
清單 4. 比較外掛程式版本
private PluginData getBetterVersionPlugin(PluginData pdo[]){ PluginData _pdObjs[] = pdo; int len = pdo.length; if(len==0) return null; Arrays.sort(_pdObjs,new Comparator() { /**Compares its two arguments for order. * Returns a negative integer, zero, or a positive integer * as the first argument is less than, equal to, or greater than * the second. **/ public int compare(Object leftObj, Object riteObj) { String leftPID = ((PluginData)leftObj). getPluginVersion().replace('.', ':'); String ritePID = ((PluginData)riteObj). getPluginVersion().replace('.', ':'); String leftID[] = leftPID.split(":"); String riteID[] = ritePID.split(":"); int maxlen = leftID.length > riteID.length ? leftID.length : riteID.length; for(int i=0; i<maxlen; i++){ int left = 0; int rite = 0; try { left = new Integer(leftID[i]).intValue(); } catch (NullPointerException e) { left = 0; } try { rite = new Integer(riteID[i]).intValue(); } catch (NullPointerException e) { rite = 0; } if(left==rite){ continue; }else{ int bigger = left > rite ? left : rite; if(bigger==left) return 1; if(bigger==rite) return -1; } } return 0; } public boolean equals(Object arg0) { return false; } }); return _pdObjs[len-1]; }
在代碼遍曆完整個連結依賴性鏈之後,我們就可以使用一個樹視圖來直觀地將其表示出來。還應該直觀地指出(請參看下圖中的紅圈)是哪一個外掛程式導致了載入失敗。
這個搜尋的結果應該類似於下圖所示:
圖 2. Dependency Walker Tree View
結束語
如果我們希望定位一些無法解析的外掛程式依賴性(缺少外掛程式或 Eclipse 由於某些原因未能載入它們),首先可以使用 Eclipse PDE Plug-in Dependencies 視圖來顯示外掛程式的依賴性。如果 Plug-in Dependencies 視圖沒有顯示我們的外掛程式,就可能希望使用本文中介紹這個工具對所有連結外掛程式檔案夾進行自動化搜尋。如果您只對某個具體的外掛程式感興趣,也可以對這段代碼進行修改來滿足您的要求。
可以從下面的 “下載” 一節獲得這個工具的原始碼。要瀏覽原始碼,請展開原始碼包,並將這個外掛程式作為一個 Eclipse 項目開啟。要使用這個工具,請將這個外掛程式解壓到 \eclipse\plugins 檔案夾中,並執行以下操作:
- 在 Eclipse 中,切換到 Window > Show View > Others > DependencyWalker Category 中,並選擇 All Plugins in Target-Platform 視圖。
- 這個視圖會顯示在指定目標平台中出現的所有外掛程式。選擇一個外掛程式並雙擊它。
- DependencyWalkerTreeView 會顯示您所選擇的外掛程式的所有依賴性。完成之後,請關閉這個視圖。