在《深入Spring IOC源碼之Resource》中已經(jīng)詳細(xì)介紹了Spring中Resource的抽象,Resource接口有很多實(shí)現(xiàn)類,我們當(dāng)然可以使用各自的構(gòu)造函數(shù)創(chuàng)建符合需求的Resource實(shí)例,然而Spring提供了ResourceLoader接口用于實(shí)現(xiàn)不同的Resource加載策略,即將不同Resource實(shí)例的創(chuàng)建交給ResourceLoader來(lái)計(jì)算。
public interface ResourceLoader {
//classpath
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
Resource getResource(String location);
ClassLoader getClassLoader();
}
在ResourceLoader接口中,主要定義了一個(gè)方法:getResource(),它通過(guò)提供的資源location參數(shù)獲取Resource實(shí)例,該實(shí)例可以是ClasPathResource、FileSystemResource、UrlResource等,但是該方法返回的Resource實(shí)例并不保證該Resource一定是存在的,需要調(diào)用exists方法判斷。該方法需要支持一下模式的資源加載:
1. URL位置資源,如”file:C:/test.dat”
2. ClassPath位置資源,如”classpath:test.dat”
3. 相對(duì)路徑資源,如”WEB-INF/test.dat”,此時(shí)返回的Resource實(shí)例根據(jù)實(shí)現(xiàn)不同而不同。
ResourceLoader接口還提供了getClassLoader()方法,在加載classpath下的資源時(shí)作為參數(shù)傳入ClassPathResource。將ClassLoader暴露出來(lái),對(duì)于想要獲取ResourceLoader使用的ClassLoader用戶來(lái)說(shuō),可以直接調(diào)用getClassLoader()方法獲得,而不是依賴于Thread Context ClassLoader,因?yàn)橛行r(shí)候ResourceLoader內(nèi)部使用自定義的ClassLoader。
在實(shí)際開(kāi)發(fā)中經(jīng)常會(huì)遇到需要通過(guò)某種匹配方式查找資源,而且可能有多個(gè)資源匹配這種模式,在Spring中提供了ResourcePatternResolver接口用于實(shí)現(xiàn)這種需求,該接口繼承自ResourceLoader接口,定義了自己的模式匹配接口:
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
ResourcePatternResolver定義了getResources()方法用于根據(jù)傳入的locationPattern查找和其匹配的Resource實(shí)例,并以數(shù)組的形式返回,在返回的數(shù)組中不可以存在相同的Resource實(shí)例。ResourcePatternResolver中還定義了”classpath*:”模式,用于表示查找classpath下所有的匹配Resource。
在Spring中,對(duì)ResourceLoader提供了DefaultResourceLoader、FileSystemResourceLoader和ServletContextResourceLoader等單獨(dú)實(shí)現(xiàn),對(duì)ResourcePatternResolver接口則提供了PathMatchingResourcePatternResolver實(shí)現(xiàn)。并且ApplicationContext接口繼承了ResourcePatternResolver,在實(shí)現(xiàn)中,ApplicationContext的實(shí)現(xiàn)類會(huì)將邏輯代理給相關(guān)的單獨(dú)實(shí)現(xiàn)類,如PathMatchingResourceLoader等。在ApplicationContext中ResourceLoaderAware接口,可以將ResourceLoader(自身)注入到實(shí)現(xiàn)該接口的Bean中,在Bean中可以將其強(qiáng)制轉(zhuǎn)換成ResourcePatternResolver接口使用(為了安全,強(qiáng)轉(zhuǎn)前需要判斷)。在Spring中對(duì)ResourceLoader相關(guān)類的類圖如下:

