博客原文请访问:http://zhuqiuhui.space/Blogs/2017/03/spring-classpath.html
Spring中的classpath与classpath*一直是开发中的心病,没有时间静下心来研究下,终于一气呵成!网上总结的也比较多,各种各样的说法,还不如自己亲自解读一下spring中的源码,这样下次再次使用心里就安心多了,欢迎支持!
一、问题描述
使用spring时import资源文件时路径查找顺序不明(开发中的疑惑),或者加载资源失败(不知道怎么更改路径)?
二、classpath代码解析
直接上代码,这是代码中的主类 PathMatchingResourcePatternResolver 类,位于包 org.springframework.core.io.support 下,该类中的 getResources 函数是逻辑的核心,如下:
public Resource[] getResources(String locationPattern)throws IOException {
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
// case 1:如果以classpath*开头且包含?或者* ,例如查找: classpath*: applicationContext-*.xml
return findPathMatchingResources(locationPattern);
}
else {
// case 2: 不包含?或者*,直接全名查找,例如查找: classpath*: applicationContext-test.xml
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}
else {
// 以 classpath:开头
int prefixEnd = locationPattern.indexOf(":") +1;
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
// case 3: 如果不是以classpath*开头且包含?或者* ,例如查找: classpath: applicationContext-*.xml
return findPathMatchingResources(locationPattern);
}
else {
// case 4: 如果不是以classpath*开头且不包含?或者* ,例如查找: classpath: applicationContext-test.xml
returnnew Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}
2.1 如果以classpath*开头且包含?或者*
例如查找: classpath*: applicationContext-*.xml ,使用findPathMatchingResources函数,看下该函数:
protected Resource[] findPathMatchingResources(String locationPattern)throws IOException {
/*函数determineRootDir函数是拿到能够确定的目录,如 classpath*:/aaa/bbb/applicationContext-*.xml 则返回classpath*:/aaa/bbb/
classpath*:/aaa/*/applicationContext-*.xml,则返回 classpath*:/aaa/
(代码就不再粘贴了,有兴趣的可以看下源码)*/
String rootDirPath = determineRootDir(locationPattern);
// 获取字符串locationPattern中后面不确定的内容
String subPattern = locationPattern.substring(rootDirPath.length());
// 递归加载已经确定的内容
Resource[] rootDirResources = getResources(rootDirPath);
Set result = new LinkedHashSet<>(16);
for (Resource rootDirResource : rootDirResources) {
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirURL = rootDirResource.getURL();
if (equinoxResolveMethod !=null) {
if (rootDirURL.getProtocol().startsWith("bundle")) {
rootDirURL = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod,null, rootDirURL);
rootDirResource = new UrlResource(rootDirURL);
}
}
if (rootDirURL.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
// is general JBoss VFS resource: "vfs"
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirURL, subPattern, getPathMatcher()));
}
elseif (ResourceUtils.isJarURL(rootDirURL) || isJarResource(rootDirResource)) {
// is zip, jar, wsjar or vfszip的一种就加载
result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirURL, subPattern));
}
else {
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
}
if (logger.isDebugEnabled()) {
logger.debug("Resolved location pattern [" + locationPattern +"] to resources " + result);
}
return result.toArray(new Resource[result.size()]);
}
总体来说:该函数把locationPattern拆分成两部分:rootDirPath 和subPattern,rootDirPath是根目录路径,subPattern是子目录路径匹配规则字符串。遍历根目录下的所有子目录、并得到所有的子目录在doFindPathMatchingFileResources(rootDirResource, subPattern)方法中,再根据子目录逐个逐个去匹配subPattern。
2.2 如果以classpath*开头且不包含?或者*,直接全名查找
例如查找: classpath*: applicationContext-test.xml,使用findAllClassPathResources函数,看下该函数:
protected Resource[] findAllClassPathResources(String location)throws IOException {
String path = location;
if (path.startsWith("/")) {
path = path.substring(1);
}
Set result = doFindAllClassPathResources(path);
if (logger.isDebugEnabled()) {
logger.debug("Resolved classpath location [" + location +"] to resources " + result);
}
return result.toArray(new Resource[result.size()]);
}
其实核心在doFindAllClassPathResources,该函数扫描该路径下的所有资料,其函数体如下:
protected Set doFindAllClassPathResources(String path) throws IOException {
Set result= new LinkedHashSet<>(16);
ClassLoader cl = getClassLoader();
Enumeration resourceUrls = (cl!= null? cl.getResources(path) : ClassLoader.getSystemResources(path));
while (resourceUrls.hasMoreElements()) {
URL url = resourceUrls.nextElement();
result.add(convertClassLoaderURL(url));
}
if ("".equals(path)) {
// 如果路径为空的话,就找到所有jar包,加到result中(该函数不再深入)
addAllClassLoaderJarRoots(cl, result);
}
return result;
}
在该函数中重要:Enumeration resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path)); 即如果当前类加载器cl不为空的话,就调用cl.getResources(path),cl为空的话就使用系统类加载器去加载,事实上cl.getResources(path)内容使用的即是双亲委派模型(当前类加载器加载资源,如果存在父加载器,则用父加载器进行加载)。
2.3 如果不是以classpath*开头且包含?或者*
例如查找: classpath: applicationContext-*.xml,使用函数findPathMatchingResources,类似2.1情况。
2.4 如果不是以classpath*开头且不包含?或者*
例如查找: classpath: applicationContext-test.xml,直接“getResourceLoader().getResource(locationPattern)”,即直接使用当然的资源加载器去加载,这里默认使用的是DefaultResourceLoader()。
三、总结及测试
写了个小测试,测试代码如下:
private staticvoid output(String location)throws Exception {
ResourcePatternResolver resourceLoader = new PathMatchingResourcePatternResolver();
Resource[] source = resourceLoader.getResources(location);
// System.out.println("source.size: " + source.length);
for (int i =0; i < source.length; i++) {
Resource resource = source[i];
System.out.println(resource);
}
}
很多个测试结果如下图: