牙牙窝

语源科技BlogJava 联系 聚合 管理
  8 Posts :: 21 Stories :: 10 Comments :: 0 Trackbacks

JAVA 类动态载入的实现

1        前言

前段时间因为项目的需要,我搞了一套类似 Servlet 重新载入的一个框架,实现了类的动态载入过程。本文写了一些我的学习成果以及心得供大家分享一下。

2        类载入的原理

(下面引用网上的一篇文章):

当 JVM ( Java 虚拟机)启动时,会形成由三个类加载器组成的初始类加载器层次结构:

        bootstrap classloader
                 |
        extension classloader
                 |
        system classloader

bootstrap classloader
- 引导(也称为原始)类加载器,它负责加载 Java 的核心类。在 Sun 的 JVM 中,在执行 java 的命令中使用 -Xbootclasspath 选项或使用 -D 选项指定 sun.boot.class.path 系统属性值可以指定附加的类。这个加载器的是非常特殊的,它实际上不是 java.lang.ClassLoader 的子类,而是由 JVM 自身实现的。大家可以通过执行以下代码来获得 bootstrap classloader 加载了那些核心类库:
    URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
    for (int i = 0; i < urls.length; i++) {
      System.out.println(urls
.toExternalForm());
    }
在我的计算机上的结果为:
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/dom.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/sax.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xalan-2.3.1.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xercesImpl-2.0.0.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xml-apis.jar
file:/C:/j2sdk1.4.1_01/jre/lib/endorsed/xsltc.jar
file:/C:/j2sdk1.4.1_01/jre/lib/rt.jar
file:/C:/j2sdk1.4.1_01/jre/lib/i18n.jar
file:/C:/j2sdk1.4.1_01/jre/lib/sunrsasign.jar
file:/C:/j2sdk1.4.1_01/jre/lib/jsse.jar
file:/C:/j2sdk1.4.1_01/jre/lib/jce.jar
file:/C:/j2sdk1.4.1_01/jre/lib/charsets.jar
file:/C:/j2sdk1.4.1_01/jre/classes
这时大家知道了为什么我们不需要在系统属性 CLASSPATH 中指定这些类库了吧,因为 JVM 在启动的时候就自动加载它们了。

extension classloader
- 扩展类加载器,它负责加载 JRE 的扩展目录( JAVA_HOME/jre/lib/ext 或者由 java.ext.dirs 系统属性指定的)中 JAR 的类包。这为引入除 Java 核心类以外的新功能提供了一个标准机制。因为默认的扩展目录对所有从同一个 JRE 中启动的 JVM 都是通用的,所以放入这个目录的 JAR 类包对所有的 JVM 和 system classloader 都是可见的。在这个实例上调用方法 getParent() 总是返回空值 null ,因为引导加载器 bootstrap classloader 不是一个真正的 ClassLoader 实例。所以当大家执行以下代码时:
    System.out.println(System.getProperty("java.ext.dirs"));
    ClassLoader extensionClassloader=ClassLoader.getSystemClassLoader().getParent();
    System.out.println("the parent of extension classloader : "+extensionClassloader.getParent());
结果为:
C:\j2sdk1.4.1_01\jre\lib\ext
the parent of extension classloader : null
extension classloader
是 system classloader 的 parent ,而 bootstrap classloader 是 extension classloader 的 parent ,但它不是一个实际的 classloader ,所以为 null 。

system classloader
- 系统(也称为应用)类加载器,它负责在 JVM 被启动时,加载来自在命令 java 中的 -classpath 或者 java.class.path 系统属性或者 CLASSPATH 操作系统属性所指定的 JAR 类包和类路径。总能通过静态方法 ClassLoader.getSystemClassLoader() 找到该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器。执行以下代码即可获得:
    System.out.println(System.getProperty("java.class.path"));
