翻译:走出类加载器迷宫

这是前几天在看类加载器机制时搜到的一篇旧文,网上搜了搜相应的中文资料,感觉很多意思没有翻译出来,这两天我试着自己翻译了一下,供同道参考。英文文章地址:Find a way out of the ClassLoader maze

走出类加载器迷宫(本人翻译,转载请注明出处)

系统类加载器当前类加载器上下文类加载器你应该用哪一个?

By Vladimir Roubtsov, JavaWorld.com, 06/06/03

June 6, 2003

Q:我什么时候Thread.getContextClassLoader()?

A:这个问题虽然不常见,却很难正确回答。它一般出现在框架编程中,作为解决类和资源动态加载的一个好方法。总的来说,当动态加载一个资源时,至少有三种类加载器可供选择: 系统类加载器(也被称为应用类加载器)(system classloader),当前类加载器current classloader),和当前线程的上下文类加载器( the current thread context classloader)。上面提到的问题指的是最后一种加载器。哪一个类加载器是正确的?

容易排除的一个选择:系统类加载器。这个类加载器处理classpath环境变量所指定的路径下的类和资源,可以通过ClassLoader.getSystemClassLoader()方法以编程式访问。所有的ClassLoader.getSystemXXX()API方法也是通过这个类加载器访问。你应该很少写代码来显式调用,而是以其它的类加载器委托给系统类加载器来代替。否则,当系统类加载器是JVM创建的最后一个类加载器时你的代码将只能工作在简单的命令行应用中。只要你把代码迁移到EJB,web应用,或Java Web Start应用中肯定会出问题。

所以,现在我们是两个选择:当前类加载器上下文类加载器。根据定义,当前类加载器加载和定义当前方法所属的那个类。这个类加载器在你使用带单个参数的Class.forName()方法,Class.getResource()方法和相似方法时会在运行时类的链接过程中被隐式调用。它也出现在像X.class语法的字母调用中。(参见"Get a Load of That Name!"获取详细信息)

线程上下文类加载器是在J2SE中被引进的。每一个线程分配一个上下文类加载器(除非线程由本地代码创建)。该加载器是通过Thread.setContextClassLoader()方法来设置。如果你在线程构造后不调用这个方法,这个线程将会从它的父线程(译者注:这里的父线程是指执行创建新线程对象语句的线程)中继承上下文类加载器。如果你在整个应用中不做任何设置,所有线程将以系统类加载器作为它们自己的上下文加载器。重要的是明白自从Web和J2EE应用服务器为了像JNDI,线程池,组件热部署等特性而采用复杂的类加载器层次结构后,这(译者注:指整个应用中不做任何设置)是很少见的情况。

为什么线程上下文类加载器在第一位?它们被介绍进J2SE时并没有大张旗鼓。从Sun公司缺乏正确的指导和文档或许解释了为什么许多开发人员会对此概念困惑。

事实上,上下文类加载器提供了一个后门绕过在J2SE中介绍的类的加载委托机制。通常情况下,一个JVM中的所有类加载器被组织成一个层次结构,使得每一个类加载器(除了启动整个JVM的原始类加载器)都有一个父加载器。当被要求加载一个类时,每一个类加载器都将先委托父加载器来加载,只有父加载器都不能成功加载时当前类加载器才会加载。

有时这种加载顺序不能正常工作,通常发生在有些JVM核心代码必须动态加载由应用程序开发人员提供的资源时。以JNDI举例:它的核心内容(从J2SE1.3开始)在rt.jar中的引导类中实现了,但是这些JNDI核心类可能加载由独立厂商实现和部署在应用程序的classpath中的JNDI提供者。这个场景要求一个父类加载器(这个例子中的原始类加载器,即加载rt.jar的加载器)去加载一个在它的子类加载器(系统类加载器)中可见的类。此时通常的J2SE委托机制不能工作,解决办法是让JNDI核心类使用线程上下文加载器,从而有效建立一条与类加载器层次结构相反方向的“通道”达到正确的委托。

另外,上段可能提醒你别的事情:用作XML解析的Java API(JAXP)。是的,当JAXP只是J2SE的扩展时,XML解析工厂使用当前类加载器作为启动解析器的实现。当JAXP作为J2SE1.4核心的一部分时,类加载改为使用线程上下文类加载器,JNDI情况完全类似(使很多程序员困惑)。明白我说缺少来自Sun的指导的意思了吗?

在这些介绍后,来看看问题的症结:剩下的两个选择都不是在任何情况下都正确的。有人认为线程类加载器应该编程新的标准方案。然而,如果多个JVM线程通过共享数据通信时这将造成一个非常混乱的类加载图景,除非他们都使用同一个上下文加载器实例。还有,委托给当前类加载器已经是一个旧规则存在于像class字面调用(即X.class)或显式调用Class.forName()的情况中(这是为什么,顺便说下,我建议避免使用这个方法的带有一个参数的版本)。即使你做出努力尽最大程度明确只使用上下文加载器,总是会有一些代码不在你的控制之下而是委托给当前加载器。这种不受控制的混合委托策略听起来相当危险。

更糟糕的是,某些应用服务器设置上下文和当前类加载器为不同的加载器实例,使得有相同类路径但却没有委派机制中的父子关系。花一秒钟想想为什么这是特别可怕的。记住类加载器加载和定义一个类会有一个JVM内部的ID。如果当前类加载器加载一个类X,然后要执行一个JNDI查找Y类的某些信息,上下文类加载器可能加载Y类。这个Y类实例将不同于在当前类加载器中同名并可见的类实例。强行类型转换时将会出现加载违反约束异常。

这种混乱将可能在Java中继续存在一段时间。拿任意一个带有任何形式的动态资源加载的J2SE API,并试着猜猜使用哪个加载策略。这里是一个样例:

  • JNDI使用上下文类加载器
  • Class.getResource()和 Class.forName() 使用当前类加载器
  • JAXP 使用上下文类加载器 (截至 J2SE 1.4)
  • java.util.ResourceBundle 使用调用的当前类加载器
  • 通过java.protocol.handler.pkgs系统属性指定的URL协议处理器只在引导类加载器和系统类加载器中查询
  • Java序列化API缺省使用调用者的当前类加载器

这些类和资源加载策略肯定是J2SE中的最不良记录。

一个Java程序员要做什么?

如果你的实现被限定于一个确定的有明确资源加载规则的框架,坚持他们。我们希望,使它们工作的负担在实现框架的人上(如应用服务器厂商,尽管他们并不总是正确的)。例如,在一个Web应用或EJB中,只要使用Class.getResource()。

在别的情况下,你可能会考虑使用一个解决方案,我发现在个人工作中很有用。下面的类作为一个全局决策点,用于获取应用程序任何给定时间中最佳的类加载器(所有的示例代码可以从download下载):

public abstract class ClassLoaderResolver
    {
        /**
         * 这个方法提供给调用此方法的人选择用于类/资源加载的最佳类加载器的实例。
         * 通常涉及JVM中调用者当前类加载器、线程上下文类加载器、系统类加载器和其他类
         * 加载器之间的选择。该加载器实例由setStrategy方法设置的IClassLoadStrategy的
         * 实例提供。
         *
         * @返回类加载器实例给调用者 [返回null表示JVM的启动类加载器]
         */
        public static synchronized ClassLoader getClassLoader ()
        {
            final Class caller = getCallerClass (0);
            final ClassLoadContext ctx = new ClassLoadContext (caller); 

            return s_strategy.getClassLoader (ctx);
        }
        public static synchronized IClassLoadStrategy getStrategy ()
        {
            return s_strategy;
        }
        public static synchronized IClassLoadStrategy setStrategy (final IClassLoadStrategy strategy)
        {
            final IClassLoadStrategy old = s_strategy;
            s_strategy = strategy; 

            return old;
        } 

        /**
         * 一个获取调用者上下文的帮助类。getClassContext()方法对
         * SecurityManager子类可见。只需要创建一个CallerResolver类的实例
         * 不必安装一个实际的安全管理器
         */
        private static final class CallerResolver extends SecurityManager
        {
            protected Class [] getClassContext ()
            {
                return super.getClassContext ();
            } 

        } // 嵌套类结束

        /*
         * 获取指定偏移量位置的当前方法调用者上下文
         */
        private static Class getCallerClass (final int callerOffset)
        {
            return CALLER_RESOLVER.getClassContext () [CALL_CONTEXT_OFFSET +
                callerOffset];
        } 

        private static IClassLoadStrategy s_strategy; //类装载时初始化(见下面的静态语句块)

        private static final int CALL_CONTEXT_OFFSET = 3; // 如果这个类重新设计时可能需要改变这个值
        private static final CallerResolver CALLER_RESOLVER; // 类装载时初始化(见下面的静态语句块)

        static
        {
            try
            {
                //如果当前安全管理器没有("createSecurityManager")运行时权限则可能会失败: 

                CALLER_RESOLVER = new CallerResolver ();
            }
            catch (SecurityException se)
            {
                throw new RuntimeException ("ClassLoaderResolver: could not create CallerResolver: " + se);
            } 

            s_strategy = new DefaultClassLoadStrategy ();
        }
}   // 类定义结束

通过ClassLoaderResolver.getClassLoader()静态方法获得一个类加载器的引用,可以用这个结果通过一般的类加载器API加载类和资源。另外,你可以用ResourceLoader作为类加载器的简易替换:

public abstract class ResourceLoader
{
    /**
     * @see java.lang.ClassLoader#loadClass(java.lang.String)
     */
    public static Class loadClass (final String name)
        throws ClassNotFoundException
    {
        final ClassLoader loader = ClassLoaderResolver.getClassLoader (1);  

            return Class.forName (name, false, loader);
        }
        /**
         * @see java.lang.ClassLoader#getResource(java.lang.String)
         */
        public static URL getResource (final String name)
        {
            final ClassLoader loader = ClassLoaderResolver.getClassLoader (1);  

            if (loader != null)
                return loader.getResource (name);
            else
                return ClassLoader.getSystemResource (name);
        }
        ... more methods ...
    } // 类定义结束

决定使用何种类加载器的策略由IClassLoadStrategy 接口实现的,这是一个可插拔的组件:

public interface IClassLoadStrategy
{
    ClassLoader getClassLoader (ClassLoadContext ctx);
} // 接口定义结束

为了帮助IClassLoadStrategy 做决定,需要传入一个ClassLoadContext 对象:

public class ClassLoadContext
{
    public final Class getCallerClass ()
    {
        return m_caller;
    }  

    ClassLoadContext (final Class caller)
    {
            m_caller = caller;
        }  

        private final Class m_caller;
} // 类定义结束

ClassLoadContext.getCallerClass()返回类给ClassLoaderResolver或ResourceLoader使用。以便实现策略可以返回调用者的类加载器(上下文加载器总是可以通过Thread.currentThread().getContextClassLoader()来获取)。需要注意的是调用者是不可变的,因此,我的API不需要现有业务方法增加额外的Class 参数,同样也可用于静态方法和初始化方法。你可以根据你的部署情况添加其它属性扩展这个context对象。

所有这些看起像设计模式中的策略模式。核心思想是将“使用上下文类加载器”和“使用当前类加载器”的决策同你的其它具体实现逻辑分开。我们很难提前预知哪个策略是正确的,这种设计,你可以随时改变策略。

我有一个默认策略实现可以在现实工作95%的情况下正确工作:

public class DefaultClassLoadStrategy implements IClassLoadStrategy
{
    public ClassLoader getClassLoader (final ClassLoadContext ctx)
    {
        final ClassLoader callerLoader = ctx.getCallerClass ().getClassLoader ();
        final ClassLoader contextLoader = Thread.currentThread ().getContextClassLoader ();  

        ClassLoader result;  

            // 如果调用者加载器和上下文加载器是父子关系,则一直选择子加载器:  

            if (isChild (contextLoader, callerLoader))
                result = callerLoader;
            else if (isChild (callerLoader, contextLoader))
                result = contextLoader;
            else
            {
                // else分支可以被合并到前一个,单独列出来是要强调在模棱两可的情况下:
                result = contextLoader;
            }  

            final ClassLoader systemLoader = ClassLoader.getSystemClassLoader ();  

            // 部署时作为启动类或启动扩展类的注意事项:
            if (isChild (result, systemLoader))
                result = systemLoader;  

            return result;
        }  

        ... more methods ...
} // 类定义结束

上面的逻辑理解起来很简单。如果调用者当前加载器和上下文加载器是父子关系,则一直选择子类加载器。子类加载器可见的资源通常也是父类加载器可见的,只要遵循J2SE的代理委托规则,大部分情况下就是正确的策略。

当前加载器和上下文加载器不是父子关系时不可能给出正确的策略。理想情况下,Java运行时不应该允许这种模棱两可的状况。一旦出现这种情形,我的代码就选择上下文加载器:这个策略基于本人大部分时间正确工作的经验。你可以根据需要修改代码。上下文加载器可能是框架组件中更好的选择,当前加载器可能是业务逻辑中更好的选择。

最后,一个简单的检查保证所选的类加载器不是系统类加载器的父加载器。如果你正在编写的代码可能部署为标准扩展库时这是个好习惯。

请注意我故意没检查资源或被加载的类的名称。如果不出意外,将变成J2SE核心的一部分的Java XML API的经验告诉你根据类名过滤不是一个好主意。我也没试验类的加载看看哪个加载器先成功加载。从根本上说检查类加载器的父子关系是一个更好和更可预测的方法。

虽然Java资源加载仍然是一个深奥的话题,随着版本的升级J2SE越来越多的依赖于各种加载策略。如果这块不给出一些有显著改进的设计方案Java将有很大的麻烦。不管你赞同还是不赞同,非常感谢你的反馈和来自个人设计经验的指正。

关于作者

Vladimir Roubtsov拥有超过13年的各种语言编程经验,1995年开始使用Java。现在,他作为Trilogy公司的高级工程师开发企业应用软件。

时间: 2024-08-09 02:19:09

翻译:走出类加载器迷宫的相关文章

<20>【掌握】《走出迷宫》游戏代码实现+【理解】《走出迷宫》游戏优化

