歡迎光臨
每天分享高質量文章

【死磕 Spring】—– IOC 之 Spring 統一資源加載策略

點擊上方“Java技術驛站”,選擇“置頂公眾號”。

有內涵、有價值的文章第一時間送達!

精品專欄

 

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

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

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

統一資源:Resource

org.springframework.core.io.Resource 為 Spring 框架所有資源的抽象和訪問接口,它繼承 org.springframework.core.io.InputStreamSource接口。作為所有資源的統一抽象,Source 定義了一些通用的方法,由子類 AbstractResource 提供統一的預設實現。定義如下:

  1. public interface Resource extends InputStreamSource {

  2.    /**

  3.     * 資源是否存在

  4.     */

  5.    boolean exists();

  6.    /**

  7.     * 資源是否可讀

  8.     */

  9.    default boolean isReadable() {

  10.        return true;

  11.    }

  12.    /**

  13.     * 資源所代表的句柄是否被一個stream打開了

  14.     */

  15.    default boolean isOpen() {

  16.        return false;

  17.    }

  18.    /**

  19.     * 是否為 File

  20.     */

  21.    default boolean isFile() {

  22.        return false;

  23.    }

  24.    /**

  25.     * 傳回資源的URL的句柄

  26.     */

  27.    URL getURL() throws IOException;

  28.    /**

  29.     * 傳回資源的URI的句柄

  30.     */

  31.    URI getURI() throws IOException;

  32.    /**

  33.     * 傳回資源的File的句柄

  34.     */

  35.    File getFile() throws IOException;

  36.    /**

  37.     * 傳回 ReadableByteChannel

  38.     */

  39.    default ReadableByteChannel readableChannel() throws IOException {

  40.        return Channels.newChannel(getInputStream());

  41.    }

  42.    /**

  43.     * 資源內容的長度

  44.     */

  45.    long contentLength() throws IOException;

  46.    /**

  47.     * 資源最後的修改時間

  48.     */

  49.    long lastModified() throws IOException;

  50.    /**

  51.     * 根據資源的相對路徑創建新資源

  52.     */

  53.    Resource createRelative(String relativePath) throws IOException;

  54.    /**

  55.     * 資源的檔案名

  56.     */

  57.    @Nullable

  58.    String getFilename();

  59.    /**

  60.     * 資源的描述

  61.     */

  62.    String getDescription();

  63. }

類結構圖如下:

