深入理解应用Java类装入器技术

众所周知,Java是一个健壮的、安全的、可扩展的平台,Java程序可以方便地移植到任何底层平台上运行,而使这一切成为现实的关键之一就是Java的类装入器(Class Loader)。

  在Java平台上,JVM负责装入和执行Java代码,它利用类装入器将Java类装入到Java运行时环境。类装入器提供了这样一种机制,它使得开始时JVM不需要知道运行时将要装入类的任何信息。几乎所有基于Java的容器,诸如EJB容器、Servlet容器等,都实现了定制的类装入器,以支持热部署、运行时平台可扩展性等功能。如果要实现这类Java容器,深入理解类装入器是必不可少的前提。

  对于企业应用的开发者来说,如果要为这类平台开发组件,有关类装入器的知识将有助于理解容器的工作机制,同时也为调试代码带来方便。本文将探讨Java类装入器的体系结构,阐述类装入器在平台安全性、可扩展性方面的意义,介绍如何实现用户自定义的类装入器。

  类装入器装入的最小执行单元是Java .class文件。Java .class文件包含Java类的二进制描述,其中有可执行的字节码以及该类用到的对其他类的引用,包括对Java标准API里面的类的引用。简单地说,类装入器首先找到要装入的Java类的字节码,读入字节码,创建一个java.lang.Class类的实例。做好这些准备之后,类就可以被JVM执行了。

  当JVM最初开始运行时,它里面不装入任何类。如果要求JVM执行一个程序,被执行的类首先装入,字节码执行期间会引用到其他类和接口,这些被引用到的类和接口随之也被装入。因此,有人把JVM的类装入方式称为“懒惰的”装入方式,即只有必须用到某个类时才会装入它(而不是预先装入各种可能用到的类),正因为如此,开始时JVM不必知道运行时要装入哪些类。在Java平台上,懒惰的装入方式是实现动态可扩展性机制的关键因素之一。在本文的后面,你将会看到通过实现一个定制的Java类装入器,我们可以为Java运行时环境加入许多有趣的功能。

一、委托模式

  Java 2运行时环境中有多个类装入器的实例,每一个类装入器的实例从不同的代码库装入Java类。例如,Java核心API类由bootstrap(或primordial)类装入器装入,应用程序的类由system(或application)类装入器装入。另外,应用程序可以自定义类装入器从指定的代码库装入类。Java 2定义了类装入器之间的父-子关系,每一个类装入器(bootstrap除外)都有一个父类装入器,形成一个由类装入器构成的树形结构,bootstrap类装入器是这个树形结构的根,因此bootstrap没有父类装入器。图1描述了类装入器之间的这种关系。



图1


  当客户程序请求类装入器装入某个类时,类装入器按照下面的算法装入一个类:

  ⑴ 首先执行一次检查,查看客户程序请求的类是否已经由当前的类装入器装入。如果是,则返回已装入的类,请求处理完毕。JVM缓冲了类装入器装入的所有类,已经装入的类不会被再次装入。

  ⑵ 如果尚未装入类,则装入类的请求被委托给父类装入器,这个操作发生在当前的类装入器尝试装入指定的类之前。委托装入类的操作一直向上传递,直至bootstrap类装入器,如前所述,bootstrap类装入器是处于树形结构的顶层,它不再有可委托的父类装入器。

  ⑶ 如果父类装入器未能装入指定的类,则当前的类装入器将尝试搜索该类。每一个类装入器有预定义的搜索和装入类的位置。例如,bootstrap类装入器搜索sun.boot.class.path系统属性中指定的位置(包括目录和zip、jar文件),system类装入器搜索的位置由JVM开始运行时传入的CLASSPATH命令行变量指定(相当于设置java.class.path系统属性)。如果找到了类,则类装入器将类返回给系统,请求处理完毕。

  ⑷ 如果找不到类,则抛出java.lang.ClassNotFoundException异常。

二、实现Java 2定制类装入器

  正如前面提到的,Java平台允许开发者实现定制的类装入器,改变默认的类装入行为。下面我们就来看看具体的实现过程。

  类装入器都是通过扩展java.lang.ClassLoader类实现的,唯一的例外是bootstrap类装入器,它是JVM中以本机代码的形式实现的)。下面这个代码片断显示了Java 2 ClassLoader相关的API:

     
     public abstract class ClassLoader extends Object {
 protected ClassLoader(ClassLoader parent);
 protected final Class defineClass( String name,byte[] b,
   int off,int len)
    throws ClassFormatError
 protected Class findClass(String className) throws 
  ClassNotFoundException
 public class loadClass(String className)
 throws ClassNotFoundException
}


  每一个类装入器创建时都有一个父类装入器,父类装入器是装入类的过程中向上委托模式必不可少的。客户程序通过调用类装入器的loadClass方法来装入一个类,启动前面介绍的类装入算法。在Java 2之前,java.lang.ClassLoader类的loadClass是一个抽象方法,要求定制类装入器在扩展java.lang.ClassLoader类时必须实现该方法。实现loadClass方法是一项相当复杂的工作,因此Java 2不再有此要求。

  由于引入了类装入过程中总是向父类装入器委托的模式,java.lang.ClassLoader已经实现了loadClass方法,它实际上是一个实现了前面介绍的类装入算法的模板方法。在类装入算法的第三个步骤中,loadClass方法调用findClass方法(Java 2开始引入),定制的类装入器必须覆盖findClass方法,提供自定义的类查找和装入机制。可以看到,Java 2极大地简化了定制类装入器的实现。下面提供了CustomClassLoader.java的一个片断(完整的代码可以从本文最后下载),CustomClassLoader.java从一个构造函数中指定的代码库装入Java类。

     
     public class CustomClassLoader extends ClassLoader {
 // 要搜索的代码库
 private List classRepository;

 public CustomClassLoader  (ClassLoader parent,String searchPath)
 {...}
 protected Class findClass(String className) throws 
  ClassNotFoundException {
 byte[] classBytes = loadFromCustomRepository(className);
 if(classBytes != null) {
    return defineClass (className,classBytes,0,classBytes.length);
 }
 //else
 throw new ClassNotFoundException(className);
 } 
 }


  findClass方法调用loadFromCustomRepository方法,后者搜索代码库寻找指定的类,如果找到,则读取并返回该类的字节码。传递给defineClass的是原始的字节码,defineClass在java.lang.ClassLoader类中定义,它将返回一个java.lang.Class对象的实例,使得新的Java类可被正在运行的Java程序调用。在这个过程中,defineClass方法还将确保定制的类装入器不会从自定义的代码库装入类来重新定义核心Java API类。另外,如果传递给defineClass的类名称以“java”开头,defineClass将抛出SecurityException异常。

  值得指出的是,对于传递给loadClass方法的字符串所描述的类,JVM不必事先知道任何有关它的信息。下面我们来看看如何应用定制的类装入器。

三、突破Java 2委托模式

  虽然前面介绍的Java 2类装入过程有其优点,但并非任何时候我们都可以遵从这种委托模式,有些时候我们必须在定制类装入器中突破Java 2预定义的模式。例如,Servlet规范建议(Section 9.7):在实现Web应用的类装入器时,封装在Web应用档案文件中的类和资源应当比容器级JAR文件中的类和资源优先装入。为达到这个建议的要求,Web应用的类装入器应当首先搜索其本地的代码库,然后再将装入类的请求委托给父类装入器,不过这已经突破了Java 2原来的类装入模式。

  Servlet规范的建议是为了让Web应用使用的类、资源能够与Servlet容器使用的类、资源有不同的版本,例如,Web应用可能要用到新版XML解析器才有的功能,而Servlet容器使用的XML解析器却不能提供这方面的功能。

  要满足Servlet规范,实现Web应用的类装入器时可以覆盖java.lang.ClassLoader类的loadClass方法,这种定制类装入器的loadClass方法可能如下所示:

     
     public Class loadClass(String name) throws ClassNotFoundException
 { 
 // 检查该类是否已经装入
 Class loadedClass = findLoadedClass(name);
 if (loadedClass == null) {
 // 在将请求委托给父类装入器之前,
 // 搜索本地代码库
 ....... 
 // 如果本地代码库中找不到类,则将
 // 请求委托给父类装入器
 loadedClass = this.getClass().getClassLoader().loadClass(name);
 }
 return loadedClass;
 }