输出结果则为用户在系统属性里面设置的 CLASSPATH 。
classloader
加载类用的是全盘负责委托机制。所谓全盘负责,即是当一个 classloader 加载一个 Class 的时候,这个 Class 所依赖的和引用的所有 Class 也由这个 classloader 负责载入,除非是显式的使用另外一个 classloader 载入;委托机制则是先让 parent (父)类加载器 ( 而不是 super ,它与 parent classloader 类不是继承关系 ) 寻找,只有在 parent 找不到的时候才从自己的类路径中去寻找。此外类加载还采用了 cache 机制,也就是如果 cache 中保存了这个 Class 就直接返回它,如果没有才从文件中读取和转换成 Class ,并存入 cache ,这就是为什么我们修改了 Class 但是必须重新启动 JVM 才能生效的原因。


每个 ClassLoader 加载 Class 的过程是:
1.
检测此 Class 是否载入过(即在 cache 中是否有此 Class ),如果有到 8, 如果没有到 2
2.
如果 parent classloader 不存在(没有 parent ,那 parent 一定是 bootstrap classloader 了),到
4
3.
请求 parent classloader 载入,如果成功到 8 ,不成功到
5
4.
请求 jvm 从 bootstrap classloader 中载入,如果成功到
8
5.
寻找 Class 文件(从与此 classloader 相关的类路径中寻找)。如果找不到则到
7.
6.
从文件中载入 Class ,到
8.
7.
抛出
ClassNotFoundException.
8.
返回
Class.

其中 5.6 步我们可以通过覆盖 ClassLoader 的 findClass 方法来实现自己的载入策略。甚至覆盖 loadClass 方法来实现自己的载入过程。


类加载器的顺序是:
先是 bootstrap classloader ,然后是 extension classloader ,最后才是 system classloader 。大家会发现加载的 Class 越是重要的越在靠前面。这样做的原因是出于安全性的考虑,试想如果 system classloader“ 亲自 ” 加载了一个具有破坏性的 “java.lang.System” 类的后果吧。这种委托机制保证了用户即使具有一个这样的类,也把它加入到了类路径中,但是它永远不会被载入,因为这个类总是由 bootstrap classloader 来加载的。大家可以执行一下以下的代码:
    System.out.println(System.class.getClassLoader());
将会看到结果是 null ,这就表明 java.lang.System 是由 bootstrap classloader 加载的,因为 bootstrap classloader 不是一个真正的 ClassLoader 实例,而是由 JVM 实现的,正如前面已经说过的。

下面就让我们来看看 JVM 是如何来为我们来建立类加载器的结构的:
sun.misc.Launcher
,顾名思义,当你执行 java 命令的时候, JVM 会先使用 bootstrap classloader 载入并初始化一个 Launcher ,执行下来代码:
   System.out.println("the Launcher's classloader is "+sun.misc.Launcher.getLauncher().getClass().getClassLoader());
结果为:
   the Launcher's classloader is null (
因为是用 bootstrap classloader 加载 , 所以 class loader 为 null)
Launcher
会根据系统和命令设定初始化好 class loader 结构, JVM 就用它来获得 extension classloader 和 system classloader, 并载入所有的需要载入的 Class ,最后执行 java 命令指定的带有静态的 main 方法的 Class 。 extension classloader 实际上是 sun.misc.Launcher$ExtClassLoader 类的一个实例, system classloader 实际上是 sun.misc.Launcher$AppClassLoader 类的一个实例。并且都是 java.net.URLClassLoader 的子类。


让我们来看看 Launcher 初试化的过程的部分代码。

Launcher
的部分代码:
public class Launcher  {
    public Launcher() {
        ExtClassLoader extclassloader;
        try {
            //
初始化 extension classloader
            extclassloader = ExtClassLoader.getExtClassLoader();
        } catch(IOException ioexception) {
            throw new InternalError("Could not create extension class loader");
        }
        try {
            //
初始化 system classloader , parent 是
extension classloader
            loader = AppClassLoader.getAppClassLoader(extclassloader);
        } catch(IOException ioexception1) {
            throw new InternalError("Could not create application class loader");
        }
        //
将 system classloader 设置成当前线程的 context classloader (将在后面加以介绍)

        Thread.currentThread().setContextClassLoader(loader);
        ......
    }
    public ClassLoader getClassLoader() {
        //
返回 system classloader
        return loader;
    }
}

