欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

走出类加载器迷宫

程序员文章站 2022-03-07 10:05:24
...

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