UML软件工程组织

在Java中用类装载框架控制类加载
作者:朱先忠编译出处:天极开发

摘要 通过构建一个能够把Java类装载隔离到一个指定的jar文件中的类装载组件容器框架,你可以确保运行时刻会装载你期望的组件版本。

Java的类装载框架强有力且具有灵活性。它允许应用程序存取类库而不必链接到静态的"include"文件。代之的是,它能够从指定位置装载包含库类和资源的档案文件,例如由CLASSPATH环境变量所定义的目录和网络位置。由系统来动态地解析对类和资源的运行时刻参考,从而简化了更新和版本发行。然而,每一个库都有其自己的依赖性集合-并且由开发者和发布人员来保证他们的应用程序适当地参考正确的版本。遗憾的是,默认的类装载系统和特定依赖性的结合可能并且确实会导致错误、系统崩溃甚至于更糟糕的情况发生。

本文中,我将向你建议一个实现类装载的容器框架,从而解决这些问题。

一、 Java Classpath

Java根据环境属性/变量CLASSPATH来指定运行时刻用来查找类和其它资源的路径。你可以通过设置CLASSPATH环境变量或使用Java命令行选项--classpath来定义CLASSPATH属性。

典型地,一个Java运行时刻以下面顺序查找和加载类:

1. 在bootstrap类列表中的类-这些是体现Java平台的类,例如在rt.jar中的类。

2. 出现在扩展类列表中的类-这些类使用扩展机制框架来扩展Java平台,使用位于运行时刻环境的/lib/ext目录下的档案文件(.jar,.zip,等等。)。

3. 用户类-这些类不使用-classpath命令行选项或CLASSPATH环境变量标识的扩展机制架构。

二、 档案与Classpath

一个档案.jar或.zip文件可以包括一个manifest文件-它们包含能够用于提供档案信息,设置档案属性,等等的入口。这个manifest文件还可以通过包括一个名为Class-Path的入口(它包含一个档案和目录列表)来扩展classpath。JDK 1.3中引入了Class-Path manifest入口用于指定可选的据需要可以加载的jar文件和目录。下面是一个Class-Path入口的例子:

Class-Path: mystuff/utils.jarmystuff/logging.jar mylib/

Java提供了一种可扩展模型用于指定装载类的位置和文件列表。然而,由此也引发了一些问题,例如,一个不同版本的库可能存在于classpath中-这超出一个执行类所期望的结果。

三、 Classpath版本冲突

在Java中,一个类的运行时刻标识是由通过其完全限定名字来定义的(在类名之前的包名,有时被作为FQN),所有这些都添加到装载类的相关装载器的ID。这样以来,由多个类加载器加载的一个类的每一个实例都将被当作是Java运行时刻的一个单独的实体。这意味着,运行时刻能够在任何时间装载同一个类的多个版本。这是一种非常有力和相当灵活的特征;然而,如果一位开发人员不认真地使用的话,某些副作用可能会令他疑惑不解。

可以设想,你在开发一个企业应用程序-它使用类似语义从多种源存取数据,例如一个文件系统和一个数据库。许多这种类型的系统都暴露一个数据存取层-通过抽象类似数据源的数据存取对象(DAO)。现在,设想你装载一个新版本的一个数据库DAO,使用一种略微不同的API来满足一个DAO客户端的新特征的要求-但是你仍然需要旧式的DAO以便适合于其它还没有为这种新的API准备好的客户端。在典型的运行时刻环境下,这种新的DAO将简单地替换旧的版本并且所有的新实例都将从新版本中创建。然而,如果在不停止运行时刻环境的前提下发生更新,那么任何已经存在的旧DAO的实例将与该新DAO的任何实例一起驻留于内存中-当创建这些新实例时。这已经足已令人疑惑了。更为糟糕的是,一位DAO客户期望创建一个旧版本的DAO的实例,但是实际上得到一个具有已改变的API的新版本的实例。正如你所见,这可能会带来一些有趣的挑战。

为了确保稳定性和安全性,调用代码必须能够指明它想使用的类的正确版本。为此,你可以创建一个类加载器,组件容器模型并且使用一些简单的类加载技术。

四、 档案与组件

因为档案文件(jar文件,zip文件,等等)与Java类加载机制和发布工具之间具有相当松的耦合性,所以它们是一种用作自定义组件容器的自然的候选。一个Java组件在一个档案文件中的打包与发布的成功依赖于:

· 能够指定要实例化一个组件的哪个版本的开发者

· 装载组件的辅助类的正确版本-根据与该组件在同一个jar文件中发现的信息。

这使得组件的开发者和消费者能够完全控制实际创建和使用每一个组件的相应版本。

在下面的几节中,我将讨论一下有关于定义组件和组件命名空间的概念。

五、 共享辅助资源

最大的问题之一是,当使用标准类加载器在Java中处理共享库时,所有的类都被加载到一个命名空间中。这使得在任何给定时刻很难使用相同库的不同版本。你所需要的是,一个组件能够定义它自己的命名空间-该组件及其所有辅助库将会装载到其中的。

