试图定位无法解析的插件依赖性是件紧张而又耗时的事情。激活每个插件都要依赖于很多其他插件,这些插件又会依赖于其他更多插件。如果 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 会显示您所选择的插件的所有依赖性。完成之后,请关闭这个视图。