Spring源碼解讀之 統一資源加載策略

在學 Java SE 的時候,我們學習了一個標準類 java.net.URL,該類在 Java SE 中的定位爲統一資源定位器(Uniform Resource Locator),但是我們知道它的實現基本只限於網絡形式發佈的資源的查找和定位。然而,實際上資源的定義比較廣泛,除了網絡形式的資源,還有以二進制形式存在的、以文件形式存在的、以字節流形式存在的等等。而且它可以存在於任何場所,比如網絡、文件系統、應用程序中。所以 java.net.URL 的侷限性迫使 Spring 必須實現自己的資源加載策略,該資源加載策略需要滿足如下要求:

  1. 職能劃分清楚。資源的定義和資源的加載應該要有一個清晰的界限
  2. 統一的抽象。統一的資源定義和資源加載策略。資源加載後要返回統一的抽象給客戶端,客戶端要對資源進行怎樣的處理,應該由抽象資源接口來界定。

1. 統一資源:Resource

org.springframework.core.io.Resource 爲 Spring 框架所有資源的抽象和訪問接口,它繼承 org.springframework.core.io.InputStreamSource接口。作爲所有資源的統一抽象,Resource 定義了一些通用的方法,由子類 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 java.nio.channels.Channels.newChannel(getInputStream());
	}

	/**
	 * 資源內容的長度
	 */
	long contentLength() throws IOException;

	/**
	 * 資源最後的修改時間
	 */
	long lastModified() throws IOException;

	/**
	 * 根據資源的相對路徑創建新資源
	 */
	Resource createRelative(String relativePath) throws IOException;

	/**
	 * 資源的文件名
	 */
	@Nullable
	String getFilename();

	/**
	 * 資源的描述
	 */
	String getDescription();

}


1.1 子類結構

類結構圖如下:

Resource ç±»å¾

從上圖可以看到,Resource 根據資源的不同類型提供不同的具體實現,如下:

  • FileSystemResource :對 java.io.File 類型資源的封裝,只要是跟 File 打交道的,基本上與 FileSystemResource 也可以打交道。支持文件和 URL 的形式,實現 WritableResource 接口,且從 Spring Framework 5.0 開始,FileSystemResource 使用 NIO2 API進行讀/寫交互。
  • ByteArrayResource :對字節數組提供的數據的封裝。如果通過 InputStream 形式訪問該類型的資源,該實現會根據字節數組的數據構造一個相應的 ByteArrayInputStream。
  • UrlResource :對 java.net.URL類型資源的封裝。內部委派 URL 進行具體的資源操作。
  • ClassPathResource :class path 類型資源的實現。使用給定的 ClassLoader 或者給定的 Class 來加載資源。
  • InputStreamResource :將給定的 InputStream 作爲一種資源的 Resource 的實現類。

1.2 AbstractResource

org.springframework.core.io.AbstractResource ,爲 Resource 接口的默認抽象實現。它實現了 Resource 接口的大部分的公共實現,作爲 Resource 接口中的重中之重,其定義如下:

public abstract class AbstractResource implements Resource {

	/**
	 * 判斷文件是否存在,若判斷過程產生異常(因爲會調用SecurityManager來判斷),就關閉對應的流
	 */
	@Override
	public boolean exists() {
		try {
		  // 基於 File 進行判斷
			return getFile().exists();
		}
		catch (IOException ex) {
			// Fall back to stream existence: can we open the stream?
			// 基於 InputStream 進行判斷
			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]; // 每次最多讀取 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();
	}

	/**
	 * 拋出 FileNotFoundException 異常,交給子類實現
	 */
	@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 抽象類,然後根據當前的具體資源特性覆蓋相應的方法即可。

1.3 其他子類

Resource 的子類,例如 FileSystemResource、ByteArrayResource 等等的代碼非常簡單。感興趣的胖友,自己去研究。

2. 統一資源定位:ResourceLoader

一開始就說了 Spring 將資源的定義和資源的加載區分開了,Resource 定義了統一的資源,那資源的加載則由 ResourceLoader 來統一定義

org.springframework.core.io.ResourceLoader 爲 Spring 資源加載的統一抽象,具體的資源加載則由相應的實現類來完成,所以我們可以將 ResourceLoader 稱作爲統一資源定位器。其定義如下:

FROM 《Spring 源碼深度解析》P16 頁

ResourceLoader,定義資源加載器,主要應用於根據給定的資源文件地址,返回對應的 Resource 。

public interface ResourceLoader {

