Java的类加载机制是非常强大的。你可以利用外部第三方的组件而不需要头文件或静态
连接。你只需要简单的把组件的JAR文件放到classpath下的目录中。运行时引
用完全是动态处理的。但如果这些第三方组件有自己的依赖关系时会怎么样呢?通常这需要
开发人员自己解决所有需要的相应版本的组件集,并且确认他们被加到classpath
中。
JAR清单文件 实际上你不需要这样做,Java的类加载机制可以更优雅地解决这个问题。一种方案是需
要每一个组件的作者在JAR清单中定义内部组件的依赖关系。这里清单是指一个被包含在
JAR中的定义文件元数据的文本文件(META-INF/MANIFEST.MF)。最
常用的属性是Main-Class,定义了通过java –jar方式定位哪个类会被调用。然而,还有一个不那么有名的属性Class-Pat
h可以用来定义他所依赖的其他JAR。Java缺省的ClassLoader会检查这
些属性并且自动附加这些特定的依赖到classpath中。
让我们来看一个例子。考虑一个实现交通模拟的Java应用,他由三个JAR组成:
·simulator-ui.jar:基于Swing的视图来显示模拟的过程。
·simulator.jar:用来表示模拟状态的数据对象和实现模拟的控制类。
·rule-engine.jar:常用的第三方规则引擎被用来建立模拟规则的模型。
simulator-ui.jar依赖simulator.jar,而simulat
or.jar依赖rule-engine.jar。
而通常执行这个应用的方法如下:
$ java -classpath
simulator-ui.jar:simulator.jar:rule-engi
ne.jar
com.oreilly.simulator.ui.Main
编者注:上面的命令行应该在同一行键入;只是由于网页布局的限制看起来好像是多行。
但我们也可以在JAR的清单文件中定义这些信息,simulator-ui的MANI
FEST.MF如下:
Main-Class: com.oreilly.simulator.ui.Main
Class-Path: simulator.jar
而simulator的MANIFEST.MF包含:
Class-Path: rule-engine.jar
rule-engine.jar或者没有清单文件,或者清单文件为空。
现在我们可以这样做:
$ java -jar simulator-ui.jar
Java会自动解析清单的入口来取得主类及修改classpath,甚至可以确定si
mulator-ui.jar的路径和解释所有与这个路径相关的Class-Path
属性,所以我们可以简单按照下面的方式之一来做:
$ java -jar ../simulator-ui.jar
$ java -jar /home/don/build/simulator-ui.jar
依赖冲突 Java的Class-Path属性的实现相对于手工定义整个classpath是一
个大的改善。然而,两种方式都有自己的限制。一个重要的限制就是你只能加载组件的一个
特定版本。这看起来是很显然的因为许多编程环境都有这个限制。但是在大的包含多个第三
方依赖的多JAR项目中依赖冲突是很常见的。
例如,你正在开发一个通过查询多个搜索引擎并比较他们的结果的搜索引擎。Google
和Amazon的Alexa都支持使用SOAP作为通讯机制的网络服务API,也都提
供了相应的Java类库方便访问这些API。让我们假设你的JAR- metasearch.jar,依赖于google.jar和amazon.jar,而
他们都依赖于公共的soap.jar。
现在是没有问题,但如果将来SOAP协议或API发生改变时会怎么样呢?很可能这两个
搜索引擎不会选择同时升级。可能在某一天你访问Amazon时需要SOAP1.x版本
而访问Google时需要SOAP2.x版本,而这两个版本的SOAP并不能在同一个
进程空间中共存。在这里,我们可能包含下面的JAR依赖:
$ cat metasearch/META-INF/MANIFEST.MF
Main-Class: com.onjava.metasearch.Main
Class-Path: google.jar amazon.jar
$ cat amazon/META-INF/MANIFEST.MF
Class-Path: soap-v1.jar
$ cat google/META-INF/MANIFEST.MF
Class-Path: soap-v2.jar
上面正确地描述了依赖关系,但这里并没有包含什么魔法--这样设置并不会像我们期望地
那样工作。如果soap-v1.jar和soap-v2.jar定义了许多相同的类,我
们肯定这是会出问题的。
$ java -jar metasearch.jar
SOAP v1: remotely invoking searchAmazon
SOAP v1: remotely invoking searchGoogle
你可以看到,soap-v1.jar被首先加在classpath中,因此实际上也只
有他会被使用。上面的例子等价于:
$ java -classpath
metasearch.jar:amazon.jar:google.jar:soa
p-v1.jar:soap-v2.jar
# WRONG!
编者注:上面的命令行应该在同一行键入;只是由于网页布局的限制看起来好像是多行。
有趣的是如果Yahoo也发布了一个网络服务API,而他看起来并没有依赖于现有的S
OAP/XML-RPC类库。在较小的项目中,组件依赖冲突常被用来作为在你只要手工
包装方案或者只需要一两个类时而不使用让你不使用全量组件(如集合类库)的原因之一。手
工包装方案有他的用处,但使用已有的组件是更普遍的方式。而且复制其他组件的类到你的
代码库永远不是一个好主意。实际上你已经与组件的开发产生分岐而且没有机会在有问题修
复或安全升级时合并他。
许多大的项目,如主要的商业组件,已经采用将他们使用的整个组件构建到他们的JAR内
部。为了这么做,他们改变了包名使其唯一(如com/acme/foobar/org/f
reeware/utility),而且直接在他们的JAR中包含类。这样做的好处是
可以防止在这些组件中多个版本的冲突,但这也是有代价的。这么做对开发人员来说完全隐
藏了对第三方的依赖。但如果这种方式大规模的应用,将会导致效率的降低(包括JAR文
件的大小和加载多个JAR版本到进程中的效率降低)。这种方式的问题在于如果两个组件
依赖于同一个版本的第三方组件时,就没有协调机制来确定共享的组件只被加载一次。这个
问题我们会在下一节进行研究。除了效率的降低外,很可能你这种绑定第三方软件的方式会
与那些软件的许可协议冲突。
另一种解决这个问题的方式是每一个组件的开发人员显式的在他的包名中编码一个版本号。S
un的javac代码就采用这个方式—一个com.sun.tools.javac.M
ain类会简单地转发给com.sun.tools.javac.v8.Maino。每
次一个新的Java版本发布,这个代码的包名就改变一次。这就允许一个组件的多个发布
版本可以共存在同一个类加载器中并且这使得版本的选择是显式的。但这也不是一个非常好
的解决方案,因为或者客户需要准确知道他们计划使用的版本而且必须改变他们的代码来转
换到新的版本,或者他们必须依赖于一个包装类来转发方案调用给最新的版本(在这种情况
下,这些包装类就会承受我们上面提到的相同问题)。
加载多个发布版本 这里我们遇到的问题在大多数项目中也存在,所有的类都会被加载到一个全局命名空间。如
果每一个组件有自己的命名空间而且他会加载所有他依赖的组件到这个命名空间而不影响进
程的其他部分,那又会怎么样呢?实际上我们可以在Java中这么做!类名不需要是唯一
的,只要类名和其所对应的ClassLoader的组合是唯一的就可以了。这意味着C
lassLoader类似于命名空间,而如果我们可以加载每一个组件在自己的Clas
sLoader中,他就可以控制如何满足依赖。他可以代理类定位给其他的包含他的依赖
组件所需要的特定版本的ClassLoader。如图1。