extension classloader
的部分代码:

static class Launcher$ExtClassLoader extends URLClassLoader {

    public static Launcher$ExtClassLoader getExtClassLoader()
        throws IOException
    {
        File afile[] = getExtDirs();
        return (Launcher$ExtClassLoader)AccessController.doPrivileged(new Launcher$1(afile));
    }
   private static File[] getExtDirs() {
        //
获得系统属性 “java.ext.dirs”
        String s = System.getProperty("java.ext.dirs");
        File afile[];
        if(s != null) {
            StringTokenizer stringtokenizer = new StringTokenizer(s, File.pathSeparator);
            int i = stringtokenizer.countTokens();
            afile = new File;
            for(int j = 0; j < i; j++)
                afile[j] = new File(stringtokenizer.nextToken());

        } else {
            afile = new File[0];
        }
        return afile;
    }
}

system classloader
的部分代码:

static class Launcher$AppClassLoader extends URLClassLoader
{

    public static ClassLoader getAppClassLoader(ClassLoader classloader)
        throws IOException
    {
        //
获得系统属性 “java.class.path”
        String s = System.getProperty("java.class.path");
        File afile[] = s != null ? Launcher.access$200(s) : new File[0];
        return (Launcher$AppClassLoader)AccessController.doPrivileged(new Launcher$2(s, afile, classloader));
    }
}

   
看了源代码大家就清楚了吧, extension classloader 是使用系统属性 “java.ext.dirs” 设置类搜索路径的,并且没有 parent 。 system classloader 是使用系统属性 “java.class.path” 设置类搜索路径的,并且有一个 parent classloader 。 Launcher 初始化 extension classloader , system classloader ,并将 system classloader 设置成为 context classloader ,但是仅仅返回 system classloader 给 JVM 。


  这里怎么又出来一个 context classloader 呢?它有什么用呢?我们在建立一个线程 Thread 的时候,可以为这个线程通过 setContextClassLoader 方法来指定一个合适的 classloader 作为这个线程的 context classloader ,当此线程运行的时候,我们可以通过 getContextClassLoader 方法来获得此 context classloader ,就可以用它来载入我们所需要的 Class 。默认的是 system classloader 。利用这个特性,我们可以 “ 打破 ”classloader 委托机制了,父 classloader 可以获得当前线程的 context classloader ,而这个 context classloader 可以是它的子 classloader 或者其他的 classloader ,那么父 classloader 就可以从其获得所需的 Class ,这就打破了只能向父 classloader 请求的限制了。这个机制可以满足当我们的 classpath 是在运行时才确定 , 并由定制的 classloader 加载的时候 , 由 system classloader( 即在 jvm classpath 中 ) 加载的 class 可以通过 context classloader 获得定制的 classloader 并加载入特定的 class( 通常是抽象类和接口 , 定制的 classloader 中是其实现 ), 例如 web 应用中的 servlet 就是用这种机制加载的 .

   
好了,现在我们了解了 classloader 的结构和工作原理,那么我们如何实现在运行时的动态载入和更新呢?只要我们能够动态改变类搜索路径和清除 classloader 的 cache 中已经载入的 Class 就行了,有两个方案,一是我们继承一个 classloader ,覆盖 loadclass 方法,动态的寻找 Class 文件并使用 defineClass 方法来;另一个则非常简单实用,只要重新使用一个新的类搜索路径来 new 一个 classloader 就行了,这样即更新了类搜索路径以便来载入新的 Class ,也重新生成了一个空白的 cache( 当然 , 类搜索路径不一定必须更改 ) 。

(结束)

 

以上所述,想必大家对 Jvm 类载入的原理有了一定的了解,大致也猜到实现的方法了吧。

 

3        实现类的动态载入

结合上面说到,要 JVM 重新载入一个类,一共有 3 种办法:(我加了一种)

