1. 程式人生 > >簡說Spring中的資源載入

簡說Spring中的資源載入

> 宣告: 本文若有 任何紕漏、錯誤,請不吝指正!謝謝! ## 問題描述 遇到一個關於資源載入的問題,因此簡單的記錄一下,對`Spring`資源載入也做一個記錄。 問題起因是使用了`@PropertySource`來進行配置檔案載入,配置路徑時,沒有使用關鍵字`classpath`來指明從`classpath`下面來查詢配置檔案。具體配置如下 ```java @PropertySource("config/application-download.yml", factory=YamlPropertySourceFactory) ``` 這種方式在啟動應用時,是沒問題的,正常。但是在build時,跑單元測試,出了問題,說無法從`ServletContext`中找到`/config/application-download.yml`,然後加上了`classpath`,再跑了下就沒錯誤了。 於是找到了處理`@PropertySource`的位置,跟蹤程式碼找到了差異的原因。 ## 原始碼解釋 `Spring`對於資源,做了一個抽象,那就是`Resource`,資源的載入使用資源載入器來進行載入,`ResourceLoader`就是這樣一個介面,用於定義對資源的載入行為的。 `Spring`中幾乎所有的`ApplicationContext`都實現了它,應用十分的廣泛。 除了各個`ApplicationContext`實現了它,它還有個可以獨立使用的實現,也就是一會要提到的。 ### DefaultResourceLoader 這個實現類,是一個在框架外部獨立使用版本,一般預設的都不簡單 ,這個也不例外。 無論從哪裡載入資源,使用`DefaultResourceLoader`來載入就行了 ```java // 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` ```java // 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` ```java /** * 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`不為`null`,`SpringBootTest`例項化一個`SpringBootMockServletContext`物件。 而正常情況下,在處理`@PropertySource`時,還沒能初始化一個`ServletContext`,因為 `@PropertySource`的處理是在`BeanDefinitionRegistryPostProcessor`執行時處理的,早於`SpringBoot`去初始化`Servlet`容器。SpringBoot建立Servlet容器是在這裡`org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh`,它的執行時機是晚於處理 `BeanFactoryPostProcessor`的`org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors`,所以 正常執行應用,肯定只會建立一個`ClassPathContextResource`資源物件,而配置檔案在`classpath`下是存在的,所以可以搜尋到。 ## 結論 結論就是不知道`SpringBootTest`是故意為之呢還是出於什麼別的考慮,也不知道除了加上`classpath`字首外是否有別的方式能解決這個問題。 不過現在看來,偷懶是不可能的呢了 ,老老實實的 把字首`classpath`給加上,就不會有