`
Virgo_S
  • 浏览: 1150976 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

JAVA 类动态载入的实现【转】

    博客分类:
  • JAVA
阅读更多
转:
http://www.blogjava.net/bigbigtooth/articles/42972.html

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 。解决方法很简单,只要该对象实现一个接口,然后用父类去强制转换就没有问题了。









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


分享到:
评论

相关推荐

    Java技术----实现JAVA的动态类载入机制

    反射API是实现动态类加载的关键工具。`java.lang.reflect`包提供了`Class`类、`Constructor`类、`Method`类和`Field`类等,它们分别代表Java类、构造器、方法和字段。通过`Class.forName()`可以获取到一个类的`Class...

    Java动态生成代码并编译载入

    比如,当需要实现动态配置的业务规则时,可以通过这种方式将用户定义的规则转换为Java代码并加载执行。再比如,某些性能敏感的系统可能使用这种方法来即时生成和优化特定场景的代码,以达到更高的运行效率。 总的来...

    基于java后端的krpano实现

    在这个项目中,Java可能被用来处理用户请求,生成动态全景图内容,或者执行特定的操作,如添加背景音乐、更新加载进度条等。这可能涉及到Spring框架、Servlets或JSP等技术,用于构建RESTful API接口,使前端与后端...

    JAVA五子棋简单实现

    【JAVA五子棋简单实现】是一个适合初学者的项目,旨在通过编程实现一个基本的五子棋游戏,以此巩固和加深对Java基础知识的理解。在这个项目中,开发者将学习到如何运用SWF(Simple Widget Framework)框架来构建用户...

    Java虚拟机模拟实现

    在深入理解Java虚拟机模拟实现的过程中,我们需要掌握以下几个关键知识点: 1. **字节码与类加载机制**:Java源代码被编译成.class文件,这些文件包含字节码,这是JVM能够理解和执行的二进制指令。类加载器是JVM的...

    worldwind java 3d模型载入源代码

    为了提高性能,WorldWind Java支持LOD(Level of Detail)技术,根据模型与相机的距离动态调整细节级别。源代码可能会包含计算和切换LOD级别的逻辑。 7. **交互与事件处理**: 可能还会涉及到用户交互,如鼠标...

    java 载入dll之后无法切换输入法测试工程(My Eclipse)

    总的来说,解决"java 载入dll之后无法切换输入法"的问题需要对Java、JNI、DLL和Windows操作系统有深入的理解,同时需要具备良好的调试技巧。通过仔细分析代码、调试和日志记录,通常能找到问题的根源并提出解决方案...

    AES256加密工具类,及其所必须的jar包

    在这个资源中,`AES256Util.java` 是一个实现了AES256加密和解密功能的Java类。通常,这个工具类会包含以下方法: 1. `encrypt()`:用于将明文数据加密为密文。它接受明文字符串和密钥作为输入,返回加密后的字节...

    深入类别载入器快速下载

    总结,深入理解Java的类别载入器不仅可以帮助我们更好地理解JVM的工作机制,还能在处理类冲突、优化性能、实现热部署等高级应用场景中提供有力支持。在日常开发中,我们应该充分利用类加载器的特性,提高代码的可...

    java实现愤怒的小鸟游戏.zip

    基于Java的愤怒的小鸟游戏的设计与实现,基本功能包括:新游戏、载入游戏、控制帮助、退出游戏等。本系统结构如下: (1)新游戏: 需要输入你的昵称; 选择难度:容易、中等、困难、噩梦(每个级别都有5个关卡) ...

    深入类别载入器

    在Java中,这种特性主要通过类加载器(Class Loader)实现。 #### 二、传统语言中的动词性实现 大多数编程语言并不天然具备动词性,如C、C++等。为了实现这种特性,程序员通常需要依赖底层操作系统提供的机制,...

    java实现英文翻译程序

    "java实现英文翻译程序" 本文将详细介绍了使用 Java 实现英文翻译程序的知识点,包括程序的架构、技术要点和代码实现等方面的内容。 标题:java 实现英文翻译程序 描述:主要为大家详细介绍了 java 实现英文翻译...

    Applet鸭子的移动

    1. Java源代码文件(.java):实现Applet类,包括鸭子的移动逻辑和双缓存的使用。 2. 图片资源文件(.gif或.png):用于表示鸭子的图形。 3. HTML文件(.html):包含Applet的嵌入代码,用于在浏览器中展示Applet。 ...

    Java类加载器:静态变量初始化.docx

    在 Java 中,类加载器是通过委派机制来实现的,即一个类加载器可以委派另一个类加载器来加载类。这篇文章将深入探讨 Java 类加载器中的静态变量初始化机制,了解其背后的工作原理和载入过程。 静态变量初始化机制 -...

    Jave深度历险(CH_02深入类别载入器)

    在《Java深度历险》这本书的第二章中,作者深入探讨了类加载器在实现Java程序动态性中的核心作用。 #### 类加载器的动态性 Java语言天生具备动态性,这意味着开发者无需依赖底层操作系统的特定机制(如动态链接库...

    04747Java程序设计.pdf

    5. **Java程序文件与字节码**:Java源程序文件的扩展名为.java,经过编译后会生成以.class为扩展名的字节码文件,由JVM负责载入和执行。 6. **API文档**:Java的API文档可以在线查阅或下载本地使用,为开发者提供...

    Java实现的简单音乐播放器功能示例

    本文主要介绍了Java实现的简单音乐播放器功能,涉及java针对多媒体文件相关载入、播放相关操作技巧。下面将详细讲解该示例中的知识点。 一、Java GUI编程 在本示例中,我们使用了Java的GUI编程来实现音乐播放器的...

    java深度历险 详细讲解了java的package机制等

    开发者还可以自定义类加载器,以实现特定的加载逻辑,如从网络或数据库加载类。 总之,`package`和`import`机制为Java程序提供了良好的结构和可维护性,Visual Studio .NET提供了与JVM交互的便捷方式,而Java 2 SDK...

Global site tag (gtag.js) - Google Analytics