四、应用实例

  类装入器提供了许多可在Java程序之中使用的强大功能,下面是几个例子。

  ■ 热部署

  所谓热部署,就是在应用正在运行的时候升级软件,却不需要重新启动应用。对于Java应用程序来说,热部署就是在运行时更新Java类文件。在基于Java的应用服务器实现热部署的过程中,类装入器扮演着重要的角色。大多数基于Java的应用服务器,包括EJB服务器和Servlet容器,都支持热部署。类装入器不能重新装入一个已经装入的类,但只要使用一个新的类装入器实例,就可以将类再次装入一个正在运行的应用程序。下面这段代码来自TestCustomLoader.java,示范了如何在Java应用程序中实现热部署:

     
     1. ClassLoader customLoader = new
2. CustomClassLoader(repository);
3. loadAndInvoke(customLoader,classToLoad);
4. System.out.println("正在等待.按
5. Enter键继续");
6. System.in.read();
7. customLoader = new CustomClassLoader
8. (repository);
9. loadAndInvoke(customLoader,classToLoad);


  我们首先创建了一个CustomClassLoader类的实例,要求从命令行参数指定的代码库装入类。loadAndInvoke装入一个类HelloWorld(也在命令行参数中指定),调用该实例的一个方法,在控制台上输出一条信息。当第六行“System.in.read()”等待用户输入时,我们可以修改HelloWorld类(例如修改它输出到控制台的信息)并重新编译。当程序继续向下运行时,第七行又创建了一个新的CustomClassLoader实例,第九行再次调用loadAndInvoke,装入更新后的HelloWorld类,向控制台输出新的提示信息。

  ■ 修改类文件

  在findClass方法中,类装入器搜索某个类的字节码,找到并读入字节码之后,可以在调用defineClass之前修改字节码。

  例如,可以在调用defindClass之前向字节码插入额外的程序调试信息;对于某些安全性要求极高的程序,类文件可以放入加密的代码库,然后由findClass方法在调用defineClass方法之前解密。又如,程序可以动态地生成字节码,而不是从代码库读取类文件——其实这就是JSP技术的基础。

  ■ 类装入器与安全机制

  由于类装入器担负着把代码装入JVM的重任,所以它的体系结构应当保证整个平台的安全性不会受到威胁。每一个类装入器要为它装入的类定义一个独立的名称空间,因此运行时,一个类由它的包名称和装入它的类装入器两者结合唯一地标识。

  在名称空间之外,类不可见,运行时不同名称空间的类之间有一种保护性的“隔离墙”,在Java 2向父类委托的类装入模式中,类装入器可以请求其父类装入器装入的类,因此类装入器需要的类不一定全部由它自己装入。

  在Java运行环境中,不同的类装入器从不同的代码库装入类,之所以要将各个类装入器代码库的位置分开,是为了便于给不同的代码库设定不同的信任级别。在JVM中,由bootstrap类装入器装入的类具有最高的信任级别,用户自定义类装入器代码库的信任级别最低。此外,类装入器可以把每一个装入的类放入一个保护域,保护域定义了代码执行时所拥有的权限。

  如果要以系统安全策略(一个java.security.Policy的实例)为基础定义代码的权限,定制类装入器必须扩展java.security.SecureClassLoad类,调用其defineClass方法,SecureClassLoad类的defineClass方法有一个java.security.CodeSource参数。defindClass方法从系统策略获得与CodeSource关联的权限,以此为基础定义一个java.security.ProtectionDomain。详细介绍该安全模型已经超出了本文的范围,请读者自行参阅有关JVM内部机制的资料。

  结束语:类装入器提供了一种强大的机制,通过这一机制我们可以在运行时对Java平台作许多有意义的扩展,实现普通Java程序运行时无法达到的目标。在当前的J2EE平台提供的某些技术中,类装入器扮演着重要的角色。本文只介绍了一部分可能的应用情景,更深入的细节和应用就有待读者自己去研究了。

  下载本文的代码: JavaClassLoadercode.zip

参考:

Dynamic Class Loading in the Java Virtual Machine:http://www.java.sun.com/people/gbracha/classloaders.ps

Secure Java Class Loading:http://java.sun.com/people/ gong/papers/ieeeic98.pdf

The Java Servlet Specification:http://jcp.org/aboutJava/ communityprocess/first/jsr154/ 
 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值