Spring中資源的加載原來是這麼一回事啊!

1. 簡介

在JDK中 java.net.URL 適用於加載資源的類,可是 URL 的實現類都是訪問網絡資源的,並無能夠從類路徑或者相對路徑獲取文件及 ServletContext , 雖然能夠經過自定義擴展URL接口來實現新的處理程序,可是這是很是複雜的,同時 URL 接口中定義的行爲也並非很理想的 ,如檢測資源的存在等的行爲,這也是 spring 爲何本身全新的開發一套本身的資源加載策略, 同時它也知足下面的特色:java

  • 單一職責原則,將資源的定義和資源的加載的模型界限劃的很是清晰
  • 採用高度抽象,統一的資源定義和資源加載的策略和行爲,資源加載返回給客戶端的是抽象的資源,客戶端根據資源的行爲定義對其進行具體化的處理

2. Resource 接口

spring 中的 Resource 接口目的在於成爲一種功能更增強大的接口,用於抽象化對具體資源的訪問,它繼承了 org.springframework.core.io.InputStreamSource 接口,做爲資源定義的頂級接口, Resource 內部定義了通用的方法,而且有它的子類 AbstractResource 來提供統一的默認實現,web

Resouerce 接口定義:spring

//資源定義接口
public interface Resource extends InputStreamSource {

    /**
     * 檢驗資源是不是物理存在
     */
    boolean exists();

    /**
     * 判斷資源是不是可讀的
     */
    default boolean isReadable() {
        return exists();
    }

    /**
     * 判斷資源是不是打開的,true爲打開
     */
    default boolean isOpen() {
        return false;
    }

    /**
     * 判斷該資源是不是文件 true爲是
     */
    default boolean isFile() {
        return false;
    }

    /**
     * 返回該資源的URL句柄
     */
    URL getURL() throws IOException;

    /**
     * 返回該資源的URI句柄
     */
    URI getURI() throws IOException;

    /**
     * 獲取該資源的File句柄
     */
    File getFile() throws IOException;

    /**
     * 返回一個ReadableByteChannel 做爲NIO中的可讀通道
     */
    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();

}

InputStreamSource 接口定義:express

public interface InputStreamSource {

    /**
     * Return an {@link InputStream} for the content of an underlying resource.
     * <p>It is expected that each call creates a <i>fresh</i> stream.
     * <p>This requirement is particularly important when you consider an API such
     * as JavaMail, which needs to be able to read the stream multiple times when
     * creating mail attachments. For such a use case, it is <i>required</i>
     * that each {@code getInputStream()} call returns a fresh stream.
     * @return the input stream for the underlying resource (must not be {@code null})
     * @throws java.io.FileNotFoundException if the underlying resource doesn't exist
     * @throws IOException if the content stream could not be opened
     */
    InputStream getInputStream() throws IOException;

}

Resource 中一些最重要的方法:數組

  • getInputStream() :找到並打開資源,並返回一個資源以 InputStream 供讀取,每次調用都會返回一個新的 InputStream ,調用者有責任關閉流
  • exists() :返回 boolean 指示此資源是否實際以物理形式存在。
  • isOpen() :返回, boolean 指示此資源是否表示具備打開流的句柄, 若是爲 trueInputStream 則不能屢次讀取,必須只讀取一次,而後將其關閉以避免資源泄漏。返回 false 全部經常使用資源實現(除外) InputStreamResource 可讀
  • getDescription() :返回對此資源的描述,以便在使用該資源時用於錯誤輸出。這一般是標準文件名或資源的實際 URL

    Resource 實現微信

