继续之前《Spring源码学习-容器初始化之FileSystemXmlApplicationContext(二)路径格式及解析方式(上)的问题

如果路径包含通配符(?,*,**)spring是怎么处理的?如果是以classpath*开头的又是如何呢?

先测试分析包含通配符(?)的。

  1. /**  
  2.  * 测试包含通配符:*,?的路径  
  3.  * <p>D:\\workspace-home\\spring-custom\\src\\main\\resources\\spring\\ap?-context.xml</p>  
  4.  * 通过读取配置文件失败的情况,因为此时Spring不支持\\路径的通配符解析  
  5.  *   
  6.  * @author lihzh  
  7.  * @date 2012-5-5 上午10:53:53  
  8.  */ 
  9. @Test 
  10. public void testAntStylePathFail() {  
  11.     String pathOne = "D:\\workspace-home\\spring-custom\\src\\main\\resources\\spring\\ap?-context.xml";  
  12.     ApplicationContext appContext = new FileSystemXmlApplicationContext(pathOne);  
  13.     assertNotNull(appContext);  
  14.     VeryCommonBean bean = null;  
  15.     try {  
  16.         bean = appContext.getBean(VeryCommonBean.class);  
  17.         fail("Should not find the [VeryCommonBean].");  
  18.     } catch (NoSuchBeanDefinitionException e) {  
  19.     }  
  20.     assertNull(bean);  

正如测试用例所写,实际是找不到该Bean的。这又是为什么?Spring不是支持通配符吗?FileSystemXmlApplicationContext的注释里也提到了通配符的情况:

  1. <p>The config location defaults can be overridden via {@link #getConfigLocations},  
  2. * Config locations can either denote concrete files like "/myfiles/context.xml"  
  3. * or Ant-style patterns like "/myfiles/*-context.xml" (see the  
  4. * {@link org.springframework.util.AntPathMatcher} javadoc for pattern details). 

 从代码中寻找答案。回到上回的else分支中,因为包含通配符,所以进入第一个子分支。

  1. /**  
  2.      * Find all resources that match the given location pattern via the  
  3.      * Ant-style PathMatcher. Supports resources in jar files and zip files  
  4.      * and in the file system.  
  5.      * @param locationPattern the location pattern to match  
  6.      * @return the result as Resource array  
  7.      * @throws IOException in case of I/O errors  
  8.      * @see #doFindPathMatchingJarResources  
  9.      * @see #doFindPathMatchingFileResources  
  10.      * @see org.springframework.util.PathMatcher  
  11.      */ 
  12.     protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {  
  13.         String rootDirPath = determineRootDir(locationPattern);  
  14.         String subPattern = locationPattern.substring(rootDirPath.length());  
  15.         Resource[] rootDirResources = getResources(rootDirPath);  
  16.         Set<Resource> result = new LinkedHashSet<Resource>(16);  
  17.         for (Resource rootDirResource : rootDirResources) {  
  18.             rootDirResource = resolveRootDirResource(rootDirResource);  
  19.             if (isJarResource(rootDirResource)) {  
  20.                 result.addAll(doFindPathMatchingJarResources(rootDirResource, subPattern));  
  21.             }  
  22.             else if (rootDirResource.getURL().getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {  
  23.                 result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirResource, subPattern, getPathMatcher()));  
  24.             }  
  25.             else {  
  26.                 result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));  
  27.             }  
  28.         }  
  29.         if (logger.isDebugEnabled()) {  
  30.             logger.debug("Resolved location pattern [" + locationPattern + "] to resources " + result);  
  31.         }  
  32.         return result.toArray(new Resource[result.size()]);  
  33.     } 

 此方法传入的完整的没有处理的路径,从第一行开始,就开始分步处理解析传入的路径,首先是决定“根”路径: determineRootDir(locationPattern) 

  1. /**  
  2.      * Determine the root directory for the given location.  
  3.      * <p>Used for determining the starting point for file matching,  
  4.      * resolving the root directory location to a <code>java.io.File</code>  
  5.      * and passing it into <code>retrieveMatchingFiles</code>, with the  
  6.      * remainder of the location as pattern.  
  7.      * <p>Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml",  
  8.      * for example.  
  9.      * @param location the location to check  
  10.      * @return the part of the location that denotes the root directory  
  11.      * @see #retrieveMatchingFiles  
  12.      */ 
  13.     protected String determineRootDir(String location) {  
  14.         int prefixEnd = location.indexOf(":") + 1;  
  15.         int rootDirEnd = location.length();  
  16.         while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {  
  17.             rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;  
  18.         }  
  19.         if (rootDirEnd == 0) {  
  20.             rootDirEnd = prefixEnd;  
  21.         }  
  22.         return location.substring(0, rootDirEnd);  
  23.     } 

