您的位置:首页 > 其它

走出ClassLoader的迷宫[翻译]

2013-05-06 13:00 281 查看
走出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开发企业软件。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: