Java利用classloader从classpath加载资源

时间:2021-09-17 15:56:00

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:/