惨淡人生,平淡生活

The Feature Is Stupid

[翻译]走出ClassLoader的迷宫

[说明]几个关键字将不翻译

  1. ClassLoader
  2. System
  3. Context
  4. Thread

走出ClassLoader的迷宫

                                                               System、Current和Context ClassLoader?分别在何种情形下使用?


1、问题:在何种情形下使用thread.getcontextclassloader()?

尽管没经常遇到这个问题,但是想获得准确的答案并不那么容易,特别是在开发应用框架的时候,你需要动态的加载一些类和资源,不可避免的你会被此困扰。一般来说,动态载入资源有三种ClassLoader可以选择,System ClassLoader(也叫App ClassLoader)、当前类的ClassLoader和CurrentThread的Context ClassLoader。那么, 如何选择使用?

首先可以简单排除的是System ClassLoader,这个ClassLoader负责从参数-classpath、-cp、和操作系统CLASSPATH中载入资源。并且,任何ClassLoader的getSystemXXX()方法都是有以上几个路径指定的。我们应该很少需要编写直接使用ClassLoader的程序,否则你的代码将只能在命令行运行,发布你的代码成为ejb、web应用或者java web start应用,我肯定他们会崩溃!

接下来,我们只剩下两个选择了:当前ClassLoader和Thread Context ClassLoader

Current ClassLoader:当前类所属的ClassLoader,在虚拟机中类之间引用,默认就是使用这个ClassLoader。另外,当你使用Class.forName(), Class.getResource()这几个不带ClassLoader参数的方法是,默认同样适用当前类的ClassLoader。你可以通过方法XX.class.GetClassLoader()获取。

Thread Context ClassLoader,没一个Thread有一个相关联系的Context ClassLoader(由native方法建立的除外),可以通过Thread.setContextClassLoader()方法设置。如果你没有主动设置,Thread默认集成Parent Thread的 Context ClassLoader(注意,是parent Thread 不是父类)。如果 你整个应用中都没有对此作任何处理,那么 所有的Thread都会以System ClassLoader作为Context ClassLoader。知道这一点很重要,因为从web服务器,java企业服务器使用一些复杂而且精巧的ClassLoader结构去实现诸如JNDI、线程池和热部署等功能以来,这种简单的情况越发的少见了。

这篇文章中为什么把Thread Context ClassLoader放在首要的位置,别人并没有大张旗鼓的介绍它?很多开发者都对此不甚了解,因为sun没有提供很好的说明文档。

事实上,Context ClassLoader提供一个突破委托代理机制的后门。虚拟机通过父子层次关系组织管理ClassLoader,没有个ClassLoader都有一个Parent ClassLoader(BootStartp不在此范围之内),当要求一个ClassLoader装载一个类是,他首先请求Parent ClassLoader去装载,只有parent ClassLoader装载失败,才会尝试自己装载。

但是,某些时候这种顺序机制会造成困扰,特别是jvm需要动态载入有开发者提供的资源时。就以JNDI为例,JNDI的类是由bootstarp ClassLoader从rt.jar中间载入的,但是JNDI具体的核心驱动是由正式的实现提供的,并且通常会处于-cp参数之下(注:也就是默认的System ClassLoader管理),这就要求bootstartp ClassLoader去载入只有SystemClassLoader可见的类,正常的逻辑就没办法处理。怎么办呢?parent可以通过获得当前调用Thread的方法获得调用线程的Context ClassLoder 来载入类。

顺带补充一句,JAXP从1.4之后也换成了类似JNDI的ClassLoader实现,嘿嘿,刚刚我说什么来着,SUN文档缺乏 ^_^

介绍完这些之后,我们走到的十字路口,任一选择都不是万能的。一些人认为Context ClassLoader将会是新的标准。但是 一旦你的多线程需要通讯某些共享数据,你会发现,你将有一张极其丑陋的ClassLoader分布图,除非所有的线程使用一样的Context ClassLoader。并且委派使用当前ClassLoder对一些方法来说是默认继承来的,比如说Class.forName()。尽管你明确的在任何你能控制的地方使用Context ClassLoader,但是毕竟还有很多代码不归你管(备注:想起一个关于UNIX名字来源的笑话)。

某些应用服务器使用不同的ClassLoder作为Context ClassLoader和当前ClassLoader,并且这些ClassLoader有着相同的ClassPath,但没有父子关系,这使得情况更复杂。请列位看官,花几秒钟时间想一想,为什么这样不好?被载入的类在虚拟机内部有一个全名称,不同的ClassLoader载入的相同名称的类是不一样的,这就隐藏了类型转换错误的隐患。(注:奶奶的 俺就遇到过,JBOSSClassLoader机制蛮挫的)

