类加载器

来源:互联网 发布:工商部门回复网络问政 编辑:程序博客网 时间:2024/06/06 15:41

原文:http://alicharles.com/article/java-classloader/


java类加载器,主要有系统加载器、当前加载器、线程上下文加载器

线程上下文加载器

一般来说,这个问题都来自于框架编程过程中需要动态的加载资源。一般来说,加载一个资源的时候,你会有至少有三个ClassLoader可以用来加载资源,系统ClassLoader(AppClassLoader),当前ClassLoader(可以理解为加载了当前Class的ClassLoader,比如你编写并部署在servlet容器中的程序,它使用WebAppClassLoader)和线程上下文ContextClassLoader。如何去选择使用哪个ClassLoader呢?

首先因该被排除掉的应该是系统ClassLoader,这个就是main的入口,通过载入-classpath的资源来加载类。 这段代码从sun.misc.Launcher.AppClassLoader中摘抄:

public static ClassLoader getAppClassLoader(final ClassLoader extcl) throws IOException {    final String s = System.getProperty("java.class.path");    final File[] path = (s == null) ? new File[0] : getClassPath(s);    return AccessController.doPrivileged(        new PrivilegedAction<AppClassLoader>() {            public AppClassLoader run() {            URL[] urls =                (s == null) ? new URL[0] : pathToURLs(path);            return new AppClassLoader(urls, extcl);        }    });}

其实AppClassLoader继承了URLClassLoader,它所做的工作,就是将java.class.path下的资源,转换为URL,然后加入到AppClassLoader中,除此没有别的特殊的地方。

能够通过静态方法ClassLoader.getSystemClassLoader()来获取到这个AppClassLoader。一般来说非常少的需求,需要获取到AppClassLoader,然后用它来加载一个类,因为都会使用其他的ClassLoader来加载类,并通过委派的方式到达AppClassLoader。

如果你编写的程序运行在最后一个ClassLoader是AppClassLoader的情况下,那么你的程序就只能在命令行下运行,因为你的程序需要依赖均在classpath下设置好,而如果将程序直接部署在WebApp容器中,那么肯定会出问题。

接下来,只有两个选择了,当前ClassLoader和线程上下文ClassLoader,以下简称:CurrentClassLoader和ContextClassLoader。

CurrentClassLoader是当前方法所属的Class,加载这个Class的ClassLoader,这样有些别扭,其实就是如果A类中有方法调用,在方法调用中用到了B,那么加载B的ClassLoader一定是加载A的ClassLoader,那么在加载B的时候,用来加载B的ClassLoader就是CurrentClassLoader。
这里简单介绍一下这个仿佛看不到的CurrentClassLoader是如何出现的。 在如下这段代码中: 

class A {     public void m() {          B b = new B();     }}

B是如何加载的呢?其实等值于A.class.getClassLoader().loadClass(“B”);通过这种方式获取到B的类型。
那么如果是这段代码:

class A {     public void m() {          Class<?> clazz = B.class;     }}

相当于Class.forName(“B”),而Class.forName进入方法后,后续的Class载入会利用Class.class.getClassLoader().loadClass(“B”),也就是利用bootstrap来载入B,但是事实上还是利用载入A的ClassLoader,也就是CurrentClassLoader来载入B,看一下Class.forName的实现:

public static Class<?> forName(String className) throws ClassNotFoundException {    return forName0(className, true, ClassLoader.getClassLoader(Reflection.getCallerClass()));}

Class.forName运作时,通过Reflection.getCallerClass(),能够获取是谁调用了Class.forName,这时Reflection.getClassClass()返回的就是A.class,这样在通过指定ClassLoader来载入B,就符合原有含义了。可以通过观察,通过Java的rt.jar中的API,返回给客户端时,都使用了获取调用者的ClassLoader的特性,因为在rt.jar中,是无法找到自定义类型的。 

通过Reflection.getCallerClass()可以获取到调用Class.forName的类的ClassLoader,从而虽然中间涉及到了bootstrap加载的类(Class),但是依旧能够维护“当前”这个语义。

Java自身除了通过Reflection.getCallerClass来获取调用的类的类型,在deSerialization中也需要知道类型的信息。在序列化后的内容中,已经包含了当前用户自定义类的类型信息,那么如何在ObjectInputStream调用中,能够拿到客户端的类型呢?通过调用Class.forName?肯定不可以,因为在ObjectInputStream中调用这个,会使用bootstrap来加载,那么它肯定加载不到所需要的类。

 答案是通过查询栈信息,通过sun.misc.VM.latestUserDefinedLoader();获取从栈上开始计算,第一个不为空(bootstrap classloader是空)的ClassLoader便返回。

 可以试想,在ObjectInputStream运作中,通过直接获取当前调用栈中,第一个非空的ClassLoader,这种做法能够非常便捷的定位用户的ClassLoader,也就是用户在进行:

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(“xx.dat”));B b = (B) ois.readObject();

这种调用的时候,依旧能够通过“当前”的ClassLoader正确的加载用户的类。

 可以说Reflection.getCallerClass和sun.misc.VM.latestuserDefinedLoader都是用来突破双亲委派模型的一种解决方式,它能让Java在bootstrap加载的代码中运行时,能够获取到外界(用户)使用的子ClassLoader。

 ContextClassLoader是作为Thread的一个成员变量出现的,一个线程在构造的时候,它会从parent线程中继承这个ClassLoader,但是Java的文档中对这个ClassLoader描述非常有限,但是它对于理解JNDI以及JAXP等技术有非常大的帮助,个人认为它是应用服务器,或者框架需要特别关注的一种ClassLoader。

 通过介绍CurrentClassLoader中,用来突破双亲委派模型的目的,而ContextClassLoader也是为了完成这个工作。试想: 如果一个JNDI的提供方,或者JAXP的提供方,他们的SPI是通过bootstrap加载的,但是他们的实现类必须通过AppClassLoader甚至是更下层的ClassLoader来加载。那么在其初始化的过程中,需要考虑如果获取到部署了SPI实现的ClassLoader,而给出的方案是使用ContextClassLoader。 在javax.xml.parsers.DocumentBuilderFactory中,进行创建SPI实现的方法: 

public static DocumentBuilderFactory newInstance() {    try {        return (DocumentBuilderFactory) FactoryFinder.find(            /* The default property name according to the JAXP spec */            "javax.xml.parsers.DocumentBuilderFactory",            /* The fallback implementation class name */            "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");    } catch (FactoryFinder.ConfigurationError e) {        throw new FactoryConfigurationError(e.getException(), e.getMessage());    }} 

可以看到通过查询一个property的Key来定位客户端的实现者,或者在找不到时,默认使用com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl。
而在FactoryFinder中,find方法通过如下的方式定位实现者:

String serviceId = "META-INF/services/" + factoryId;InputStream is = null;ClassLoader cl = ss.getContextClassLoader();boolean useBSClsLoader = false;if (cl != null) {    is = ss.getResourceAsStream(cl, serviceId);    if (is == null) {        cl = FactoryFinder.class.getClassLoader();        is = ss.getResourceAsStream(cl, serviceId);        useBSClsLoader = true;    }} else {    cl = FactoryFinder.class.getClassLoader();    is = ss.getResourceAsStream(cl, serviceId);    useBSClsLoader = true;}

可以看到cl变量,就是当前线程的ContextClassLoader,选择使用这种方式,是因为不同的部署(通过classpath启动的控制台程序,通过Webapp部署的程序)方式不同,最终都需要有一个用户ClassLoder来查找到客户端的实现,通过前面的Reflection.getCallerClass或者sun.misc.VM中获取最近一个不为空的ClassLoder的方式都不能很好的满足要求,那么就利用一个指定的ClassLoder来完成,也就是接口实现者能够很明确的被这个ClassLorder加载,这个选择就是ContextClassLoader。

可以看出来CurrentClassLoader对用户来说是自动的,隐式的,而ContextClassLoader需要显示的使用,先进行设置然后再进行使用。如果未来Java有更多种的类加载方式,并且看到从目前的发行版中,越来越多的方式被加入,如果没有一个良好的描述和规划,那么这将成为一个严重的问题。



0 0
原创粉丝点击