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

走出类加载器迷宫

阅读更多

这是前几天在看类加载器机制时搜到的一篇旧文,网上搜了搜相应的中文资料,感觉很多意思没有翻译出来,这两天我试着自己翻译了一下,供同道参考。英文文章地址: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公司的高级工程师开发企业应用软件。

6
2
分享到:
评论
1 楼 lengyun3566 2013-10-16  
这篇文章很好,早就读过
感谢翻译

相关推荐

    Java课程设计走迷宫.zip_Java课程设计走迷宫_java课程设计_paymx9_迷宫 java_迷宫Java设计

    在走迷宫游戏中,这可能涉及处理无效的用户输入、资源加载失败等。 8. **文件操作**:为了保存和加载游戏进度,可以学习如何在Java中读写文件,例如使用File、FileInputStream和FileOutputStream等类。 9. **测试...

    森林冰火人之走迷宫

    开发过程中,开发者会使用Java的调试工具如JDB或者IDE的内置调试器来找出并修复错误。同时,通过优化代码和资源管理,提高游戏的运行效率。 总的来说,《森林冰火人之走迷宫》不仅是一场迷宫冒险之旅,更是一次...

    珠子走出迷宫HTML5小游戏.zip

    《珠子走出迷宫HTML5小游戏》是一款基于Web技术实现的互动娱乐项目,它充分利用了HTML5、CSS、JavaScript以及jQuery等技术栈,为用户提供了一种在浏览器中体验的趣味迷宫游戏。这款游戏的设计和开发充分展示了现代...

    数据结构课程设计---迷宫的源代码

    代码中提到了`SaveMaze`和`LoadMaze`函数,它们分别用于保存和加载迷宫数据到文件。这涉及到了文件的读写操作,即如何将程序中的数据持久化到磁盘,以及如何从磁盘中恢复数据供程序使用。 #### 2. 错误处理 `Error`...

    Swing写的老鼠走迷宫游戏,mvc含最佳路径算法

    标题中的“Swing写的老鼠走迷宫游戏,mvc含最佳路径算法”表明这是一个使用Java Swing库开发的桌面应用程序,其设计模式遵循Model-View-Controller(MVC)。MVC是一种将软件的业务逻辑、用户界面和数据访问分离开来...

    走迷宫算法mvc++6.0win32应用程序

    总之,"走迷宫算法mvc++6.0win32应用程序"是一个结合了算法与图形用户界面的项目,它展示了如何在C++环境下用MFC实现一个交互式的迷宫解决器。通过这个项目,开发者不仅可以学习到迷宫算法的实现,还能加深对Win32...

    基于Java的迷宫程序(带界面)

    错误处理确保了在遇到意外情况时,程序能够优雅地处理并给出反馈,而状态管理则保证了用户在操作过程中的体验,如保存和加载游戏进度。 在【maze2】这个文件中,可能包含了项目的源代码文件、资源文件、配置文件等...

    java课设走迷宫(含代码).pdf

    异常处理机制确保了只有有效的迷宫文件才能被加载,否则会提示错误信息。 4. **道路和障碍设计模块**: 此模块负责创建和展示迷宫地图中的道路和障碍物,允许玩家自定义图像,以增加游戏的个性化体验。 5. **动漫...

    c语言游戏迷宫

    8. **错误处理(Error Handling)**:对于超出步数限制或无法解出迷宫的情况,程序需要有适当的错误处理机制。 9. **用户输入(User Input)**:玩家的移动决策需要通过键盘输入捕获,这通常涉及到标准输入流...

    基于 Python + Pygame + AI算法的迷宫小游戏.7z

    这些文件可能包含以下部分:主程序文件(初始化游戏环境,处理用户输入,控制游戏流程)、迷宫生成模块(生成随机或预定义的迷宫地图)、AI模块(实现搜索算法)、游戏对象类(如玩家、AI玩家、墙壁等的定义)以及...

    用JavaScript canvas做的走迷宫游戏,肝了一下午,请帮忙点个赞!

    在这款使用JavaScript和canvas制作的走迷宫游戏中,开发者通过编程技术创造了一个互动式的迷宫探索体验。canvas是HTML5中的一个核心元素,它提供了一个二维的画布,允许开发者通过JavaScript来绘制图形、动画,甚至...

    MazeGame:在UE4中制作的关于迷宫的游戏

    在这个名为"MazeGame"的项目中,我们将深入探讨如何利用UE4和C++技术来构建一个迷宫探索类游戏。 首先,我们要理解C++在UE4中的重要性。C++是UE4的主要编程语言,提供了底层控制和高性能的优势。在创建MazeGame时,...

    汇编语言写的改编打字游戏

    .exe文件是Windows操作系统中的可执行文件格式,它包含了编译后的机器码以及必要的元数据,使得操作系统可以加载并执行程序。在这个打字游戏中,.exe文件就是汇编语言源码经过汇编和链接后生成的可执行文件,用户...

    Q_learning代码实例

    在这个"Q_learning代码实例"中,我们可以看到如何用Python实现Q学习算法来解决小方块走迷宫的问题。 首先,我们来看`maze_env.py`文件。这个文件通常包含了环境的定义,即迷宫的构建和小方块的移动规则。环境类可能...

    MarioMaze

    在这款游戏中,玩家可以扮演经典角色马里奥,穿越各种复杂地形,解决谜题,最终成功走出迷宫。下面我们将详细讨论这个项目中涉及的Java编程知识点。 首先,Java是这个项目的基础语言,它是一种跨平台的面向对象编程...

    第二章IEEE电脑鼠原理与技术

    根据提供的文件信息,我们可以总结出电脑鼠是一种集成了机电一体化知识的智能机器人,它能够在特定的迷宫环境中进行自主导航与探索。为了在比赛中获胜,电脑鼠必须具备以下三种基本能力: 1. **感知能力**:通过...

    asuro英文版说明书

    这种编程方式使得ASURO能够执行多种任务,如线性追踪、光源追踪、自主导航、电脑遥控、RC5 TV遥控器控制、唱歌、走迷宫以及避障等。 ASURO的一大特点是其开放源代码政策,这鼓励学生深入理解机器人技术和学习编程...

Global site tag (gtag.js) - Google Analytics