從上圖可以看到,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 接口中的重中之重,其定義如下:

  1. public abstract class AbstractResource implements Resource {

  2.    /**

  3.     * 判斷檔案是否存在,若判斷過程產生異常(因為會呼叫SecurityManager來判斷),就關閉對應的流

  4.     */

  5.    @Override

  6.    public boolean exists() {

  7.        try {

  8.            return getFile().exists();

  9.        }

  10.        catch (IOException ex) {

  11.            // Fall back to stream existence: can we open the stream?

  12.            try {

  13.                InputStream is = getInputStream();

  14.                is.close();

  15.                return true;

  16.            }

  17.            catch (Throwable isEx) {

  18.                return false;

  19.            }

  20.        }

  21.    }

  22.    /**

  23.     * 直接傳回true,表示可讀

  24.     */

  25.    @Override

  26.    public boolean isReadable() {

  27.        return true;

  28.    }

  29.    /**

  30.     * 直接傳回 false,表示未被打開

  31.     */

  32.    @Override

  33.    public boolean isOpen() {

  34.        return false;

  35.    }

  36.    /**

  37.     *  直接傳回false,表示不為 File

  38.     */

  39.    @Override

  40.    public boolean isFile() {

  41.        return false;

  42.    }

  43.    /**

  44.     * 丟擲 FileNotFoundException 異常,交給子類實現

  45.     */

  46.    @Override

  47.    public URL getURL() throws IOException {

  48.        throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");

  49.    }

  50.    /**

  51.     * 基於 getURL() 傳回的 URL 構建 URI

  52.     */

  53.    @Override

  54.    public URI getURI() throws IOException {

  55.        URL url = getURL();

  56.        try {

  57.            return ResourceUtils.toURI(url);

  58.        }

  59.        catch (URISyntaxException ex) {

  60.            throw new NestedIOException("Invalid URI [" + url + "]", ex);

  61.        }

  62.    }

  63.    /**

  64.     * 丟擲 FileNotFoundException 異常,交給子類實現

  65.     */

  66.    @Override

  67.    public File getFile() throws IOException {

  68.        throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");

  69.    }

  70.    /**

  71.     * 根據 getInputStream() 的傳回結果構建 ReadableByteChannel

  72.     */

  73.    @Override

  74.    public ReadableByteChannel readableChannel() throws IOException {

  75.        return Channels.newChannel(getInputStream());

  76.    }

  77.    /**

  78.     * 獲取資源的長度

  79.     *

  80.     * 這個資源內容長度實際就是資源的位元組長度,通過全部讀取一遍來判斷

  81.     */

  82.    @Override

  83.    public long contentLength() throws IOException {

  84.        InputStream is = getInputStream();

  85.        try {

  86.            long size = 0;

  87.            byte[] buf = new byte[255];

  88.            int read;

  89.            while ((read = is.read(buf)) != -1) {

  90.                size += read;

  91.            }

  92.            return size;

  93.        }

  94.        finally {

  95.            try {

  96.                is.close();

  97.            }

  98.            catch (IOException ex) {

  99.            }

  100.        }

  101.    }

  102.    /**

  103.     * 傳回資源最後的修改時間

  104.     */

  105.    @Override

  106.    public long lastModified() throws IOException {

  107.        long lastModified = getFileForLastModifiedCheck().lastModified();

  108.        if (lastModified == 0L) {

  109.            throw new FileNotFoundException(getDescription() +

  110.                    " cannot be resolved in the file system for resolving its last-modified timestamp");

  111.        }

  112.        return lastModified;

  113.    }

  114.    protected File getFileForLastModifiedCheck() throws IOException {

  115.        return getFile();

  116.    }

  117.    /**

  118.     * 交給子類實現

  119.     */

  120.    @Override

  121.    public Resource createRelative(String relativePath) throws IOException {

  122.        throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());

  123.    }

  124.    /**

  125.     * 獲取資源名稱,預設傳回 null

  126.     */

  127.    @Override

  128.    @Nullable

  129.    public String getFilename() {

  130.        return null;

  131.    }

  132.    /**

  133.     * 傳回資源的描述

  134.     */

  135.    @Override

  136.    public String toString() {

  137.        return getDescription();

  138.    }

  139.    @Override

  140.    public boolean equals(Object obj) {

  141.        return (obj == this ||

  142.            (obj instanceof Resource && ((Resource) obj).getDescription().equals(getDescription())));

  143.    }

  144.    @Override

  145.    public int hashCode() {

  146.        return getDescription().hashCode();

  147.    }

  148. }

如果我們想要實現自定義的 Resource,記住不要實現 Resource 接口,而應該繼承 AbstractResource 抽象類,然後根據當前的具體資源特性改寫相應的方法即可。

統一資源定位:ResourceLoader

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

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

  1. public interface ResourceLoader {

  2.    String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;

  3.    Resource getResource(String location);

  4.    ClassLoader getClassLoader();

  5. }

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 用戶來說,可以直接呼叫該方法來獲取,

對於想要獲取 ResourceLoader 使用的 ClassLoader 用戶來說,可以直接呼叫 getClassLoader() 方法獲得。在分析 Resource 時,提到了一個類 ClassPathResource ,這個類是可以根據指定的 ClassLoader 來加載資源的。

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

DefaultResourceLoader

與 DefaultResource 相似,DefaultResourceLoader 是 ResourceLoader 的預設實現,它接收 ClassLoader 作為建構式的引數或者使用不帶引數的建構式,在使用不帶引數的建構式時,使用的 ClassLoader 為預設的 ClassLoader(一般為 Thread.currentThread().getContextClassLoader()),可以通過 ClassUtils.getDefaultClassLoader()獲取。當然也可以呼叫 setClassLoader()方法進行後續設置。如下:

  1.    public DefaultResourceLoader() {

  2.        this.classLoader = ClassUtils.getDefaultClassLoader();

  3.    }

  4.    public DefaultResourceLoader(@Nullable ClassLoader classLoader) {

  5.        this.classLoader = classLoader;

  6.    }

  7.    public void setClassLoader(@Nullable ClassLoader classLoader) {

  8.        this.classLoader = classLoader;

  9.    }

  10.    @Override

  11.    @Nullable

  12.    public ClassLoader getClassLoader() {

  13.        return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());

  14.    }

ResourceLoader 中最核心的方法為 getResource(),它根據提供的 location 傳回相應的 Resource,而 DefaultResourceLoader 對該方法提供了核心實現(它的兩個子類都沒有提供改寫該方法,所以可以斷定ResourceLoader 的資源加載策略就封裝 DefaultResourceLoader中),如下:

  1.    public Resource getResource(String location) {

  2.        Assert.notNull(location, "Location must not be null");

  3.        for (ProtocolResolver protocolResolver : this.protocolResolvers) {

  4.            Resource resource = protocolResolver.resolve(location, this);

  5.            if (resource != null) {

  6.                return resource;

  7.            }

  8.        }

  9.        if (location.startsWith("/")) {

  10.            return getResourceByPath(location);

  11.        }

  12.        else if (location.startsWith(CLASSPATH_URL_PREFIX)) {

  13.            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());

  14.        }

  15.        else {

  16.            try {

  17.                // Try to parse the location as a URL...

  18.                URL url = new URL(location);

  19.                return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));

  20.            }

  21.            catch (MalformedURLException ex) {

  22.                // No URL -> resolve as resource path.

  23.                return getResourceByPath(location);

  24.            }

  25.        }

  26.    }

首先通過 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 接口,僅有一個方法 Resourceresolve(Stringlocation,ResourceLoaderresourceLoader),該方法接收兩個引數:資源路徑location,指定的加載器 ResourceLoader,傳回為相應的 Resource 。在 Spring 中你會發現該接口並沒有實現類,它需要用戶自定義,自定義的 Resolver 如何加入 Spring 體系呢?呼叫 DefaultResourceLoader.addProtocolResolver() 即可,如下:

  1.    public void addProtocolResolver(ProtocolResolver resolver) {

  2.        Assert.notNull(resolver, "ProtocolResolver must not be null");

  3.        this.protocolResolvers.add(resolver);

  4.    }

下麵示例是演示 DefaultResourceLoader 加載資源的具體策略,代碼如下(該示例參考《Spring 解密》 P89):

  1.        ResourceLoader resourceLoader = new DefaultResourceLoader();

  2.        Resource fileResource1 = resourceLoader.getResource("D:/Users/chenming673/Documents/spark.txt");

  3.        System.out.println("fileResource1 is FileSystemResource:" + (fileResource1 instanceof FileSystemResource));

  4.        Resource fileResource2 = resourceLoader.getResource("/Users/chenming673/Documents/spark.txt");

  5.        System.out.println("fileResource2 is ClassPathResource:" + (fileResource2 instanceof ClassPathResource));

  6.        Resource urlResource1 = resourceLoader.getResource("file:/Users/chenming673/Documents/spark.txt");

  7.        System.out.println("urlResource1 is UrlResource:" + (urlResource1 instanceof UrlResource));

  8.        Resource urlResource2 = resourceLoader.getResource("http://www.baidu.com");

  9.        System.out.println("urlResource1 is urlResource:" + (urlResource2 instanceof  UrlResource));

運行結果:

  1. fileResource1 is FileSystemResource:false

  2. fileResource2 is ClassPathResource:true

  3. urlResource1 is UrlResource:true

  4. 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 型別傳回,這樣我們就可以得到想要的資源型別,如下:

  1.    @Override

  2.    protected Resource getResourceByPath(String path) {

  3.        if (path.startsWith("/")) {

  4.            path = path.substring(1);

  5.        }

  6.        return new FileSystemContextResource(path);

  7.    }

FileSystemContextResource 為 FileSystemResourceLoader 的內部類,它繼承 FileSystemResource。

  1.    private static class FileSystemContextResource extends FileSystemResource implements ContextResource {

  2.        public FileSystemContextResource(String path) {

  3.            super(path);

  4.        }

  5.        @Override

  6.        public String getPathWithinContext() {

  7.            return getPath();

  8.        }

  9.    }

在建構式中也是呼叫 FileSystemResource 的構造方法來構造 FileSystemResource 的。

如果將上面的示例將 DefaultResourceLoader 改為 FileSystemContextResource ,則 fileResource1 則為 FileSystemResource。

ResourcePatternResolver

ResourceLoader 的 ResourcegetResource(Stringlocation) 每次只能根據 location 傳回一個 Resource,當需要加載多個資源時,我們除了多次呼叫 getResource() 外別無他法。ResourcePatternResolver 是 ResourceLoader 的擴展,它支持根據指定的資源路徑匹配樣式每次傳回多個 Resource 實體,其定義如下:

  1. public interface ResourcePatternResolver extends ResourceLoader {

  2.    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

  3.    Resource[] getResources(String locationPattern) throws IOException;

  4. }

