Spring源码_02-IoC 之深入剖析Spring资源加载


我准备战斗到最后,不是因为我勇敢,是我想见证一切。 --双雪涛《猎人》

[TOC]
Thinking

  1. 一个技术,为什么要用它,解决了那些问题?
  2. 如果不用会怎么样,有没有其它的解决方法?
  3. 对比其它的解决方案,为什么最终选择了这种,都有何利弊?
  4. 你觉得项目中还有那些地方可以用到,如果用了会带来那些问题?
  5. 这些问题你又如何去解决的呢?

本文主要基于 Spring 5.2.2.RELEASE

春天这么春天,冬天还会远吗?Fuker!!!

资源加载器
在上文简单介绍了,Ioc的基本原理,和简单的实例,作为程序入口,首先要考虑的是,Spring到底是如何加载XML文件的。只有加载到了指定的文件,才能进行下面更加伟大的事情。

​ 在Java中,将不同来源的资源抽象成URL,通过注册不同的handler(URLStreamHandler)来处理不同来源的资源的读取逻辑,一般handler的类型使用不同的前缀The protocol to use (ftp, http, nntp, ... etc.) .协议来识别的。但是Java内部并没有定义针对于protocol is classpath:/ServletContext等协议,而且并没有提供对这些协议并没有提供基本方法,如检查当前资源是否存在、检查当前资源是否可读等方法。

所以Spring提供了非常多,且职能划分非常清楚的资源加载器。形成了上文提到的Resource体系

1、统一资源接口:Resource

Spring提供了配置文件的封装,将所有类型的文件都将返回InputStream的对象。

Resource提供了所有Spring内部需要用到的底层资源:File、URL、Classpath等。并且提供了很多通用的检查方法。

public interface InputStreamSource {
    InputStream getInputStream() throws IOException;

}

public interface Resource extends InputStreamSource {
    //判断资源是否存在
    boolean exists();
//资源是否可读
    default boolean isReadable() {
        return exists();
    }
//资源所代表的句柄是否处于打开状态
    default boolean isOpen() {
        return false;
    }
//确定此资源是否代表文件系统中的文件
    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();

}

1.1、子类结构

Spring针对不同来源的资源文件都有相应的Resource实现。

​ 类结构图


Resouce类图
  • FileSystemResouce:对java.io.dile类型资源的封装,只要是跟File大叫的,基本上都可以使用FileSystemResource
    • Spring5.0之后,FileSystemResouce使用NIO 2 API对读写进行交互,
    • Spring5.1之后,可以使用java.nio.file.Path构造,这样构造的文件对象都可以只是用类下的getFile()操作,同样也是使用NIO 2 API
    • 源码解析比较简单,就不全部贴出来了。
public class FileSystemResource extends AbstractResource implements WritableResource {

    private final String path;

    @Nullable
    private final File file;

    private final Path filePath;
    // 比较重要的 提示到spring5.1之后添加的两个可用于 path 的构造函数
   /**
     * 在spring 5.1 之后支持 使用java.nio.file.Path 来构建。
     * 并且很好的支持 nio2,而非 java.io.File
     */
    public FileSystemResource(Path filePath) {
        Assert.notNull(filePath, "Path must not be null");
        this.path = StringUtils.cleanPath(filePath.toString());
        this.file = null;
        this.filePath = filePath;
    }

    /**
     * 使用 FileSystem 句柄来构建,仅依靠使用getFile()方法,获取到的对象,直接使用nio2 并非java.io.File来处理所有的文件交互。都是使用NIO2 API进行文件交互
     */
    public FileSystemResource(FileSystem fileSystem, String path) {
        Assert.notNull(fileSystem, "FileSystem must not be null");
        Assert.notNull(path, "Path must not be null");
        this.path = StringUtils.cleanPath(path);
        this.file = null;
        this.filePath = fileSystem.getPath(this.path).normalize();
    }
  • ByteArrayResource:对于从任何给定的字节数组加载内容很有用,并且针对于InputStreamResource一次读取来说,ByteArrayResource针对于多次读取内容。

    • 如果通过 InputStream 形式访问该类型的资源,该实现会根据字节数组的数据构造一个相应的 ByteArrayInputStream。

