天天看點

spring-core 源碼學習-IoC 之 Spring 統一資源加載政策(二)1. 統一資源:Resource2. 統一資源定位:ResourceLoader3. 小結

spring-core 統一資源加載政策

  • 1. 統一資源:Resource
    • 1.1 子類結構
    • 1.2 AbstractResource
  • 2. 統一資源定位:ResourceLoader
    • 2.1 子類結構
    • 2.1 DefaultResourceLoader
      • 2.1.1 構造函數
      • 2.1.2 getResource 方法
      • 2.1.3 ProtocolResolver
      • 2.1.4 示例
    • 2.2 FileSystemResourceLoader
      • 2.2.1 FileSystemContextResource
    • 2.3 ClassRelativeResourceLoader
    • 2.4 ResourcePatternResolver
    • 2.5 PathMatchingResourcePatternResolver
      • 2.5.1 構造函數
      • 2.5.2 getResource
      • 2.5.3 getResources
      • 2.5.4 findAllClassPathResources
      • 2.5.5 findPathMatchingResources
        • 2.5.5.1 determineRootDir
  • 3. 小結

在學 Java SE 的時候,我們學習了一個标準類 java.net.URL,該類在 Java SE 中的定位為統一資源定位器(Uniform Resource Locator),但是我們知道它的實作基本隻限于網絡形式釋出的資源的查找和定位。然而,實際上資源的定義比較廣泛,除了網絡形式的資源,還有以二進制形式存在的、以檔案形式存在的、以位元組流形式存在的等等。而且它可以存在于任何場所,比如網絡、檔案系統、應用程式中。是以 java.net.URL 的局限性迫使 Spring 必須實作自己的資源加載政策,該資源加載政策需要滿足如下要求:

職能劃厘清楚。資源的定義和資源的加載應該要有一個清晰的界限;

統一的抽象。統一的資源定義和資源加載政策。資源加載後要傳回統一的抽象給用戶端,用戶端要對資源進行怎樣的處理,應該由抽象資源接口來界定。

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 子類結構

spring-core 源碼學習-IoC 之 Spring 統一資源加載政策(二)1. 統一資源:Resource2. 統一資源定位:ResourceLoader3. 小結

從上圖可以看到,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 抽象類,然後根據目前的具體資源特性覆寫相應的方法即可。

2. 統一資源定位:ResourceLoader

Spring 将資源的定義和資源的加載區分開了,Resource 定義了統一的資源,那資源的加載則由 ResourceLoader 來統一定義。

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

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 統一的資源加載器,它提供了統一的抽象,具體的實作則由相應的子類來負責實作,其類的類結構圖如下:

spring-core 源碼學習-IoC 之 Spring 統一資源加載政策(二)1. 統一資源:Resource2. 統一資源定位:ResourceLoader3. 小結

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.3 ClassRelativeResourceLoader

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

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]);
}
           

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

确定目錄,擷取該目錄下得所有資源。

在所獲得的所有資源後,進行疊代比對擷取我們想要的資源。

2.5.5.1 determineRootDir

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

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/

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 ,進行操作調試。