美文网首页程序员
系统、当前与线程上下文类加载器

系统、当前与线程上下文类加载器

作者: weipeng2k | 来源:发表于2018-01-06 16:00 被阅读0次

    来自:http://www.javaworld.com/article/2077344/core-java/find-a-way-out-of-the-classloader-maze.html

    问题:应该在什么时候使用线程上下文加载器?

    回答

           虽然不经常被问起这个问题,但是这个问题却很难给予一个正确的答案。一般来说,这个问题都来自于框架编程过程中需要动态的加载资源。一般来说,加载一个资源的时候,你会有至少有三个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中,除此没有别的特殊的地方。

    限定在Java9之前的版本

           能够通过静态方法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加载的,但是他们的实现类必须通过应用ClassLoader甚至是更下层的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需要显示的使用,先进行设置然后再进行使用。

           可以参看:https://github.com/weipeng2k/jarviewer
    其中提到了ResourceLoader,这个可以用来合适的加载一个Class。

           如果未来Java有更多种的类加载方式,并且看到从目前的发行版中,越来越多的方式被加入,如果没有一个良好的描述和规划,那么这将成为一个严重的问题。

    相关文章

      网友评论

        本文标题:系统、当前与线程上下文类加载器

        本文链接:https://www.haomeiwen.com/subject/xjmynxtx.html