    •     /**
          ```
           * This implementation returns a ByteArrayInputStream for the
           * underlying byte array.
           * @see java.io.ByteArrayInputStream
             使用该实例对象,调用getInputStream() 返回一个InputStream下ByteArrayInputStream对象
           */
          @Override
          public InputStream getInputStream() throws IOException {
              return new ByteArrayInputStream(this.byteArray);
          }
      
      
      
  • UrlResource:对 java.net.URL类型资源的封装。内部委派 URL 进行具体的资源操作。并且支持java.net.URL 的解析。

  • ClassPathResource :class path 类型资源的实现。使用给定的 ClassLoader 或者给定的 Class 来加载资源。

  • InputStreamResource :将给定的 InputStream 作为一种资源的 Resource 的实现类。

1.2、AbstractResource

​ 为 Resource接口的默认抽象实现。它实现了 Resource接口的大部分的公共实现,作为 Resource接口中的重中之重,其定义如下:

​ 用于Resource便捷基类。言外之意就是,对Resource预执行的典型行为。

 * <p>The "exists" method will check whether a File or InputStream can
 * be opened; "isOpen" will always return false; "getURL" and "getFile"
 * throw an exception; and "toString" will return the description.
 如果一个File/InputStream处于打开状态,isOpen 会返回false 
     getURL getFile 始终直接抛出异常  实际是交付于子类去具体实现
     toString 会返回异常的描述。
public abstract class AbstractResource implements Resource {

    /**
     * 判断文件是否存在,若判断过程产生异常(因为会调用SecurityManager来判断),就关闭对应的流
     */
    @Override
    public boolean exists() {
        try {
          // 基于 File 进行判断
            return getFile().exists();
        }
        catch (IOException ex) {
            // Fall back to stream existence: can we open the stream?
            // 基于 InputStream 进行判断
            try {
                InputStream is = getInputStream();
                is.close();
                return true;
            } catch (Throwable isEx) {
                return false;
            }
        }
    }

    /**
     * This implementation always returns {@code true} for a resource
     * that {@link #exists() exists} (revised as of 5.1).
     
     在5.1 之前 此方法始终返回True,5.1 之后,需要判断资源是否存在
     */
    @Override
    public boolean isReadable() {
        return exists();
    }


    /**
     * 直接返回 false,表示未被打开
     */
    @Override
    public boolean isOpen() {
        return false;
    }

    /**
     * 直接返回false,表示不为 File
     */
    @Override
    public boolean isFile() {
        return false;
    }

    /**
     * 抛出 FileNotFoundException 异常,交给子类实现
     */
    @Override
    public URL getURL() throws IOException {
        throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");

    }

    /**
     * 基于 getURL() 返回的 URL 构建 URI
     */
    @Override
    public URI getURI() throws IOException {
        URL url = getURL();
        try {
            return ResourceUtils.toURI(url);
        } catch (URISyntaxException ex) {
            throw new NestedIOException("Invalid URI [" + url + "]", ex);
        }
    }

    /**
     * 抛出 FileNotFoundException 异常,交给子类实现
     */
    @Override
    public File getFile() throws IOException {
        throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
    }

    /**
     * 根据 getInputStream() 的返回结果构建 ReadableByteChannel
     */
    @Override
    public ReadableByteChannel readableChannel() throws IOException {
        return Channels.newChannel(getInputStream());
    }

    /**
     * 获取资源的长度
     *
     * 这个资源内容长度实际就是资源的字节长度,通过全部读取一遍来判断
     */
    @Override
    public long contentLength() throws IOException {
        InputStream is = getInputStream();
        try {
            long size = 0;
            byte[] buf = new byte[255]; // 每次最多读取 255 字节
            int read;
            while ((read = is.read(buf)) != -1) {
                size += read;
            }
            return size;
        } finally {
            try {
                is.close();
            } catch (IOException ex) {
            }
        }
    }

    /**
     * 返回资源最后的修改时间
     */
    @Override
    public long lastModified() throws IOException {
        long lastModified = getFileForLastModifiedCheck().lastModified();
        if (lastModified == 0L) {
            throw new FileNotFoundException(getDescription() +
                    " cannot be resolved in the file system for resolving its last-modified timestamp");
        }
        return lastModified;
    }