1.         重新写一个新的路径,更换包名或类名都是可行的方法。

2.         自己写一个类的寻找机制取代 SystemClassLoader 中的,再调用 defineClass 方法。

3.         覆盖 loadClass 方法,自己实现一个类载入过程。

 

显而易见,第一种方法是我自己添加上去的,而且应该是用得最多得方法,因为在对 JVM 的类搜索、载入机制不是很熟悉的前提下,只要做一些简单的修改就能达到所要的效果。(至少在我的项目中,这是首选,因为不会出错)

但如果第一种方法能实现,本文就没有写的意义了。在一个 7 x 24 的大型系统中(如电信,银行等),用第一种方法,就凸现出它最大问题,就是代码变得无比庞大。因为每一个类的名字不同导致相同功能的类具有两个或以上的类文件存在,对代码的开发和维护来说,这是非常严重的问题。

好,现在又退回到开始,排除掉第一种方法,如果使用第三种方法,应该是可控性最高的方法,但本人才疏学浅,没有把握把整个类载入过程摸透,因此还是排除的第三种方法。

剩下只有第二种方法了。在 ClassLoader 中, defineClass 方法是比较好理解的,只要传入类的字节流即可,因此,我只要重写一次类文件的载入。下面是我的实现代码:

public class testClassLoader extends ClassLoader {

    private static testClassLoader cl = null;

    private static boolean flag = true;

    private InputStream classFile = null;

    private String name = null;

  

    /**

     * @param name String  类全名

     * @param url URL  类路径

     * @throws FileNotFoundException

     * @throws IOException

     */

    public testClassLoader(String name, URL url) throws

            FileNotFoundException, IOException {

        super(getSystemClassLoader());

        this.name = name + ".class";

 

        // 打开 URL 指定的资源

        URLConnection con = url.openConnection();

        InputStream classIs = con.getInputStream();

        this.classFile = classIs;

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        byte buf[] = new byte[1024];

        // 读取文件流

        for (int i = 0; (i = classIs.read(buf)) != -1; ) {

            baos.write(buf, 0, i);

        }

        classIs.close();

        baos.close();

 

        // 创建新的类对象

        byte[] data = baos.toByteArray();

        defineClass(name, data, 0, data.length);

    }

 

    /**

     * 重载 getResourceAsStream() 是为了返回该类的文件流。

     * @return an InputStream of the class bytes, or null

     */

    public InputStream getResourceAsStream(String resourceName) {

        try {

            if (resourceName.equals(name)) {

                return this.classFile;

            }

        } catch (Exception e) {

            return null;

        }

        return null;

    }

}

       相信大家已经明白了吧。我在 ClassLoader 构造时便载入了指定的类文件,因此,就跳过了在 new 新的对象时去查找 Cache 中该类是否有载入的过程。(注明一下:我的类路径用了 URL 的方式传入,目的是为了适应多平台的机制,因为 Window 和 Linux 的路径命名不一样)

相信聪明的你一定猜到后面应该怎么做了吧,没错,就是用 Class.forName 构建类:

String name = "test.testBlank";

       URL url = new URL("file:/c:/test/classes/test/testBlank.class");

ClassLoader cl = new testClassLoader(name, url);

Class c = Class.forName(name, false, cl);

// 实例化

Object obj = c.newInstance();

testInterface i = (testInterface) obj;

i.run();

 

       你一定注意到了,为什么我用了一个 testInterface ,其实自定义 ClassLoader 中,不能直接以当前的类去直接强制转换,因为在 JVM 中,不同的 ClassLoader 载入同一个类,在 JVM 中是两个完全不同的类。因此,默认的 class 文件都是 SystemClassLoader ,所以会抛出 ClassCastException 。解决方法很简单,只要该对象实现一个接口,然后用父类去强制转换就没有问题了。

 

       以上只是我在学习过程中的一点积累,如果有什么错误,欢迎大家评论。

 

posted on 2006-04-25 11:25 大牙 阅读(779) 评论(0)  编辑  收藏 所属分类: 架构师历程