Figure 1. Decentralized class loaders 然而这个架构并不比绑定每一个依赖的JAR在自己的JAR中好多少。我们需要的是一个
可以确保每一个组件版本仅被一个类加载器加载的中央集权。图2中的架构可以确定每一个
组件版本仅被加载一次。

Figure 2. Class loaders with mediator 为了实现这种方式,我们需要创建两个不同类型的类加载器。每一个ComponentC
lassLoader需要扩展Java的URLClassLoader来提供需要的逻
辑来从一个JAR中获取.class文件。当然他也会执行两个其他的任务。在创建的时
候,他会获取JAR清单文件并定位一个新属性Restricted-Class-Pa
th。不像Sun提供的Class-Path属性,这个属性暗示特定的JAR应该只对
这个组件有效。
public class ComponentClassLoader extends URLClassLoader {
// ... public ComponentClassLoader (MasterClassLoader master, File file)
{
// ... JarFile jar = new JarFile(file);
Manifest man = jar.getManifest();
Attributes attr = man.getMainAttributes();
List l = new ArrayList();
String str = attr.getValue("Restricted-Class-Path");
if (str != null) {
StringTokenizer tok = new StringTokenizer(str);
while (tok.hasMoreTokens()) {
l.add(new File(file.getParentFile(),
tok.nextToken());
}
}
this.dependencies = l;
} public Class loadClass (String name, boolean resolve)
throws ClassNotFoundException {
try {
// Try to load the class from our JAR.
return loadClassForComponent(name, resolve);
} catch (ClassNotFoundException ex) {}
// Couldn't find it -- let the master look for it
// in another components.
return master.loadClassForComponent(name,
resolve, dependencies);
}
public Class loadClassForComponent (String name,
boolean resolve)
throws ClassNotFoundException
{
C