1. 程式人生 > >Spring原始碼學習

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方法,實際是呼叫了AntPathMatcherdoMatch方法,
   /**
	 * 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的解析方式,我們已經基本瞭解了。本來想把**的情況一起介紹了,不過考慮的篇幅過長,我們下次再一起研究吧。

寫在最後:所有研究均為筆者