ResourcePatternResolver 在 ResourceLoader 的基礎上增加了 getResources(StringlocationPattern),以支持根據路徑匹配樣式傳回多個 Resource 實體,同時也新增了一種新的協議前綴 classpath*:,該協議前綴由其子類負責實現。

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

PathMatchingResourcePatternResolver 提供了三個構造方法,如下:

  1. public PathMatchingResourcePatternResolver() {

  2.        this.resourceLoader = new DefaultResourceLoader();

  3.    }

  4.    public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {

  5.        Assert.notNull(resourceLoader, "ResourceLoader must not be null");

  6.        this.resourceLoader = resourceLoader;

  7.    }

  8.    public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) {

  9.        this.resourceLoader = new DefaultResourceLoader(classLoader);

  10.    }

PathMatchingResourcePatternResolver 在實體化的時候,可以指定一個 ResourceLoader,如果不指定的話,它會在內部構造一個 DefaultResourceLoader。

Resource getResource(String location)

  1.    @Override

  2.    public Resource getResource(String location) {

  3.        return getResourceLoader().getResource(location);

  4.    }

getResource() 方法直接委托給相應的 ResourceLoader 來實現,所以如果我們在實體化的 PathMatchingResourcePatternResolver 的時候,如果不知道 ResourceLoader ,那麼在加載資源時,其實就是 DefaultResourceLoader 的過程。其實在下麵介紹的 Resource[]getResources(StringlocationPattern) 也相同,只不過傳回的資源時多個而已。

Resource[] getResources(String locationPattern)

  1.    public Resource[] getResources(String locationPattern) throws IOException {

  2.        Assert.notNull(locationPattern, "Location pattern must not be null");

  3.        // 以 classpath*: 開頭

  4.        if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {

  5.            // 路徑包含通配符

  6.            if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {

  7.                return findPathMatchingResources(locationPattern);

  8.            }

  9.            else {

  10.                // 路徑不包含通配符

  11.                return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));

  12.            }

  13.        }

  14.        else {

  15.            int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :

  16.                    locationPattern.indexOf(':') + 1);

  17.            // 路徑包含通配符

  18.            if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {

  19.                return findPathMatchingResources(locationPattern);

  20.            }

  21.            else {

  22.                return new Resource[] {getResourceLoader().getResource(locationPattern)};

  23.            }

  24.        }

  25.    }

處理邏輯如下圖:

下麵就 findAllClassPathResources()findAllClassPathResources() 做詳細分析。

findAllClassPathResources()

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

  1.    protected Resource[] findAllClassPathResources(String location) throws IOException {

  2.        String path = location;

  3.        if (path.startsWith("/")) {

  4.            path = path.substring(1);

  5.        }

  6.        Set<Resource> result = doFindAllClassPathResources(path);

  7.        if (logger.isDebugEnabled()) {

  8.            logger.debug("Resolved classpath location [" + location + "] to resources " + result);

  9.        }

  10.        return result.toArray(new Resource[0]);

  11.    }

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

  1.    protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {

  2.        Set<Resource> result = new LinkedHashSet<>(16);

  3.        ClassLoader cl = getClassLoader();

  4.        Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));

  5.        while (resourceUrls.hasMoreElements()) {

  6.            URL url = resourceUrls.nextElement();

  7.            result.add(convertClassLoaderURL(url));

  8.        }

  9.        if ("".equals(path)) {

  10.            addAllClassLoaderJarRoots(cl, result);

  11.        }

  12.        return result;

  13.    }

doFindAllClassPathResources() 根據 ClassLoader 加載路徑下的所有資源。在加載資源過程中如果,在構造 PathMatchingResourcePatternResolver 實體的時候如果傳入了 ClassLoader,則呼叫其 getResources(),否則呼叫 ClassLoader.getSystemResources(path)ClassLoader.getResources()如下:

  1.    public Enumeration<URL> getResources(String name) throws IOException {

  2.        @SuppressWarnings("unchecked")

  3.        Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration>[2];

  4.        if (parent != null) {

  5.            tmp[0] = parent.getResources(name);

  6.        } else {

  7.            tmp[0] = getBootstrapResources(name);

  8.        }

  9.        tmp[1] = findResources(name);

  10.        return new CompoundEnumeration<>(tmp);

  11.    }

