碰到的問題
假設我們希望在 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 會顯示您所選擇的插件的所有依賴性。完成之後,請關閉這個視圖。
本文配套源碼