原文出自:http://cmsblogs.com
該資源加載政策需要滿足如下要求:
- 職能劃厘清楚。資源的定義和資源的加載應該要有一個清晰的界限;
- 統一的抽象。統一的資源定義和資源加載政策。資源加載後要傳回統一的抽象給用戶端,用戶端要對資源進行怎樣的處理,應該由抽象資源接口來界定。
統一資源: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 來加載資源的。
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 加載資源的具體政策,代碼如下:
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()
當 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 類,使得自定義實作更加友善和清晰。
- DefaultResource 為 Resource 的預設實作,它對 Resource 接口做了一個統一的實作,子類繼承該類後隻需要覆寫相應的方法即可,同時對于自定義的 Resource 我們也是繼承該類。
- DefaultResourceLoader 同樣也是 ResourceLoader 的預設實作,在自定 ResourceLoader 的時候我們除了可以繼承該類外還可以實作 ProtocolResolver 接口來實作自定資源加載協定。
- DefaultResourceLoader 每次隻能傳回單一的資源,是以 Spring 針對這個提供了另外一個接口 ResourcePatternResolver ,該接口提供了根據指定的 locationPattern 傳回多個資源的政策。其子類 PathMatchingResourcePatternResolver 是一個集大成者的 ResourceLoader ,因為它即實作了 Resource getResource(String location) 也實作了 Resource[] getResources(String locationPattern)。