Resource類圖

  • UrlResource : 包裝一個 java.net.URL ,可用於訪問一般能夠經過 URL 訪問的任何對象,例如文件, HTTP 目標, FTP 目標等。全部 URL 都有一個標準化的 String 表示形式,所以適當的標準化前綴可用於指示另外一種 URL 類型。如: file : 訪問文件系統路徑, http : 經過 HTTP 協議 ftp : 訪問資源,經過 FTP 訪問資源等
  • ClassPathResource : 此類表示應從類路徑獲取的資源。它使用線程上下文類加載器( ClassLoader ),給定的類加載器或給定的類來加載資源
  • FileSystemResource : 是一個 Resource 執行 java.io.Filejava.nio.file.Path 類型資源的封裝,它支持 FileURL , 實現 WritableResource 接口,且從 Spring Framework 5.0 開始, FileSystemResource 使用 NIO2 API 進行讀/寫交互
  • ServletContextResource : 該 ServletContex t資源解釋相關 Web 應用程序的根目錄內的相對路徑。
  • InputStreamResource : 將給定的 InputStream 做爲一種資源的 Resource 的實現類
  • ByteArrayResource : 這是Resource給定字節數組的實現。它爲給定的字節數組建立一個 ByteArrayInputStream

3. ResourceLoader 接口

ResourceLoader 主要是用於返回(即加載) Resource 對象,主要定義:網絡

public interface ResourceLoader {

    /** Pseudo URL prefix for loading from the class path: "classpath:". */
    String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;

    /**
     * 返回指定路徑的資源處理器
     * 必須支持徹底限定的網址: "file:C:/test.dat"
     * 必須支持ClassPath 的 URL  :"classpath:test.dat"
     * 必須支持相對路徑  : "WEB-INF/test.dat"
     * 並不能保證資源是否物理存在,須要本身去檢測經過existence
     * 再spring中全部的應用上下文都去實現這個接口,能夠進行資源的加載
     */
    Resource getResource(String location);

    /**
     * 返回當前類的 ClassLoader 對象
     */
    @Nullable
    ClassLoader getClassLoader();

}
  • 應用上下文即容器都有實現 ResourceLoader 這個接口,全部的上下文均可以用於獲取 Resource 實例對象
  • 咱們能夠在特定的應用上下文中經過 getResource() 來獲取特定類型的 Resource 實例,可是的保證 location 路徑沒有特殊的前綴,如 classpatch: 等,若是有特定前綴慢麼會強制使用相應的資源類型,與上下文無關。
Prefix Example Explanation
classpath: classpath:com/myapp/config.xml 從類路徑加載
file: file:///data/config.xml 從文件系統做爲 URL 加載
http: https://myserver/logo.png 按照URL形式加載
(none) /data/config.xml 取決於應用上下文

ResourceLoader 的子類結構:app

### 3.1 DefaultResourceLoader ide

這個類是 ResourceLoader 的默認實現類,與 Resource 接口的 AbstractResource 同樣,函數

#### 3.1.1. 構造函數

  • 提供有參和無參的構造函數,有參構造函數接受 ClassLoader 類型,如不帶參數則使用默認的 ClassLoader , Thread.currentThread()#getContextClassLoader()

核心代碼代碼,部分省去:

public class DefaultResourceLoader implements ResourceLoader {

    @Nullable
    private ClassLoader classLoader;

    private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);

    private final Map<Class<?>, Map<Resource, ?>> resourceCaches = new ConcurrentHashMap<>(4);

    /**
     * 無參構造函數
     * @see java.lang.Thread#getContextClassLoader()
     */
    public DefaultResourceLoader() {
        this.classLoader = ClassUtils.getDefaultClassLoader();
    }

    /**
     * 帶ClassLoader的有參構造函數
     */
    public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    /**
     * 設置 ClassLoader
     */
    public void setClassLoader(@Nullable ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    /**
     * Return the ClassLoader to load class path resources with.、
     * @see ClassPathResource
     */
    @Override
    @Nullable
    public ClassLoader getClassLoader() {
        return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
    }

    /**
     * Obtain a cache for the given value type, keyed by {@link Resource}.
     * @param valueType the value type, e.g. an ASM {@code MetadataReader}
     * @return the cache {@link Map}, shared at the {@code ResourceLoader} level
     * @since 5.0
     */
    @SuppressWarnings("unchecked")
    public <T> Map<Resource, T> getResourceCache(Class<T> valueType) {
        return (Map<Resource, T>) this.resourceCaches.computeIfAbsent(valueType, key -> new ConcurrentHashMap<>());
    }

    /**
     * Clear all resource caches in this resource loader.
     * @since 5.0
     * @see #getResourceCache
     */
    public void clearResourceCaches() {
        this.resourceCaches.clear();
    }
}