#include <stdio.h> #define COL 6 #define ROW 6 int main(int argc, const char * argv[]) { //****** 定义变量 ********** //1.定义变量,地图.存储用户输入的方向.小人的位置     char map[ROW][COL]={         {'#','#','#','#','#','#'},         {'#','O','#','#',' ',' '},         {'#'

走出迷宫, 你适合什么职业?

[走出迷宫, 你适合什么职业? ] 终点A的人适合职业: police.教练.作家. 终点B的人适合职业: 漫画家.会计.导演.设计师. 终点C的人适合职业: 领导.律师.指挥. 终点D的人适合职业: 医生.教师.歌手.记者.工人. 终点E的人适合职业: 演员.司机.商人.基层管理人员. 我走出来是 C...可是我是老师... 走出迷宫, 你适合什么职业?

1254:走出迷宫

题目来源:http://ybt.ssoier.cn:8088/problem_show.php?pid=1254 1254:走出迷宫 时间限制: 1000 ms         内存限制: 65536 KB提交数: 2502     通过数: 1154 [题目描述] 当你站在一个迷宫里的时候,往往会被错综复杂的道路弄得失去方向感,如果你能得到迷宫地图,事情就会变得非常简单. 假设你已经得到了一个n*m的迷宫的图纸,请你找出从起点到出口的最短路. [输入] 第一行是两个整数n和m(1≤n,m≤10

深入理解JVM虚拟机7:JNDI,OSGI,Tomcat类加载器实现

打破双亲委派模型 JNDI JNDI 的理解 JNDI是 Java 命名与文件夹接口(Java Naming and Directory Interface),在J2EE规范中是重要的规范之中的一个,不少专家觉得,没有透彻理解JNDI的意义和作用,就没有真正掌握J2EE特别是EJB的知识. 那么,JNDI究竟起什么作用?//带着问题看文章是最有效的 要了解JNDI的作用,我们能够从“假设不用JNDI我们如何做?用了JNDI后我们又将如何做?”这个问题来探讨. 没有JNDI的做法: 程序猿开发时,

一篇文章读懂Java类加载器

Java类加载器算是一个老生常谈的问题,大多Java工程师也都对其中的知识点倒背如流,最近在看源码的时候发现有一些细节的地方理解还是比较模糊,正好写一篇文章梳理一下. 关于Java类加载器的知识,网上一搜一大片,我自己也看过很多文档,博客.资料虽然很多,但还是希望通过本文尽量写出一些自己的理解,自己的东西.如果只是重复别人写的内容那就失去写作的意义了. 类加载器结构 名称解释: 根类加载器,也叫引导类加载器.启动类加载器.由于它不属于Java类库,这里就不说它对应的类名了,很多人喜欢称Boots

框架学习前基础加强 泛型,注解,反射(泛型&注解)应用案例,IOC,Servlet3.0,动态代理,类加载器

泛型 1. 泛型类 :具有一个或多个类型变量的类,称之为泛型类! class A<T> { } 2. 在创建泛型类实例时,需要为其类型变量赋值 A<String> a = new A<String>(); * 如果创建实例时,不给类型变量赋值,那么会有一个警告! 3. 泛型方法 :具有一个或多个类型变量的方法,称之为泛型方法! class A<T> { public T fun(T t1) {} } fun()方法不是泛型方法!它是泛型类中的一个方法! pu

Java类加载器深入解析(二)

在做Java开发时了解Java类加载机制是非常好的.而对类加载机制的基本理解对Java开发人员处理类加载器(ClassLoader)相关的异常也很有帮助. 类加载器委托机制 Java类的装载是通过类加载器(CL)来完成的,这些类加载器负责将类加载到JVM中.简单的应用可以使用java平台自带的类加载器来加载自身的类,而稍微复杂一些的应用则倾向于自定义类加载来加载自身的类. 在java中类加载器是以树状结构组织的.通过请求一个类加载器并通过在其缓存中查找来确定某个类是否已被加载.如果在缓存中已经有

Android插件化探索(一)类加载器DexClassLoader

本文部分内容参考自<Android内核剖析> 基本概念 在Java环境中,有个概念叫做"类加载器"(ClassLoader),其作用是动态装载Class文件.标准的Java SDK中有一个ClassLoader类,借助它可以装载想要的Class文件,每个ClassLoader对象在初始化时必须指定Class文件的路径 没有使用过ClassLoader的读者可能会问:"在过去的程序开发中,当我们需要某个类时,只需使用import关键字包含该类就可以了,为什么还要类加

JVM类加载器及Java类的生命周期

预定义类加载器(三种): 启动(Bootstrap)类加载器: 是用本地代码实现的类装入器,它负责将<Java_Runtime_Home>/lib下面的类库加载到内存中(比如rt.jar).由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作.扩展扩展(Extension)类加载器: 是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的.它负责将< Java_R