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);
}
});
}
同加载类一样,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:/