宣告: 本文若有 任何紕漏、錯誤,請不吝指正!謝謝!
問題描述
遇到一個關於資源載入的問題,因此簡單的記錄一下,對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
不為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
給加上,就不會有問題了