3.1.2 getResource() 核心方法

ResourceLoader 中最核心的方法,他根據傳入的 location 來返回相應的Resource,而 DefaultResourceLoader 對其作了核心實現, 子類都沒覆蓋該方法,因此咱們能夠判定 ResourceLoader 加載資源的核心策略都在 DefaultResourceLoader

核心代碼:

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

        //1. 經過 ProtocolResolver 協議解析器來記載資源
        for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
            Resource resource = protocolResolver.resolve(location, this);
            if (resource != null) {
                return resource;
            }
        }

        //2.若是location是以 / 開頭則返回 ClassPathContextResource 類型的 資源
        if (location.startsWith("/")) {
            return getResourceByPath(location);
        }//3.若是是以 classpath: 開頭,則返回 ClassPathResource 類型的資源
        else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
        }
        else {
            try {
                //4.若是不是以上兩種,則判斷是不是 File URL ,若是是返回FileUrlResource 不然 返回UrlResource
                // 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.
                //5.最後則返回ClassPathContextResource
                return getResourceByPath(location);
            }
        }
    }

上述代碼中具體說明了執行的流程,其中 getResourceByPath(location) 的具體實現代碼以下:

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

3.1.3 ProtocolResolver

全限定類名: org.springframework.core.io.ProtocolResolver ,是一個接口,用於用戶自定義協議資源解析策略,是 DefaultResourceLoaderSPI ,容許處理自定義協議而無需將加載程序實現(或應用程序上下文實現)爲子類,即不須要繼承 ResourceLoader 的子類 DefaultResourceLoader , 而直接實現 ProtocolResolver 接口就能夠自定義 ResourceLoader

@FunctionalInterface
public interface ProtocolResolver {

    /**
     * 使用指定的ResourceLoader  來解析location路徑的 資源
     * 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
     * @return a corresponding {@code Resource} handle if the given location
     * matches this resolver's protocol, or {@code null} otherwise
     */
    @Nullable
    Resource resolve(String location, ResourceLoader resourceLoader);

}
在spring中該類並無任何實現類,他須要用戶本身實現,那麼自定義的 ProtocolResolver 如何加載到spring中呢?在咱們 DefaultResourceLoader 類中有一個方法 addProtocolResolver(ProtocolResolver resolver) 則是用來添加的
/**
     * Register the given resolver with this resource loader, allowing for
     * additional protocols to be handled.
     * <p>Any such resolver will be invoked ahead of this loader's standard
     * resolution rules. It may therefore also override any default rules.
     * @since 4.3
     * @see #getProtocolResolvers()
     */
    public void addProtocolResolver(ProtocolResolver resolver) {
        Assert.notNull(resolver, "ProtocolResolver must not be null");
        this.protocolResolvers.add(resolver);
    }

3.2 FileSystemResourceLoader

DefaultResourceLoadergetResourceByPath() 方法的處理是直接返回了一個 ClassPathContextResource 類型的資源,這實際上是不完善的,在spring中 FileSystemResourceLoader 類繼承了 DefaultResourceLoader ,同時重寫了 getResourceByPath() 方法,使用標準的文件系統讀入,而且返回 FileSystemContextResource 類型

public class FileSystemResourceLoader extends DefaultResourceLoader {

    /**
     * Resolve resource paths as file system paths.
     * <p>Note: Even if a given path starts with a slash, it will get
     * interpreted as relative to the current VM working directory.
     * @param path the path to the resource
     * @return the corresponding Resource handle
     * @see FileSystemResource
     * @see org.springframework.web.context.support.ServletContextResourceLoader#getResourceByPath
     */
    @Override
    protected Resource getResourceByPath(String path) {
        if (path.startsWith("/")) {
            path = path.substring(1);
        }
        return new FileSystemContextResource(path);
    }