    protected File getFileForLastModifiedCheck() throws IOException {
        return getFile();
    }

    /**
     * 抛出 FileNotFoundException 异常,交给子类实现
     */
    @Override
    public Resource createRelative(String relativePath) throws IOException {
        throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());
    }

    /**
     * 获取资源名称,默认返回 null ,交给子类实现
     */
    @Override
    @Nullable
    public String getFilename() {
        return null;
    }

    /**
     * 返回资源的描述
     */
    @Override
    public String toString() {
        return getDescription();
    }

    @Override
    public boolean equals(Object obj) {
        return (obj == this ||
            (obj instanceof Resource && ((Resource) obj).getDescription().equals(getDescription())));
    }

    @Override
    public int hashCode() {
        return getDescription().hashCode();
    }

}

如果我们想要实现自定义的 Resource,记住不要实现 Resource接口,而应该继承 AbstractResource抽象类,然后根据当前的具体资源特性覆盖相应的方法即可。

2、统一资源定位:ResouceLoader

​ 有了资源,肯定得需要资源加载器来加载它们。Spring将这两部分分开

  • Resouce:定义了统一得资源。
  • ResouceLoader:定义了统一得资源加载器

ResouceLoader:定义资源加载器,主要应用于根据给定得资源文件地址返回对应得Resouce。

public interface ResourceLoader {

    String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; // CLASSPATH URL 前缀。默认为:"classpath:"
    
    /**
    * Return a Resource handle for the specified resource location.
     * <p>The handle should always be a reusable resource descriptor,
     * allowing for multiple {@link Resource#getInputStream()} calls.
     * <p><ul>
     * <li>Must support fully qualified URLs, e.g. "file:C:/test.dat".
     * <li>Must support classpath pseudo-URLs, e.g. "classpath:test.dat".
     * <li>Should support relative file paths, e.g. "WEB-INF/test.dat".
     * (This will be implementation-specific, typically provided by an
     * ApplicationContext implementation.)
     * </ul>
     * <p>Note that a Resource handle does not imply an existing resource;
     * you need to invoke {@link Resource#exists} to check for existence.
     */

    // 返回一个 指定得资源句柄
    Resource getResource(String location);

    // 获取ClassLoader对象,对于ResourceLoader获取ClassLoader可以直接使用此方法,不需要使用线程的上下文对象获取
    ClassLoader getClassLoader();

}
  • getResource:getResource(String location)提供的一个Resource实例,但是不确保该资源是否存在,需要调用Resource#exists来验证。

    • 该方法支持的文件路径类型:

      • 必须支持绝对路径:file:C:/test.dat
      • 必须支持类路径,classpath:classpath:test.dat
      • 应该支持相对路径:WEB-INF/test.dat

      此时返回的Resource实例,根据实际实现类的不同而不同。

    • 该方法主要实现是在其DefaultResourceLoader中具体实现,后面会详细分析DefaultResourceLoader是如何实现的。

  • getClassLoader()方法,返回ClassLoader实例,对于想要获取 ResourceLoader使用的 ClassLoader用户来说,可以直接调用该方法来获取。

2.1 子类结构

​ 作为Spring同意的资源加载器,它提供了同意的抽象,具体的实现则由相应的子类类负责实现。类图如下:

类结构图

2.2 DefaultResourceLoader详解

​ 默认的资源加载器,直接实现ResourceLoader,作为org.springframework.context.support.AbstractApplicationContext的基类。也可以单独使用

2.2.1构造函数
    @Nullable
    private ClassLoader classLoader;    

