在學 Java SE 的時候我們學習了一個標準類 java.net.URL
,該類在 Java SE 中的定位爲統一資源定位器(Uniform Resource Locator),但是我們知道它的實現基本只限於網絡形式發佈的資源的查找和定位。然而,實際上資源的定義比較廣泛,除了網絡形式的資源,還有以二進制形式存在的、以文件形式存在的、以字節流形式存在的等等。而且它可以存在於任何場所,比如網絡、文件系統、應用程序中。所以 java.net.URL
的侷限性迫使 Spring 必須實現自己的資源加載策略,該資源加載策略需要滿足如下要求:
- 職能劃分清楚。資源的定義和資源的加載應該要有一個清晰的界限;
- 統一的抽象。統一的資源定義和資源加載策略。資源加載後要返回統一的抽象給客戶端,客戶端要對資源進行怎樣的處理,應該由抽象資源接口來界定。
統一資源:Resource
org.springframework.core.io.Resource
爲 Spring 框架所有資源的抽象和訪問接口,它繼承 org.springframework.core.io.InputStreamSource
接口。作爲所有資源的統一抽象,Source 定義了一些通用的方法,由子類 AbstractResource
提供統一的默認實現。定義如下:
public interface Resource extends InputStreamSource {
/**
* 資源是否存在
*/
boolean exists();
/**
* 資源是否可讀
*/
default boolean isReadable() {
return true;
}
/**
* 資源所代表的句柄是否被一個stream打開了
*/
default boolean isOpen() {
return false;
}
/**
* 是否爲 File
*/
default boolean isFile() {
return false;
}
/**
* 返回資源的URL的句柄
*/
URL getURL() throws IOException;
/**
* 返回資源的URI的句柄
*/
URI getURI() throws IOException;
/**
* 返回資源的File的句柄
*/
File getFile() throws IOException;
/**
* 返回 ReadableByteChannel
*/
default ReadableByteChannel readableChannel() throws IOException {
return Channels.newChannel(getInputStream());
}
/**
* 資源內容的長度
*/
long contentLength() throws IOException;
/**
* 資源最後的修改時間
*/
long lastModified() throws IOException;
/**
* 根據資源的相對路徑創建新資源
*/
Resource createRelative(String relativePath) throws IOException;
/**
* 資源的文件名
*/
@Nullable
String getFilename();
/**
* 資源的描述
*/
String getDescription();
}
類結構圖如下:
從上圖可以看到,Resource 根據資源的不同類型提供不同的具體實現,如下:
- FileSystemResource:對
java.io.File
類型資源的封裝,只要是跟 File 打交道的,基本上與 FileSystemResource 也可以打交道。支持文件和 URL 的形式,實現 WritableResource 接口,且從 Spring Framework 5.0 開始,FileSystemResource 使用NIO.2 API進行讀/寫交互 - ByteArrayResource:對字節數組提供的數據的封裝。如果通過 InputStream 形式訪問該類型的資源,該實現會根據字節數組的數據構造一個相應的 ByteArrayInputStream。
- UrlResource:對
java.net.URL
類型資源的封裝。內部委派 URL 進行具體的資源操作。 - ClassPathResource:class path 類型資源的實現。使用給定的 ClassLoader 或者給定的 Class 來加載資源。
- InputStreamResource:將給定的 InputStream 作爲一種資源的 Resource 的實現類。
AbstractResource 爲 Resource 接口的默認實現,它實現了 Resource 接口的大部分的公共實現,作爲 Resource 接口中的重中之重,其定義如下:
public abstract class AbstractResource implements Resource {
/**
* 判斷文件是否存在,若判斷過程產生異常(因爲會調用SecurityManager來判斷),就關閉對應的流
*/
@Override
public boolean exists() {
try {
return getFile().exists();
}
catch (IOException ex) {
// Fall back to stream existence: can we open the stream?
try {
InputStream is = getInputStream();
is.close();
return true;
}
catch (Throwable isEx) {
return false;
}
}
}
/**
* 直接返回true,表示可讀
*/
@Override
public boolean isReadable() {
return true;
}
/**
* 直接返回 false,表示未被打開
*/
@Override
public boolean isOpen() {
return false;
}
/**
* 直接返回false,表示不爲 File
*/
@Override
public boolean isFile() {
return false;
}
/**
* 拋出 FileNotFoundException 異常,交給子類實現
*/
@Override
public URL getURL() throws IOException {
throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");
}
/**
* 基於 getURL() 返回的 URL 構建 URI
*/
@Override
public URI getURI() throws IOException {
URL url = getURL();
try {
return ResourceUtils.toURI(url);
}
catch (URISyntaxException ex) {
throw new NestedIOException("Invalid URI [" + url + "]", ex);
}
}
/**
* 拋出 FileNotFoundException 異常,交給子類實現
*/
@Override
public File getFile() throws IOException {
throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
}
/**
* 根據 getInputStream() 的返回結果構建 ReadableByteChannel
*/
@Override
public ReadableByteChannel readableChannel() throws IOException {
return Channels.newChannel(getInputStream());
}
/**
* 獲取資源的長度
*
* 這個資源內容長度實際就是資源的字節長度,通過全部讀取一遍來判斷
*/
@Override
public long contentLength() throws IOException {
InputStream is = getInputStream();
try {
long size = 0;
byte[] buf = new byte[255];
int read;
while ((read = is.read(buf)) != -1) {
size += read;
}
return size;
}
finally {
try {
is.close();
}
catch (IOException ex) {
}
}
}
/**
* 返回資源最後的修改時間
*/
@Override
public long lastModified() throws IOException {
long lastModified = getFileForLastModifiedCheck().lastModified();
if (lastModified == 0L) {
throw new FileNotFoundException(getDescription() +
" cannot be resolved in the file system for resolving its last-modified timestamp");
}
return lastModified;
}
protected File getFileForLastModifiedCheck() throws IOException {
return getFile();
}
/**
* 交給子類實現
*/
@Override
public Resource createRelative(String relativePath) throws IOException {
throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());
}
/**
* 獲取資源名稱,默認返回 null
*/
@Override
@Nullable
public String getFilename() {
return null;
}
/**
* 返回資源的描述
*/
@Override
public String toString() {
return getDescription();
}
@Override
public boolean equals(Object obj) {
return (obj == this ||
(obj instanceof Resource && ((Resource) obj).getDescription().equals(getDescription())));
}
@Override
public int hashCode() {
return getDescription().hashCode();
}
}
如果我們想要實現自定義的 Resource,記住不要實現 Resource 接口,而應該繼承 AbstractResource 抽象類,然後根據當前的具體資源特性覆蓋相應的方法即可。
統一資源定位:ResourceLoader
一開始就說了 Spring 將資源的定義和資源的加載區分開了,Resource 定義了統一的資源,那資源的加載則由 ResourceLoader 來統一定義。
org.springframework.core.io.ResourceLoader
爲 Spring 資源加載的統一抽象,具體的資源加載則由相應的實現類來完成,所以我們可以將 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 用戶來說,可以直接調用該方法來獲取,
在分析 Resource 時,提到了一個類 ClassPathResource ,這個類是可以根據指定的 ClassLoader 來加載資源的。
作爲 Spring 統一的資源加載器,它提供了統一的抽象,具體的實現則由相應的子類來負責實現,其類的類結構圖如下:
DefaultResourceLoader
DefaultResourceLoader 是 ResourceLoader 的默認實現,它接收 ClassLoader 作爲構造函數的參數或者使用不帶參數的構造函數,在使用不帶參數的構造函數時,使用的 ClassLoader 爲默認的 ClassLoader(一般爲Thread.currentThread().getContextClassLoader()
),可以通過 ClassUtils.getDefaultClassLoader()
獲取。當然也可以調用 setClassLoader()
方法進行後續設置。如下:
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 對該方法提供了核心實現(它的兩個子類都沒有提供覆蓋該方法,所以可以斷定ResourceLoader 的資源加載策略就封裝 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 類型資源並返回,在構造該資源時,通過
getClassLoader()
獲取當前的 ClassLoader。 - 構造 URL ,嘗試通過它進行資源定位,若沒有拋出 MalformedURLException 異常,則判斷是否爲 FileURL , 如果是則構造 FileUrlResource 類型資源,否則構造 UrlResource。若在加載過程中拋出 MalformedURLException 異常,則委派
getResourceByPath()
實現資源定位加載。
ProtocolResolver ,用戶自定義協議資源解決策略,作爲 DefaultResourceLoader 的 SPI,它允許用戶自定義資源加載協議,而不需要繼承 ResourceLoader 的子類。在介紹 Resource 時,提到如果要實現自定義 Resource,我們只需要繼承 DefaultResource 即可,但是有了 ProtocolResolver 後,我們不需要直接繼承 DefaultResourceLoader,改爲實現 ProtocolResolver 接口也可以實現自定義的 ResourceLoader。 ProtocolResolver 接口,僅有一個方法 Resource resolve(String location, ResourceLoader resourceLoader)
,該方法接收兩個參數:資源路徑location,指定的加載器 ResourceLoader,返回爲相應的 Resource 。在 Spring 中你會發現該接口並沒有實現類,它需要用戶自定義,自定義的 Resolver 如何加入 Spring 體系呢?調用 DefaultResourceLoader.addProtocolResolver()
即可,如下:
public void addProtocolResolver(ProtocolResolver resolver) {
Assert.notNull(resolver, "ProtocolResolver must not be null");
this.protocolResolvers.add(resolver);
}
下面示例是演示 DefaultResourceLoader 加載資源的具體策略,代碼如下(該示例參考《Spring 解密》 P89):
ResourceLoader resourceLoader = new DefaultResourceLoader();
Resource fileResource1 = resourceLoader.getResource("D:/Users/chenming673/Documents/spark.txt");
System.out.println("fileResource1 is FileSystemResource:" + (fileResource1 instanceof FileSystemResource));
Resource fileResource2 = resourceLoader.getResource("/Users/chenming673/Documents/spark.txt");
System.out.println("fileResource2 is ClassPathResource:" + (fileResource2 instanceof ClassPathResource));
Resource urlResource1 = resourceLoader.getResource("file:/Users/chenming673/Documents/spark.txt");
System.out.println("urlResource1 is UrlResource:" + (urlResource1 instanceof UrlResource));
Resource urlResource2 = resourceLoader.getResource("http://www.baidu.com");
System.out.println("urlResource1 is urlResource:" + (urlResource2 instanceof UrlResource));
運行結果:
fileResource1 is FileSystemResource:false
fileResource2 is ClassPathResource:true
urlResource1 is UrlResource:true
urlResource1 is urlResource:true
其實對於 fileResource1 我們更加希望是 FileSystemResource 資源類型,但是事與願違,它是 ClassPathResource 類型。在getResource()
資源加載策略中,我們知道 D:/Users/chenming673/Documents/spark.txt
資源其實在該方法中沒有相應的資源類型,那麼它就會在拋出 MalformedURLException 異常時通過 getResourceByPath()
構造一個 ClassPathResource 類型的資源。而指定有協議前綴的資源路徑,則通過 URL 就可以定義,所以返回的都是UrlResource類型。
FileSystemResourceLoader
從上面的示例我們看到,其實 DefaultResourceLoader 對getResourceByPath(String)
方法處理其實不是很恰當,這個時候我們可以使用 FileSystemResourceLoader ,它繼承 DefaultResourceLoader 且覆寫了 getResourceByPath(String)
,使之從文件系統加載資源並以 FileSystemResource 類型返回,這樣我們就可以得到想要的資源類型,如下:
@Override
protected Resource getResourceByPath(String path) {
if (path.startsWith("/")) {
path = path.substring(1);
}
return new FileSystemContextResource(path);
}
FileSystemContextResource 爲 FileSystemResourceLoader 的內部類,它繼承 FileSystemResource。
private static class FileSystemContextResource extends FileSystemResource implements ContextResource {
public FileSystemContextResource(String path) {
super(path);
}
@Override
public String getPathWithinContext() {
return getPath();
}
}
在構造器中也是調用 FileSystemResource 的構造方法來構造 FileSystemContextResource 的。
如果將上面的示例將 DefaultResourceLoader 改爲 FileSystemContextResource ,則 fileResource1 則爲 FileSystemResource。
ResourcePatternResolver
ResourceLoader 的 Resource getResource(String location)
每次只能根據 location 返回一個 Resource,當需要加載多個資源時,我們除了多次調用 getResource()
外別無他法。ResourcePatternResolver 是 ResourceLoader 的擴展,它支持根據指定的資源路徑匹配模式每次返回多個 Resource 實例,其定義如下:
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
ResourcePatternResolver 在 ResourceLoader 的基礎上增加了 getResources(String locationPattern)
,以支持根據路徑匹配模式返回多個 Resource 實例,同時也新增了一種新的協議前綴 classpath*:
,該協議前綴由其子類負責實現。
PathMatchingResourcePatternResolver 爲 ResourcePatternResolver 最常用的子類,它除了支持 ResourceLoader 和 ResourcePatternResolver 新增的 classpath*: 前綴外,還支持 Ant 風格的路徑匹配模式(類似於 **/*.xml
)。
PathMatchingResourcePatternResolver 提供了三個構造方法,如下:
public PathMatchingResourcePatternResolver() {
this.resourceLoader = new DefaultResourceLoader();
}
public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
Assert.notNull(resourceLoader, "ResourceLoader must not be null");
this.resourceLoader = resourceLoader;
}
public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) {
this.resourceLoader = new DefaultResourceLoader(classLoader);
}
PathMatchingResourcePatternResolver 在實例化的時候,可以指定一個 ResourceLoader,如果不指定的話,它會在內部構造一個 DefaultResourceLoader。
Resource getResource(String location)
@Override
public Resource getResource(String location) {
return getResourceLoader().getResource(location);
}
getResource()
方法直接委託給相應的 ResourceLoader 來實現,所以如果我們在實例化的 PathMatchingResourcePatternResolver 的時候,如果不知道 ResourceLoader ,那麼在加載資源時,其實就是 DefaultResourceLoader 的過程。其實在下面介紹的 Resource[] getResources(String locationPattern)
也相同,只不過返回的資源時多個而已。
Resource[] getResources(String locationPattern)
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
// 以 classpath*: 開頭
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
// 路徑包含通配符
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
return findPathMatchingResources(locationPattern);
}
else {
// 路徑不包含通配符
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}
else {
int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
locationPattern.indexOf(':') + 1);
// 路徑包含通配符
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
return findPathMatchingResources(locationPattern);
}
else {
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}
處理邏輯如下圖:
下面就 findAllClassPathResources()
做詳細分析。
findAllClassPathResources()
當 locationPattern 以 classpath*: 開頭但是不包含通配符,則調用findAllClassPathResources()
方法加載資源。該方法返回 classes 路徑下和所有 jar 包中的所有相匹配的資源。
protected Resource[] findAllClassPathResources(String location) throws IOException {
String path = location;
if (path.startsWith("/")) {
path = path.substring(1);
}
Set<Resource> result = doFindAllClassPathResources(path);
if (logger.isDebugEnabled()) {
logger.debug("Resolved classpath location [" + location + "] to resources " + result);
}
return result.toArray(new Resource[0]);
}
真正執行加載的是在 doFindAllClassPathResources()
方法,如下:
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
Set<Resource> result = new LinkedHashSet<>(16);
ClassLoader cl = getClassLoader();
Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
while (resourceUrls.hasMoreElements()) {
URL url = resourceUrls.nextElement();
result.add(convertClassLoaderURL(url));
}
if ("".equals(path)) {
addAllClassLoaderJarRoots(cl, result);
}
return result;
}
doFindAllClassPathResources()
根據 ClassLoader 加載路徑下的所有資源。在加載資源過程中如果,在構造 PathMatchingResourcePatternResolver 實例的時候如果傳入了 ClassLoader,則調用其 getResources()
,否則調用ClassLoader.getSystemResources(path)
。 ClassLoader.getResources()
如下:
public Enumeration<URL> getResources(String name) throws IOException {
@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
if (parent != null) {
tmp[0] = parent.getResources(name);
} else {
tmp[0] = getBootstrapResources(name);
}
tmp[1] = findResources(name);
return new CompoundEnumeration<>(tmp);
}
看到這裏是不是就已經一目瞭然了?如果當前父類加載器不爲 null,則通過父類向上迭代獲取資源,否則調用 getBootstrapResources()
。這裏是不是特別熟悉,(^▽^)。
若 path 爲 空(“”)時,則調用 addAllClassLoaderJarRoots()
方法。該方法主要是加載路徑下得所有 jar 包,方法較長也沒有什麼實際意義就不貼出來了。
通過上面的分析,我們知道 findAllClassPathResources()
其實就是利用 ClassLoader 來加載指定路徑下的資源,不管它是在 class 路徑下還是在 jar 包中。如果我們傳入的路徑爲空或者 /
,則會調用 addAllClassLoaderJarRoots()
方法加載所有的 jar 包。
findAllClassPathResources()
當 locationPattern 以 classpath*: 開頭且當中包含了通配符,則調用該方法進行資源加載。如下:
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
// 確定跟路徑
String rootDirPath = determineRootDir(locationPattern);
String subPattern = locationPattern.substring(rootDirPath.length());
// 獲取根據路徑下得資源
Resource[] rootDirResources = getResources(rootDirPath);
Set<Resource> result = new LinkedHashSet<>(16);
for (Resource rootDirResource : rootDirResources) {
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirUrl = rootDirResource.getURL();
// bundle 資源類型
if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
if (resolvedUrl != null) {
rootDirUrl = resolvedUrl;
}
rootDirResource = new UrlResource(rootDirUrl);
}
// VFS 資源
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
// Jar
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
}
else {
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
}
if (logger.isDebugEnabled()) {
logger.debug("Resolved location pattern [" + locationPattern + "] to resources " + result);
}
return result.toArray(new Resource[0]);
}
方法有點兒長,但是思路還是很清晰的,主要分兩步:
- 確定目錄,獲取該目錄下得所有資源
- 在所獲得的所有資源中進行迭代匹配獲取我們想要的資源。
在這個方法裏面我們要關注兩個方法,一個是 determineRootDir()
,一個是 doFindPathMatchingFileResources()
。
determineRootDir()
主要是用於確定根路徑,如下:
protected String determineRootDir(String location) {
int prefixEnd = location.indexOf(':') + 1;
int rootDirEnd = location.length();
while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
}
if (rootDirEnd == 0) {
rootDirEnd = prefixEnd;
}
return location.substring(0, rootDirEnd);
}
該方法一定要給出一個確定的根目錄。該根目錄用於確定文件的匹配的起始點,將根目錄位置的資源解析爲 java.io.File
並將其傳遞到 retrieveMatchingFiles()
,其餘爲知用於模式匹配,找出我們所需要的資源。
確定根路徑如下:
原路徑 | 確定根路徑 |
---|---|
classpath*:test/cc*/spring-*.xml |
classpath*:test/ |
classpath*:test/aa/spring-*.xml |
classpath*:test/aa/ |
確定根路徑後,則調用 getResources()
方法獲取該路徑下得所有資源,然後迭代資源獲取符合條件的資源。
至此 Spring 整個資源記載過程已經分析完畢。下面簡要總結下:
- Spring 提供了 Resource 和 ResourceLoader 來統一抽象整個資源及其定位。使得資源與資源的定位有了一個更加清晰的界限,並且提供了合適的 Default 類,使得自定義實現更加方便和清晰。
- AbstractResource 爲 Resource 的默認實現,它對 Resource 接口做了一個統一的實現,子類繼承該類後只需要覆蓋相應的方法即可,同時對於自定義的 Resource 我們也是繼承該類。
- DefaultResourceLoader 同樣也是 ResourceLoader 的默認實現,在自定 ResourceLoader 的時候我們除了可以繼承該類外還可以實現 ProtocolResolver 接口來實現自定資源加載協議。
- DefaultResourceLoader 每次只能返回單一的資源,所以 Spring 針對這個提供了另外一個接口 ResourcePatternResolver ,該接口提供了根據指定的 locationPattern 返回多個資源的策略。其子類 PathMatchingResourcePatternResolver 是一個集大成者的 ResourceLoader ,因爲它即實現了
Resource getResource(String location)
也實現了Resource[] getResources(String locationPattern)
。