    /**
     * 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();
        }
    }

}
  • 咱們能夠從 上面的代碼中看到 在 FileSystemResourceLoader 中有一個私有的內部類 FileSystemContextResource , 這個類繼承了 FileSystemResource ,同時實現了 ContextResource 接口
  • FileSystemContextResource 經過構造函數調用 FileSystemResource 的構造函數,建立 FileSystemResource 類型資源定義,同時實現 ContextResource 是爲了實現其中的 getPathWithinContext() 方法,這個方法是用來獲取上下文根路徑的, 源碼中這樣寫的 :
/**
* Return the path within the enclosing 'context'.  
 * This is typically path relative to a context-specific root directory,  
 * e.g. a ServletContext root or a PortletContext root.  
 */

3.3 ClassRelativeResourceLoader

org.springframework.core.io.ClassRelativeResourceLoader 類也是 DefaultResourceLoader 的另外一個實現子類,與 FileSystemResourceLoader 相似,也一樣重寫了 getResourceByPath() 方法,也內部維護了一個私有的內部類 ClassRelativeContextResource , 具體代碼以下:

/**
 * 從給定的 class 下加載資源
 * {@link ResourceLoader} implementation that interprets plain resource paths
 * as relative to a given {@code java.lang.Class}.
 *
 * @author Juergen Hoeller
 * @since 3.0
 * @see Class#getResource(String)
 * @see ClassPathResource#ClassPathResource(String, Class)
 */
public class ClassRelativeResourceLoader extends DefaultResourceLoader {

    private final Class<?> clazz;

    /**
     * Create a new ClassRelativeResourceLoader for the given class.
     * @param clazz the class to load resources through
     */
    public ClassRelativeResourceLoader(Class<?> clazz) {
        Assert.notNull(clazz, "Class must not be null");
        this.clazz = clazz;
        setClassLoader(clazz.getClassLoader());
    }

    /**
     * 重寫getResourceByPath 方法 , 返回一個ClassRelativeContextResource 資源類型
     * @param path the path to the resource
     * @return
     */
    @Override
    protected Resource getResourceByPath(String path) {
        return new ClassRelativeContextResource(path, this.clazz);
    }

    /**
     * 繼承 ClassPathResource  定義資源類型,實現ContextResource 中的 getPathWithinContext 方法,
     *
     * ClassPathResource that explicitly expresses a context-relative path
     * through implementing the ContextResource interface.
     */
    private static class ClassRelativeContextResource extends ClassPathResource implements ContextResource {

        private final Class<?> clazz;

        /**
         * 調用父類 ClassPathResource 對資源進行初始化
         * @param path
         * @param clazz
         */
        public ClassRelativeContextResource(String path, Class<?> clazz) {
            super(path, clazz);
            this.clazz = clazz;
        }

        @Override
        public String getPathWithinContext() {
            return getPath();
        }

        /**
         * 重寫 ClassPathContext 中方法, 經過給定的路徑返回一個ClassRelativeContextResource資源
         * @param relativePath the relative path (relative to this resource)
         * @return
         */
        @Override
        public Resource createRelative(String relativePath) {
            String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath);
            return new ClassRelativeContextResource(pathToUse, this.clazz);
        }
    }

}

3.4 ResourcePatternResolver

org.springframework.core.io.support.ResourcePatternResolver 是對 ResourceLoader 的一個擴展,咱們在 ResourceLoader 中經過 getResource 方法獲取 Resource 實例時,只能經過一個 location 來獲取一個 Resource , 而不能獲取到多個 Resource , 當咱們須要加載多個資源時,只能經過調用屢次的該方法來實現,因此spring 提供了 ResourcePatternResolver 對其進行了擴展,實現了經過 location 來加載多個資源,類的定義以下:

public interface ResourcePatternResolver extends ResourceLoader {

    /**
     * Pseudo URL prefix for all matching resources from the class path: "classpath*:"
     * This differs from ResourceLoader's classpath URL prefix in that it
     * retrieves all matching resources for a given name (e.g. "/beans.xml"),
     * for example in the root of all deployed JAR files.
     * @see org.springframework.core.io.ResourceLoader#CLASSPATH_URL_PREFIX
     */
    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

    /**
     * Resolve the given location pattern into Resource objects.
     * <p>Overlapping resource entries that point to the same physical
     * resource should be avoided, as far as possible. The result should
     * have set semantics.
     * @param locationPattern the location pattern to resolve
     * @return the corresponding Resource objects
     * @throws IOException in case of I/O errors
     */
    Resource[] getResources(String locationPattern) throws IOException;

}
  • 能夠看到 ResourcePatternResolver 新增長了一個方法 getResources ,返回一個 Resource 數組
  • 這裏咱們要注意, ResourcePatternResolver 增長了一個新的協議前綴 classpath*: , 看到這裏是否是你們能夠很熟悉的想起咱們在平時配置路徑時常常會寫 classpath:classpath*: ,那麼他們的區別就在這裏,他們的資源加載方式時不同的

3.5 PathMatchingResourcePatternResolver

org.springframework.core.io.support.PathMatchingResourcePatternResolverResourcePatternResolver 的一個主要實現類,也是使用較多的一個實現類,咱們能夠來看一下,它主要實現了 新增前綴的解析,同時還支持 Ant 風格的路徑匹配模式(如 : "**/*.xml" )

3.5.1 構造函數

PathMatchingResourcePatternResolver 提供了三個構造函數:

/**
     * 內置 資源定位加載器
     */
    private final ResourceLoader resourceLoader;

    /**
     * Ant路徑匹配器
     */
    private PathMatcher pathMatcher = new AntPathMatcher();

    /**
     * 無參構造函數,當不指定內部加載器類型時,默認是 DefaultResourceLoader
     * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader.
     * <p>ClassLoader access will happen via the thread context class loader.
     * @see org.springframework.core.io.DefaultResourceLoader
     */
    public PathMatchingResourcePatternResolver() {
        this.resourceLoader = new DefaultResourceLoader();
    }

    /**
     * 指定特定的資源定位加載器
     * Create a new PathMatchingResourcePatternResolver.
     * <p>ClassLoader access will happen via the thread context class loader.
     * @param resourceLoader the ResourceLoader to load root directories and
     * actual resources with
     */
    public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
        Assert.notNull(resourceLoader, "ResourceLoader must not be null");
        this.resourceLoader = resourceLoader;
    }

    /**
     * 使用默認的資源加載器,可是傳入 classLoader ,使用特定的類加載
     * Create a new PathMatchingResourcePatternResolver with a DefaultResourceLoader.
     * @param classLoader the ClassLoader to load classpath resources with,
     * or {@code null} for using the thread context class loader
     * at the time of actual resource access
     * @see org.springframework.core.io.DefaultResourceLoader
     */
    public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) {
        this.resourceLoader = new DefaultResourceLoader(classLoader);
    }
  • 咱們能夠看到,當構造函數不提供 ResourceLoader 時,默認是 DefaultResourceLoader

3.5.2 getResource

PathMatchingResourcePatternResolver 中的 getResource 方法的實現是調用了 傳入的 ResourceLoader 或者默認的 DefaultResourceLoader , 具體的代碼實現以下:

/**
     * 調用getResourceLoader 獲取當前的 ResourceLoader 
     * @param location the resource location
     * @return
     */
    @Override
    public Resource getResource(String location) {
        return getResourceLoader().getResource(location);
    }
    
        /**
     * Return the ResourceLoader that this pattern resolver works with.
     */
    public ResourceLoader getResourceLoader() {
        return this.resourceLoader;
    }

3.5.3 getResources

實現了 ResourcePatternResolvergetResources 方法,能夠經過 location 加載多個資源,進行分類處理,若是是沒有 classpath*: 前綴以及不包含通配符的狀況下直接調用當前類的 ResourceLoader 來進行處理,其餘按具體來處理,主要涉及兩個方法 #findPathMatchingResources(...)#findAllClassPathResources(...)

