Spring原始碼學習
先測試分析包含萬用字元(?)的。
/**
* 測試包含萬用字元:*,?的路徑
* <p>;D:\\workspace-home\\spring-custom\\src\\main\\resources\\spring\\ap?-context.xml</p>;
* 通過讀取配置檔案失敗的情況,因為此時Spring不支援\\路徑的萬用字元解析
*
* @author lihzh
* @date 2012-5-5 上午10:53:53
*/
@Test
public void testAntStylePathFail() {
String pathOne = "D:\\workspace-home\\spring-custom\\src\\main\\resources\\spring\\ap?-context.xml";
ApplicationContext appContext = new FileSystemXmlApplicationContext(pathOne);
assertNotNull(appContext);
VeryCommonBean bean = null;
try {
bean = appContext.getBean(VeryCommonBean.class);
fail("Should not find the [VeryCommonBean]." );
} catch (NoSuchBeanDefinitionException e) {
}
assertNull(bean);
}
該測試用例是可以正常通過測試,也就是是找不到該Bean的。這又是為什麼? Spring不是支援萬用字元嗎?FileSystemXmlApplicationContext的註釋裡也提到了萬用字元的情況:
* <p>;The config location defaults can be overridden via {@link #getConfigLocations},
* Config locations can either denote concrete files like "/myfiles/context.xml"
* or Ant-style patterns like "/myfiles/*-context.xml" (see the
* {@link org.springframework.util.AntPathMatcher} javadoc for pattern details).
從程式碼中尋找答案。回到上回的else分支中,因為包含萬用字元,所以進入第一個子分支。
/**
* Find all resources that match the given location pattern via the
* Ant-style PathMatcher. Supports resources in jar files and zip files
* and in the file system.
* @param locationPattern the location pattern to match
* @return the result as Resource array
* @throws IOException in case of I/O errors
* @see #doFindPathMatchingJarResources
* @see #doFindPathMatchingFileResources
* @see org.springframework.util.PathMatcher
*/
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
String rootDirPath = determineRootDir(locationPattern);
String subPattern = locationPattern.substring(rootDirPath.length());
Resource[] rootDirResources = getResources(rootDirPath);
Set<Resource>; result = new LinkedHashSet<Resource>;(16);
for (Resource rootDirResource : rootDirResources) {
rootDirResource = resolveRootDirResource(rootDirResource);
if (isJarResource(rootDirResource)) {
result.addAll(doFindPathMatchingJarResources(rootDirResource, subPattern));
}
else if (rootDirResource.getURL().getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirResource, subPattern, getPathMatcher()));
}
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()]);
}
此方法傳入的完整的沒有處理的路徑,從第一行開始,就開始分步處理解析傳入的路徑,首先是決定根路徑: determineRootDir(locationPattern)
/**
* Determine the root directory for the given location.
* <p>;Used for determining the starting point for file matching,
* resolving the root directory location to a <code>;java.io.File</code>;
* and passing it into <code>;retrieveMatchingFiles</code>;, with the
* remainder of the location as pattern.
* <p>;Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml",
* for example.
* @param location the location to check
* @return the part of the location that denotes the root directory
* @see #retrieveMatchingFiles
*/
protected String determineRootDir(String location) {
int prefixEnd = location.indexOf(":") + 1;
int rootDirEnd = location.length();
while (rootDirEnd >; prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
}
if (rootDirEnd == 0) {
rootDirEnd = prefixEnd;
}
return location.substring(0, rootDirEnd);
}
這個”根”,就是不包含萬用字元的最長的部分,以我們的路徑為例,這個”根”本來應該是: D:\workspace-home\spring-custom\src\main\resources\spring\,但是實際上,從determineRootDir的實現可以看出,
首先,先找到冒號’:’索引位,賦值給 prefixEnd。
然後,在從冒號開始到最後的字串中,迴圈判斷是否包含萬用字元,如果包含,則截斷最後一個由”/”分割的部分,例如:在我們路徑中,就是最後的ap?-context.xml這一段。再迴圈判斷剩下的部分,直到剩下的路徑中都不包含萬用字元。
如果查詢完成後,rootDirEnd=0了,則將之前賦值的prefixEnd的值賦給rootDirEnd,也就是”:”所在的索引位。
最後,將字串從開始截斷rootDirEnd。
我們的問題,就出在關鍵的第二步,Spring這裡只在字串中查詢”/”,並沒有支援”\\“這樣的路徑分割方式,所以,自然找不到”\\“,rootDirEnd = -1 + 1 = 0。所以迴圈後,階段出來的路徑就是D:自然Spring會找不到配置檔案,容器無法初始化
基於以上分析,我們將路徑修改為:D:/workspace-home/spring-custom/src/main/resources/spring/ap?-context.xml,再測試如下:
/**
* 測試包含萬用字元:*,?的路徑
* <p>;D:/workspace-home/spring-custom/src/main/resources/spring/ap?-context.xml</p>;
* 通過讀取配置檔案
*
* @author lihzh
* @date 2012-5-5 上午10:53:53
*/
@Test
public void testAntStylePath() {
String pathOne = "D:/workspace-home/spring-custom/src/main/resources/spring/ap?-context.xml";
ApplicationContext appContext = new FileSystemXmlApplicationContext(pathOne);
assertNotNull(appContext);
VeryCommonBean bean = appContext.getBean(VeryCommonBean.class);
assertNotNull(bean);
assertEquals("verycommonbean-name", bean.getName());
}
測試通過。
剛才僅僅分析了,我們之前路徑的問題所在,還有一點我想也是大家關心的,就是萬用字元是怎麼匹配的呢?那我們就繼續分析原始碼,回到 findPathMatchingResources方法。
將路徑分成包含萬用字元和不包含的兩部分後,Spring會將根路徑生成一個Resource,用的還是getResources方法。然後檢查根路徑的型別,是否是Jar路徑?是否是VFS路徑?對於我們這種普通路徑,自然走到最後的分支。
/**
* Find all resources in the file system that match the given location pattern
* via the Ant-style PathMatcher.
* @param rootDirResource the root directory as Resource
* @param subPattern the sub pattern to match (below the root directory)
* @return the Set of matching Resource instances
* @throws IOException in case of I/O errors
* @see #retrieveMatchingFiles
* @see org.springframework.util.PathMatcher
*/
protected Set<Resource>; doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
throws IOException {
File rootDir;
try {
rootDir = rootDirResource.getFile().getAbsoluteFile();
}
catch (IOException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Cannot search for matching files underneath " + rootDirResource +
" because it does not correspond to a directory in the file system", ex);
}
return Collections.emptySet();
}
return doFindMatchingFileSystemResources(rootDir, subPattern);
}
/**
* Find all resources in the file system that match the given location pattern
* via the Ant-style PathMatcher.
* @param rootDir the root directory in the file system
* @param subPattern the sub pattern to match (below the root directory)
* @return the Set of matching Resource instances
* @throws IOException in case of I/O errors
* @see #retrieveMatchingFiles
* @see org.springframework.util.PathMatcher
*/
protected Set<Resource>; doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
if (logger.isDebugEnabled()) {
logger.debug("Looking for matching resources in directory tree [" + rootDir.getPath() + "]");
}
Set<File>; matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
Set<Resource>; result = new LinkedHashSet<Resource>;(matchingFiles.size());
for (File file : matchingFiles) {
result.add(new FileSystemResource(file));
}
return result;
}
/**
* Retrieve files that match the given path pattern,
* checking the given directory and its subdirectories.
* @param rootDir the directory to start from
* @param pattern the pattern to match against,
* relative to the root directory
* @return the Set of matching File instances
* @throws IOException if directory contents could not be retrieved
*/
protected Set<File>; retrieveMatchingFiles(File rootDir, String pattern) throws IOException {
if (!rootDir.exists()) {
// Silently skip non-existing directories.
if (logger.isDebugEnabled()) {
logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist");
}
return Collections.emptySet();
}
if (!rootDir.isDirectory()) {
// Complain louder if it exists but is no directory.
if (logger.isWarnEnabled()) {
logger.warn("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory");
}
return Collections.emptySet();
}
if (!rootDir.canRead()) {
if (logger.isWarnEnabled()) {
logger.warn("Cannot search for matching files underneath directory [" + rootDir.getAbsolutePath() +
"] because the application is not allowed to read the directory");
}
return Collections.emptySet();
}
String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
if (!pattern.startsWith("/")) {
fullPattern += "/";
}
fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");
Set<File>; result = new LinkedHashSet<File>;(8);
doRetrieveMatchingFiles(fullPattern, rootDir, result);
return result;
}
/**
* Recursively retrieve files that match the given pattern,
* adding them to the given result list.
* @param fullPattern the pattern to match against,
* with prepended root directory path
* @param dir the current directory
* @param result the Set of matching File instances to add to
* @throws IOException if directory contents could not be retrieved
*/
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File>; result) throws IOException {
if (logger.isDebugEnabled()) {
logger.debug("Searching directory [" + dir.getAbsolutePath() +
"] for files matching pattern [" + fullPattern + "]");
}
File[] dirContents = dir.listFiles();
if (dirContents == null) {
if (logger.isWarnEnabled()) {
logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
}
return;
}
for (File content : dirContents) {
String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
if (!content.canRead()) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
"] because the application is not allowed to read the directory");
}
}
else {
doRetrieveMatchingFiles(fullPattern, content, result);
}
}
if (getPathMatcher().match(fullPattern, currPath)) {
result.add(content);
}
}
}
主要的匹配工作,是從doRetrieveMatchingFiles方法開始的。前面的都是簡單的封裝過渡,在retrieveMatchingFiles中判斷了下根路徑是否存在、是否是資料夾、是否可讀。否則都直接返回空集合。都滿足了以後才進入,doRetrieveMatchingFiles方法。在該方法中
- 首先,列出該資料夾下的所有檔案。
- 然後,遍歷所有檔案,如果仍是資料夾,遞迴呼叫doRetrieveMatchingFiles方法。如果不是,則呼叫getPathMatcher().match(fullPattern, currPath)進行檔名的最後匹配,將滿足條件放入結果集。該match方法,實際是呼叫了AntPathMatcher的doMatch方法,
/**
* Actually match the given <code>;path</code>; against the given <code>;pattern</code>;.
* @param pattern the pattern to match against
* @param path the path String to test
* @param fullMatch whether a full pattern match is required (else a pattern match
* as far as the given base path goes is sufficient)
* @return <code>;true</code>; if the supplied <code>;path</code>; matched, <code>;false</code>; if it didn't
*/
protected boolean doMatch(String pattern, String path, boolean fullMatch,
Map<String, String>; uriTemplateVariables) {
if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {
return false;
}
String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator);
String[] pathDirs = StringUtils.tokenizeToStringArray(path, this.pathSeparator);
int pattIdxStart = 0;
int pattIdxEnd = pattDirs.length - 1;
int pathIdxStart = 0;
int pathIdxEnd = pathDirs.length - 1;
// Match all elements up to the first **
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
String patDir = pattDirs[pattIdxStart];
if ("**".equals(patDir)) {
break;
}
if (!matchStrings(patDir, pathDirs[pathIdxStart], uriTemplateVariables)) {
return false;
}
pattIdxStart++;
pathIdxStart++;
}
if (pathIdxStart >; pathIdxEnd) {
// Path is exhausted, only match if rest of pattern is * or **'s
if (pattIdxStart >; pattIdxEnd) {
return (pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) :
!path.endsWith(this.pathSeparator));
}
if (!fullMatch) {
return true;
}
if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {
return true;
}
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
else if (pattIdxStart >; pattIdxEnd) {
// String not exhausted, but pattern is. Failure.
return false;
}
else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {
// Path start definitely matches due to "**" part in pattern.
return true;
}
// up to last '**'
while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {
String patDir = pattDirs[pattIdxEnd];
if (patDir.equals("**")) {
break;
}
if (!matchStrings(patDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {
return false;
}
pattIdxEnd--;
pathIdxEnd--;
}
if (pathIdxStart >; pathIdxEnd) {
// String is exhausted
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {
int patIdxTmp = -1;
for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {
if (pattDirs[i].equals("**")) {
patIdxTmp = i;
break;
}
}
if (patIdxTmp == pattIdxStart + 1) {
// '**/**' situation, so skip one
pattIdxStart++;
continue;
}
// Find the pattern between padIdxStart & padIdxTmp in str between
// strIdxStart & strIdxEnd
int patLength = (patIdxTmp - pattIdxStart - 1);
int strLength = (pathIdxEnd - pathIdxStart + 1);
int foundIdx = -1;
strLoop:
for (int i = 0; i <= strLength - patLength; i++) {
for (int j = 0; j < patLength; j++) {
String subPat = pattDirs[pattIdxStart + j + 1];
String subStr = pathDirs[pathIdxStart + i + j];
if (!matchStrings(subPat, subStr, uriTemplateVariables)) {
continue strLoop;
}
}
foundIdx = pathIdxStart + i;
break;
}
if (foundIdx == -1) {
return false;
}
pattIdxStart = patIdxTmp;
pathIdxStart = foundIdx + patLength;
}
for (int i = pattIdxStart; i <= pattIdxEnd; i++) {
if (!pattDirs[i].equals("**")) {
return false;
}
}
return true;
}
比較方法如下
- 首先,分別將輸入路徑和待比較路徑,按照檔案分隔符分割成字串陣列。(例如:{”D:”, “workspace-home”, “spring-custom”…})
- 然後,設定好起始和結束位後,對這兩個陣列進行while迴圈(程式碼中第一個while迴圈),逐斷比較匹配(matchStrings)情況。如果有一段不滿足則返回fasle。
由於我們當前的測試路徑中不包含的部分,所以主要的判斷基本都在第一個while就可以搞定。這部分工作自然是由matchStrings**完成的。
如果讓你完成一個萬用字元路徑匹配的功能,你會如何去做?是否自然的聯想到了正則?似乎是個好選擇,看看spring是怎麼做的。
private boolean matchStrings(String pattern, String str, Map<String, String>; uriTemplateVariables) {
AntPathStringMatcher matcher = new AntPathStringMatcher(pattern, str, uriTemplateVariables);
return matcher.matchStrings();
}
在構造AntPathStringMatcher例項的時候,spring果然也建立了正則:
AntPathStringMatcher(String pattern, String str, Map<String, String>; uriTemplateVariables) {
this.str = str;
this.uriTemplateVariables = uriTemplateVariables;
this.pattern = createPattern(pattern);
}
private Pattern createPattern(String pattern) {
StringBuilder patternBuilder = new StringBuilder();
Matcher m = GLOB_PATTERN.matcher(pattern);
int end = 0;
while (m.find()) {
patternBuilder.append(quote(pattern, end, m.start()));
String match = m.group();
if ("?".equals(match)) {
patternBuilder.append('.');
}
else if ("*".equals(match)) {
patternBuilder.append(".*");
}
else if (match.startsWith("{") && match.endsWith("}")) {
int colonIdx = match.indexOf(':');
if (colonIdx == -1) {
patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
variableNames.add(m.group(1));
}
else {
String variablePattern = match.substring(colonIdx + 1, match.length() - 1);
patternBuilder.append('(');
patternBuilder.append(variablePattern);
patternBuilder.append(')');
String variableName = match.substring(1, colonIdx);
variableNames.add(variableName);
}
}
end = m.end();
}
patternBuilder.append(quote(pattern, end, pattern.length()));
return Pattern.compile(patternBuilder.toString());
}
簡單說,就是spring先用正則:
private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}");
找到路徑中的”?”和”*“萬用字元,然後轉換為Java正則的任意字元”.”和”.*“。生成另一個正則表示式去匹配查詢到的檔案的路徑。如果匹配則返回true。
至此,對於路徑中包含?和*的情況解析spring的解析方式,我們已經基本瞭解了。本來想把**的情況一起介紹了,不過考慮的篇幅過長,我們下次再一起研究吧。
寫在最後:所有研究均為筆者