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

【死磕 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)

分享創造快樂

© 2024 知識星球   網站地圖