这个“根”,就是不包含通配符的最长的部分,以我们的路径为例,这个“根”本来应该是: 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,
测试通过

 
刚才仅仅分析了,我们之前路径的问题所在,还有一点我想也是大家关心的,就是通配符是怎么匹配的呢?那我们就继续分析源码,回到 findPathMatchingResources 方法。

 
将路径分成包含通配符和不包含的两部分后,Spring会将根路径生成一个Resource,用的还是getResources方法。然后检查根路径的类型,是否是Jar路径?是否是VFS路径?对于我们这种普通路径,自然走到最后的分支。
  1. /**  
  2.      * Find all resources in the file system that match the given location pattern  
  3.      * via the Ant-style PathMatcher.  
  4.      * @param rootDirResource the root directory as Resource  
  5.      * @param subPattern the sub pattern to match (below the root directory)  
  6.      * @return the Set of matching Resource instances  
  7.      * @throws IOException in case of I/O errors  
  8.      * @see #retrieveMatchingFiles  
  9.      * @see org.springframework.util.PathMatcher  
  10.      */ 
  11.     protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)  
  12.             throws IOException {  
  13.  
  14.         File rootDir;  
  15.         try {  
  16.             rootDir = rootDirResource.getFile().getAbsoluteFile();  
  17.         }  
  18.         catch (IOException ex) {  
  19.             if (logger.isWarnEnabled()) {  
  20.                 logger.warn("Cannot search for matching files underneath " + rootDirResource +  
  21.                         " because it does not correspond to a directory in the file system", ex);  
  22.             }  
  23.             return Collections.emptySet();  
  24.         }  
  25.         return doFindMatchingFileSystemResources(rootDir, subPattern);  
  26.     }  
  1. /**  
  2.      * Retrieve files that match the given path pattern,  
  3.      * checking the given directory and its subdirectories.  
  4.      * @param rootDir the directory to start from  
  5.      * @param pattern the pattern to match against,  
  6.      * relative to the root directory  
  7.      * @return the Set of matching File instances  
  8.      * @throws IOException if directory contents could not be retrieved  
  9.      */ 
  10.     protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException {  
  11.         if (!rootDir.exists()) {  
  12.             // Silently skip non-existing directories.  
  13.             if (logger.isDebugEnabled()) {  
  14.                 logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist");  
  15.             }  
  16.             return Collections.emptySet();  
  17.         }  
  18.         if (!rootDir.isDirectory()) {  
  19.             // Complain louder if it exists but is no directory.  
  20.             if (logger.isWarnEnabled()) {  
  21.                 logger.warn("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory");  
  22.             }  
  23.             return Collections.emptySet();  
  24.         }  
  25.         if (!rootDir.canRead()) {  
  26.             if (logger.isWarnEnabled()) {  
  27.                 logger.warn("Cannot search for matching files underneath directory [" + rootDir.getAbsolutePath() +  
  28.                         "] because the application is not allowed to read the directory");  
  29.             }  
  30.             return Collections.emptySet();  
  31.         }  
  32.         String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");  
  33.         if (!pattern.startsWith("/")) {  
  34.             fullPattern += "/";  
  35.         }  
  36.         fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");  
  37.         Set<File> result = new LinkedHashSet<File>(8);  
  38.         doRetrieveMatchingFiles(fullPattern, rootDir, result);  
  39.         return result;  
  40.     } 
  1. /**  
  2.      * Recursively retrieve files that match the given pattern,  
  3.      * adding them to the given result list.  
  4.      * @param fullPattern the pattern to match against,  
  5.      * with prepended root directory path  
  6.      * @param dir the current directory  
  7.      * @param result the Set of matching File instances to add to  
  8.      * @throws IOException if directory contents could not be retrieved  
  9.      */ 
  10.     protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {  
  11.         if (logger.isDebugEnabled()) {  
  12.             logger.debug("Searching directory [" + dir.getAbsolutePath() +  
  13.                     "] for files matching pattern [" + fullPattern + "]");  
  14.         }  
  15.         File[] dirContents = dir.listFiles();  
  16.         if (dirContents == null) {  
  17.             if (logger.isWarnEnabled()) {  
  18.                 logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");  
  19.             }  
  20.             return;  
  21.         }  
  22.         for (File content : dirContents) {  
  23.             String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");  
  24.             if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {  
  25.                 if (!content.canRead()) {  
  26.                     if (logger.isDebugEnabled()) {  
  27.                         logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +  
  28.                                 "] because the application is not allowed to read the directory");  
  29.                     }  
  30.                 }  
  31.                 else {  
  32.                     doRetrieveMatchingFiles(fullPattern, content, result);  
  33.                 }  
  34.             }  
  35.             if (getPathMatcher().match(fullPattern, currPath)) {  
  36.                 result.add(content);  
  37.             }  
  38.         }  
  39.     } 