@Override
    public Resource[] getResources(String locationPattern) throws IOException {
        Assert.notNull(locationPattern, "Location pattern must not be null");
        //1. 判斷 是否是classpath* 開頭的
        if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
            //1.1.進行路徑匹配校驗 是否包含通配符
            // 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 {
                //1.2 不包含通配符
                // all class path resources with the given name
                return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
            }
        }
        else {
            // 2. 不是classpath前綴開頭
            // Generally only look for a pattern after a prefix here,
            // and on Tomcat only after the "*/" separator for its "war:" protocol.
            int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
                    locationPattern.indexOf(':') + 1);
            //2.1 校驗是否包含通配符
            if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
                // a file pattern
                return findPathMatchingResources(locationPattern);
            }
            else {
                //2.2 不包含通配符 使用內部 ResourceLoader 進行資源加載 默認是 DefaultReourceLoader
                // a single resource with the given name
                return new Resource[] {getResourceLoader().getResource(locationPattern)};
            }
        }
    }

3.5.4 findPathMatchingResources

上面代碼中咱們能夠看到,當存在通配符時都會執行 #findPathMatchingResources(...) 方法,咱們來看一下方法的定義:

/**
     * 經過ant解析器來對給定的路徑下的全部模糊資源進行解析和匹配
     * 支持jar和zip以及系統中的文件資源
     * Find all resources that match the given location pattern via the
     * Ant-style PathMatcher. Supports resources in jar files and zip files
     * and in the file system.
     * @param locationPattern the location pattern to match
     * @return the result as Resource array
     * @throws IOException in case of I/O errors
     * @see #doFindPathMatchingJarResources
     * @see #doFindPathMatchingFileResources
     * @see org.springframework.util.PathMatcher
     */
    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);
        }
        //轉換爲數組返回
        return result.toArray(new Resource[0]);
    }
  • spring 中不少真真作操做的方法命名都是以 do 開頭,咱們從上面能夠看到核心方法 #doFindPathMatchingFileResources(...)#doFindPathMatchingJarResources(...) 這兩個基本同樣知識解析不懂的文件類型,另外還有一個方法 #determineRootDir(...) 方法實現了路徑的解析,下面咱們簡單看一這兩個實現。
3.5.4.1 determineRootDir(... )

determineRootDir 方法主要用於根路徑的獲取,解析路徑中的通配符,代碼以下:

/**
     * 經過給定的路徑來獲取根目錄路徑
     * Determine the root directory for the given location.
     * <p>Used for determining the starting point for file matching,
     * resolving the root directory location to a {@code java.io.File}
     * and passing it into {@code retrieveMatchingFiles}, with the
     * remainder of the location as pattern.
     * <p>Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml",
     * for example.
     * @param location the location to check
     * @return the part of the location that denotes the root directory
     * @see #retrieveMatchingFiles
     */
    protected String determineRootDir(String location) {
        //1. 找到最後 路徑中出現的 : 的索引 +1 ,這裏注意咱們的路徑時 相似 : classpath*: /web-inf/*.xml
        int prefixEnd = location.indexOf(':') + 1;
        //2. 獲取跟路徑長度
        int rootDirEnd = location.length();
        //3.判斷冒號後面的路徑是否包含通配符 若是包含,則截斷最後一個由」/」分割的部分。
        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);
    }

舉例看一下:

原路徑 獲取跟路徑
classpath*:/test/aa*/app-*.xml classpath*:/test/
classpath*:/test/aa/app-*.xml classpath*:/test/aa
3.5.4.2 doFindPathMatchingFileResources(... )

#doFindPathMatchingFileResources(...)#doFindPathMatchingJarResources(...) 方法的的內部基本一致,只是解析不一樣的類型文件,咱們這裏只看其中一個則可,你們能夠自行比對二者的區別。

  • 咱們跟一下 #doFindPathMatchingFileResources(...) 方法,方法內部調用較深,因此下面我主要把代碼貼出來,註釋已有,相信能夠看的懂
  • #doFindPathMatchingFileResources(...) 代碼:
/**
     * 查找文件系統符合給定的location的資源, 路徑符合 ant 樣式的通配符
     * Find all resources in the file system that match the given location pattern
     * via the Ant-style PathMatcher.
     * @param rootDirResource the root directory as Resource
     * @param subPattern the sub pattern to match (below the root directory)
     * @return a mutable Set of matching Resource instances
     * @throws IOException in case of I/O errors
     * @see #retrieveMatchingFiles
     * @see org.springframework.util.PathMatcher
     */
    protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
            throws IOException {

        File rootDir;
        try {
            //獲取絕對路徑對應的文件目錄
            rootDir = rootDirResource.getFile().getAbsoluteFile();
        }
        catch (FileNotFoundException ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Cannot search for matching files underneath " + rootDirResource +
                        " in the file system: " + ex.getMessage());
            }
            return Collections.emptySet();
        }
        catch (Exception ex) {
            if (logger.isInfoEnabled()) {
                logger.info("Failed to resolve " + rootDirResource + " in the file system: " + ex);
            }
            return Collections.emptySet();
        }
        //調用真真處理方法
        return doFindMatchingFileSystemResources(rootDir, subPattern);
    }
  • 上面方法中主要調用了核心流程 #doFindMatchingFileSystemResources(...) , 代碼以下:
/**
     * 經過ant通配符的subPattern與已經獲取的根目錄rootDir來組合獲取全部在文件系統中的資源
     * 如:咱們原本的 url: 'classpath*:/test/aa/app-*.xml'
     * 那麼這裏rootDir:classpath*:/test/aa/   subPattern :app-*.xml
     * Find all resources in the file system that match the given location pattern
     * via the Ant-style PathMatcher.
     * @param rootDir the root directory in the file system
     * @param subPattern the sub pattern to match (below the root directory)
     * @return a mutable Set of matching Resource instances
     * @throws IOException in case of I/O errors
     * @see #retrieveMatchingFiles
     * @see org.springframework.util.PathMatcher
     */
    protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
        if (logger.isTraceEnabled()) {
            logger.trace("Looking for matching resources in directory tree [" + rootDir.getPath() + "]");
        }
        //調用真實處理方法,獲取set集合的File
        Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
        Set<Resource> result = new LinkedHashSet<>(matchingFiles.size());
        //將獲取的File轉換爲 FileSystemResource 同時添加到result結果集中
        for (File file : matchingFiles) {
            result.add(new FileSystemResource(file));
        }
        return result;
    }

上面方法主要是將獲取的 Set<File> 的結果進行轉換,將資源類型轉換爲 FileSystemResource , 上面方法的核心方法是 #retrieveMatchingFiles(...)

  • #retrieveMatchingFiles(...) 代碼以下:
/**
     *
     * Retrieve files that match the given path pattern,
     * checking the given directory and its subdirectories.
     * @param rootDir the directory to start from
     * @param pattern the pattern to match against,
     * relative to the root directory
     * @return a mutable Set of matching Resource instances
     * @throws IOException if directory contents could not be retrieved
     */
    protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException {
        //1.不存在直接返回空集合
        if (!rootDir.exists()) {
            // Silently skip non-existing directories.
            if (logger.isDebugEnabled()) {
                logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist");
            }
            return Collections.emptySet();
        }
        //2.不是目錄直接返回空
        if (!rootDir.isDirectory()) {
            // Complain louder if it exists but is no directory.
            if (logger.isInfoEnabled()) {
                logger.info("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory");
            }
            return Collections.emptySet();
        }
        //3/判斷是否可讀
        if (!rootDir.canRead()) {
            if (logger.isInfoEnabled()) {
                logger.info("Skipping search for matching files underneath directory [" + rootDir.getAbsolutePath() +
                        "] because the application is not allowed to read the directory");
            }
            return Collections.emptySet();
        }
        //4.將全部的系統分割器轉換爲 /
        String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
        //5.若子路徑開頭沒有 / 則父路徑要最後添加 /
        if (!pattern.startsWith("/")) {
            fullPattern += "/";
        }
        fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");
        Set<File> result = new LinkedHashSet<>(8);
        //真真處理的方法
        doRetrieveMatchingFiles(fullPattern, rootDir, result);
        return result;
    }

