在《深入Spring IOC源碼之Resource》中已經詳細介紹了Spring中Resource的抽象,Resource接口有很多實現類,我們當然可以使用各自的構造函數創建符合需求的Resource實例,然而Spring提供了ResourceLoader接口用于實現不同的Resource加載策略,即將不同Resource實例的創建交給ResourceLoader來計算。
public interface ResourceLoader {
//classpath
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
Resource getResource(String location);
ClassLoader getClassLoader();
}
在ResourceLoader接口中,主要定義了一個方法:getResource(),它通過提供的資源location參數獲取Resource實例,該實例可以是ClasPathResource、FileSystemResource、UrlResource等,但是該方法返回的Resource實例并不保證該Resource一定是存在的,需要調用exists方法判斷。該方法需要支持一下模式的資源加載:
1. URL位置資源,如”file:C:/test.dat”
2. ClassPath位置資源,如”classpath:test.dat”
3. 相對路徑資源,如”WEB-INF/test.dat”,此時返回的Resource實例根據實現不同而不同。
ResourceLoader接口還提供了getClassLoader()方法,在加載classpath下的資源時作為參數傳入ClassPathResource。將ClassLoader暴露出來,對于想要獲取ResourceLoader使用的ClassLoader用戶來說,可以直接調用getClassLoader()方法獲得,而不是依賴于Thread Context ClassLoader,因為有些時候ResourceLoader內部使用自定義的ClassLoader。
在實際開發中經常會遇到需要通過某種匹配方式查找資源,而且可能有多個資源匹配這種模式,在Spring中提供了ResourcePatternResolver接口用于實現這種需求,該接口繼承自ResourceLoader接口,定義了自己的模式匹配接口:
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
ResourcePatternResolver定義了getResources()方法用于根據傳入的locationPattern查找和其匹配的Resource實例,并以數組的形式返回,在返回的數組中不可以存在相同的Resource實例。ResourcePatternResolver中還定義了”classpath*:”模式,用于表示查找classpath下所有的匹配Resource。
在Spring中,對ResourceLoader提供了DefaultResourceLoader、FileSystemResourceLoader和ServletContextResourceLoader等單獨實現,對ResourcePatternResolver接口則提供了PathMatchingResourcePatternResolver實現。并且ApplicationContext接口繼承了ResourcePatternResolver,在實現中,ApplicationContext的實現類會將邏輯代理給相關的單獨實現類,如PathMatchingResourceLoader等。在ApplicationContext中ResourceLoaderAware接口,可以將ResourceLoader(自身)注入到實現該接口的Bean中,在Bean中可以將其強制轉換成ResourcePatternResolver接口使用(為了安全,強轉前需要判斷)。在Spring中對ResourceLoader相關類的類圖如下:
DefaultResourceLoader類
DefaultResourceLoader是ResourceLoader的默認實現,AbstractApplicationContext繼承該類(關于這個繼承,簡單吐槽一下,Spring內部感覺有很多這種個人感覺使用組合更合適的繼承,比如還有AbstractBeanFactory繼承自FactoryBeanRegisterySupport,這個讓我看起來有點不習慣,而且也增加了類的繼承關系)。它接收ClassLoader作為構造函數的參數,或使用不帶參數的構造函數,此時ClassLoader使用默認的ClassLoader(一般為Thread Context ClassLoader),ClassLoader也可以通過set方法后繼設置。
其最主要的邏輯實現在getResource方法中,該方法首先判斷傳入的location是否以”classpath:”開頭,如果是,則創建ClassPathResource(移除”classpath:”前綴),否則嘗試創建UrlResource,如果當前location沒有定義URL的協議(即以”file:”、”zip:”等開頭,比如使用相對路徑”resources/META-INF/MENIFEST.MF),則創建UrlResource會拋出MalformedURLException,此時調用getResourceByPath()方法獲取Resource實例。getResourceByPath()方法默認返回ClassPathContextResource實例,在FileSystemResourceLoader中有不同實現。
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方法的實現邏輯和DefaultResourceLoader相同,不同的是它實現了自己的getResourceByPath方法,即當UrlResource創建失敗時,它會使用FileSystemContextResource實例而不是ClassPathContextResource:
protected Resource getResourceByPath(String path) {
if (path != null && path.startsWith("/")) {
path = path.substring(1);
}
return new FileSystemContextResource(path);
}
使用該類時要特別注意的一點:即使location以”/”開頭,資源的查找還是相對于VM啟動時的相對路徑而不是絕對路徑(從以上代碼片段也可以看出,它會先截去開頭的”/”),這個和Servlet Container保持一致。如果需要使用絕對路徑,需要添加”file:”前綴。
ServletContextResourceLoader類
ServletContextResourceLoader類繼承自DefaultResourceLoader,和FileSystemResourceLoader一樣,它的getResource方法的實現邏輯和DefaultResourceLoader相同,不同的是它實現了自己的getResourceByPath方法,即當UrlResource創建失敗時,它會使用ServletContextResource實例:
protected Resource getResourceByPath(String path) {
return new ServletContextResource(this.servletContext, path);
}
這里的path即使以”/”開頭,也是相對ServletContext的路徑,而不是絕對路徑,要使用絕對路徑,需要添加”file:”前綴。
PathMatchingResourcePatternResolver類
PathMatchingResourcePatternResolver類實現了ResourcePatternResolver接口,它包含了對ResourceLoader接口的引用,在對繼承自ResourceLoader接口的方法的實現會代理給該引用,同時在getResources()方法實現中,當找到一個匹配的資源location時,可以使用該引用解析成Resource實例。默認使用DefaultResourceLoader類,用戶可以使用構造函數傳入自定義的ResourceLoader。
PathMatchingResourcePatternResolver還包含了一個對PathMatcher接口的引用,該接口基于路徑字符串實現匹配處理,如判斷一個路徑字符串是否包含通配符(’*’、’?’),判斷給定的path是否匹配給定的pattern等。Spring提供了AntPathMatcher對PathMatcher的默認實現,表達該PathMatcher是采用Ant風格的實現。其中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是否是一個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,它只是做部分匹配,即當發現給定path匹配給定path的可能性比較大時,即返回true。在PathMatchingResourcePatternResolver中,可以先使用它確定需要全面搜索的范圍,然后在這個比較小的范圍內再找出所有的資源文件全路徑做匹配運算。
在AntPathMatcher中,都使用doMatch方法實現,match方法的fullMatch為true,而matchStart的fullMatch為false:
protected boolean doMatch(String pattern, String path, boolean fullMatch)
doMatch的基本算法如下:
1. 檢查pattern和path是否都以”/”開頭或者都不是以”/”開頭,否則,返回false。
2. 將pattern和path都以”/”為分隔符,分割成兩個字符串數組pattArray和pathArray。
3. 從頭遍歷兩個字符串數組,如果遇到兩給字符串不匹配(兩個字符串的匹配算法再下面介紹),返回false,否則,直到遇到pattArray中的”**”字符串,或pattArray和pathArray中有一個遍歷完。
4. 如果pattArray遍歷完:
a) pathArray也遍歷完,并且pattern和path都以”/”結尾或都不以”/”,返回true,否則返回false。
b) pattArray沒有遍歷完,但fullMatch為false,返回true。
c) pattArray只剩最后一個”*”,同時path以”/”結尾,返回true。
d) pattArray剩下的字符串都是”**”,返回true,否則返回false。
5. 如果pathArray沒有遍歷完,而pattArray遍歷完了,返回false。
6. 如果pathArray和pattArray都沒有遍歷完,fullMatch為false,而且pattArray下一個字符串為”**”時,返回true。
7. 從后開始遍歷pathArray和pattArray,如果遇到兩個字符串不匹配,返回false,否則,直到遇到pattArray中的”**”字符串,或pathArray和pattArray中有一個和之前的遍歷索引相遇。
8. 如果是因為pathArray與之前的遍歷索引相遇,此時,如果沒有遍歷完的pattArray所有字符串都是”**”,則返回true,否則,返回false。
9. 如果pathArray和pattArray中間都沒有遍歷完:
a) 去除pattArray中相鄰的”**”字符串,并找到其下一個”**”字符串,其索引號為pattIdxTmp,他們的距離即為s
b) 從剩下的pathArray中的第i個元素向后查找s個元素,如果找到所有s個元素都匹配,則這次查找成功,記i為temp,如果沒有找到這樣的s個元素,返回false。
c) 將pattArray的起始索引設置為pattIdxTmp,將pathArray的索引號設置為temp+s,繼續查找,直到pattArray或pathArray遍歷完。
10. 如果pattArray沒有遍歷完,但剩下的元素都是”**”,返回true,否則返回false。
對路徑字符串數組中的字符串匹配算法如下:
1. 記pattern為模式字符串,str為要匹配的字符串,將兩個字符串轉換成兩個字符數組pattArray和strArray。
2. 遍歷pattArray直到遇到’*’字符。
3. 如果pattArray中不存在’*’字符,則只有在pattArray和strArray的長度相同兩個字符數組中所有元素都相同,其中pattArray中的’?’字符可以匹配strArray中的任何一個字符,否則,返回false。
4. 如果pattArray只包含一個’*’字符,返回true
5. 遍歷pattArray和strArray直到pattArray遇到’*’字符或strArray遍歷完,如果存在不匹配的字符,返回false。
6. 如果因為strArray遍歷完成,而pattArray剩下的字符都是’*’,返回true,否則返回false
7. 從末尾開始遍歷pattArray和strArray,直到pattArray遇到’*’字符,或strArray遇到之前的遍歷索引,中間如果遇到不匹配字符,返回false
8. 如果strArray遍歷完,而剩下的pattArray字符都是’*’字符,返回true,否則返回false
9. 如果pattArray和strArray都沒有遍歷完(類似之前的算法):
a) 去除pattArray相鄰的’*’字符,查找下一個’*’字符,記其索引號為pattIdxTmp,兩個’*’字符的相隔距離為s
b) 從剩下的strArray中的第i個元素向后查找s個元素,如果有找到所有s個元素都匹配,則這次查找成功,記i為temp,如果沒有到這樣的s個元素,返回false。
c) 將pattArray的起始索引設置為pattIdxTmp,strArray的起始索引設置為temp+s,繼續查找,直到pattArray或strArray遍歷完。
10. 如果pattArray沒有遍歷完,但剩下的元素都是’*’,返回true,否則返回false
String extractPathWithinPattern(String pattern, String path):
去除path中和pattern相同的字符串,只保留匹配的字符串。比如如果pattern為”/doc/csv/*.htm”,而path為”/doc/csv/commit.htm”,則該方法的返回值為commit.htm。該方法默認pattern和path已經匹配成功,因而算法比較簡單:
以’/’分割pattern和path為兩個字符串數組pattArray和pathArray,遍歷pattArray,如果該字符串包含’*’或’?’字符,則并且pathArray的長度大于當前索引號,則將該字符串添加到結果中。
遍歷完pattArray后,如果pathArray長度大于pattArray,則將剩下的pathArray都添加到結果字符串中。
最后返回該字符串。
不過也正是因為該算法實現比較簡單,因而它的結果貌似不那么準確,比如pattern的值為:/com/**/levin/**/commit.html,而path的值為:/com/citi/cva/levin/html/commit.html,其返回結果為:citi/levin/commit.html
現在言歸正傳,看一下PathMatchingResourcePatternResolver中的getResources方法的實現:
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)};
}
}
}
對classpath下的資源,相同名字的資源可能存在多個,如果使用”classpath*:”作為前綴,表明需要找到classpath下所有該名字資源,因而需要調用findClassPathResources方法查找classpath下所有該名稱的Resource,對非classpath下的資源,對于不存在模式字符的location,一般認為一個location對應一個資源,因而直接調用ResourceLoader.getResource()方法即可(對classpath下沒有以”classpath*:”開頭的location也適用)。
findClassPathResources方法實現相對比較簡單:
適用ClassLoader.getResources()方法,遍歷結果URL集合,將每個結果適用UrlResource封裝,最后組成一個Resource數組返回即可。
對包含模式匹配字符的location來說,需要調用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中最長的沒有出現模式匹配字符的路徑
2. subPattern則表示rootDirPath之后的包含模式匹配字符的路徑信pattern
3. 使用getResources()獲取rootDirPath下的所有資源數組。
4. 遍歷這個數組。
a) 對jar中的資源,使用doFindPathMatchingJarResources()方法來查找和匹配。
b) 對非jar中資源,使用doFindPathMatchingFileResources()方法來查找和匹配。
doFindPathMatchingJarResources()實現:
1. 計算當前Resource在Jar文件中的根路徑rootEntryPath。
2. 遍歷Jar文件中所有entry,如果當前entry名以rootEntryPath開頭,并且之后的路徑信息和之前從patternLocation中截取出的subPattern使用PathMatcher匹配,若匹配成功,則調用rootDirResource.createRelative方法創建一個Resource,將新創建的Resource添加入結果集中。
doFindPathMatchingFileResources()實現:
1. 獲取要查找資源的根路徑(根路徑全名)
2. 遞歸獲得根路徑下的所有資源,使用PathMatcher匹配,如果匹配成功,則創建FileSystemResource,并將其加入到結果集中。在遞歸進入一個目錄前首先調用PathMatcher.matchStart()方法,以先簡單的判斷是否需要遞歸進去,以提升性能。
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()方法存在的限制,當傳入一個空字符串時,它只能從classpath的文件目錄下查找,而不會從Jar文件的根目錄下查找,因而對”classpath*:”前綴的資源來說,找不到Jar根路徑下的資源。即如果我們有以下定義:”classpath*:*.xml”,如果只有在Jar文件的根目錄下存在*.xml文件,那么這個pattern將返回空的Resource數組。解決方法是不要再Jar文件根目錄中放文件,可以將這些文件放到Jar文件中的resources、config等目錄下去。并且也不要在”classpath*:”之后加一些通配符,如”classpath*:**/*Enum.class”,至少在”classpath*:”后加入一個不存在通配符的路徑名。
ServletContextResourcePatternResolver類
ServletContextResourcePatternResolver類繼承自PathMatchingResourcePatternResolver類,它重寫了父類的文件查找邏輯,即對ServletContextResource資源使用ServletContext.getResourcePaths()方法來查找參數目錄下的文件,而不是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對ResourcePatternResolver接口的實現
在AbstractApplicationContext中,對ResourcePatternResolver的實現只是簡單的將getResources()方法的實現代理給resourcePatternResolver字段,而該字段默認在AbstractApplicationContext創建時新建一個PathMatchingResourcePatternResolver實例:
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);
}
文章列表