Java利用classloader从classpath加载资源

Java利用classloader从classpath加载资源


          我们都知道classloader的getResource、getResources等方法可以加载classpath中的资源。classloader获取资源传入的参数是相对于classpath的相对路径,如果某个资源想要被classloader的加载到就要放到当前的classpath中,或者把资源所在的目录或者jar包文件作为classpath。

     Java程序在启动时可以指定多个位置作为classpath,每个位置都可以用URL来描述,不同的位置之间用分号分隔。根据Java命令的提示,目录、jar文件或者普通的zip文件都可以作为虚拟机识别的classpath。

     那么虚拟机是怎样利用这些位置加载资源的呢,下面深入JDK的源码来探究一下。众所周知,负责加载应用程序的类和资源的类加载器是AppClassloader,其实加载一个类之前也是先把类的名字转换成资源的名字,先加载.class类文件的资源再从中加载出具体的类对象。那么先来看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);

            // Note: on bugid 4256530
            // Prior implementations of this doPrivileged() block supplied
            // a rather restrictive ACC via a call to the private method
            // AppClassLoader.getContext(). This proved overly restrictive
            // when loading  classes. Specifically it prevent
            // accessClassInPackage.sun.* grants from being honored.
            //
            return AccessController.doPrivileged(
                new PrivilegedAction<AppClassLoader>() {
                    public AppClassLoader run() {
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
        }

    这是JDK中sun.misc.Launcher中的源码,可见虚拟机中classpath位置来自于"java.class.path"的系统属性。如果把这个属性值打印出来,会发现它包含了所有通过-classpath参数指定的目录和jar包。它们有个共同点,就是都可以表示为一个File对象,代表了系统中的一个目录或具体文件。最终这些File对象都转换成了URL的形式,传递给了AppClassloader, AppClassloader的父类正是URLClassLoader,它把自己负责加载的目录都保存在内部的URLClassPath对象中。注意这里说的父类是真正继承的父类,而不是父加载器,类加载器的双亲委派原则是利用组合而不是继承实现的。

     同加载类一样,classloader在加载一个资源的时候默认也使用了双亲委派的原则,如果可以通过父加载找到资源则自己就不必再继续查找,直接返回父加载器找到的资源即可:

    public URL getResource(String name) {
        URL url;
        if (parent != null) {
            url = parent.getResource(name);
        } else {
            url = getBootstrapResource(name);
        }
        if (url == null) {
            url = findResource(name);
        }
        return url;
    }

      与加载类不同的是,虚拟机加载一个类的时候,只需要找到一个对应的class文件(即使classpath下有多个满足条件的class文件)。有时我们希望通过一个统一的名字获取classpath下所有名字相同的资源,而不是找到一个后立即返回。那么,Classloader的getResources提供了这个功能:
    public Enumeration<URL> getResources(String name) throws IOException {
        Enumeration[] tmp = new Enumeration[2];
        if (parent != null) {
            tmp[0] = parent.getResources(name);
        } else {
            tmp[0] = getBootstrapResources(name);
        }
        tmp[1] = findResources(name);

        return new CompoundEnumeration<>(tmp);
    }

      刚才也有提到, classpath下有可能存在名字相同的资源,而getResource方法只返回第一个找到的资源。getResources的逻辑其实就是通过各种途径找到所有的资源,最后合并在一起返回。那么,资源查找的过程是怎么样的呢?有没有固定的优先级顺序呢?下面我们看看URLClassloader的findResource方法:
    public URL findResource(final String name) {
        /*
         * The same restriction to finding classes applies to resources
         */
        URL url = AccessController.doPrivileged(
            new PrivilegedAction<URL>() {
                public URL run() {
                    return ucp.findResource(name, true);
                }
            }, acc);

        return url != null ? ucp.checkURL(url) : null;
    }

     跟到URLClassPath中,
    public URL findResource(String name, boolean check) {
        Loader loader;
        for (int i = 0; (loader = getLoader(i)) != null; i++) {
            URL url = loader.findResource(name, check);
            if (url != null) {
                return url;
            }
        }
        return null;
    }

     从表面上看,负责加载资源的有若干个loader,loader的个数不确定,顺序是否一定不能确定。还得要看看getLoader(i)是什么鬼
     private synchronized Loader getLoader(int index) {
        if (closed) {
            return null;
        }
         // Expand URL search path until the request can be satisfied
         // or the URL stack is empty.
        while (loaders.size() < index + 1) {
            // Pop the next URL from the URL stack
            URL url;
            synchronized (urls) {
                if (urls.empty()) {
                    return null;
                } else {
                    url = urls.pop();
                }
            }
            // Skip this URL if it already has a Loader. (Loader
            // may be null in the case where URL has not been opened
            // but is referenced by a JAR index.)
            String urlNoFragString = URLUtil.urlNoFragString(url);
            if (lmap.containsKey(urlNoFragString)) {
                continue;
            }
            // Otherwise, create a new Loader for the URL.
            Loader loader;
            try {
                loader = getLoader(url);
                // If the loader defines a local class path then add the
                // URLs to the list of URLs to be opened.
                URL[] urls = loader.getClassPath();
                if (urls != null) {
                    push(urls);
                }
            } catch (IOException e) {
                // Silently ignore for now...
                continue;
            }
            // Finally, add the Loader to the search path.
            loaders.add(loader);
            lmap.put(urlNoFragString, loader);
        }
        return loaders.get(index);
    }

    虚拟机并不会提前创建好所有的loader,而是到需要的时候才去创建,怪不得会出现刚才那么怪异的遍历所有loader的代码。loader的创建顺序依赖URL的顺序,因为创建loader总是先从存储URL的栈中出栈一个URL,然后获取加载这个URL下资源的loader。
    
    URLClasspath中的URL来自于classpath参数,通过改变classpath参数中文件的顺序,如分别执行java -cp lib/*;classes xxx.xxx.MainClass与 java -cp classes;lib/* xxx.xxx.MainClass 。可以得出如下结论,Classloader在加载资源时,查找资源的位置顺序与-classpath中指定的顺序一致。如果资源在classes目录和jar包中同时存在,参数为lib/*;classes则首先找到的是jar包中的资源,参数为classes;lib/*则首先找到的是classes目录中的资源。

    创建loader时,从URL栈从出栈一个URL,然后创建对应的loader。都知道栈是先进后出的,既然扫描资源的位置顺序与参数中指定的顺序一致,那么肯定在入栈的时候是倒序入栈的。结果不出意外,URLClassPath中代码如下:
    private void push(URL[] us) {
        synchronized (urls) {
            for (int i = us.length - 1; i >= 0; --i) {
                urls.push(us[i]);
            }
        }
    }

   另外,普通目录中的资源与jar文件中的资源的URL的协议是不同的。如果打印出来,可以看到它们开头分别为file:/和jar:file:/
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值