	String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; // CLASSPATH URL 前綴。默認爲:"classpath:"

	Resource getResource(String location);

	ClassLoader getClassLoader();

}

 

  • #getResource(String location) 方法,根據所提供資源的路徑 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 來加載資源的。

2.1 子類結構

作爲 Spring 統一的資源加載器,它提供了統一的抽象,具體的實現則由相應的子類來負責實現,其類的類結構圖如下:

ResourceLoader ç±»å¾

2.1 DefaultResourceLoader

與 AbstractResource 相似,org.springframework.core.io.DefaultResourceLoader 是 ResourceLoader 的默認實現。

2.1.1 構造函數

它接收 ClassLoader 作爲構造函數的參數,或者使用不帶參數的構造函數。

  • 在使用不帶參數的構造函數時,使用的 ClassLoader 爲默認的 ClassLoader(一般 Thread.currentThread()#getContextClassLoader() )。
  • 在使用參數的構造函數時,可以通過 ClassUtils#getDefaultClassLoader()獲取。

代碼如下:

@Nullable
private ClassLoader classLoader;

public DefaultResourceLoader() { // 無參構造函數
	this.classLoader = ClassUtils.getDefaultClassLoader();
}

public DefaultResourceLoader(@Nullable ClassLoader 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());
}

 

  • 另外,也可以調用 #setClassLoader() 方法進行後續設置。

2.1.2 getResource 方法

ResourceLoader 中最核心的方法爲 #getResource(String location) ,它根據提供的 location 返回相應的 Resource 。而 DefaultResourceLoader 對該方法提供了核心實現(因爲,它的兩個子類都沒有提供覆蓋該方法,所以可以斷定 ResourceLoader 的資源加載策略就封裝在 DefaultResourceLoader 中),代碼如下:

// DefaultResourceLoader.java

@Override
public Resource getResource(String location) {
    Assert.notNull(location, "Location must not be null");

    // 首先,通過 ProtocolResolver 來加載資源
    for (ProtocolResolver protocolResolver : this.protocolResolvers) {
        Resource resource = protocolResolver.resolve(location, this);
        if (resource != null) {
            return resource;
        }
    }
    // 其次,以 / 開頭,返回 ClassPathContextResource 類型的資源
    if (location.startsWith("/")) {
        return getResourceByPath(location);
    // 再次,以 classpath: 開頭,返回 ClassPathResource 類型的資源
    } else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
        return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
    // 然後,根據是否爲文件 URL ,是則返回 FileUrlResource 類型的資源,否則返回 UrlResource 類型的資源
    } 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) {
            // 最後,返回 ClassPathContextResource 類型的資源
            // No URL -> resolve as resource path.
            return getResourceByPath(location);
        }
    }
}

 

  • 首先,通過 ProtocolResolver 來加載資源,成功返回 Resource 。
  • 其次,若 location 以 "/" 開頭,則調用 #getResourceByPath() 方法,構造 ClassPathContextResource 類型資源並返回。代碼如下:

protected Resource getResourceByPath(String path) {
	return new ClassPathContextResource(path, getClassLoader());
}

 

  • 再次,若 location 以 "classpath:" 開頭,則構造 ClassPathResource 類型資源並返回。在構造該資源時,通過 #getClassLoader() 獲取當前的 ClassLoader。
  • 然後,構造 URL ,嘗試通過它進行資源定位,若沒有拋出 MalformedURLException 異常,則判斷是否爲 FileURL , 如果是則構造 FileUrlResource 類型的資源,否則構造 UrlResource 類型的資源。
  • 最後,若在加載過程中拋出 MalformedURLException 異常,則委派 #getResourceByPath() 方法,實現資源定位加載。😈 實際上,和【其次】相同落。

2.1.3 ProtocolResolver

org.springframework.core.io.ProtocolResolver ,用戶自定義協議資源解決策略,作爲 DefaultResourceLoader 的 SPI:它允許用戶自定義資源加載協議,而不需要繼承 ResourceLoader 的子類。
在介紹 Resource 時,提到如果要實現自定義 Resource,我們只需要繼承 AbstractResource 即可,但是有了 ProtocolResolver 後,我們不需要直接繼承 DefaultResourceLoader,改爲實現 ProtocolResolver 接口也可以實現自定義的 ResourceLoader。

ProtocolResolver 接口,僅有一個方法 Resource resolve(String location, ResourceLoader resourceLoader) 。代碼如下:

/**
 * 使用指定的 ResourceLoader ,解析指定的 location 。
 * 若成功,則返回對應的 Resource 。
 *
 * Resolve the given location against the given resource loader
 * if this implementation's protocol matches.
 * @param location the user-specified resource location 資源路徑
 * @param resourceLoader the associated resource loader 指定的加載器 ResourceLoader
 * @return a corresponding {@code Resource} handle if the given location
 * matches this resolver's protocol, or {@code null} otherwise 返回爲相應的 Resource
 */
@Nullable
Resource resolve(String location, ResourceLoader resourceLoader);

在 Spring 中你會發現該接口並沒有實現類,它需要用戶自定義,自定義的 Resolver 如何加入 Spring 體系呢?調用 DefaultResourceLoader#addProtocolResolver(ProtocolResolver) 方法即可。代碼如下:

/**
 * ProtocolResolver 集合
 */
private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);

public void addProtocolResolver(ProtocolResolver resolver) {
	Assert.notNull(resolver, "ProtocolResolver must not be null");
	this.protocolResolvers.add(resolver);
}

2.1.4 示例

下面示例是演示 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 類型。爲什麼呢?在 DefaultResourceLoader#getResource() 方法的資源加載策略中,我們知道 "D:/Users/chenming673/Documents/spark.txt" 地址,其實在該方法中沒有相應的資源類型,那麼它就會在拋出 MalformedURLException 異常時,通過 DefaultResourceLoader#getResourceByPath(...) 方法,構造一個 ClassPathResource 類型的資源。
  • 而 urlResource1 和 urlResource2 ,指定有協議前綴的資源路徑,則通過 URL 就可以定義,所以返回的都是 UrlResource 類型。

2.2 FileSystemResourceLoader

從上面的示例,我們看到,其實 DefaultResourceLoader 對#getResourceByPath(String) 方法處理其實不是很恰當,這個時候我們可以使用 org.springframework.core.io.FileSystemResourceLoader 。它繼承 DefaultResourceLoader ,且覆寫了 #getResourceByPath(String) 方法,使之從文件系統加載資源並以 FileSystemResource 類型返回,這樣我們就可以得到想要的資源類型。代碼如下:

@Override
protected Resource getResourceByPath(String path) {
	// 截取首 /
	if (path.startsWith("/")) {
		path = path.substring(1);
	}
	// 創建 FileSystemContextResource 類型的資源
	return new FileSystemContextResource(path);
}

 

2.2.1 FileSystemContextResource

FileSystemContextResource ,爲 FileSystemResourceLoader 的內部類,它繼承 FileSystemResource 類,實現 ContextResource 接口。代碼如下:

/**
 * FileSystemResource that explicitly expresses a context-relative path
 * through implementing the ContextResource interface.
 */
private static class FileSystemContextResource extends FileSystemResource implements ContextResource {

	public FileSystemContextResource(String path) {
		super(path);
	}

	@Override
	public String getPathWithinContext() {
		return getPath();
	}
}
  • 在構造器中,也是調用 FileSystemResource 的構造函數來構造 FileSystemResource 的。
  • 爲什麼要有 FileSystemContextResource 類的原因是,實現 ContextResource 接口,並實現對應的 #getPathWithinContext() 接口方法。

2.2.2 示例

😈 在回過頭看 「2.1.4 示例」 ,如果將 DefaultResourceLoader 改爲 FileSystemResourceLoader ,則 fileResource1 則爲 FileSystemResource 類型的資源。

2.3 ClassRelativeResourceLoader

org.springframework.core.io.ClassRelativeResourceLoader ,是 DefaultResourceLoader 的另一個子類的實現。和 FileSystemResourceLoader 類似,在實現代碼的結構上類似,也是覆寫 #getResourceByPath(String path) 方法,並返回其對應的 ClassRelativeContextResource 的資源類型。

感興趣的胖友,可以看看 《Spring5:就這一次,搞定資源加載器之ClassRelativeResourceLoader》 文章。

ClassRelativeResourceLoader 擴展的功能是,可以根據給定的class 所在包或者所在包的子包下加載資源。

2.4 ResourcePatternResolver

ResourceLoader 的 Resource getResource(String location) 方法,每次只能根據 location 返回一個 Resource 。當需要加載多個資源時,我們除了多次調用 #getResource(String location) 方法外,別無他法。org.springframework.core.io.support.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*:",該協議前綴由其子類負責實現。

2.5 PathMatchingResourcePatternResolver