/**
     * Create a new DefaultResourceLoader.
     * <p>ClassLoader access will happen using the thread context class loader
     * at the time of this ResourceLoader's initialization.
     * @see java.lang.Thread#getContextClassLoader()
     */
    public DefaultResourceLoader() {
        this.classLoader = ClassUtils.getDefaultClassLoader();
    }

    /**
     * Create a new DefaultResourceLoader.
     * @param classLoader the ClassLoader to load class path resources with, or {@code null}
     * for using the thread context class loader at the time of actual resource access
     */
    public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
        this.classLoader = classLoader;
    }


    /**
     * Specify the ClassLoader to load class path resources with, or {@code null}
     * for using the thread context class loader at the time of actual resource access.
     * <p>The default is that ClassLoader access will happen using the thread context
     * class loader at the time of this ResourceLoader's initialization.
     *
     * 指定 加载器,如果为空,则加载线程上下文中的 资源加载器
     */
    public void setClassLoader(@Nullable ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    /**
     * Return the ClassLoader to load class path resources with.
     * <p>Will get passed to ClassPathResource's constructor for all
     * ClassPathResource objects created by this resource loader.
     *
     * 返回并给未初始化的resources的所有对象添加classloader实例。
     * @see Thread.currentThread().getContextClassLoader();
     * @see ClassPathResource
     */
    @Override
    @Nullable
    public ClassLoader getClassLoader() {
        return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
    }

在使用无参构造时,一般会使用线程上下文的ClassLoader来作为默认的ClassLoader(一般 Thread.currentThread()#getContextClassLoader()

在使用带参构造的时候,但是允许classloader为空,在获取classloader对象时,会对其进行判断。

2.2.2 getResource()

    @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. 以 / 开头,返回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.  然后,根据是否为问价URL,则返回FileUrlResource 类型的资源,否则但会UrlResource 类型的资源。
                // 尝试 将资源解析成为 URL
                // 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. 最后,如果解析URL 失败,则返回 ClassPathContextResource 类型的资源
                return getResourceByPath(location);
            }
        }
    }

上述源代码。逻辑非常清晰:

  1. 首先spring会尝试从资源策略上加载resource,没有的话进行下面的判断。
  2. location"/" 开头,则调用 #getResourceByPath() 方法,构造 ClassPathContextResource类型资源并返回。代码如下:
    protected Resource getResourceByPath(String path) {
        return new ClassPathContextResource(path, getClassLoader());
    }
  1. location"classpath:" 开头,则构造 ClassPathResource类型资源并返回。在构造该资源时,通过 #getClassLoader() 获取当前的 ClassLoader。
  2. 尝试构造URL,尝试通过解析的URL来对资源进行定位。若失败则抛出MalformedURLException,若解析成功,会判断类型是否为文件系统资源(文件资源类型包括三种类型:Filevfsvfsfile)。是:则返回FileUrlResource,否则返回UrlResource类型。
  3. 解析报错,就会当作 第2步处理,返回一个ClassPathContextResource类型资源
2.2.2.1 ProtocolResolver 解析

​ spring提供的 用户自定义资源加载器

@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中,并没有任何的实现或者继承。这就是spring专门提供的一个供开发人员自定义资源加载的一个公共接口,在自定义资源加载协议时,不需要去继承ResourceLoader的子类。

​ 在介绍Resource时,提到了如果要实现自定义Resoucre,我们只需要继承AbstractResource即可,但是在ResourceLoader中,有了ProtocolResolver后,不需要继承DefaultResourceLoader,改为实现ProtocolResolver接口,也可以实现自定义的ResourceLoader

spring时如何将自定义的resourceLoader加载到spring体系中的呢?

​ 通过#addProtocolResolver

    /**
     * Register the given resolver with this resource loader, allowing for
     * additional protocols to be handled.
     *
     * 添加自定义资源加载器,从而使spring 支持其它的资源加载协议。
     * <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);
    }
2.2.3 测试 实例
package com.spring.ioc.resource_loader.example01;

import org.springframework.core.io.*;

/**
 * 资源加载器 测试
 * 查看其加载流程
 *
 * @author by Mr. Li
 * @date 2020/1/29 15:14
 */
public class DefaultResourceLoaderTest {

