了解完了构造函数,我们回到上节《Spring源码学习-容器初始化之FileSystemXmlApplicationContext(一)构造函数》留下的思考的问题:

  1. 支持路径格式的研究。(绝对?相对?通配符?classpath格式又如何?)
  2. 配合placeholder使用的路径问题研究。 
  3. 路径如何解析?

下面,我们就来一一验证和解答。

先放出本次测试用的配置文件(app-context和test.properties):
  1. <bean id="placeHolderConfig" 
  2.  class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> 
  3.  <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE" />  
  4.  <property name="locations"> 
  5.  <list> 
  6.  <value>classpath*:spring/test.properties</value> 
  7.  </list> 
  8.  </property> 
  9.  </bean> 
  10.  <bean id="veryCommonBean" class="kubi.coder.bean.VeryCommonBean"> 
  11.  <property name="name" value="${test.name}"></property> 
  12.  </bean> 

  1. test.name=verycommonbean-name 

首先想到的自然是最普通的绝对路径

  1. /** 
  2.   * 测试通过普通的绝对路径: 
  3.   * <p>D:\\workspace-home\\spring-custom\\src\\main\\resources\\spring\\app-context.xml</p> 
  4.   * 读取配置文件 
  5.   *  
  6.   * @author lihzh 
  7.   * @date 2012-5-5 上午10:53:53 
  8.   */ 
  9.  @Test 
  10.  public void testPlainAbsolutePath() { 
  11.  String path = "D:\\workspace-home\\spring-custom\\src\\main\\resources\\spring\\app-context.xml"
  12.  ApplicationContext appContext = new FileSystemXmlApplicationContext(path); 
  13.  assertNotNull(appContext); 
  14.  VeryCommonBean bean = appContext.getBean(VeryCommonBean.class); 
  15.  assertNotNull(bean); 
  16.  assertEquals("verycommonbean-name", bean.getName()); 
  17.  } 

测试通过,我们来看下Spring是怎么找到该文件的。之前已经说过refresh这个函数,是Spring生命周期的开始,我们就以它为入口,顺藤摸瓜,时序图如下:

最终,我们找到解析路径的关键方法,PathMatchingResourcePatternResolver的getResources方法和DefaultResourceLoader中的getResource方法:

  1. public Resource[] getResources(String locationPattern) throws IOException { 
  2.  Assert.notNull(locationPattern, "Location pattern must not be null"); 
  3.  if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) { 
  4.  // a class path resource (multiple resources for same name possible) 
  5.  if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) { 
  6.  // a class path resource pattern 
  7.  return findPathMatchingResources(locationPattern); 
  8.  } 
  9.  else { 
  10.  // all class path resources with the given name 
  11.  return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); 
  12.  } 
  13.  } 
  14.  else { 
  15.  // Only look for a pattern after a prefix here 
  16.  // (to not get fooled by a pattern symbol in a strange prefix). 
  17.  int prefixEnd = locationPattern.indexOf(":") + 1
  18.  if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) { 
  19.  // a file pattern 
  20.  return findPathMatchingResources(locationPattern); 
  21.  } 
  22.  else { 
  23.  // a single resource with the given name 
  24.  return new Resource[] {getResourceLoader().getResource(locationPattern)}; 
  25.  } 
  26.  } 
  27.  } 

  1. public Resource getResource(String location) { 
  2.  Assert.notNull(location, "Location must not be null"); 
  3.  if (location.startsWith(CLASSPATH_URL_PREFIX)) { 
  4.  return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); 
  5.  } 
  6.  else { 
  7.  try { 
  8.  // Try to parse the location as a URL... 
  9.  URL url = new URL(location); 
  10.  return new UrlResource(url); 
  11.  } 
  12.  catch (MalformedURLException ex) { 
  13.  // No URL -> resolve as resource path. 
  14.  return getResourceByPath(location); 
  15.  } 
  16.  } 
  17.  } 

 其中常量

CLASSPATH_ALL_URL_PREFIX = "classpath*:";
CLASSPATH_URL_PREFIX = "classpath:";
我们输入的路径是绝对路径:"D:\\workspace-home\\spring-custom\\src\\main\\resources\\spring\\app-context.xml"。不是以classpath*开头的,所以会落入else之中。在else中:getPathMatcher().isPattern(),实际是调用AntPathMatcher中的isPattern()方法:
  1. public boolean isPattern(String path) { 
  2.         return (path.indexOf('*') != -1 || path.indexOf('?') != -1); 
  3.     } 
是用来判断":"以后的路径中是否包含通配符“*”或者 "?"。
我们的路径显然也不包含,所以最终会直接走入getResource方法。
仍然,路径既不是以classpath开头的,也不是URL格式的路径,所以最终会落入 getResourceByPath(location)这个分支,而我们之前介绍过,这个方法恰好是在FileSystemXmlApplicationContext这个类中复写过的:
  1. protected Resource getResourceByPath(String path) { 
  2.  if (path != null && path.startsWith("/")) { 
  3.  path = path.substring(1); 
  4.  } 
  5.  return new FileSystemResource(path); 
  6.  } 

 我们给的路径不是以"/"开头,所以直接构造了一个FileSystemResource:

  1. public FileSystemResource(String path) { 
  2.  Assert.notNull(path, "Path must not be null"); 
  3.  this.file = new File(path); 
  4.  this.path = StringUtils.cleanPath(path); 
  5.  } 

 

即用路径直接构造了一个File。这里StringUtil.cleanPath方法:
主要是将传入的路径规范化,比如将windows的路径分隔符“\\”替换为标准的“/“,如果路径中含有.(当前文件夹),或者..(上层文件夹),则计算出其真实路径。而File本身是支持这样的路径的,也就是说,spring可以支持这样的路径。出于好奇,我们也针对这个方法测试如下:
  1. /** 
  2.   * 测试通过含有.或者..的绝对路径 
  3.   * <p>D:\\workspace-home\\spring-custom\\.\\src\\main\\resources\\spring\\..\\spring\\app-context.xml</p> 
  4.   * 读取配置文件 
  5.   *  
  6.   * @author lihzh 
  7.   * @date 2012-5-5 上午10:53:53 
  8.   */ 
  9.  @Test 
  10.  public void testContainDotAbsolutePath() { 
  11.  String path = "D:\\workspace-home\\spring-custom\\.\\src\\main\\resources\\spring\\..\\spring\\app-context.xml"
  12.  ApplicationContext appContext = new FileSystemXmlApplicationContext(path); 
  13.  assertNotNull(appContext); 
  14.  VeryCommonBean bean = appContext.getBean(VeryCommonBean.class); 
  15.  assertNotNull(bean); 
  16.  assertEquals("verycommonbean-name", bean.getName()); 
  17.  } 

容器可以正常初始化。路径计算正确。
 
补充说明:Spring最终读取配置文件,是通过InputStream加载的,Spring中的各种Resource的最上层接口InputStreamResource中定义了唯一的一个方法getInputStream。也就是说,只要保证各Resource的实现类的getInputStream方法能够正常获取流,Spring容器即可解析初始化。对于FileSystemResource而已,其实现如下:
  1. /** 
  2.   * This implementation opens a FileInputStream for the underlying file. 
  3.   * @see java.io.FileInputStream 
  4.   */ 
  5.  public InputStream getInputStream() throws IOException { 
  6.  return new FileInputStream(this.file); 
  7.  } 

所以,我们说,此时只有是File正常支持的格式,Spring才能正常初始化。
 
继续回到前面的话题。我们目前只验证else分支中的catch分支。根据代码分析,即使是FileSystemXmlApplicationContext也可以支持Classpath格式的路径和URL格式的路径的。验证如下:
  1. /** 
  2.   * 测试通过含有.或者..的绝对路径 
  3.   * <p>file:/D:\\workspace-home\\spring-custom\\src\\main\\resources\\spring\\app-context.xml</p> 
  4.   * 读取配置文件 
  5.   *  
  6.   * @author lihzh 
  7.   * @date 2012-5-5 上午10:53:53 
  8.   */ 
  9.  @Test 
  10.  public void testURLAbsolutePath() { 
  11.  String path = "file:/D:\\workspace-home\\spring-custom\\src\\main\\resources\\spring\\app-context.xml"
  12.  ApplicationContext appContext = new FileSystemXmlApplicationContext(path); 
  13.  assertNotNull(appContext); 
  14.  VeryCommonBean bean = appContext.getBean(VeryCommonBean.class); 
  15.  assertNotNull(bean); 
  16.  assertEquals("verycommonbean-name", bean.getName()); 
  17.  } 
  18.   
  19.  /** 
  20.   * 测试通过Classpath类型的路径 
  21.   * <p>classpath:spring/app-context.xml</p> 
  22.   * 通过读取配置文件 
  23.   *  
  24.   * @author lihzh 
  25.   * @date 2012-5-5 上午10:53:53 
  26.   */ 
  27.  @Test 
  28.  public void testClassPathStylePath() { 
  29.  String path = "classpath:spring/app-context.xml"
  30.  ApplicationContext appContext = new FileSystemXmlApplicationContext(path); 
  31.  assertNotNull(appContext); 
  32.  VeryCommonBean bean = appContext.getBean(VeryCommonBean.class); 
  33.  assertNotNull(bean); 
  34.  assertEquals("verycommonbean-name", bean.getName()); 
  35.  } 
 

验证通过,并且通过debug确认,确实走入了相应的分支,分别构造了UrlResource和ClassPathResource实例。所以,之后Spring会分别调用这个两个Resource中的getInputStream方法获取流,解析配置文件。附上这两个类中的getInputStream方法,有兴趣的可以继续研究:

  1.        /** 
  2.  * This implementation opens an InputStream for the given URL. 
  3.  * It sets the "UseCaches" flag to <code>false</code>, 
  4.  * mainly to avoid jar file locking on Windows. 
  5.  * @see java.net.URL#openConnection() 
  6.  * @see java.net.URLConnection#setUseCaches(boolean) 
  7.  * @see java.net.URLConnection#getInputStream() 
  8.  */ 
  9. public InputStream getInputStream() throws IOException { 
  10. URLConnection con = this.url.openConnection(); 
  11. ResourceUtils.useCachesIfNecessary(con); 
  12. try { 
  13. return con.getInputStream(); 
  14. catch (IOException ex) { 
  15. // Close the HTTP connection (if applicable). 
  16. if (con instanceof HttpURLConnection) { 
  17. ((HttpURLConnection) con).disconnect(); 
  18. throw ex; 
  19.  
  20.        /** 
  21.  * This implementation opens an InputStream for the given class path resource. 
  22.  * @see java.lang.ClassLoader#getResourceAsStream(String) 
  23.  * @see java.lang.Class#getResourceAsStream(String) 
  24.  */ 
  25. public InputStream getInputStream() throws IOException { 
  26. InputStream is; 
  27. if (this.clazz != null) { 
  28. is = this.clazz.getResourceAsStream(this.path); 
  29. else { 
  30. is = this.classLoader.getResourceAsStream(this.path); 
  31. if (is == null) { 
  32. throw new FileNotFoundException( 
  33. getDescription() + " cannot be opened because it does not exist"); 
  34. return is; 

上述两个实现所属的类,我想应该一目了然吧~~
 
至此,我们算是分析验证通过了一个小分支下的支持的路径的情况,其实,这只是这些都是最简单直接的。回想刚才的分析,如果路径包含通配符(?,*)spring是怎么处理的?如果是以classpath*开头的又是如何呢??鉴于害怕文章过长,我们下回分解…………o(∩_∩)o