DefaultResourceLoader類
DefaultResourceLoader是ResourceLoader的默認(rèn)實(shí)現(xiàn),AbstractApplicationContext繼承該類(關(guān)于這個(gè)繼承,簡(jiǎn)單吐槽一下,Spring內(nèi)部感覺(jué)有很多這種個(gè)人感覺(jué)使用組合更合適的繼承,比如還有AbstractBeanFactory繼承自FactoryBeanRegisterySupport,這個(gè)讓我看起來(lái)有點(diǎn)不習(xí)慣,而且也增加了類的繼承關(guān)系)。它接收ClassLoader作為構(gòu)造函數(shù)的參數(shù),或使用不帶參數(shù)的構(gòu)造函數(shù),此時(shí)ClassLoader使用默認(rèn)的ClassLoader(一般為Thread Context ClassLoader),ClassLoader也可以通過(guò)set方法后繼設(shè)置。
其最主要的邏輯實(shí)現(xiàn)在getResource方法中,該方法首先判斷傳入的location是否以”classpath:”開(kāi)頭,如果是,則創(chuàng)建ClassPathResource(移除”classpath:”前綴),否則嘗試創(chuàng)建UrlResource,如果當(dāng)前location沒(méi)有定義URL的協(xié)議(即以”file:”、”zip:”等開(kāi)頭,比如使用相對(duì)路徑”resources/META-INF/MENIFEST.MF),則創(chuàng)建UrlResource會(huì)拋出MalformedURLException,此時(shí)調(diào)用getResourceByPath()方法獲取Resource實(shí)例。getResourceByPath()方法默認(rèn)返回ClassPathContextResource實(shí)例,在FileSystemResourceLoader中有不同實(shí)現(xiàn)。
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
if (location.startsWith(CLASSPATH_URL_PREFIX)) {
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// Try to parse the location as a URL...
URL url = new URL(location);
return new UrlResource(url);
}
catch (MalformedURLException ex) {
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
}
}
protected Resource getResourceByPath(String path) {
return new ClassPathContextResource(path, getClassLoader());
}
FileSystemResourceLoader類
FileSystemResourceLoader繼承自DefaultResourceLoader,它的getResource方法的實(shí)現(xiàn)邏輯和DefaultResourceLoader相同,不同的是它實(shí)現(xiàn)了自己的getResourceByPath方法,即當(dāng)UrlResource創(chuàng)建失敗時(shí),它會(huì)使用FileSystemContextResource實(shí)例而不是ClassPathContextResource:
protected Resource getResourceByPath(String path) {
if (path != null && path.startsWith("/")) {
path = path.substring(1);
}
return new FileSystemContextResource(path);
}
使用該類時(shí)要特別注意的一點(diǎn):即使location以”/”開(kāi)頭,資源的查找還是相對(duì)于VM啟動(dòng)時(shí)的相對(duì)路徑而不是絕對(duì)路徑(從以上代碼片段也可以看出,它會(huì)先截去開(kāi)頭的”/”),這個(gè)和Servlet Container保持一致。如果需要使用絕對(duì)路徑,需要添加”file:”前綴。
ServletContextResourceLoader類
ServletContextResourceLoader類繼承自DefaultResourceLoader,和FileSystemResourceLoader一樣,它的getResource方法的實(shí)現(xiàn)邏輯和DefaultResourceLoader相同,不同的是它實(shí)現(xiàn)了自己的getResourceByPath方法,即當(dāng)UrlResource創(chuàng)建失敗時(shí),它會(huì)使用ServletContextResource實(shí)例:
protected Resource getResourceByPath(String path) {
return new ServletContextResource(this.servletContext, path);
}
這里的path即使以”/”開(kāi)頭,也是相對(duì)ServletContext的路徑,而不是絕對(duì)路徑,要使用絕對(duì)路徑,需要添加”file:”前綴。
PathMatchingResourcePatternResolver類
PathMatchingResourcePatternResolver類實(shí)現(xiàn)了ResourcePatternResolver接口,它包含了對(duì)ResourceLoader接口的引用,在對(duì)繼承自ResourceLoader接口的方法的實(shí)現(xiàn)會(huì)代理給該引用,同時(shí)在getResources()方法實(shí)現(xiàn)中,當(dāng)找到一個(gè)匹配的資源location時(shí),可以使用該引用解析成Resource實(shí)例。默認(rèn)使用DefaultResourceLoader類,用戶可以使用構(gòu)造函數(shù)傳入自定義的ResourceLoader。
PathMatchingResourcePatternResolver還包含了一個(gè)對(duì)PathMatcher接口的引用,該接口基于路徑字符串實(shí)現(xiàn)匹配處理,如判斷一個(gè)路徑字符串是否包含通配符(’*’、’?’),判斷給定的path是否匹配給定的pattern等。Spring提供了AntPathMatcher對(duì)PathMatcher的默認(rèn)實(shí)現(xiàn),表達(dá)該PathMatcher是采用Ant風(fēng)格的實(shí)現(xiàn)。其中PathMatcher的接口定義如下:
public interface PathMatcher {
boolean isPattern(String path);
boolean match(String pattern, String path);
boolean matchStart(String pattern, String path);
String extractPathWithinPattern(String pattern, String path);
}
isPattern(String path):
判斷path是否是一個(gè)pattern,即判斷path是否包含通配符:
public boolean isPattern(String path) {
return (path.indexOf('*') != -1 || path.indexOf('?') != -1);
}
match(String pattern, String path):
判斷給定path是否可以匹配給定pattern:
matchStart(String pattern, String path):
判斷給定path是否可以匹配給定pattern,該方法不同于match,它只是做部分匹配,即當(dāng)發(fā)現(xiàn)給定path匹配給定path的可能性比較大時(shí),即返回true。在PathMatchingResourcePatternResolver中,可以先使用它確定需要全面搜索的范圍,然后在這個(gè)比較小的范圍內(nèi)再找出所有的資源文件全路徑做匹配運(yùn)算。
在AntPathMatcher中,都使用doMatch方法實(shí)現(xiàn),match方法的fullMatch為true,而matchStart的fullMatch為false:
protected boolean doMatch(String pattern, String path, boolean fullMatch)
doMatch的基本算法如下:
1. 檢查pattern和path是否都以”/”開(kāi)頭或者都不是以”/”開(kāi)頭,否則,返回false。
2. 將pattern和path都以”/”為分隔符,分割成兩個(gè)字符串?dāng)?shù)組pattArray和pathArray。
3. 從頭遍歷兩個(gè)字符串?dāng)?shù)組,如果遇到兩給字符串不匹配(兩個(gè)字符串的匹配算法再下面介紹),返回false,否則,直到遇到pattArray中的”**”字符串,或pattArray和pathArray中有一個(gè)遍歷完。
4. 如果pattArray遍歷完:
a) pathArray也遍歷完,并且pattern和path都以”/”結(jié)尾或都不以”/”,返回true,否則返回false。
b) pattArray沒(méi)有遍歷完,但fullMatch為false,返回true。
c) pattArray只剩最后一個(gè)”*”,同時(shí)path以”/”結(jié)尾,返回true。
d) pattArray剩下的字符串都是”**”,返回true,否則返回false。
5. 如果pathArray沒(méi)有遍歷完,而pattArray遍歷完了,返回false。
6. 如果pathArray和pattArray都沒(méi)有遍歷完,fullMatch為false,而且pattArray下一個(gè)字符串為”**”時(shí),返回true。
7. 從后開(kāi)始遍歷pathArray和pattArray,如果遇到兩個(gè)字符串不匹配,返回false,否則,直到遇到pattArray中的”**”字符串,或pathArray和pattArray中有一個(gè)和之前的遍歷索引相遇。
8. 如果是因?yàn)?/span>pathArray與之前的遍歷索引相遇,此時(shí),如果沒(méi)有遍歷完的pattArray所有字符串都是”**”,則返回true,否則,返回false。
9. 如果pathArray和pattArray中間都沒(méi)有遍歷完:
a) 去除pattArray中相鄰的”**”字符串,并找到其下一個(gè)”**”字符串,其索引號(hào)為pattIdxTmp,他們的距離即為s
b) 從剩下的pathArray中的第i個(gè)元素向后查找s個(gè)元素,如果找到所有s個(gè)元素都匹配,則這次查找成功,記i為temp,如果沒(méi)有找到這樣的s個(gè)元素,返回false。
c) 將pattArray的起始索引設(shè)置為pattIdxTmp,將pathArray的索引號(hào)設(shè)置為temp+s,繼續(xù)查找,直到pattArray或pathArray遍歷完。
10. 如果pattArray沒(méi)有遍歷完,但剩下的元素都是”**”,返回true,否則返回false。
對(duì)路徑字符串?dāng)?shù)組中的字符串匹配算法如下:
1. 記pattern為模式字符串,str為要匹配的字符串,將兩個(gè)字符串轉(zhuǎn)換成兩個(gè)字符數(shù)組pattArray和strArray。
2. 遍歷pattArray直到遇到’*’字符。
3. 如果pattArray中不存在’*’字符,則只有在pattArray和strArray的長(zhǎng)度相同兩個(gè)字符數(shù)組中所有元素都相同,其中pattArray中的’?’字符可以匹配strArray中的任何一個(gè)字符,否則,返回false。
4. 如果pattArray只包含一個(gè)’*’字符,返回true
5. 遍歷pattArray和strArray直到pattArray遇到’*’字符或strArray遍歷完,如果存在不匹配的字符,返回false。
6. 如果因?yàn)?/span>strArray遍歷完成,而pattArray剩下的字符都是’*’,返回true,否則返回false
7. 從末尾開(kāi)始遍歷pattArray和strArray,直到pattArray遇到’*’字符,或strArray遇到之前的遍歷索引,中間如果遇到不匹配字符,返回false
8. 如果strArray遍歷完,而剩下的pattArray字符都是’*’字符,返回true,否則返回false
9. 如果pattArray和strArray都沒(méi)有遍歷完(類似之前的算法):
a) 去除pattArray相鄰的’*’字符,查找下一個(gè)’*’字符,記其索引號(hào)為pattIdxTmp,兩個(gè)’*’字符的相隔距離為s
b) 從剩下的strArray中的第i個(gè)元素向后查找s個(gè)元素,如果有找到所有s個(gè)元素都匹配,則這次查找成功,記i為temp,如果沒(méi)有到這樣的s個(gè)元素,返回false。
c) 將pattArray的起始索引設(shè)置為pattIdxTmp,strArray的起始索引設(shè)置為temp+s,繼續(xù)查找,直到pattArray或strArray遍歷完。
10. 如果pattArray沒(méi)有遍歷完,但剩下的元素都是’*’,返回true,否則返回false
String extractPathWithinPattern(String pattern, String path):
去除path中和pattern相同的字符串,只保留匹配的字符串。比如如果pattern為”/doc/csv/*.htm”,而path為”/doc/csv/commit.htm”,則該方法的返回值為commit.htm。該方法默認(rèn)pattern和path已經(jīng)匹配成功,因而算法比較簡(jiǎn)單:
以’/’分割pattern和path為兩個(gè)字符串?dāng)?shù)組pattArray和pathArray,遍歷pattArray,如果該字符串包含’*’或’?’字符,則并且pathArray的長(zhǎng)度大于當(dāng)前索引號(hào),則將該字符串添加到結(jié)果中。
遍歷完pattArray后,如果pathArray長(zhǎng)度大于pattArray,則將剩下的pathArray都添加到結(jié)果字符串中。
最后返回該字符串。
不過(guò)也正是因?yàn)樵撍惴▽?shí)現(xiàn)比較簡(jiǎn)單,因而它的結(jié)果貌似不那么準(zhǔn)確,比如pattern的值為:/com/**/levin/**/commit.html,而path的值為:/com/citi/cva/levin/html/commit.html,其返回結(jié)果為:citi/levin/commit.html
現(xiàn)在言歸正傳,看一下PathMatchingResourcePatternResolver中的getResources方法的實(shí)現(xiàn):
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
// a class path resource (multiple resources for same name possible)
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
// a class path resource pattern
return findPathMatchingResources(locationPattern);
}
else {
// all class path resources with the given name
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}
else {
// Only look for a pattern after a prefix here
// (to not get fooled by a pattern symbol in a strange prefix).
int prefixEnd = locationPattern.indexOf(":") + 1;
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
// a file pattern
return findPathMatchingResources(locationPattern);
}
else {
// a single resource with the given name
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}
對(duì)classpath下的資源,相同名字的資源可能存在多個(gè),如果使用”classpath*:”作為前綴,表明需要找到classpath下所有該名字資源,因而需要調(diào)用findClassPathResources方法查找classpath下所有該名稱的Resource,對(duì)非classpath下的資源,對(duì)于不存在模式字符的location,一般認(rèn)為一個(gè)location對(duì)應(yīng)一個(gè)資源,因而直接調(diào)用ResourceLoader.getResource()方法即可(對(duì)classpath下沒(méi)有以”classpath*:”開(kāi)頭的location也適用)。
findClassPathResources方法實(shí)現(xiàn)相對(duì)比較簡(jiǎn)單:
適用ClassLoader.getResources()方法,遍歷結(jié)果URL集合,將每個(gè)結(jié)果適用UrlResource封裝,最后組成一個(gè)Resource數(shù)組返回即可。
對(duì)包含模式匹配字符的location來(lái)說(shuō),需要調(diào)用findPathMatchingResources方法:
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
String rootDirPath = determineRootDir(locationPattern);
String subPattern = locationPattern.substring(rootDirPath.length());
Resource[] rootDirResources = getResources(rootDirPath);
Set result = new LinkedHashSet(16);
for (int i = 0; i < rootDirResources.length; i++) {
Resource rootDirResource = resolveRootDirResource(rootDirResources[i]);
if (isJarResource(rootDirResource)) {
result.addAll(doFindPathMatchingJarResources(rootDirResource, subPattern));
}
else {
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
}
if (logger.isDebugEnabled()) {
logger.debug("Resolved location pattern [" + locationPattern + "] to resources " + result);
}
return (Resource[]) result.toArray(new Resource[result.size()]);
}
1. determinRootDir()方法返回locationPattern中最長(zhǎng)的沒(méi)有出現(xiàn)模式匹配字符的路徑
2. subPattern則表示rootDirPath之后的包含模式匹配字符的路徑信pattern
3. 使用getResources()獲取rootDirPath下的所有資源數(shù)組。
4. 遍歷這個(gè)數(shù)組。
a) 對(duì)jar中的資源,使用doFindPathMatchingJarResources()方法來(lái)查找和匹配。
b) 對(duì)非jar中資源,使用doFindPathMatchingFileResources()方法來(lái)查找和匹配。
doFindPathMatchingJarResources()實(shí)現(xiàn):
1. 計(jì)算當(dāng)前Resource在Jar文件中的根路徑rootEntryPath。
2. 遍歷Jar文件中所有entry,如果當(dāng)前entry名以rootEntryPath開(kāi)頭,并且之后的路徑信息和之前從patternLocation中截取出的subPattern使用PathMatcher匹配,若匹配成功,則調(diào)用rootDirResource.createRelative方法創(chuàng)建一個(gè)Resource,將新創(chuàng)建的Resource添加入結(jié)果集中。
doFindPathMatchingFileResources()實(shí)現(xiàn):
1. 獲取要查找資源的根路徑(根路徑全名)
2. 遞歸獲得根路徑下的所有資源,使用PathMatcher匹配,如果匹配成功,則創(chuàng)建FileSystemResource,并將其加入到結(jié)果集中。在遞歸進(jìn)入一個(gè)目錄前首先調(diào)用PathMatcher.matchStart()方法,以先簡(jiǎn)單的判斷是否需要遞歸進(jìn)去,以提升性能。
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set result) throws IOException {
if (logger.isDebugEnabled()) {
logger.debug("Searching directory [" + dir.getAbsolutePath() +
"] for files matching pattern [" + fullPattern + "]");
}
File[] dirContents = dir.listFiles();
if (dirContents == null) {
throw new IOException("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
}
for (int i = 0; i < dirContents.length; i++) {
File content = dirContents[i];
String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
doRetrieveMatchingFiles(fullPattern, content, result);
}
if (getPathMatcher().match(fullPattern, currPath)) {
result.add(content);
}
}
}
最后,需要注意的是,由于ClassLoader.getResources()方法存在的限制,當(dāng)傳入一個(gè)空字符串時(shí),它只能從classpath的文件目錄下查找,而不會(huì)從Jar文件的根目錄下查找,因而對(duì)”classpath*:”前綴的資源來(lái)說(shuō),找不到Jar根路徑下的資源。即如果我們有以下定義:”classpath*:*.xml”,如果只有在Jar文件的根目錄下存在*.xml文件,那么這個(gè)pattern將返回空的Resource數(shù)組。解決方法是不要再Jar文件根目錄中放文件,可以將這些文件放到Jar文件中的resources、config等目錄下去。并且也不要在”classpath*:”之后加一些通配符,如”classpath*:**/*Enum.class”,至少在”classpath*:”后加入一個(gè)不存在通配符的路徑名。
ServletContextResourcePatternResolver類
ServletContextResourcePatternResolver類繼承自PathMatchingResourcePatternResolver類,它重寫(xiě)了父類的文件查找邏輯,即對(duì)ServletContextResource資源使用ServletContext.getResourcePaths()方法來(lái)查找參數(shù)目錄下的文件,而不是File.listFiles()方法:
protected Set doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException {
if (rootDirResource instanceof ServletContextResource) {
ServletContextResource scResource = (ServletContextResource) rootDirResource;
ServletContext sc = scResource.getServletContext();
String fullPattern = scResource.getPath() + subPattern;
Set result = new LinkedHashSet(8);
doRetrieveMatchingServletContextResources(sc, fullPattern, scResource.getPath(), result);
return result;
}
else {
return super.doFindPathMatchingFileResources(rootDirResource, subPattern);
}
}
AbstractApplicationContext對(duì)ResourcePatternResolver接口的實(shí)現(xiàn)
在AbstractApplicationContext中,對(duì)ResourcePatternResolver的實(shí)現(xiàn)只是簡(jiǎn)單的將getResources()方法的實(shí)現(xiàn)代理給resourcePatternResolver字段,而該字段默認(rèn)在AbstractApplicationContext創(chuàng)建時(shí)新建一個(gè)PathMatchingResourcePatternResolver實(shí)例:
public AbstractApplicationContext(ApplicationContext parent) {
this.parent = parent;
this.resourcePatternResolver = getResourcePatternResolver();
}
protected ResourcePatternResolver getResourcePatternResolver() {
return new PathMatchingResourcePatternResolver(this);
}
public Resource[] getResources(String locationPattern) throws IOException {
return this.resourcePatternResolver.getResources(locationPattern);
}