    public static void main(String[] args) {
        ResourceLoader resourceLoader = new DefaultResourceLoader(); // 使用无参构造
        // 测试使用 文件 资源加载器
        FileSystemResource fileSystemResource = new FileSystemResource("E:/idea_workspace/springcloud2.0/spring-framework/spring-mytest/lg.txt");
        System.out.println(fileSystemResource.isFile()); // true
        Resource fileResource1 = resourceLoader.getResource("E:/idea_workspace/springcloud2.0/spring-framework/spring-mytest/lg.txt");
        System.out.println("fileResource1 is FileSystemResource : " + (fileResource1 instanceof FileSystemResource));// false

        Resource fileResource2 = resourceLoader.getResource("/idea_workspace/springcloud2.0/spring-framework/spring-mytest/lg.txt");
        System.out.println("fileResource2 is ClassPathResource : " + (fileResource2 instanceof ClassPathResource));// true

        Resource urlResource1 = resourceLoader.getResource("file:/idea_workspace/springcloud2.0/spring-framework/spring-mytest/lg.txt");
        System.out.println("urlResource1 is UrlResource : " + (urlResource1 instanceof UrlResource));// true
        System.out.println("urlResource1 is FileSystemResource : " + (urlResource1 instanceof FileSystemResource));// false
        System.out.println("urlResource1 is FileUrlResource : " + (urlResource1 instanceof FileUrlResource));// true

        Resource urlResource2 = resourceLoader.getResource("http://www.baidu.com");
        System.out.println("urlResource2 is UrlResource : " + (urlResource2 instanceof UrlResource));// true


    }
}

其实我们通常想要的是在指定绝对路径时,返回的对象应该是一个FileSystemResource对象,其实不然,在观察源码中,并没有以绝对路径作为判断,在getResource方法中,将该绝对路径尝试尝试进行解析时,会直接报错,因为URL在本地文件系统中仅支持三种协议以Filevfsvfsfile开头的。所以在第一个文件判断中,绝对路径返回的对象类型应该是在getResource方法中的最后一步,返回的ClassPathContextResource类型。

Spring资源加载器抽象和缺省实现 -- ResourceLoader + DefaultResourceLoader(摘)

使用自定义资源加载器添加对盘符得支持

​ 上面示例中说到,全路径/绝对路径并不能得到我们想得到的FileSystemResource类型。我们根据spring给的ProtocolResolver接口,实现自定义的资源加载策略,添加对全路径盘符的支持。

import org.springframework.core.io.*;

import java.util.regex.Pattern;

/**
 * 自定义  资源加载 协议
 *
 * @author by Mr. Li
 * @date 2020/1/29 16:03
 */
public class ProtocolResolverTest implements ProtocolResolver {
    @Override
    public Resource resolve(String location, ResourceLoader resourceLoader) {
        if (pattern(location))
            return new FileSystemResource(location);
        return null;
    }

    private boolean pattern(String location) {
        Pattern compile = Pattern.compile("^[A-z]:/");
        return compile.matcher(location).find();

    }

    public static void main(String[] args) {
        DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
        resourceLoader.addProtocolResolver(new ProtocolResolverTest());
        Resource fileResource1 = resourceLoader.getResource("E:/idea_workspace/springcloud2.0/spring-framework/spring-mytest/lg.txt");
        System.out.println("fileResource1 is FileSystemResource : " + (fileResource1 instanceof FileSystemResource));// true
    }
}

2.3 FileSystemResourceLoader

FileSystemResourceLoader继承了默认文件加载器,并且复写了#getResourceByPath方法,从而返回FileSystemResource对象。

    /**
     * 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.
     *
     * 将资源路径解析为 系统文件路径,
     * 即使给定的路径是以/开头的,也会被解析为当前VM 工作的目录地址。
     * 当前系统路径。
     * @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);
        }
        // 返回 FileSystemContextResource 对象
        return new FileSystemContextResource(path);
    }
2.3.1 FileSystemContextResource

​ 该类是FileSystemResourceLoader的一个内部类。

    /**
     * FileSystemResource that explicitly expresses a context-relative path
     * through implementing the ContextResource interface.
     * 针对上下文相对路径的FileSystemResource对象。
     */
    private static class FileSystemContextResource extends FileSystemResource implements ContextResource {

        // 构造器中 直接调用父类构造
        public FileSystemContextResource(String path) {
            super(path);
        }

        @Override
        public String getPathWithinContext() {
            return getPath();
        }
    }
  • 为什么不直接返回FileSystemResource对象呢?
    • 因为为了实现ContextResource接口,实现#getPathWithinContext()方法,用于根据上下文对象获取文件资源的路径。
2.3.2 实例