因为在Java中,一个类的运行时刻标识是使用类的完全限定名和其加载器的ID来定义的,所以一个命名空间已经相应于每一个类加载器存在。因此,你可以使用类加载器来构建一个组件容器,由它来定义一个组件及其依赖对象的一个命名空间。

例如,如果我有一个命名为"com.jeffhanson.components.HelloWorld"的类,我想运行它的两个版本,那么解决方案是,使用一个类装载器创建HelloWorld类的一个版本的一个实例,而使用另一个类装载器创建另一个版本的HelloWorld类。图1展示了这一概念。

 图1.使用多个类装载器:由于Java命名惯例特征的影响,使用不同的类装载器将定义不同的命名空间。

正如我将在本文中所要展示的,使用两个不同的类装载器来实例化一个类的技术实际上创建了一个虚拟的命名空间。然而,我实际上刚好创建了同一个版本的类的多个实例。

为了便于加载和实例化同一个类的多个版本,我将展示(在下面的几节中)一个组件-容器框架-它基于类装载器命名空间机制以允许装载同一个类的不同版本。

 六、 利用Classloader命名空间

你可以把组件容器框架实现为一个容器实体-负责加载在jar或zip档案中定义的组件以及该组件需要的辅助类。这个框架的创建目标是:

1. 允许开发者指定实例化一个组件的哪个版本。

2. 基于与组件在同一个jar文件中找到的信息为每个组件装载正确的辅助类。

3. 跨组件共享辅助类和档案。

你将需要一个配置文件来定义组件及其相应的辅助文件,正如下列示例所展示的:

<?xml version="1.0"?>
 <component name="com.jeffhanson.components.HelloWorld">
  <component-archive>
   HelloWorldComponentV1.jar
  </component-archive>
  <ancillary-resources>
  <ancillary-resource>
   log4j-1.2.12.jar
  </ancillary-resource>
  <ancillary-resource>
   concurrent-1.3.4.jar
  </ancillary-resource>
  </ancillary-resources>
</component>

你可以把上面例子中的元素与下面例子中的元素进行比较。唯一的改变是组件档案元素的值。这个组件元素值定义了包含每一个版本组件的档案的名字。

<?xml version="1.0"?>
<component name=
"com.jeffhanson.components.HelloWorld">
<component-archive>
HelloWorldComponentV2.jar
</component-archive>
<ancillary-resources>
<ancillary-resource>
log4j-1.2.12.jar
</ancillary-resource>
<ancillary-resource>
concurrent-1.3.4.jar
</ancillary-resource>
</ancillary-resources>
</component>

为了确保框架仅从指定位置加载类,你必须创建一个新的扩展URLClassLoader的ClassLoader。重载loadClass方法以防止到它的调用传播到默认的类装载器的父级-并因此从标准classpath中加载类。这样以来,就可以把类搜索限定到提供给类装载器的URL并且让你把特定jar文件位置提供给装载组件的类装载器。

下列代码展示了组件的类装载机制:

package com.jeffhanson.components;
import java.net.URL;
import java.net.URLClassLoader;
public class RestrictedURLClassLoader
extends URLClassLoader {
 public RestrictedURLClassLoader( URL[] urls) { super(urls, null);)
}


图2.组件容器框架类关系:该图展示了存在于组件容器框架中的类之间的关系。

public Class loadClass(String name)
throws ClassNotFoundException
{
 Class cls = super.loadClass(name);
 if (cls == null)
 {
  throw new ClassNotFoundException("Restricted ClassLoader" + " is unable to find class: " + name);
 }
 return cls;
}
}

这个受限制的类装载器由组件容器使用来装载组件和任何指定的辅助类。

该组件容器使用当前线程的上下文类装载器来查找该组件的URL。然后,这个URL被加入到受限制的类装载器并且用于实例化该组件。然后,该组件类被组件容器缓冲以便于后面的调用。列表1展示了该组件容器的代码,图2展示了在组件容器框架中的类之间的关系。
七、 装载特定类版本

现在,你可以使用该容器和受限制的类装载器来从指定档案中装载包含版本信息的类的组件。

列表2展示了如何实例化组件容器的实例并且使用配置文件名初始化它们-针对两种版本的HelloWorld组件。然后,每个组件版本被装载和实例化-使用ComponentContainer类的createComponent方法。

对于每一个实例化的组件对象的调用将产生每个组件的期望版本相应的结果。

 

图3.组件序列图:该图展示了组件容器框架创建一个组件的序列。

图3中的序列图展示了框架用于装载和创建一个组件的步骤。

注意,在实例化一个默认类装载器之前,对于RestrictedURLClassLoader类的调用终止,从而把类搜索限制为提供给RestrictedURLClassLoader实例的URL。

八、 小结

总之,你已经从本文中看到了怎样构建一个类装载组件容器框架。这样以来,便利了在一个自包含上下文中定义、版本化和创建Java组件。以这种方式来利用Java的类装载能力可以把类装载约束到指定位置,从而让你同时装载不同版本的类-在同一个运行JVM中创建的和使用的类。

 

版权所有:UML软件工程组织