Java是如何加载资源文件的?(源码解毒)

         上文提到应老板要求开发一个测试工具能方便的加载存于文件中的测试参数,当时考虑既然是测试,把测试参数文件和测试类放在一起岂不是很方便,但是老板说:我的需求是你把测试参数文件放到统一文件夹下比如resources目录下,当然你做的这个也可以保留。 好吧,既然老板都说了,我就开干呗,主要问题是如何在CaseloaderSupplier中获取resources文件夹的路径,很多人的第一反应是直接new File("/src/test/resources")不行吗? 当然是不行的,因为通过maven编译以后会把资源文件拷贝到target目录下边,直接通过file是定位不到的。唯一的解决方案(我目前感觉是唯一的)是使用Java提供的抽象方法:resources。可以通过Class.getResources()或者ClassLoader.getResources()。 Class.getResource底层也是将加载任务委托给ClassLoader做的。 当然,在做这个之前对resource不是太熟悉,只是偶尔会调用一下接口,所以感觉还是详细了解一下resource心里有底一点,google了很多文章,大概意思是getResource("")获取的是当前调用类路径,比如当前类路径为file:/Users/caiyao/workservice/test-caseloader/target/classes/base/ (打包以后的),getResource("/")这个获取的是classpath的根目录,比如file:/Users/caiyao/workservice/test-caseloader/target/classes/(注意:没有包名)。问题是老板要求测试参数类要在test包下的resources目录里,这样获取的是main的包下面的,那么怎样才能让在测试的时候(执行@Test方法)加载test包下的资源呢? 当然在思考这个问题之前,我还是在执行@Test时试了一下,令人迷惑的是,在执行@Test的时候拿到的已经是test包下的资源了!!!file:/Users/caiyao/workservice/test-caseloader/target/test-classes/ 如何办到的,详细看下源码:

首先看下getResource方法

public java.net.URL getResource(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class.
return ClassLoader.getSystemResource(name);
}
return cl.getResource(name);
}
c1.getResource(name)这句可以看出Class是把获取资源委托给ClassLoader来执行的。进入ClassLoader的getResoure方法(java.lang.ClassLoader#getResource):
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;
}
从这里可以看出Java获取资源和加载类是同样的道理,使用的是双亲委托机制,首先执行父类加载器的getResource方法,如果父类加载器为空则执行Bootstrap的类加载器,如果父类加载的getResource没有获取到值
再从自己的上下文查找资源,该方法真正执行操作的是findResource(name)这个步骤,进入java.lang.ClassLoader#findResource,ClassLoader的默认实现直接返回null,应该看ClassLoader的实现类URLClassLoader
(从很少的源码阅读经验中我发现通过IDE断点调试能避免很多不必要的代码阅读,让注意力更集中于关注的这条线),从调试的过程可以看出,最后的结果是由AppClassLoader返回,这点很重要,后面要用到:
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;
}
AccessController.doPrivileged方法是一个native方法,无法通过IDE进去调试,但是可以对ucp.findResource(name,true)打断点,通过调试可以看出正是该方法的执行返回了
file:/Users/caiyao/workservice/test-caseloader/target/test-classes/,要注意一点这里调用的findResource是通过该类的一个成员属性ucp中进去的,ucp持有该方法的一些上下文,断点进sun.misc.URLClassPath#findResource方法:
public URL findResource(String var1, boolean var2) {
int[] var4 = this.getLookupCache(var1);

URLClassPath.Loader var3;
for(int var5 = 0; (var3 = this.getNextLoader(var4, var5)) != null; ++var5) {
URL var6 = var3.findResource(var1, var2);
if (var6 != null) {
return var6;
}
}

return null;
}
从该方法可以看出URL是通过this.getNextLoader(var4, var5))返回的Loader里得到的,而this正是上面URLClassLoader的成员属性ucp,回到URLClassLoader类中寻找ucp的初始化代码,可以看到一个单参数的构造方法:
public URLClassLoader(URL[] urls) {
super();
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
ucp = new URLClassPath(urls);
this.acc = AccessController.getContext();
}然后需要找到urls是重什么地方传递过来的,我以前大致了解过Java的类加载过程,知道类加载器是由AppClassLoader / ExtClassLoader / BootstrapClassLoader这样的一个层级结构组成,它们由Launcher初始化,从上面
已经得知最后的资源路径是由AppClassLoader得到,所以可以猜测AppClassLoader是URLClassLoader的子类,可能在Launcher中初始化,从Launcher中可以找到如下代码:
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
证实了猜想,AppClassLoader从java.class.path变量中获取资源路径,而java.class.path这个变量由在什么地方设置的呢? 肯定在执行java命令时设置的参数,可以 ps aux |grep Test1(Test1是我测试类的名字)查看到
当前执行的测试进程的信息,果然在执行Java命令时把如下两个路径都加到了classpath中:

/Users/caiyao/workservice/test-caseloader/target/test-classes
/Users/caiyao/workservice/test-caseloader/target/classes

而且test-classes在前面,从sun.misc.URLClassPath#findResource中的代码可以看出,在遍历classpath的时候,一旦发现了一个存在就不会再往后遍历,所以在执行test方法的时候只会拿到test目录,至于为什么在执行Test的时候自动把test的资源路径加到了classpath里,这个我没有再深入研究,猜测应该是maven做的操作,因为target这个目录就是maven的规定。

终于找到原因,可以安心的写代码了~~~

posted @ 2018-07-13 19:01  Birding  阅读(949)  评论(0编辑  收藏  举报