​ 回头来看2.2.3 测试 实例,如果将DefaultResourceLoader换成FileSystemResourceLoader,返回的类型则为FileSystemResource,就可以符合常规了。

        // 2.2.3 测试 将DefaultResourceLoader 换成 FileSystemResourceLoader
        FileSystemResourceLoader fileSystemResourceLoader = new FileSystemResourceLoader();
        Resource fileSystemResourceLoader1 = fileSystemResourceLoader.getResource("E:/idea_workspace/springcloud2.0/spring-framework/spring-mytest/lg.txt");
        System.out.println("fileSystemResourceLoader1 is FileSystemResource : " + (fileSystemResourceLoader1 instanceof FileSystemResource));// true

2.4 ClassRelativeResourceLoader

ClassRelativeResourceLoader继承了DefaultResourceLoader并且重写了#getResourceByPath()方法。

​ 根据给定的类对象,加载以类路径下或子文件下的文件。下面通过几个例子来说明ClassRelativeResourceLoader的具体实现步骤。

Spring5:就这一次,搞定资源加载器之ClassRelativeResourceLoader

2.4.1 ResourcePatternResolver

​ 新增classpath*:格式的新协议前缀。

具体实现由子类实现。

public interface ResourcePatternResolver extends ResourceLoader {

    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

    Resource[] getResources(String locationPattern) throws IOException;

}

根据给定的路径返回多个Resource实例。下面详细分析下,如何匹配多个路径。

2.5 PathMatchingResourcePatternResolver

org.springframework.core.io.support.PathMatchingResourcePatternResolver ,为 ResourcePatternResolver 最常用的子类,它除了支持 ResourceLoader 和 ResourcePatternResolver 新增的 "classpath*:" 前缀外,还支持 Ant 风格的路径匹配模式(类似于 "**/*.xml")。

2.5.1 构造函数

PathMatchingResourcePatternResolver 提供了三个构造函数,如下:

/**
 * 内置的 ResourceLoader 资源定位器
 */
private final ResourceLoader resourceLoader;
/**
 * Ant 路径匹配器
 */
private PathMatcher pathMatcher = new AntPathMatcher();

public PathMatchingResourcePatternResolver() {
    this.resourceLoader = new DefaultResourceLoader();
}

public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
    Assert.notNull(resourceLoader, "ResourceLoader must not be null");
    this.resourceLoader = resourceLoader;
}

public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) {
    this.resourceLoader = new DefaultResourceLoader(classLoader);
}
  • PathMatchingResourcePatternResolver 在实例化的时候,可以指定一个 ResourceLoader,如果不指定的话,它会在内部构造一个 DefaultResourceLoader 。
  • pathMatcher 属性,默认为 AntPathMatcher 对象,用于支持 Ant 类型的路径匹配。

2.5.2 getResource

@Override
public Resource getResource(String location) {
    return getResourceLoader().getResource(location);
}

public ResourceLoader getResourceLoader() {
    return this.resourceLoader;
}

该方法,直接委托给相应的 ResourceLoader 来实现。所以,如果我们在实例化的 PathMatchingResourcePatternResolver 的时候,如果未指定 ResourceLoader 参数的情况下,那么在加载资源时,其实就是 DefaultResourceLoader 的过程。

其实在下面介绍的 Resource[] getResources(String locationPattern) 方法也相同,只不过返回的资源是多个而已。

2.5.3 getResources

@Override
public Resource[] getResources(String locationPattern) throws IOException {
    Assert.notNull(locationPattern, "Location pattern must not be null");
    // 以 "classpath*:" 开头
    if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
        // 路径包含通配符
        // a class path resource (multiple resources for same name possible)
        if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
            // a class path resource pattern
            return findPathMatchingResources(locationPattern);
        // 路径不包含通配符
        } else {
            // all class path resources with the given name
            return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
        }
    // 不以 "classpath*:" 开头
    } else {
        // Generally only look for a pattern after a prefix here, // 通常只在这里的前缀后面查找模式
        // and on Tomcat only after the "*/" separator for its "war:" protocol. 而在 Tomcat 上只有在 “*/ ”分隔符之后才为其 “war:” 协议
        int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
                locationPattern.indexOf(':') + 1);
        // 路径包含通配符
        if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
            // a file pattern
            return findPathMatchingResources(locationPattern);
        // 路径不包含通配符
        } else {
            // a single resource with the given name
            return new Resource[] {getResourceLoader().getResource(locationPattern)};
        }
    }
}

处理逻辑如下图:


逻辑图
  • "classpath*:" 开头,且路径不包含通配符,直接委托给相应的 ResourceLoader 来实现。
  • 其他情况,调用 #findAllClassPathResources(...)、或 #findPathMatchingResources(...) 方法,返回多个 Resource 。下面,我们来详细分析。

2.5.4 findAllClassPathResources

locationPattern"classpath*:" 开头但是不包含通配符,则调用 #findAllClassPathResources(...) 方法加载资源。该方法返回 classes 路径下和所有 jar 包中的所有相匹配的资源。

protected Resource[] findAllClassPathResources(String location) throws IOException {
    String path = location;
    // 去除首个 /
    if (path.startsWith("/")) {
        path = path.substring(1);
    }
    // 真正执行加载所有 classpath 资源
    Set<Resource> result = doFindAllClassPathResources(path);
    if (logger.isTraceEnabled()) {
        logger.trace("Resolved classpath location [" + location + "] to resources " + result);
    }
    // 转换成 Resource 数组返回
    return result.toArray(new Resource[0]);
}

真正执行加载的是在 #doFindAllClassPathResources(...) 方法,代码如下:

protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
    Set<Resource> result = new LinkedHashSet<>(16);
    ClassLoader cl = getClassLoader();
    // <1> 根据 ClassLoader 加载路径下的所有资源
    Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
    // <2>
    while (resourceUrls.hasMoreElements()) {
        URL url = resourceUrls.nextElement();
        // 将 URL 转换成 UrlResource
        result.add(convertClassLoaderURL(url));
    }
    // <3> 加载路径下得所有 jar 包
    if ("".equals(path)) {
        // The above result is likely to be incomplete, i.e. only containing file system references.
        // We need to have pointers to each of the jar files on the classpath as well...
        addAllClassLoaderJarRoots(cl, result);
    }
    return result;
}
  • <1> 处,根据 ClassLoader 加载路径下的所有资源。在加载资源过程时,如果在构造 PathMatchingResourcePatternResolver 实例的时候如果传入了 ClassLoader,则调用该 ClassLoader 的 #getResources() 方法,否则调用 ClassLoader#getSystemResources(path) 方法。另外,ClassLoader#getResources() 方法,代码如下:

    // java.lang.ClassLoader.java
    public Enumeration<URL> getResources(String name) throws IOException {
        @SuppressWarnings("unchecked")
        Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
        if (parent != null) {
            tmp[0] = parent.getResources(name);
        } else {
            tmp[0] = getBootstrapResources(name);
        }
        tmp[1] = findResources(name);
    
        return new CompoundEnumeration<>(tmp);
    }
    
    • 看到这里是不是就已经一目了然了?如果当前父类加载器不为 null ,则通过父类向上迭代获取资源,否则调用 #getBootstrapResources() 。这里是不是特别熟悉,()。
  • <2> 处,遍历 URL 集合,调用 #convertClassLoaderURL(URL url) 方法,将 URL 转换成 UrlResource 对象。代码如下:

    protected Resource convertClassLoaderURL(URL url) {
      return new UrlResource(url);
    }
    
  • <3> 处,若 path 为空(“”)时,则调用 #addAllClassLoaderJarRoots(...)方法。该方法主要是加载路径下得所有 jar 包。

通过上面的分析,我们知道 #findAllClassPathResources(...) 方法,其实就是利用 ClassLoader 来加载指定路径下的资源,不论它是在 class 路径下还是在 jar 包中。如果我们传入的路径为空或者 /,则会调用 #addAllClassLoaderJarRoots(...) 方法,加载所有的 jar 包。

2.5.5 findPathMatchingResources

locationPattern 中包含了通配符,则调用该方法进行资源加载。代码如下:

protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
    // 确定根路径、子路径
    String rootDirPath = determineRootDir(locationPattern);
    String subPattern = locationPattern.substring(rootDirPath.length());
    // 获取根据路径下的资源
    Resource[] rootDirResources = getResources(rootDirPath);
    // 遍历,迭代
    Set<Resource> result = new LinkedHashSet<>(16);
    for (Resource rootDirResource : rootDirResources) {
        rootDirResource = resolveRootDirResource(rootDirResource);
        URL rootDirUrl = rootDirResource.getURL();
        // bundle 资源类型
        if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
            URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
            if (resolvedUrl != null) {
                rootDirUrl = resolvedUrl;
            }
            rootDirResource = new UrlResource(rootDirUrl);
        }
        // vfs 资源类型
        if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
            result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
        // jar 资源类型
        } else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
            result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
        // 其它资源类型
        } else {
            result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
        }
    }
    if (logger.isTraceEnabled()) {
        logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
    }
    // 转换成 Resource 数组返回
    return result.toArray(new Resource[0]);
}

方法有点儿长,但是思路还是很清晰的,主要分两步:

  1. 确定目录,获取该目录下得所有资源。
  2. 在所获得的所有资源后,进行迭代匹配获取我们想要的资源。

在这个方法里面,我们要关注两个方法,一个是 #determineRootDir(String location) 方法,一个是 #doFindPathMatchingXXXResources(...) 等方法。

2.5.5.1 determineRootDir

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

/**
 * 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) {
    // 找到冒号的后一位
    int prefixEnd = location.indexOf(':') + 1;
    // 根目录结束位置
    int rootDirEnd = location.length();
    // 在从冒号开始到最后的字符串中,循环判断是否包含通配符,如果包含,则截断最后一个由”/”分割的部分。
    // 例如:在我们路径中,就是最后的ap?-context.xml这一段。再循环判断剩下的部分,直到剩下的路径中都不包含通配符。
    while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
        rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
    }
    // 如果查找完成后,rootDirEnd = 0 了,则将之前赋值的 prefixEnd 的值赋给 rootDirEnd ,也就是冒号的后一位
    if (rootDirEnd == 0) {
        rootDirEnd = prefixEnd;
    }
    // 截取根目录
    return location.substring(0, rootDirEnd);
}

方法比较绕,效果如下示例:

原路径 确定根路径
classpath*:test/cc*/spring-*.xml classpath*:test/
classpath*:test/aa/spring-*.xml classpath*:test/aa/

2.5.5.2 doFindPathMatchingXXXResources

来自艿艿

#doFindPathMatchingXXXResources(...) 方法,是个泛指,一共对应三个方法:

  • #doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPatter) 方法
  • #doFindPathMatchingFileResources(rootDirResource, subPattern) 方法
  • VfsResourceMatchingDelegate#findMatchingResources(rootDirUrl, subPattern, pathMatcher) 方法

因为本文重在分析 Spring 统一资源加载策略的整体流程。相对来说,上面几个方法的代码量会比较多。所以本文不再追溯,推荐阅读如下文章:

3. 小结

至此 Spring 整个资源记载过程已经分析完毕。下面简要总结下:

  • Spring 提供了 Resource 和 ResourceLoader 来统一抽象整个资源及其定位。使得资源与资源的定位有了一个更加清晰的界限,并且提供了合适的 Default 类,使得自定义实现更加方便和清晰。
  • AbstractResource 为 Resource 的默认抽象实现,它对 Resource 接口做了一个统一的实现,子类继承该类后只需要覆盖相应的方法即可,同时对于自定义的 Resource 我们也是继承该类。
  • DefaultResourceLoader 同样也是 ResourceLoader 的默认实现,在自定 ResourceLoader 的时候我们除了可以继承该类外还可以实现 ProtocolResolver 接口来实现自定资源加载协议。
  • DefaultResourceLoader 每次只能返回单一的资源,所以 Spring 针对这个提供了另外一个接口 ResourcePatternResolver ,该接口提供了根据指定的 locationPattern 返回多个资源的策略。其子类 PathMatchingResourcePatternResolver 是一个集大成者的 ResourceLoader ,因为它即实现了 Resource getResource(String location) 方法,也实现了 Resource[] getResources(String locationPattern) 方法。

另外,我们可以发现,Resource 和 ResourceLoader 核心是在,spring-core 项目中。

如果想要调试本小节的相关内容,可以直接使用 Resource 和 ResourceLoader 相关的 API ,进行操作调试。

本文仅供笔者本人学习,一起进步!

加油!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容