主要的匹配工作,是从doRetrieveMatchingFiles 方法开始的。前面的都是简单的封装过渡,在retrieveMatchingFiles中判断了下根路径是否存在、是否是文件夹、是否可读。否则都直接返回空集合。都满足了以后才进入,doRetrieveMatchingFiles 方法。在该方法中,

 
首先,列出该文件夹下的所有文件。
然后,遍历所有文件,如果仍是文件夹,递归调用doRetrieveMatchingFiles方法。如果不是,则调用getPathMatcher().match(fullPattern, currPath)进行文件名的最后匹配,将满足条件放入结果集。

 
该match方法,实际是调用了AntPathMatcher的doMatch方法,
  1. /**  
  2.      * Actually match the given <code>path</code> against the given <code>pattern</code>.  
  3.      * @param pattern the pattern to match against  
  4.      * @param path the path String to test  
  5.      * @param fullMatch whether a full pattern match is required (else a pattern match  
  6.      * as far as the given base path goes is sufficient)  
  7.      * @return <code>true</code> if the supplied <code>path</code> matched, <code>false</code> if it didn't  
  8.      */ 
  9.     protected boolean doMatch(String pattern, String path, boolean fullMatch,  
  10.             Map<String, String> uriTemplateVariables) {  
  11.  
  12.         if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {  
  13.             return false;  
  14.         }  
  15.  
  16.         String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator);  
  17.         String[] pathDirs = StringUtils.tokenizeToStringArray(path, this.pathSeparator);  
  18.  
  19.         int pattIdxStart = 0;  
  20.         int pattIdxEnd = pattDirs.length - 1;  
  21.         int pathIdxStart = 0;  
  22.         int pathIdxEnd = pathDirs.length - 1;  
  23.  
  24.         // Match all elements up to the first **  
  25.         while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {  
  26.             String patDir = pattDirs[pattIdxStart];  
  27.             if ("**".equals(patDir)) {  
  28.                 break;  
  29.             }  
  30.             if (!matchStrings(patDir, pathDirs[pathIdxStart], uriTemplateVariables)) {  
  31.                 return false;  
  32.             }  
  33.             pattIdxStart++;  
  34.             pathIdxStart++;  
  35.         }  
  36.  
  37.         if (pathIdxStart > pathIdxEnd) {  
  38.             // Path is exhausted, only match if rest of pattern is * or **'s  
  39.             if (pattIdxStart > pattIdxEnd) {  
  40.                 return (pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) :  
  41.                         !path.endsWith(this.pathSeparator));  
  42.             }  
  43.             if (!fullMatch) {  
  44.                 return true;  
  45.             }  
  46.             if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) {  
  47.                 return true;  
  48.             }  
  49.             for (int i = pattIdxStart; i <= pattIdxEnd; i++) {  
  50.                 if (!pattDirs[i].equals("**")) {  
  51.                     return false;  
  52.                 }  
  53.             }  
  54.             return true;  
  55.         }  
  56.         else if (pattIdxStart > pattIdxEnd) {  
  57.             // String not exhausted, but pattern is. Failure.  
  58.             return false;  
  59.         }  
  60.         else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {  
  61.             // Path start definitely matches due to "**" part in pattern.  
  62.             return true;  
  63.         }  
  64.  
  65.         // up to last '**'  
  66.         while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {  
  67.             String patDir = pattDirs[pattIdxEnd];  
  68.             if (patDir.equals("**")) {  
  69.                 break;  
  70.             }  
  71.             if (!matchStrings(patDir, pathDirs[pathIdxEnd], uriTemplateVariables)) {  
  72.                 return false;  
  73.             }  
  74.             pattIdxEnd--;  
  75.             pathIdxEnd--;  
  76.         }  
  77.         if (pathIdxStart > pathIdxEnd) {  
  78.             // String is exhausted  
  79.             for (int i = pattIdxStart; i <= pattIdxEnd; i++) {  
  80.                 if (!pattDirs[i].equals("**")) {  
  81.                     return false;  
  82.                 }  
  83.             }  
  84.             return true;  
  85.         }  
  86.  
  87.         while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {  
  88.             int patIdxTmp = -1;  
  89.             for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {  
  90.                 if (pattDirs[i].equals("**")) {  
  91.                     patIdxTmp = i;  
  92.                     break;  
  93.                 }  
  94.             }  
  95.             if (patIdxTmp == pattIdxStart + 1) {  
  96.                 // '**/**' situation, so skip one  
  97.                 pattIdxStart++;  
  98.                 continue;  
  99.             }  
  100.             // Find the pattern between padIdxStart & padIdxTmp in str between  
  101.             // strIdxStart & strIdxEnd  
  102.             int patLength = (patIdxTmp - pattIdxStart - 1);  
  103.             int strLength = (pathIdxEnd - pathIdxStart + 1);  
  104.             int foundIdx = -1;  
  105.  
  106.             strLoop:  
  107.             for (int i = 0; i <= strLength - patLength; i++) {  
  108.                 for (int j = 0; j < patLength; j++) {  
  109.                     String subPat = pattDirs[pattIdxStart + j + 1];  
  110.                     String subStr = pathDirs[pathIdxStart + i + j];  
  111.                     if (!matchStrings(subPat, subStr, uriTemplateVariables)) {  
  112.                         continue strLoop;  
  113.                     }  
  114.                 }  
  115.                 foundIdx = pathIdxStart + i;  
  116.                 break;  
  117.             }  
  118.  
  119.             if (foundIdx == -1) {  
  120.                 return false;  
  121.             }  
  122.  
  123.             pattIdxStart = patIdxTmp;  
  124.             pathIdxStart = foundIdx + patLength;  
  125.         }  
  126.  
  127.         for (int i = pattIdxStart; i <= pattIdxEnd; i++) {  
  128.             if (!pattDirs[i].equals("**")) {  
  129.                 return false;  
  130.             }  
  131.         }  
  132.  
  133.         return true;  
  134.     }  