看到這裡是不是就已經一目瞭然了?如果當前父類加載器不為 null,則通過父類向上迭代獲取資源,否則呼叫 getBootstrapResources()。這裡是不是特別熟悉,(^▽^)。

若 path 為 空(“”)時,則呼叫 addAllClassLoaderJarRoots()方法。該方法主要是加載路徑下得所有 jar 包,方法較長也沒有什麼實際意義就不貼出來了。

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

findAllClassPathResources()

當 locationPattern 以 classpath*: 開頭且當中包含了通配符,則呼叫該方法進行資源加載。如下:

  1. protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {

  2.        // 確定跟路徑

  3.        String rootDirPath = determineRootDir(locationPattern);

  4.        String subPattern = locationPattern.substring(rootDirPath.length());

  5.        // 獲取根據路徑下得資源

  6.        Resource[] rootDirResources = getResources(rootDirPath);

  7.        Set<Resource> result = new LinkedHashSet<>(16);

  8.        for (Resource rootDirResource : rootDirResources) {

  9.            rootDirResource = resolveRootDirResource(rootDirResource);

  10.            URL rootDirUrl = rootDirResource.getURL();

  11.            // bundle 資源型別

  12.            if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {

  13.                URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);

  14.                if (resolvedUrl != null) {

  15.                    rootDirUrl = resolvedUrl;

  16.                }

  17.                rootDirResource = new UrlResource(rootDirUrl);

  18.            }

  19.            // VFS 資源

  20.            if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {

  21.                result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));

  22.            }

  23.            // Jar

  24.            else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {

  25.                result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));

  26.            }

  27.            else {

  28.                result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));

  29.            }

  30.        }

  31.        if (logger.isDebugEnabled()) {

  32.            logger.debug("Resolved location pattern [" + locationPattern + "] to resources " + result);

  33.        }

  34.        return result.toArray(new Resource[0]);

  35.    }

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

  1. 確定目錄,獲取該目錄下得所有資源

  2. 在所獲得的所有資源中進行迭代匹配獲取我們想要的資源。

在這個方法裡面我們要關註兩個方法,一個是 determineRootDir(),一個是 doFindPathMatchingFileResources()

determineRootDir()主要是用於確定根路徑,如下:

  1.    protected String determineRootDir(String location) {

  2.        int prefixEnd = location.indexOf(':') + 1;

  3.        int rootDirEnd = location.length();

  4.        while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {

  5.            rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;

  6.        }

  7.        if (rootDirEnd == 0) {

  8.            rootDirEnd = prefixEnd;

  9.        }

  10.        return location.substring(0, rootDirEnd);

  11.    }

該方法一定要給出一個確定的根目錄。該根目錄用於確定檔案的匹配的起始點,將根目錄位置的資源解析為 java.io.File 並將其傳遞到 retrieveMatchingFiles(),其餘為知用於樣式匹配,找出我們所需要的資源。

確定根路徑如下:

至此 Spring 整個資源記載過程已經分析完畢。下麵簡要總結下:確定根路徑後,則呼叫 getResources() 方法獲取該路徑下得所有資源,然後迭代資源獲取符合條件的資源。

  • Spring 提供了 Resource 和 ResourceLoader 來統一抽象整個資源及其定位。使得資源與資源的定位有了一個更加清晰的界限,並且提供了合適的 Default 類,使得自定義實現更加方便和清晰。

  • DefaultResource 為 Resource 的預設實現,它對 Resource 接口做了一個統一的實現,子類繼承該類後只需要改寫相應的方法即可,同時對於自定義的 Resource 我們也是繼承該類。

  • DefaultResourceLoader 同樣也是 ResourceLoader 的預設實現,在自定 ResourceLoader 的時候我們除了可以繼承該類外還可以實現 ProtocolResolver 接口來實現自定資源加載協議。

  • DefaultResourceLoader 每次只能傳回單一的資源,所以 Spring 針對這個提供了另外一個接口 ResourcePatternResolver ,該接口提供了根據指定的 locationPattern 傳回多個資源的策略。其子類 PathMatchingResourcePatternResolver 是一個集大成者的 ResourceLoader ,因為它即實現了 ResourcegetResource(Stringlocation)也實現了 Resource[]getResources(StringlocationPattern)

END

赞(0)

分享創造快樂

© 2020 知識星球   网站地图