org.springframework.core.io.support.PathMatchingResourcePatternResolver ,爲 ResourcePatternResolver 最常用的子類,它除了支持 ResourceLoader 和 ResourcePatternResolver 新增的 "classpath*:" 前綴外,還支持 Ant 風格的路徑匹配模式(類似於 "**/*.xml")。

2.5.1 構造函數

PathMatchingResourcePatternResolver 提供了三個構造函數,如下:

/**
 * 內置的 ResourceLoader 資源定位器
 */
private final ResourceLoader resourceLoader;
/**
 * Ant 路徑匹配器
 */
private PathMatcher pathMatcher = new AntPathMatcher();

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 。
  • pathMatcher 屬性,默認爲 AntPathMatcher 對象,用於支持 Ant 類型的路徑匹配。

2.5.2 getResource

@Override
public Resource getResource(String location) {
	return getResourceLoader().getResource(location);
}

public ResourceLoader getResourceLoader() {
	return this.resourceLoader;
}

該方法,直接委託給相應的 ResourceLoader 來實現。所以,如果我們在實例化的 PathMatchingResourcePatternResolver 的時候,如果未指定 ResourceLoader 參數的情況下,那麼在加載資源時,其實就是 DefaultResourceLoader 的過程。

其實在下面介紹的 Resource[] getResources(String locationPattern) 方法也相同,只不過返回的資源是多個而已。

2.5.3 getResources

@Override
public Resource[] getResources(String locationPattern) throws IOException {
    Assert.notNull(locationPattern, "Location pattern must not be null");
    // 以 "classpath*:" 開頭
    if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
        // 路徑包含通配符
        // a class path resource (multiple resources for same name possible)
        if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
            // a class path resource pattern
            return findPathMatchingResources(locationPattern);
        // 路徑不包含通配符
        } else {
            // all class path resources with the given name
            return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
        }
    // 不以 "classpath*:" 開頭
    } else {
        // Generally only look for a pattern after a prefix here, // 通常只在這裏的前綴後面查找模式
        // and on Tomcat only after the "*/" separator for its "war:" protocol. 而在 Tomcat 上只有在 “*/ ”分隔符之後才爲其 “war:” 協議
        int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
                locationPattern.indexOf(':') + 1);
        // 路徑包含通配符
        if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
            // a file pattern
            return findPathMatchingResources(locationPattern);
        // 路徑不包含通配符
        } else {
            // a single resource with the given name
            return new Resource[] {getResourceLoader().getResource(locationPattern)};
        }
    }
}

處理邏輯如下圖:

 

  •  "classpath*:" 開頭,且路徑不包含通配符,直接委託給相應的 ResourceLoader 來實現。
  • 其他情況,調用 #findAllClassPathResources(...)、或 #findPathMatchingResources(...) 方法,返回多個 Resource 。下面,我們來詳細分析。

2.5.4 findAllClassPathResources

當 locationPattern 以 "classpath*:" 開頭但是不包含通配符,則調用 #findAllClassPathResources(...) 方法加載資源。該方法返回 classes 路徑下和所有 jar 包中的所有相匹配的資源。

protected Resource[] findAllClassPathResources(String location) throws IOException {
	String path = location;
	// 去除首個 /
	if (path.startsWith("/")) {
		path = path.substring(1);
	}
	// 真正執行加載所有 classpath 資源
	Set<Resource> result = doFindAllClassPathResources(path);
	if (logger.isTraceEnabled()) {
		logger.trace("Resolved classpath location [" + location + "] to resources " + result);
	}
	// 轉換成 Resource 數組返回
	return result.toArray(new Resource[0]);
}

真正執行加載的是在 #doFindAllClassPathResources(...) 方法,代碼如下:

protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
	Set<Resource> result = new LinkedHashSet<>(16);
	ClassLoader cl = getClassLoader();
	// <1> 根據 ClassLoader 加載路徑下的所有資源
	Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
	// <2>
	while (resourceUrls.hasMoreElements()) {
		URL url = resourceUrls.nextElement();
		// 將 URL 轉換成 UrlResource
		result.add(convertClassLoaderURL(url));
	}
	// <3> 加載路徑下得所有 jar 包
	if ("".equals(path)) {
		// The above result is likely to be incomplete, i.e. only containing file system references.
		// We need to have pointers to each of the jar files on the classpath as well...
		addAllClassLoaderJarRoots(cl, result);
	}
	return result;
}