比较方法如下,

 
首先,分别将输入路径和待比较路径,按照文件分隔符分割成字符串数组。(例如:{”D:“, "workspace-home", "spring-custom"...})
然后,设置好起始和结束位后,对这两个数组进行while循环(代码中第一个while循环),逐断比较匹配(matchStrings)情况。如果有一段不满足则返回fasle。

 
由于我们当前的测试路径中不包含**的部分,所以主要的判断基本都在第一个while就可以搞定。这部分工作自然是由matchStrings完成的。

 
试想一下:如果让你完成一个通配符路径匹配的功能,你会如何去做?是否自然的联想到了正则?似乎是个好选择,看看spring是怎么做的。
  1. private boolean matchStrings(String pattern, String str, Map<String, String> uriTemplateVariables) {  
  2.         AntPathStringMatcher matcher = new AntPathStringMatcher(pattern, str, uriTemplateVariables);  
  3.         return matcher.matchStrings();  
  4.     } 

在构造AntPathStringMatcher实例的时候,spring果然也创建了正则:

  1. AntPathStringMatcher(String pattern, String str, Map<String, String> uriTemplateVariables) {  
  2.         this.str = str;  
  3.         this.uriTemplateVariables = uriTemplateVariables;  
  4.         this.pattern = createPattern(pattern);  
  5.     }  
  6.  
  7. private Pattern createPattern(String pattern) {  
  8.         StringBuilder patternBuilder = new StringBuilder();  
  9.         Matcher m = GLOB_PATTERN.matcher(pattern);  
  10.         int end = 0;  
  11.         while (m.find()) {  
  12.             patternBuilder.append(quote(pattern, end, m.start()));  
  13.             String match = m.group();  
  14.             if ("?".equals(match)) {  
  15.                 patternBuilder.append('.');  
  16.             }  
  17.             else if ("*".equals(match)) {  
  18.                 patternBuilder.append(".*");  
  19.             }  
  20.             else if (match.startsWith("{") && match.endsWith("}")) {  
  21.                 int colonIdx = match.indexOf(':');  
  22.                 if (colonIdx == -1) {  
  23.                     patternBuilder.append(DEFAULT_VARIABLE_PATTERN);  
  24.                     variableNames.add(m.group(1));  
  25.                 }  
  26.                 else {  
  27.                     String variablePattern = match.substring(colonIdx + 1, match.length() - 1);  
  28.                     patternBuilder.append('(');  
  29.                     patternBuilder.append(variablePattern);  
  30.                     patternBuilder.append(')');  
  31.                     String variableName = match.substring(1, colonIdx);  
  32.                     variableNames.add(variableName);  
  33.                 }  
  34.             }  
  35.             end = m.end();  
  36.         }  
  37.         patternBuilder.append(quote(pattern, end, pattern.length()));  
  38.         return Pattern.compile(patternBuilder.toString());  
  39.     } 

 简单说,就是spring先用正则:

  1. private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}"); 
找到路径中的"?"和"*"通配符,然后转换为Java正则的任意字符"."和".*"。生成另一个正则表达式去匹配查找到的文件的路径。如果匹配则返回true。

 
至此,对于路径中包含?和*的情况解析spring的解析方式,我们已经基本了解了。本来想把**的情况一起介绍了,不过考虑的篇幅过长,我们下次再一起研究吧。

 
写在最后:所有研究均为笔者工作之余消遣之做,错误指出还望指出,欢迎各种形式和内容的探讨。

博主推荐:

Java Coder技术交流高级群:91513074

推荐文章:

 

最普通IT男-苦逼coder杂谈