咱們能夠看到方法中主要是作了一些校驗和轉換,真真的處理是調用了 #doRetrieveMatchingFiles(...) 方法,

  • #doRetrieveMatchingFiles(...) 方法定義:
/**
     * 遞歸遍歷 dir 目錄 結合fullpattern 進行路徑匹配,將符合的資源所有放入result中
     * Recursively retrieve files that match the given pattern,
     * adding them to the given result list.
     * @param fullPattern the pattern to match against,
     * with prepended root directory path
     * @param dir the current directory
     * @param result the Set of matching File instances to add to
     * @throws IOException if directory contents could not be retrieved
     */
    protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
        if (logger.isTraceEnabled()) {
            logger.trace("Searching directory [" + dir.getAbsolutePath() +
                    "] for files matching pattern [" + fullPattern + "]");
        }
        //遍歷目錄
        for (File content : listDirectory(dir)) {
            //獲取當前文件/目錄的路徑同時分隔符所有替換爲 /
            String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
            //若是是目錄 同時和 fullPattern匹配 則進遞歸
            if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
                if (!content.canRead()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
                                "] because the application is not allowed to read the directory");
                    }
                }
                else {
                    doRetrieveMatchingFiles(fullPattern, content, result);
                }
            }
            //若是是文件則進行匹配
            if (getPathMatcher().match(fullPattern, currPath)) {
                result.add(content);
            }
        }
    }

3.5.5 findAllClassPathResources

上面分析了當有通配符時的方法調用過程,那麼這裏咱們來分析當沒有通配符時的方法調用

  • #findAllClassPathResources(...) 方法代碼:
/**
     * 經過ClassLoader 來 加載全部的 class location
     * Find all class location resources with the given location via the ClassLoader.
     * Delegates to {@link #doFindAllClassPathResources(String)}.
     * @param location the absolute path within the classpath
     * @return the result as Resource array
     * @throws IOException in case of I/O errors
     * @see java.lang.ClassLoader#getResources
     * @see #convertClassLoaderURL
     */
    protected Resource[] findAllClassPathResources(String location) throws IOException {
        String path = location;
        if (path.startsWith("/")) {
            path = path.substring(1);
        }
        //真實處理方法, 獲得資源結果集
        Set<Resource> result = doFindAllClassPathResources(path);
        if (logger.isTraceEnabled()) {
            logger.trace("Resolved classpath location [" + location + "] to resources " + result);
        }
        return result.toArray(new Resource[0]);
    }
  • #doFindAllClassPathResources(...) 方法代碼:
/**
     * Find all class location resources with the given path via the ClassLoader.
     * Called by {@link #findAllClassPathResources(String)}.
     * @param path the absolute path within the classpath (never a leading slash)
     * @return a mutable Set of matching Resource instances
     * @since 4.1.1
     */
    protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
        Set<Resource> result = new LinkedHashSet<>(16);
        ClassLoader cl = getClassLoader();
        //1.經過ClassLoader獲取全部的URl
        Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
        while (resourceUrls.hasMoreElements()) {
            //將URl轉換爲  UrlResource
            URL url = resourceUrls.nextElement();
            result.add(convertClassLoaderURL(url));
        }
        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...
            //添加全部的jar包
            addAllClassLoaderJarRoots(cl, result);
        }
        return result;
    }

方法內相對簡單,主要是經過ClassLoader來加載目錄下的jar資源,詳細再也不貼出來,能夠自行查看

本文由AnonyStar 發佈,可轉載但需聲明原文出處。 仰慕「優雅編碼的藝術」 堅信熟能生巧,努力改變人生 歡迎關注微信公帳號 :coder簡碼 獲取更多優質文章
相關文章
相關標籤/搜索