天天看點

Spring源碼分析2---IOC 之 Spring 統一資源加載政策

原文出自:http://cmsblogs.com

該資源加載政策需要滿足如下要求:

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

統一資源: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();

}
           

類結構圖如下:

Spring源碼分析2---IOC 之 Spring 統一資源加載政策

從上圖可以看到,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]);
    }
           

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

  1. 确定目錄,擷取該目錄下得所有資源
  2. 在所獲得的所有資源中進行疊代比對擷取我們想要的資源。

在這個方法裡面我們要關注兩個方法,一個是 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 整個資源記載過程已經分析完畢。下面簡要總結下:

  1. Spring 提供了 Resource 和 ResourceLoader 來統一抽象整個資源及其定位。使得資源與資源的定位有了一個更加清晰的界限,并且提供了合适的 Default 類,使得自定義實作更加友善和清晰。
  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)。