<1> 處,根據 ClassLoader 加載路徑下的所有資源。在加載資源過程時,如果在構造 PathMatchingResourcePatternResolver 實例的時候如果傳入了 ClassLoader,則調用該 ClassLoader 的 #getResources() 方法,否則調用 ClassLoader#getSystemResources(path) 方法。另外,ClassLoader#getResources() 方法,代碼如下:

// java.lang.ClassLoader.java
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() 。這裏是不是特別熟悉,(^▽^)。

  • <2> 處,遍歷 URL 集合,調用 #convertClassLoaderURL(URL url) 方法,將 URL 轉換成 UrlResource 對象。代碼如下:

protected Resource convertClassLoaderURL(URL url) {
	return new UrlResource(url);
}
  • <3> 處,若 path 爲空(“”)時,則調用 #addAllClassLoaderJarRoots(...)方法。該方法主要是加載路徑下得所有 jar 包,方法較長也沒有什麼實際意義就不貼出來了。感興趣的胖友,自己可以去看看。😈 當然,可能代碼也比較長哈。

通過上面的分析,我們知道 #findAllClassPathResources(...) 方法,其實就是利用 ClassLoader 來加載指定路徑下的資源,不論它是在 class 路徑下還是在 jar 包中。如果我們傳入的路徑爲空或者 /,則會調用 #addAllClassLoaderJarRoots(...) 方法,加載所有的 jar 包。

2.5.5 findPathMatchingResources

當 locationPattern 中包含了通配符,則調用該方法進行資源加載。代碼如下:

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.isTraceEnabled()) {
        logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
    }
    // 轉換成 Resource 數組返回
    return result.toArray(new Resource[0]);
}

方法有點兒長,但是思路還是很清晰的,主要分兩步:

  1. 確定目錄,獲取該目錄下得所有資源。
  2. 在所獲得的所有資源後,進行迭代匹配獲取我們想要的資源。

在這個方法裏面,我們要關注兩個方法,一個是 #determineRootDir(String location) 方法,一個是 #doFindPathMatchingXXXResources(...) 等方法。

2.5.5.1 determineRootDir

determineRootDir(String location) 方法,主要是用於確定根路徑。代碼如下:

/**
 * Determine the root directory for the given location.
 * <p>Used for determining the starting point for file matching,
 * resolving the root directory location to a {@code java.io.File}
 * and passing it into {@code retrieveMatchingFiles}, with the
 * remainder of the location as pattern.
 * <p>Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml",
 * for example.
 * @param location the location to check
 * @return the part of the location that denotes the root directory
 * @see #retrieveMatchingFiles
 */
protected String determineRootDir(String location) {
	// 找到冒號的後一位
	int prefixEnd = location.indexOf(':') + 1;
	// 根目錄結束位置
	int rootDirEnd = location.length();
	// 在從冒號開始到最後的字符串中,循環判斷是否包含通配符,如果包含,則截斷最後一個由”/”分割的部分。
	// 例如:在我們路徑中,就是最後的ap?-context.xml這一段。再循環判斷剩下的部分,直到剩下的路徑中都不包含通配符。
	while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
		rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
	}
	// 如果查找完成後,rootDirEnd = 0 了,則將之前賦值的 prefixEnd 的值賦給 rootDirEnd ,也就是冒號的後一位
	if (rootDirEnd == 0) {
		rootDirEnd = prefixEnd;
	}
	// 截取根目錄
	return location.substring(0, rootDirEnd);
}

方法比較繞,效果如下示例:

原路徑 確定根路徑
classpath*:test/cc*/spring-*.xml classpath*:test/
classpath*:test/aa/spring-*.xml classpath*:test/aa/

2.5.5.2 doFindPathMatchingXXXResources

#doFindPathMatchingXXXResources(...) 方法,是個泛指,一共對應三個方法:

  • #doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPatter) 方法
  • #doFindPathMatchingFileResources(rootDirResource, subPattern) 方法
  • VfsResourceMatchingDelegate#findMatchingResources(rootDirUrl, subPattern, pathMatcher) 方法

因爲本文重在分析 Spring 統一資源加載策略的整體流程。相對來說,上面幾個方法的代碼量會比較多。所以本文不再追溯,感興趣的胖友,推薦閱讀如下文章:

3. 小結

至此 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) 方法。

另外,如果胖友認真的看了本文的包結構,我們可以發現,Resource 和 ResourceLoader 核心是在,spring-core 項目中。

如果想要調試本小節的相關內容,可以直接使用 Resource 和 ResourceLoader 相關的 API ,進行操作調試。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章