这种混乱事实上在java类中也有,试着去猜测任何一个包含动态加载的java规范的ClassLoader机制,以下是一个清单:

  • JNDI uses context classloaders
  • Class.getResource() and Class.forName() use the current classloader
  • JAXP uses context classloaders (as of J2SE 1.4)
  • java.util.ResourceBundle uses the caller's current classloader
  • URL protocol handlers specified via java.protocol.handler.pkgs system property are looked up in the bootstrap and system classloaders only
  • Java Serialization API uses the caller's current classloader by default

而且关于这些资源的类加载机制文档时很少。

java开发人员应该怎么做?

如果你的实现是利用特定的框架,那么恭喜你,实现它远比实现框架要简单得多!例如,在web应用和EJB应用中,你仅仅只要使用 Class.getResource()就足够了。

其他的情形下,俺有个建议(这个原则是俺工作中发现的,侵权必究,抵制盗版。),

下面这个类可以在整个应用中的任何地方使用,作为一个全局的ClassLoader(所有的示例代码可以从download下载):

 1 public abstract class ClassLoaderResolver {
 2 /**
 3 * This method selects the best classloader instance to be used for
 4 * class/resource loading by whoever calls this method. The decision
 5 * typically involves choosing between the caller's current, thread context,
 6 * system, and other classloaders in the JVM and is made by the
 7 * {@link IClassLoadStrategy} instance established by the last call to
 8 * {@link #setStrategy}.
 9 *
10 @return classloader to be used by the caller ['null' indicates the
11 * primordial loader]
12 */
13 public static synchronized ClassLoader getClassLoader() {
14 final Class caller = getCallerClass(0);
15 final ClassLoadContext ctx = new ClassLoadContext(caller);
16 
17 return s_strategy.getClassLoader(ctx);
18 }
19 
20 public static synchronized IClassLoadStrategy getStrategy() {
21 return s_strategy;
22 }
23 
24 public static synchronized IClassLoadStrategy setStrategy(
25 final IClassLoadStrategy strategy) {
26 final IClassLoadStrategy old = s_strategy;
27 s_strategy = strategy;
28 
29 return old;
30 }
31 
32 /**
33 * A helper class to get the call context. It subclasses SecurityManager to
34 * make getClassContext() accessible. An instance of CallerResolver only
35 * needs to be created, not installed as an actual security manager.
36 */
37 private static final class CallerResolver extends SecurityManager {
38 protected Class[] getClassContext() {
39 return super.getClassContext();
40 }
41 
42 // End of nested class
43 
44 /*
45 * Indexes into the current method call context with a given offset.
46 */
47 private static Class getCallerClass(final int callerOffset) {
48 return CALLER_RESOLVER.getClassContext()[CALL_CONTEXT_OFFSET
49 + callerOffset];
50 }
51 
52 private static IClassLoadStrategy s_strategy; // initialized in <clinit>
53 
54 private static final int CALL_CONTEXT_OFFSET = 3// may need to change if
55 // this class is
56 // redesigned
57 private static final CallerResolver CALLER_RESOLVER; // set in <clinit>
58 
59 static {
60 try {
61 // This can fail if the current SecurityManager does not allow
62 // RuntimePermission ("createSecurityManager"):
63 
64 CALLER_RESOLVER = new CallerResolver();
65 catch (SecurityException se) {
66 throw new RuntimeException(
67 "ClassLoaderResolver: could not create CallerResolver: "
68 + se);
69 }
70 
71 s_strategy = new DefaultClassLoadStrategy();
72 }
73 // End of class.
74 
75 
76 


通过ClassLoaderResolver.getClassLoader()方法获得一个ClassLoader的引用,并且利用正常的ClassLoader的api去加载资源,你也可以使用 ResourceLoader API作为备选方案

 1 public abstract class ResourceLoader {
 2 
 3 /**
 4  * @see java.lang.ClassLoader#loadClass(java.lang.String)
 5  */
 6 public static Class loadClass (final String name)throws ClassNotFoundException{
 7 
 8 final ClassLoader loader = ClassLoaderResolver.getClassLoader (1);
 9 
10 return Class.forName (name, false, loader);
11 
12 }
13 
14 /**
15 
16 @see java.lang.ClassLoader#getResource(java.lang.String)
17 
18 */    
19 
20 
21 public static URL getResource (final String name){
22 
23 final ClassLoader loader = ClassLoaderResolver.getClassLoader (1);
24 
25 if (loader != null)return loader.getResource (name);
26 else return ClassLoader.getSystemResource (name);
27 }
28  more methods 
29 
30 // End of class

而决定使用何种ClassLoader策略是由接口实现的,这是一种插件机制,方便变更。

public interface IClassLoadStrategy{
ClassLoader getClassLoader (ClassLoadContext ctx);
// End of interface

它需要一个ClassLoader Context 对象去决定使用何种ClassLoader策略。
 1 public class ClassLoadContext{
 2 
 3 public final Class getCallerClass (){
 4 return m_caller;
 5 }
 6 
 7 ClassLoadContext (final Class caller){
 8 m_caller = caller;
 9 
10 }
11 
12 private final Class m_caller;
13 
14 // End of class

ClassLoadContext.getCallerClass()返回调用者给ClassLoaderResolver 或者 ResourceLoader,因此能获得调用者的ClassLoader。需要注意的是,调用者是不会变的 (注:作者使用的final修饰字)。俺的方法不需要对现有的业务方法做扩展,而且可以作为静态方法是用。而且,你可以根据自己的业务场景实现独特的ClassLoaderContext。

看出来没,这是一种很熟悉的设计模式,XD ,把获得ClassLoader的策略从业务中独立出来,这个策略可以是"总是用ContextClassLoader"或者"总是用当前ClassLoader"。想预先知道那种策略是正确的比较困难,那么这种模式可以让你简单的改变策略。

俺写了一个默认的实现,基本可以对付95%的场景(enjoy yourself)

 1 public class DefaultClassLoadStrategy implements IClassLoadStrategy{
 2 
 3 public ClassLoader getClassLoader (final ClassLoadContext ctx){
 4 
 5 final ClassLoader callerLoader = ctx.getCallerClass ().getClassLoader ();
 6 
 7 final ClassLoader contextLoader = Thread.currentThread ().getContextClassLoader ();
 8 
 9 ClassLoader result;
10 // If 'callerLoader' and 'contextLoader' are in a parent-child
11 // relationship, always choose the child:
12 if (isChild (contextLoader, callerLoader))result = callerLoader;
13 else if (isChild (callerLoader, contextLoader))result = contextLoader;
14 else{
15 // This else branch could be merged into the previous one,
16 // but I show it here to emphasize the ambiguous case:
17 result = contextLoader;
18 }
19 final ClassLoader systemLoader = ClassLoader.getSystemClassLoader ();
20 
21 
22 // Precaution for when deployed as a bootstrap or extension class:
23 if (isChild (result, systemLoader))result = systemLoader;
24 return result;
25 }
26 
27 
28 
29  more methods 
30 
31 // End of class
32 


上面的逻辑比较简单,如果当前ClassLoader和Context ClassLoader是父子关系,那就总选儿子,根据委托原则,这个很容易理解。

如果两人平级,选择正确的ClassLoader很重要,运行时不允许含糊。这种情况下,我的代码选择Context ClassLoader(这是俺个人的经验之谈),当然也不要担心不能改变,你能随便根据需要改变。一般而言,Context ClassLoader比较适合框架,而Current ClassLoader在业务逻辑中用的更多。

最后,检查确保选中的ClassLoader不是System ClassLoader的parent,一旦高于System ClassLoader ,请使用System ClassLoader(你的类部署在Ext路径下面,就会出现这种情况)。

请注意,俺故意没关注被载入资源的名称。Java XML API 成为java 核心api的经历告诉我们,根据资源名称过滤是很不cool的idea。而且 我也没有去确认到底哪个ClassLoader被取得了,因为只要清楚原理,这很容易被推理出来。(哈哈,俺是强淫)

尽管讨论java 的ClassLoader不是一个很cool的话题(译者注,当年不cool,但是现在很cool),而且Java EE的ClassLoader策略越发的依赖各种平台的升级。如果这没有一个更好的设计的话,将会变成一个大大的问题。不敢您是否同意俺的观点,俺尊重你说话的权利,所以请给俺分享您的意见经验。

作者介绍:

Vladimir Roubtsov,曾经使用多种语言有超过13年的编程经历(恩 现在应该超过15年了 hoho),95年开始接触java(hoho 俺是99年看的第一本java书)。现在为Trilogy in Austin, Texas开发企业软件。



翻译完了,MMD 翻译还是很麻烦的。 XD ........

43 Things :

posted on 2008-05-30 18:22 季失羽 阅读(5040) 评论(0)  编辑  收藏


只有注册用户登录后才能发表评论。


网站导航: