簡說Spring中的資源加載

聲明: 本文若有 任何紕漏、錯誤,請不吝指正!謝謝!

問題描述

遇到一個關於資源加載的問題,因此簡單的記錄一下,對Spring資源加載也做一個記錄。

問題起因是使用了@PropertySource來進行配置文件加載,配置路徑時,沒有使用關鍵字classpath來指明從classpath下面來查找配置文件。具體配置如下

 @PropertySource("config/application-download.yml", factory=YamlPropertySourceFactory)

這種方式在啓動應用時,是沒問題的,正常。但是在build時,跑單元測試,出了問題,說無法從ServletContext中找到/config/application-download.yml,然後加上了classpath,再跑了下就沒錯誤了。

於是找到了處理@PropertySource的位置,跟蹤代碼找到了差異的原因。

源碼解釋

Spring對於資源,做了一個抽象,那就是Resource,資源的加載使用資源加載器來進行加載,ResourceLoader就是這樣一個接口,用於定義對資源的加載行爲的。

Spring中幾乎所有的ApplicationContext都實現了它,應用十分的廣泛。

除了各個ApplicationContext實現了它,它還有個可以獨立使用的實現,也就是一會要提到的。

DefaultResourceLoader

這個實現類,是一個在框架外部獨立使用版本,一般默認的都不簡單 ,這個也不例外。

無論從哪裏加載資源,使用DefaultResourceLoader來加載就行了

// org.springframework.core.io.DefaultResourceLoader#getResource
@Override
public Resource getResource(String location) {
   Assert.notNull(location, "Location must not be null");

   // 這個是提供的SPI使用的,沒有采用子類實現的方式 
   for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
      Resource resource = protocolResolver.resolve(location, this);
      if (resource != null) {
         return resource;
      }
   }
   // 如果以/開頭,使用純路徑的方式,比如./config.properties
   if (location.startsWith("/")) {
      return getResourceByPath(location);
   }
   // 如果以classpath:開頭,創建一個ClassPathResource資源對象 
   // 底層使用的是Class#getResourceAsStream,ClassLoader#getResourceAsStream
   // 或者 ClassLoader#getSystemResourceAsStream,具體有機會再詳細解釋下這些
   else 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...
         // 如果上面的判斷不滿足,直接使用java.net.URL來生成一個URL對象,
         // 如果location爲null,或者location沒有指定協議,或者協議不能被識別
         // 就會拋出異常
         URL url = new URL(location);
         //file:開頭的 會使用創建一個FileUrlResource
         return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
      }
      catch (MalformedURLException ex) {
         // 沒有指定協議
         return getResourceByPath(location);
      }
   }
}
protected Resource getResourceByPath(String path) {
  	// ClassPathResource的子類
	return new ClassPathContextResource(path, getClassLoader());
}

這個類在Spring中被廣泛使用,或者更具體的說,這個類的getResource方法,幾乎遇到資源相關的加載動作都會調用到它。

各個ApplicationContext應該是加載資源最多的地方了,而AbstractApplicationContext正是繼承了DefaultResourceLoader,纔有了這中加載資源的能力。

不過DefaultResourceLoader也留給了子類的擴展點,主要是通過重寫getResourceByPath這個方法。這裏是繼承的方式,也可以重寫 getResource方法,這個方法在GenericApplicationContext中被重寫了, 不過也沒有做過多的操作,這裏主要是可以在一個context中設置自己的資源加載器,一旦設置了,會將 ApplicationContext中所有的資源委託給它加載,一般不會有這個操作 。

遇到的問題 ,正是因爲子類對 getResourceByPath的重寫 ,導致了不一樣的行爲。

經過跟蹤源碼發現,正常啓動應用的時候,實例化的是一個 AnnotationConfigServletWebServerApplicationContext實例 ,這個類繼承自ServletWebServerApplicationContext,在ServletWebServerApplicationContext中重寫了getResourceByPath

//  org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#getResourceByPath
@Override
protected Resource getResourceByPath(String path) {
   if (getServletContext() == null) {
      // ServletContext爲null,從classpath去查找
      return new ClassPathContextResource(path, getClassLoader());
   }
   // 否則從ServletContext去查找
   return new ServletContextResource(getServletContext(), path);
}

而通過 Debug發現,在使用SpirngBootTest執行單元測試,它實例化的是org.springframework.web.context.support.GenericWebApplicationContext

/**
 * This implementation supports file paths beneath the root of the ServletContext.
 * @see ServletContextResource
 * 這裏就是直接從ServletContext中去查找資源,一般就是webapp目錄下。
 */
@Override
protected Resource getResourceByPath(String path) {
   Assert.state(this.servletContext != null, "No ServletContext available");
   return new ServletContextResource(this.servletContext, path);
}

並且這裏ServletContext不爲nullSpringBootTest實例化一個SpringBootMockServletContext對象。

而正常情況下,在處理@PropertySource時,還沒能初始化一個ServletContext,因爲 @PropertySource的處理是在BeanDefinitionRegistryPostProcessor執行時處理的,早於SpringBoot去初始化Servlet容器。SpringBoot創建Servlet容器是在這裏org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh,它的執行時機是晚於處理 BeanFactoryPostProcessororg.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors,所以 正常運行應用,肯定只會創建一個ClassPathContextResource資源對象,而配置文件在classpath下是存在的,所以可以搜索到。

結論

結論就是不知道SpringBootTest是故意爲之呢還是出於什麼別的考慮,也不知道除了加上classpath前綴外是否有別的方式能解決這個問題。

不過現在看來,偷懶是不可能的呢了 ,老老實實的 把前綴classpath給加上,就不會有問題了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章