Spring-統一資源載入策略
前言
在前面我們初步簡單的分析了一下BeanFactory的體系結構,第一步我們需要從配置檔案中讀取配置資訊,JDK所提供的訪問資源的類(如java.net.URL、File等),並不能很好的滿足各種底層資源的訪問需求,比如缺少從類路徑或者Web容器的上下文獲取資源的操作類。
Spring 設計了一個Resource介面,它為應用提供了更強的底層資源訪問能力。該介面擁有對應不同資源型別的實現類,而且Spring的Resource介面及其實現類可以在脫離Spring框架的情況下使用。
資源
體系結構
上圖是resource介面的部分實現類
1、WritableResource
: 可寫資源介面
2、ByteArrayResource
3、ClassPathResource
:類路徑下的資源,資源以相對於類路徑的方式表示
4、FileSystemResource
:檔案系統資源,資源以檔案系統路徑的方式表示,如C://bean.xml
5、ServletContextResource
:為訪問Web容器上下文中的資源而設計的類,負責以相對於Web應用根目錄的路徑載入資源。它支援以流和URL的方式訪問,在WAR解包的情況下,也可以通過File方式訪問。該類還可以直接從JAR包中訪問資源。
6、PathResource
:Path封裝了java.net.URL、java.nio.file.Path、檔案系統資源,它使使用者能夠訪問任何可以通過URL、Path、系統檔案路徑表示的資源,如檔案系統的資源、HTTP資源等。
Resource 介面
/** * 資源是否存在 */ boolean exists(); /** * 資源是否可讀 */ default boolean isReadable() { return true; } /** * 資源是否被打開了 */ default boolean isOpen() { return false; } default boolean isFile() { return false; } /** * 返回資源的URL的控制代碼 */ URL getURL() throws IOException; /** * 返回資源的URI的控制代碼 */ URI getURI() throws IOException; File getFile() throws IOException; /** * 通過nio channel 訪問 */ default ReadableByteChannel readableChannel() throws IOException { return Channels.newChannel(getInputStream()); } long contentLength() throws IOException; long lastModified() throws IOException; /** * 根據資源的相對路徑建立新資源 */ Resource createRelative(String relativePath) throws IOException; String getFilename(); //資源描述 String getDescription();
通過定義的介面訪問,我們可以查詢資源狀態,內容,如果需要自定義Resource,可以繼承AbstractResource
覆蓋相應的方法即可。
AbstractResource
為 Resource 介面的預設實現,它實現了 Resource 介面的大部分的公共實現,鑑於篇幅這裡就不展示原始碼,可以自行檢視。
資源載入器
資源是有了,但是如何去查詢和定位這些資源,則應該是ResourceLoader
的職責了,ResourceLoader
介面是資源查詢定位策略的統一抽象,具體的資源查詢定位策略則由相應的ResourceLoader
實現類給出。
ResourceLoader
public interface ResourceLoader {
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
Resource getResource(String location);
ClassLoader getClassLoader();
}
ResourceLoader 介面提供兩個方法:getResource()
、getClassLoader()
。
getResource()
根據所提供資源的路徑 location 返回 Resource 例項,但是它不確保該 Resource 一定存在,需要呼叫 Resource.exist()
方法判斷。該方法支援以下模式的資源載入:
- URL位置資源,如"file:C:/test.dat"
- ClassPath位置資源,如"classpath:test.dat"
- 相對路徑資源,如"WEB-INF/test.dat",此時返回的Resource例項根據實現不同而不同
該方法的主要實現是在其子類 DefaultResourceLoader 中實現,具體過程我們在分析 DefaultResourceLoader 時做詳細說明。
getClassLoader()
返回 ClassLoader 例項,對於想要獲取 ResourceLoader 使用的 ClassLoader 使用者來說,可以直接呼叫該方法來獲取.
對於想要獲取 ResourceLoader 使用的 ClassLoader 使用者來說,可以直接呼叫 getClassLoader()
方法獲得。在分析 Resource 時,提到了一個類 ClassPathResource ,這個類是可以根據指定的 ClassLoader 來載入資源的。
作為 Spring 統一的資源載入器,它提供了統一的抽象,具體的實現則由相應的子類來負責實現,下面是ResourceLoader 的部分類結構圖:
DefaultResourceLoader
DefaultResourceLoader 是 ResourceLoader 的預設實現,它接收 ClassLoader 作為建構函式的引數或者使用不帶引數的建構函式,在使用不帶引數的建構函式時,使用的 ClassLoader 為預設的 ClassLoader
//自定義資源解析,後面分析
private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);
public DefaultResourceLoader() {
this.classLoader = ClassUtils.getDefaultClassLoader();
}
public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
}
public void setClassLoader(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
@Nullable
public ClassLoader getClassLoader() {
return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
}
ResourceLoader 中最主要的方法為 getResource()
,它根據提供的 location 返回相應的 Resource,而 DefaultResourceLoader 對該方法提供了核心實現:
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
for (ProtocolResolver protocolResolver : this.protocolResolvers) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
if (location.startsWith("/")) {
return getResourceByPath(location);
}
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...
URL url = new URL(location);
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
}
}
首先通過 ProtocolResolver 來載入資源,成功返回 Resource,否則執行如下邏輯:
- 若 location 以 / 開頭,則呼叫
getResourceByPath()
構造 ClassPathContextResource 型別資源並返回。 - 若 location 以 classpath: 開頭,則構造 ClassPathResource 型別資源並返回。
- 構造 URL ,嘗試通過它進行資源定位,若沒有丟擲 MalformedURLException 異常,則判斷是否為 FileURL , 如果是則構造 FileUrlResource 型別資源,否則構造 UrlResource。若在載入過程中丟擲 MalformedURLException 異常,則委派
getResourceByPath()
實現資源定位載入。
如果通過URL沒有定位到資源,那麼將會丟擲 MalformedURLException
異常,此時會通過getResourceByPath
來進行資源查詢,在DefaultResourceLoader 中getResourceByPath 方法預設實現邏輯是構造ClassPathResource
型別的資源
protected Resource getResourceByPath(String path) {
return new ClassPathContextResource(path, getClassLoader());
}
ProtocolResolver
ProtocolResolver ,使用者自定義協議資源解決策略,它允許使用者自定義資源載入協議,而不需要繼承 ResourceLoader 的子類。在介紹 Resource 時,提到如果要實現自定義 Resource,我們只需要繼承 DefaultResource 即可,但是有了 ProtocolResolver 後,我們不需要直接繼承 DefaultResourceLoader,改為實現 ProtocolResolver 介面也可以實現自定義的 ResourceLoader。
ProtocolResolver 介面,僅有一個方法 Resource resolve(String location, ResourceLoader resourceLoader)
,該方法接收兩個引數:資源路徑location,指定的載入器 ResourceLoader,返回為相應的 Resource 。
在 Spring 中介面並沒有實現類,它需要使用者自定義,自定義的 Resolver 可以通過呼叫DefaultResourceLoader.addProtocolResolver()
新增,如下:
public void addProtocolResolver(ProtocolResolver resolver) {
Assert.notNull(resolver, "ProtocolResolver must not be null");
this.protocolResolvers.add(resolver);
}
FileSystemResourceLoader
FileSystemResourceLoader
繼承DefaultResourceLoader,DefaultResourceLoader 在最後getResourceByPath 中通過構造ClassPathContextResource 資源,在FileSystemResourceLoader 中重寫了該方法,使之從檔案系統載入資源。
@Override
protected Resource getResourceByPath(String path) {
if (path.startsWith("/")) {
path = path.substring(1);
}
return new FileSystemContextResource(path);
}
ResourcePatternResolver
ResourceLoader 的 Resource getResource(String location)
每次只能根據 location 返回一個 Resource,當需要載入多個資源時,只能多次呼叫 getResource()
。
ResourcePatternResolver 是 ResourceLoader 的擴充套件,它支援根據指定的資源路徑匹配模式每次返回多個 Resource 例項。
ResourcePatternResolver 在 ResourceLoader 的基礎上增加了 getResources(String locationPattern)
,以支援根據路徑匹配模式返回多個 Resource 例項,同時也新增了一種新的協議字首 classpath*:
,該協議字首由其子類負責實現。
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
PathMatchingResourcePatternResolver
PathMatchingResourcePatternResolver 為 ResourcePatternResolver 最常用的子類,它除了支援 ResourceLoader 和 ResourcePatternResolver 新增的 classpath: *字首外,還支援 Ant 風格的路徑匹配模式(類似於 **/.xml
)。
Ant 風格的資源地址支援3種匹配符:
- ?:匹配檔名中的一個字元
- *:匹配檔名中任意字元
- **:匹配多層路徑
至於 PathMatchingResourcePatternResolver 這裡就不進入分析了,可以自己去看看,目前我們主要還是關注Spring的體系結構。
總結
1、Spring 提供了 Resource 和 ResourceLoader 來統一抽象整個資源及其定位。 將資源的定義和資源的載入區分開了,職責更加單一和清晰。
2、DefaultResource 為 Resource 的預設實現,它對 Resource 介面做了一個統一的實現,子類繼承該類後只需要覆蓋相應的方法即可,同時對於自定義的 Resource 我們也是繼承該類。
3、DefaultResourceLoader 同樣也是 ResourceLoader 的預設實現,在自定 ResourceLoader 的時候我們除了可以繼承該類外還可以實現 ProtocolResolver 介面來實現自定資源載入協議。
4、DefaultResourceLoader 每次只能返回單一的資源,所以 Spring 針對這個提供了另外一個介面 ResourcePatternResolver ,該介面提供了根據指定的 locationPattern 返回多個資源的策略。其子類 PathMatchingResourcePatternResolver 是一個集大成者的 ResourceLoader ,因為它即實現了 Resource getResource(String location)
也實現了 Resource[] getResources(String locationPattern)
,支援 Ant 風格的路徑匹配模式。
5、在DefaultResourceLoader 中 如果定位不到資源,則最後將委派給 getResourceByPath()
實現資源定位載入,DefaultResourceLoader 中getResourceByPath 返回一個ClassPathResource 型別的資源,在FileSystemResourceLoader 中 重寫了該方法,返回一個 FileSystemResource 型別的資源。
6、ResourcePatternResolver 更多樣化的資源查詢策略,它支援根據指定的資源路徑匹配模式
每次返回多個 Resource 例項,其主要代表實現類為 PathMatchingResourcePatternResolver。
參考
Spring 揭祕
Spring 技術內幕
精通Spring 4.x 企業應用開發實戰