Spring maven 專案中的classpath路徑問題
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);
}
}
很多個測試結果如下圖: