Spring5源码分析(005)——IoC篇之统一资源加载Resource和ResourceLoader

 注:《Spring5源码分析》汇总可参考:Spring5源码分析(002)——博客汇总 


本文主要介绍 Spring 的统一资源 Resource 及其加载策略 ResourceLoader,目录如下:

 

1、为什么会先从 Resource 和 ResourceLoader 入手进行介绍?

  为什么开篇会是先对统一资源 Resource 和资源加载 ResourceLoader 来进行分析?因为跟创建 IoC 容器密切相关,无论是 BeanFactory 还是 ApplicationContext。就如官方参考文档提到的 https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/spring-framework-reference/core.html#beans-factory-instantiation

  After you learn about Spring’s IoC container, you may want to know more about Spring’s Resource abstraction (as described in Resources), which provides a convenient mechanism for reading an InputStream from locations defined in a URI syntax. In particular, Resource paths are used to construct applications contexts, as described in Application Contexts and Resource Paths.

  看看大家都比较熟悉的 spring hello world (此处省略 HelloWorldService 和相关 xml,请自行脑补):

// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");

// retrieve configured instance
HelloWorldService helloWorldService = context.getBean("helloWorldService", HelloWorldService.class);

// use configured instance
helloWorldService.sayHelloWorld();

  还有其他的很多创建 ApplicationContext 的例子:

ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

ApplicationContext context = new ClassPathXmlApplicationContext("file:C:/config/services.xml");

ApplicationContext context = new ClassPathXmlApplicationContext("classpath:/config/services.xml");

ApplicationContext context = new ClassPathXmlApplicationContext("classpath:/config/services-*.xml");

ApplicationContext context = new ClassPathXmlApplicationContext("classpath*:/config/services.xml");

ApplicationContext context = new ClassPathXmlApplicationContext("classpath*:/config/services-*.xml");

ApplicationContext context = new ClassPathXmlApplicationContext("classpath*:/**/services-*.xml");

ApplicationContext context = new ClassPathXmlApplicationContext("classpath*:/**/*.xml");

   没错,这里所涉及到的 xml 都被抽象为 Resource,而加载 Resource(这里指所有的 xml)则需要使用 ResourceLoader,实际上就是 ResourceLoader 这货负责找到所有的 xml(loadBeanDefinitions 前需要找到有 bean 元数据定义的 xml Resource)。

  注:虽说在注解使用大行其道,甚至是在 springboot 约定大于配置,基本上可以完全去 XML 化的情况下下,大家对 @ComponentScan、@Service、@Repository、@Component、@Autowired、@Qualifier、@Bean、@Configuration 等这些常用注解应该说是耳熟能详,而且也都是信手沾来,传统的基于 xml 的配置也是越来越少使用了,不过即便如此,xml 配置还是有一定的存在意义的,相对于基于注解的配置,其好处在于:只配置 xml 而非侵入、配置可以中心化管理、增删改 bean 定义元数据无需重新编译。具体参考官网对于两者的一些说明:Are annotations better than XML for configuring Spring? https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/spring-framework-reference/core.html#beans-annotation-config

  接下来我们看看 org.springframework.beans.factory.support.AbstractBeanDefinitionReader 中的一个重要接口,这里暂时不细讲,只需要知道的是:IoC 容器需要 BeanDefinitionReader 来读取解析配置所有的 BeanDefinition,而配置元数据来源(之一)则是前面配置的 xml。 

/**
 * Load bean definitions from the specified resource location.
 * <p>The location can also be a location pattern, provided that the
 * ResourceLoader of this bean definition reader is a ResourcePatternResolver.
 * @param location the resource location, to be loaded with the ResourceLoader
 * (or ResourcePatternResolver) of this bean definition reader
 * @param actualResources a Set to be filled with the actual Resource objects
 * that have been resolved during the loading process. May be {@code null}
 * to indicate that the caller is not interested in those Resource objects.
 * @return the number of bean definitions found
 * @throws BeanDefinitionStoreException in case of loading or parsing errors
 * @see #getResourceLoader()
 * @see #loadBeanDefinitions(org.springframework.core.io.Resource)
 * @see #loadBeanDefinitions(org.springframework.core.io.Resource[])
 */
public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
   ResourceLoader resourceLoader = getResourceLoader();
   if (resourceLoader == null) {
      throw new BeanDefinitionStoreException(
            "Cannot load bean definitions from location [" + location + "]: no ResourceLoader available");
   }

   if (resourceLoader instanceof ResourcePatternResolver) {
      // Resource pattern matching available.
      try {
         Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
         int count = loadBeanDefinitions(resources);
         if (actualResources != null) {
            Collections.addAll(actualResources, resources);
         }
         if (logger.isTraceEnabled()) {
            logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]");
         }
         return count;
      }
      catch (IOException ex) {
         throw new BeanDefinitionStoreException(
               "Could not resolve bean definition resource pattern [" + location + "]", ex);
      }
   }
   else {
      // Can only load single resources by absolute URL.
      Resource resource = resourceLoader.getResource(location);
      int count = loadBeanDefinitions(resource);
      if (actualResources != null) {
         actualResources.add(resource);
      }
      if (logger.isTraceEnabled()) {
         logger.trace("Loaded " + count + " bean definitions from location [" + location + "]");
      }
      return count;
   }
}

   这里的重点之一就是中间这一句,这里的 location 可以当作是前面传递进来的参数,也就是那些 xml 路径。很明显这里就是通过 ResourceLoader 来找到所有的 xml Resource,然后作为 BeanDefinitionReader 的输入来进行解析。

Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);

   至此可以大致知道了为何会对 Resource 和 ResourceLoader 先进行分析的原因。

 

2、关于统一资源和资源加载策略

  官网对于 org.springframework.core.io.Resource 的一段说明: https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/spring-framework-reference/core.html#resources-introduction

  Java’s standard java.net.URL class and standard handlers for various URL prefixes, unfortunately, are not quite adequate enough for all access to low-level resources. For example, there is no standardized URL implementation that may be used to access a resource that needs to be obtained from the classpath or relative to a ServletContext. While it is possible to register new handlers for specialized URL prefixes (similar to existing handlers for prefixes such as http:), this is generally quite complicated, and the URL interface still lacks some desirable functionality, such as a method to check for the existence of the resource being pointed to.

  Spring’s Resource interface is meant to be a more capable interface for abstracting access to low-level resources.

  在 Java 中,将不同来源的资源抽象成 java.net.URL ,即统一资源定位器(Uniform Resource Locator),然后通过注册不同的 handler ( URLStreamHandler )来处理不同来源的资源的读取逻辑,一般 handler 的类型使用不同前缀(协议, Protocol )来识别,如“file:”“http:” “jar:”等,然而 URL 没有默认定义相对 Classpath 或 ServletContext 等资源的 handler ,虽然可以注册自己的 URLStreamHandler 来解析特定的 URL 前缀(协议), 比如“classpath:”,然而这需要了解 Url 的实现机制,实现也比较复杂,而且 Url 也没有提供一些基本的方法,如检查当前资源是否存在、检查当前资源是否可读等方法。 因而 Spring 对其内部使用到的资源实现了自己的抽象结构 : Resource 接口封装底层资源,(《Spring源码深度解析 第二版》,略微修改)然后通过 ResourceLoader 接口来实现 Resource 的加载策略,也即是提供了统一的资源定义和资源加载策略的抽象。通过不同策略进行的所有资源加载,都可以返回统一的抽象给客户端,客户端对资源可以进行的操作,则由 Resource 接口进行界定,具体如何处理,则交由不同来源的资源实现类来实现。

  简单总结:

  • Resource:提供统一的资源定义抽象,界定了对资源可以进行的处理操作。
    • 例如:文件资源( FileSystemResource ) 、 Classpath 资源( ClassPathResource )、 URL 资源( UrlResource )、 InputStream 资源( InputStreamResource ) 、Byte 数组( ByteArrayResource )等 。
  • ResourceLoader:提供统一的资源加载策略抽象,返回统一的 Resource 资源抽象给客户端。

 

3、统一资源 Resource

  org.springframework.core.io.Resource 为 Spring 框架用到的所有资源提供了一个统一的抽象接口,它继承了 org.springframework.core.io.InputStreamSource 接口,而 InputStreamSource 接口则用于将对应的资源封装为 Java 的标准 InputStream。Resource 接口是具体资源访问策略的抽象,也是所有资源访问类所实现的接口。 Resource 接口提供的通用方法如下(详情可参考 API 文档 or 源码注释):

public interface Resource extends InputStreamSource {

    /**
     * 检查资源是否物理形式实际存在
     */
    boolean exists();

    /**
     * 资源是否可读取
     */
    default boolean isReadable() {
        return exists();
    }

    /**
     * 资源文件是否打开状态,如果资源文件不能多次读取,每次读取结束应该显式关闭,以防止资源泄漏。
     */
    default boolean isOpen() {
        return false;
    }

    /**
     * 是否是文件系统中的文件 File
     */
    default boolean isFile() {
        return false;
    }

    /**
     * 返回资源对应的 URL 句柄
     */
    URL getURL() throws IOException;

    /**
     * 返回资源对应的 URI 句柄
     */
    URI getURI() throws IOException;

    /**
     * 返回资源对应的 File 句柄
     */
    File getFile() throws IOException;

    /**
     * 返回 ReadableByteChannel
     */
    default ReadableByteChannel readableChannel() throws IOException {
        return Channels.newChannel(getInputStream());
    }

    /**
     * 返回资源的内容长度
     */
    long contentLength() throws IOException;

    /**
     * 资源的最后修改时间
     */
    long lastModified() throws IOException;

    /**
     * 根据资源的相对路径创建对应的资源
     */
    Resource createRelative(String relativePath) throws IOException;

    /**
     * 资源的文件名,不带路径信息的文件名
     */
    @Nullable
    String getFilename();

    /**
     * 资源的描述信息,用来在错误处理中打印信息 。
     */
    String getDescription();

}

   另外就是继承的 InputStreamSource 接口的方法如下:

public interface InputStreamSource {

   /**
    * 返回底层资源的标准输入流。每次调用都返回新的输入流,调用者必须负责关闭输入流。
    */
   InputStream getInputStream() throws IOException;

}

  

3.1、Resource 的类继承结构

  Resource 的部分类继承结构如下图:

  底层资源可能会有各种来源,像文件系统、Url、classpath,甚至是 servletcontext 等,因此,Resource 需要根据资源的不同类型提供不同的具体实现,如 文件( FileSystemResource ) 、 Classpath 资源( ClassPathResource )、 URL 资源( UrlResource )、 InputStream 资源( InputStreamResource ) 、Byte 数组( ByteArrayResource )等。上图只是展示了部分常见的实现类(继承结构图来自 IntelliJ IDEA,对着类右键 ==》Diagrams ==》 Show Diagram...,然后可以对着类图中的类右键 ==》 Show Implementation 展示实现类 或者 Show Parent 展示父类,还有其他的显示属性、方法等。),相关说明如下:

  • ClassPathResource:class path 类型资源的封装实现类,内部使用给定的 ClassLoader 或者给定的 Class 进行资源加载。
    • Resource implementation for class path resources. Uses either a given ClassLoader or a given Class for loading resources.
    • Supports resolution as java.io.File if the class path resource resides in the file system, but not for resources in a JAR. Always supports resolution as URL.
  • FileSystemResource:java.io.File 和 java.nio.file.Path 类型资源的封装实现类,用于处理文件系统资源。支持 File 和 Url 的形式。从 Spring Framework 5.0 开始使用 NIO2 进行 读/写 交互。从 5.1 开始,还可能是通过 Path 句柄来进行构造,这种场景下它将通过 NIO2进行所有的文件系统交互,只有通过 getFile() 时才转转为 File。
    • Resource implementation for java.io.File and java.nio.file.Path handles with a file system target. Supports resolution as a File and also as a URL. Implements the extended WritableResource interface.
    • Note: As of Spring Framework 5.0, this Resource implementation uses NIO.2 API for read/write interactions. As of 5.1, it may be constructed with a Path handle in which case it will perform all file system interactions via NIO.2, only resorting to File on getFile().
  • ByteArrayResource:对 byte 数组的封装实现类,会根据给定的 byte 数组构造一个对应的 ByteArrayInputStream 作为 InputStream 类型的返回。
    • Resource implementation for a given byte array.
    • Creates a ByteArrayInputStream for the given byte array.
    • Useful for loading content from any given byte array, without having to resort to a single-use InputStreamResource. Particularly useful for creating mail attachments from local content, where JavaMail needs to be able to read the stream multiple times.
  • UrlResource:对 java.net.URL 类型资源的封装实现类。支持 URL 和 File(使用 file: 协议的时候)的形式
    • Resource implementation for java.net.URL locators. Supports resolution as a URL and also as a File in case of the "file:" protocol.
  • InputStreamResource:将给定的 InputStream 作为资源的封装实现类。只有当其他类型都无法使用的时候才会用到,尽量使用相匹配的类型进行处理。
    • Resource implementation for a given InputStream.
    • Should only be used if no other specific Resource implementation is applicable. In particular, prefer ByteArrayResource or any of the file-based Resource implementations where possible.
    • In contrast to other Resource implementations, this is a descriptor for an already opened resource - therefore returning true from isOpen(). Do not use an InputStreamResource if you need to keep the resource descriptor somewhere, or if you need to read from a stream multiple times.

相关注释说明可以参考上面贴出来的英文,或者直接在源码中看相关的文档注释(其实也就是来自文档注释)。

 

3.2、AbstractResource

  org.springframework.core.io.AbstractResource 是 Resource 接口的抽象子类,提供了大部分接口方法的典型预实现。exists 方法会检查相关的 File 或者 InputStream 能否打开;isOpen 方法则总是返回 false ;getURL 和 getFile 方法则默认直接抛出异常,这个需要具体的资源实现来进行判断,因为 AbstractResource 并不清楚具体的资源类型;toString 方法则返回对应的描述信息,也即是 getDescription()。

  • Convenience base class for Resource implementations, pre-implementing typical behavior.
  • 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.

  AbstractResource 中的具体是实现如下:

public abstract class AbstractResource implements Resource {

    /**
     * 检查文件是否存在,或者检查有对应的流 InputStream 存在,此时需要关闭流
     */
    @Override
    public boolean exists() {
        // Try file existence: can we find the file in the file system?
        // 先判断文件 File 是否存在:基于 File 的判断
        if (isFile()) {
            try {
                return getFile().exists();
            }
            catch (IOException ex) {
                Log logger = LogFactory.getLog(getClass());
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not retrieve File for existence check of " + getDescription(), ex);
                }
            }
        }
        // Fall back to stream existence: can we open the stream?
        // 其次检查是否是可以打开的流 InputStream:基于 InputStream 的判断
        try {
            getInputStream().close();
            return true;
        }
        catch (Throwable ex) {
            Log logger = LogFactory.getLog(getClass());
            if (logger.isDebugEnabled()) {
                logger.debug("Could not retrieve InputStream for existence check of " + getDescription(), ex);
            }
            return false;
        }
    }

    /**
     * 对于存在的资源,此实现总是返回true(从5.1版修订),通过 exists() 进行判断
     */
    @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[256];
            int read;
            while ((read = is.read(buf)) != -1) {
                size += read;
            }
            return size;
        }
        finally {
            try {
                is.close();
            }
            catch (IOException ex) {
                Log logger = LogFactory.getLog(getClass());
                if (logger.isDebugEnabled()) {
                    logger.debug("Could not close content-length InputStream for " + getDescription(), ex);
                }
            }
        }
    }

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

    /**
     * 返回相应的资源文件用于检查最后修改时间,内部直接使用 getFile() 实现
     */
    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 boolean equals(@Nullable Object other) {
        return (this == other || (other instanceof Resource &&
                ((Resource) other).getDescription().equals(getDescription())));
    }

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

    /**
     * 这里获取资源的描述信息作为返回
     */
    @Override
    public String toString() {
        return getDescription();
    }

}

   Spring 的很多顶层设计接口,都会有相关的 Abstract / Default 子类来处理一些典型的预实现,如果需要自定义这些接口的继承类,比如这里需要自定义的 Resource ,则建议直接继承 AbstractResource ,然后根据具体的资源特性重写相关的方法,而不是直接继承顶层接口 Resource,重写全部方法。

  Spring 提供的资源具体实现类在上面已经简单介绍了下,这里就不一一具体说明,有兴趣的读者可以去翻一翻对应的源码进行研究。

 

4、统一资源加载策略 ResourceLoader

  org.springframework.core.io.ResourceLoader 提供了资源加载策略的统一抽象,具体的资源加载则由对应的实现类来进行加载策略的实现。说是加载,其实理解为资源定位会更清晰点,也就是统一资源定位器(Uniform Resource Locator),ResourceLoader 其实就是根据相关的资源文件地址来定位所有的资源,并作为标准的 Resource 进行返回。

  ResourceLoader 的内部接口定义如下: 

public interface ResourceLoader {
    /** Pseudo URL prefix for loading from the class path: "classpath:". */
    // 用于从类路径加载的伪URL前缀:“classpath:”
    String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;


    /**
     * 根据指定的资源路径返回对应的资源 Resource,该 Resource 句柄
     * 应该总是一个可重用的资源描述符,允许多个 Resource.getInputStream()调用。
     * 需要注意的是该 Resource 句柄 并不确保对应的资源一定存在,需要调用
     * Resource.exists() 来进行实际的判断。
     * 该方法支持以下这些模式的资源加载:
     *      · 全限定路径 URL 位置的资源,如:"file:C:/test.dat".
     *      · classpath 类路径位置的资源,如 "classpath:test.dat".
     *      · 相对路径的资源,如 "WEB-INF/test.dat". 这种情况下会根据不同实现返回不同的 Resource 实例
     */
    Resource getResource(String location);

    /**
     * 返回当前 ResourceLoader 所用到的 ClassLoader ,
     * 需要直接访问 ClassLoader 的客户端,可以通过 ResourceLoader 以这种统一的方式来直接获取 ClassLoader 。
     * Resource 中的实现类 ClassPathResource 可以根据指定的 ClassLoader 进行资源加载
     */
    @Nullable
    ClassLoader getClassLoader();

}

  

4.1、ResourceLoader 的类继承结构

  ResourceLoader 的部分类继承结构如下图:

  下面分别通过 DefaultResourceLoader 和 ResourcePatternResolver 2个分支来进行详细介绍。

 

4.2、DefaultResourceLoader

  Default 类与 Abstract 类有些相似,都是提供了接口的一些典型的预实现。org.springframework.core.io.DefaultResourceLoader 为 ResourceLoader 提供了默认实现。

 

4.2.1、DefaultResourceLoader 的内部属性

  这里比较重要的是 classLoader 和 protocolResolvers,ClassLoader 是用于加载 classpath 下的资源的,而 ProtocolResolver 则是用户自定义的加载策略。接下来会进行相关介绍。 

// 类加载器
@Nullable
private ClassLoader classLoader;

// 用户自定义的特定协议资源加载解析策略接口,用于自定义加载策略
private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);

// 各种类型资源的缓存
private final Map<Class<?>, Map<Resource, ?>> resourceCaches = new ConcurrentHashMap<>(4);

  

4.2.2、DefaultResourceLoader 的构造函数

  DefaultResourceLoader 的构造函数也比较简单,一个不带参的空构造函数和一个指定 ClassLoader 的构造函数。

  需要说明的是,ClassLoader 还可以通过 setClassLoader() 来进行指定,这里可以使用 ClassUtils.getDefaultClassLoader(),内部也是优先使用 Thread.currentThread().getContextClassLoader() 来获取,也即是执行线程的 ClassLoader。 

/**
 * 无参构造函数,这里内部实际上就是优先使用 Thread.currentThread().getContextClassLoader()
 */
public DefaultResourceLoader() {
    this.classLoader = ClassUtils.getDefaultClassLoader();
}

/**
 * 指定 ClassLoader 的带参构造函数
 */
public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
    this.classLoader = classLoader;
}


/**
 * 设置指定的 ClassLoader,也可以使用默认的 Thread.currentThread().getContextClassLoader()
 */
public void setClassLoader(@Nullable ClassLoader classLoader) {
    this.classLoader = classLoader;
}

@Override
@Nullable
public ClassLoader getClassLoader() {
    return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
}

  

4.2.3、ProtocolResolver 自定义的资源加载策略

  org.springframework.core.io.ProtocolResolver 是特定协议的资源加载解析策略接口。用作 DefaultResourceLoader 的SPI,允许在不继承 DefaultResourceLoader(或 ApplicationContext 实现类)的情况下处理自定义协议。这样,如果需要自定义资源 Resource 和相应的加载策略,则可以通过继承 AbstractResource 来实现对应的资源,然后再增加相关的 ProtocolResolver 来实现对应的资源定位加载策略,这样就不需要再继承 DefaultResourceLoader 了。

  ProtocolResolver 内部仅有一个用于资源定位解析的方法 Resource resolve(String location, ResourceLoader resourceLoader); 

  ProtocolResolver 整个接口代码如下: 

/**
 * A resolution strategy for protocol-specific resource handles.
 *
 * <p>Used as an SPI for {@link DefaultResourceLoader}, allowing for
 * custom protocols to be handled without subclassing the loader
 * implementation (or application context implementation).
 *
 * <p>特定协议的资源加载解决策略接口。
 * 用作 DefaultResourceLoader 的SPI,允许在不继承 DefaultResourceLoader(或 ApplicationContext 实现类)的情况下处理自定义协议。
 *
 * @author Juergen Hoeller
 * @since 4.3
 * @see DefaultResourceLoader#addProtocolResolver
 */
@FunctionalInterface
public interface ProtocolResolver {

   /**
    * Resolve the given location against the given resource loader
    * if this implementation's protocol matches.
    * <p>根据指定的 ResourceLoader 来解析对应的资源路径,若成功则返回相应的 Resource
    * @param location the user-specified resource location 指定的资源路径
    * @param resourceLoader the associated resource loader 指定的 ResourceLoader
    * @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 接口的任何实现类,这个完全需要用户自己进行定义实现,然后再通过 DefaultResourceLoader.addProtocolResolver(ProtocolResolver resolver) 注册到 Spring 中,该方法代码如下: 

/**
 * 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.
 * <p>注册自定义的特定协议资源加载解决器,允许处理其他协议的资源
 * 需要注意的是这些解析器在资源加载时会先执行,因此可能会覆盖其他默认的加载规则
 * @since 4.3
 * @see #getProtocolResolvers()
 */
public void addProtocolResolver(ProtocolResolver resolver) {
    Assert.notNull(resolver, "ProtocolResolver must not be null");
    this.protocolResolvers.add(resolver);
}

 

4.2.4、getResource 方法

  接下来看看核心方法 getResource(String location) 的具体实现,它会根据提供的 location 返回对应的 Resource。DefaultResourceLoader 的子类 ClassRelativeResourceLoader 和 FileSystemResourceLoader 并没有覆盖这个方法,因此 ResourceLoader 的资源加载策略就是依靠 DefaultResourceLoader 来实现的,具体实现如下:

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

    // 首先,先使用自定义的加载解析策略 ProtocolResolver 来加载资源,解析得到就直接返回相应资源
    for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
        Resource resource = protocolResolver.resolve(location, this);
        if (resource != null) {
            return resource;
        }
    }
    // 然后,以 / 开头的资源,则返回 ClassPathContextResource 类型的资源
    if (location.startsWith("/")) {
        return getResourceByPath(location);
    }
    // 之后,以 classpath: 开头的,返回 ClassPathResource 类型的资源
    else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
        return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
    }
    else {
        try {
            // 判断是否为文件 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) {
            // 最后,则返回 ClassPathContextResource 类型的资源
            // No URL -> resolve as resource path.
            return getResourceByPath(location);
        }
    }
}

   其中 getResourceByPath(String path) 方法如下:

/**
 * 默认直接返回 ClassPathContextResource,子类可以覆盖重写
 */
protected Resource getResourceByPath(String path) {
    return new ClassPathContextResource(path, getClassLoader());
}

   将注释整理下来,看下一个完整的加载定位策略:

  • 首先,使用自定义的加载解析策略 ProtocolResolver 来加载资源,解析得到就直接返回相应资源
  • 然后,以 / 开头的资源,则调用 getResourceByPath(String path) 返回 ClassPathContextResource 类型的资源
  • 之后,以 classpath: 开头的,返回 ClassPathResource 类型的资源
  • 接着,判断是否为文件 Url,是则返回 FileUrlResource 类型的资源;否则返回 UrlResource 类型的资源
  • 最后,若出现了 MalformedURLException 异常,则还是委派 getResourceByPath(String path) 返回 ClassPathContextResource 类型的资源。这样就基本完成了资源的定位加载。

 

4.2.5、示例

(这一段是参考的)DefaultResourceLoader 加载资源的具体策略演示代码(《Spring 揭秘》 P89):

ResourceLoader resourceLoader = new DefaultResourceLoader();

Resource fileResource1 = resourceLoader.getResource("D:/Users/test/Documents/spring.txt");
System.out.println("fileResource1 is FileSystemResource:" + (fileResource1 instanceof FileSystemResource));

Resource fileResource2 = resourceLoader.getResource("/Users/test/Documents/spring.txt");
System.out.println("fileResource2 is ClassPathResource:" + (fileResource2 instanceof ClassPathResource));

Resource urlResource1 = resourceLoader.getResource("file:/Users/test/Documents/spring.txt");
System.out.println("urlResource1 is UrlResource:" + (urlResource1 instanceof UrlResource));

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

   运行结果:

fileResource1 is FileSystemResource:false
fileResource2 is ClassPathResource:true
urlResource1 is UrlResource:true
urlResource1 is urlResource:true
  • 对于 fileResource1 的结果,可能会有点奇怪,为何是 ClassPathResource ,而不是 FileSystemResource 类型的资源?DefaultResourceLoader.getResource(String location) 在解析 "D:/Users/test/Documents/spring.txt" 的过程中没有遇到匹配的,于是抛出了 MalformedURLException,然后通过 getResourceByPath(String path) 返回默认的 ClassPathResource 类型的资源。
  • urlResource1 和 urlResource2 的协议就比较清晰了,通过 URL 来进行定义,返回的都是 UrlResource 类型资源。

 

4.3、FileSystemResourceLoader

  通过上面的示例,我们可以看到,如果是文件系统的文件资源,DefaultResourceLoader.getResourceByPath(String path) 的处理是不恰当的。这个时候就需要使用 org.springframework.core.io.FileSystemResourceLoader 了,它重写了 DefaultResourceLoader.getResourceByPath(String path) 方法,返回 FileSystemResource 类型的资源以便可以从文件系统进行加载,最终得到我们需要的资源类型: 

public class FileSystemResourceLoader extends DefaultResourceLoader {

    /**
     * 返回 FileSystemContextResource 类型的资源
     * 将资源路径解析为文件系统路径,以 / 开头的会被解析为 VM 当前工作目录的相对路径
     */
    @Override
    protected Resource getResourceByPath(String path) {
        // 截取开头的 /,会当作 context 上下文的额相对路径
          if (path.startsWith("/")) {
            path = path.substring(1);
        }
        return new FileSystemContextResource(path);
    }


    /**
     * 通过实现 ContextResource 接口显式地表示上下文相关路径的 FileSystemResource
     */
    private static class FileSystemContextResource extends FileSystemResource implements ContextResource {

        public FileSystemContextResource(String path) {
            super(path);
        }

        // 返回文件资源的路径
        @Override
        public String getPathWithinContext() {
            return getPath();
        }
    }

}

    这里的 FileSystemContextResource 是 FileSystemResourceLoader 的内部类,它继承了 FileSystemResource 类,还实现了 ContextResource 接口,既可以表示实际文件系统的资源,也可以表示 context 环境相对路径的资源。FileSystemContextResource 的构造函数也比较简单,直接调用父类 FileSystemResourceLoader 的构造函来实现。

  4.2.5 的示例中,如果使用 FileSystemResourceLoader 进行加载,则可以得到 FileSystemResource 类型资源的 fileResource1 。

 

4.4、ClassRelativeResourceLoader

  org.springframework.core.io.ClassRelativeResourceLoader 是 DefaultResourceLoader 的另一个子类: 

/**
 * ResourceLoader 的实现类,将普通的资源路径解析为给定 java.lang.Class 相关的资源,
 * 用于解析加载 Class 所在包或所在包的子包下的资源
 */
public class ClassRelativeResourceLoader extends DefaultResourceLoader {

    private final Class<?> clazz;


    /**
     * 根据指定的 class 创建 ClassRelativeResourceLoader 实例,
     * 内部会通过 class 来获取对应的 ClassLoader,这个 ClassLoader 可以用于
     * 自定义解析策略的加载,也可以用于 ClassPathResource 类型的资源(协议是 classpath:),
     * 参考 DefaultResourceLoader.getResource()
     */
    public ClassRelativeResourceLoader(Class<?> clazz) {
        Assert.notNull(clazz, "Class must not be null");
        this.clazz = clazz;
        setClassLoader(clazz.getClassLoader());
    }

    // 返回 ClassRelativeContextResource 类型的资源
    @Override
    protected Resource getResourceByPath(String path) {
        return new ClassRelativeContextResource(path, this.clazz);
    }


    /**
     * 通过实现 ContextResource 接口显式地表示上下文相关路径的 ClassRelativeContextResource
     */
    private static class ClassRelativeContextResource extends ClassPathResource implements ContextResource {

        private final Class<?> clazz;

        public ClassRelativeContextResource(String path, Class<?> clazz) {
            super(path, clazz);
            this.clazz = clazz;
        }

        // 返回文件资源的路径
        @Override
        public String getPathWithinContext() {
            return getPath();
        }

        // 根据资源的相对路径创建对应的资源,内部会将相对路径拼接进去
        @Override
        public Resource createRelative(String relativePath) {
            String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath);
            return new ClassRelativeContextResource(pathToUse, this.clazz);
        }
    }

}

 

4.5、ResourcePatternResolver

  ResourceLoader 的 Resource getResource(String location) 方法每次只返回一个 Resource,无法加载多个资源,所以一般会是对应到具体的某个资源文件。如果需要加载多个资源的,则需要用 ResourceLoader 的另一个扩展类 org.springframework.core.io.support.ResourcePatternResolver,它支持根据指定的资源路径一次返回多个 Resource 实例对象: 

public interface ResourcePatternResolver extends ResourceLoader {

    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

    /**
     * 返回指定路径的 Resource 数组实例对象
     */
    Resource[] getResources(String locationPattern) throws IOException;

}

   ResourcePatternResolver 增加了一种伪 URL 协议:“classpath*:”,用于解析加载从当前 classpath 下以及所有 jar 包中的相关路径的资源 Resource;而新增加的 Resource[] getResources(String locationPattern) 则可以根据指定的路径同时返回多个 Resource 实例。

 

4.6、PathMatchingResourcePatternResolver

  来到重点了,前面看到过 ApplicationContext 是通过 ResourcePatternResolver 来获取到资源的,再具体一点,其实是它的实现类 org.springframework.core.io.support.PathMatchingResourcePatternResolver,除了支持 ResourceLoader ,以及 ResourcePatternResolver 新增的 “classpath*:” 协议外,还支持 Ant 风格的路径匹配模式,例如常见的 **/*.xml 。相对地,PathMatchingResourcePatternResolver 的实现也较前面介绍的复杂一些。

 

4.6.1、构造函数和属性

  PathMatchingResourcePatternResolver 提供了3个构造函数,都与 ResourceLoader 有关:

  • 实例化时需要指定 ResourceLoader,默认是 DefaultResourceLoader。
  • PathMatcher pathMatcher 属性,默认是 AntPathMatcher,Ant 类型的路径匹配实现类 
/**
 * 内置的 ResourceLoader 资源加载定位器
 */
private final ResourceLoader resourceLoader;
/**
 * Ant 路径匹配器,用于支持 Ant 类型的路径匹配
 */
private PathMatcher pathMatcher = new AntPathMatcher();

/**
 * 没有指定的话就是用默认的 DefaultResourceLoader
 */
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);
}

 

4.6.2、getResource 方法

  getResource 直接通过委托给 ResourceLoader 来进行加载,也即是默认的 DefaultResourceLoader 进行加载: 

@Override
@Nullable
public ClassLoader getClassLoader() {
    return getResourceLoader().getClassLoader();
}

// 使用内置的 ResourceLoader 进行加载,如果不是 ant 风格的路径,应该都是单一的 Resource
@Override
public Resource getResource(String location) {
    return getResourceLoader().getResource(location);
}

 

4.6.3、getResources 方法

  getResources 方法用于加载并返回多个资源,Ant 风格匹配加载的就是这个方法在执行的(比较常见的就是各种带 * 的路径匹配)。

@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*:" 开头的,都调用 findPathMatchingResources(String locationPattern) 进行多个资源的加载;
  • 以 "classpath*:" 开头,但路径不包含通配符的,则调用 findAllClassPathResources(String location) 进行加载
  • 不是 "classpath*:" 开头,也不包含通配符,那么理论上应该是某个资源路径,直接使用 ResourceLoader 来进行加载。

  下面将对 findPathMatchingResources(String locationPattern) 和 findAllClassPathResources(String location) 进行分析。

 

4.6.4、findAllClassPathResources 方法

  以 "classpath*:" 开头,但路径不包含通配符的,则调用 findAllClassPathResources(String location) 进行加载,该方法返回 classes 路径下和所有 jar 包中与路径匹配的所有资源。

  这种情况下比较常见的是不同的 jar 包但是相同路径的配置文件,例如,当前项目有个 /spring/app-service.xml,需要引用内部团队其他关联依赖 B.jar ,而且 B.jar 中也有同名的配置文件 /spring/app-service.xml,如果需要都加载,那就可以使用 classpath*:/spring/app-service.xml 来进行配置和加载。

  findAllClassPathResources(String location) 的内部实现如下: 

/**
 * 通过类加载器找到 classes 路径和所有 jar 包中与给定路径相匹配的资源,
 * 委托给 doFindAllClassPathResources(String) 进行实际的处理
 */
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]);
}

/**
 * <p>通过类加载器找到 classes 路径和所有 jar 包中与给定路径相匹配的资源
 */
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;
}

/**
 * 将 ClassLoader 返回的给定 URL 转换成 Resource,默认是 UrlResource
 */
protected Resource convertClassLoaderURL(URL url) {
    return new UrlResource(url);
}

   实际上执行加载的是 doFindAllClassPathResources(String path),处理过程如下:

  • 1. 通过 ClassLoader 进行资源的加载,如果实例化 PathMatchingResourcePatternResolver 时已指定了 ResourceLoader,则使用这个ResourceLoader 的 ClassLoader 作为委托对象,通过调用其 getResources(String name) 方法来进行处理,否则通过 ClassLoader.getSystemResources(String name) 来进行处理。ClassLoader.getResources(String name) 的代码实现如下: 
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);
}

   这里比较明显,如果 ClassLoader 有父类加载器,则通过父类加载器迭代加载获取资源,直至最后则是调用 getBootstrapResources(name) 进行资源获取。

  • 2. 迭代遍历 1 中获取到的 URL 集合资源,通过 convertClassLoaderURL(URL url) 将 URL 转换成 UrlResource 实例对象。
  • 3. 如果路径 path 为""(classpath*: 或者 classpath*:/),则通过 addAllClassLoaderJarRoots(@Nullable ClassLoader classLoader, Set<Resource> result) 方法加载路径下所有 jar 包的资源。有兴趣的可以继续跟进看看。

  综上,findAllClassPathResources(String location) 方法其实就是利用 ClassLoader 来加载指定路径下的资源,包括 classes 路径下和 jar 包的资源。如果传入的路径为空或者是 / (classpath*: 或者 classpath*:/),则会通过 addAllClassLoaderJarRoots 方法来加载所有的 jar 包的资源。

 

4.6.5、findPathMatchingResources 方法

  路径中包含通配符的,都是使用这个方法进行资源的加载: 

/**
 * <通过 Ant 风格的 PathMatcher 来查找匹配给定路径的所有资源,
 * 支持 在 jar 包、zip 文件、和文件系统中查找相关资源
 */
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]);
}

   这里加载主要分为2步:

  • 确定根目录(通配符之前目录),获取该目录下的所有资源
  • 然后进行迭代匹配获取所有需要的资源。

  根据常见的场景,我们比较关注的是以下这2个(类)方法:

  • determineRootDir(String location)
  • doFindPathMatchingXxxResources

 

4.6.5.1、determineRootDir 方法

  determineRootDir 方法主要用于确认根路径: 

/**
 * <p>确定指定路径的根路径
 */
protected String determineRootDir(String location) {
    // 冒号之后一位,即协议后面的位置
    int prefixEnd = location.indexOf(':') + 1;
    // 目录结束位置
    int rootDirEnd = location.length();
    // 循环判断是否有分隔符,如果有,则截断最后一个 / 之后的部分
    // 例如 classpath*:spring/*-service.xml,循环判断后剩下 spring/ 已经不包含通配符了
    // 如果是 classpath*:*-service.xml,则会出现 rootDirEnd == 0 的情况
    while (rootDirEnd > prefixEnd && getPathMatcher().isPattern(location.substring(prefixEnd, rootDirEnd))) {
        // 通配符至少占 1 个,再加上分隔符,所以 -2;最后 +1 表示返回结果要包含分隔符
        rootDirEnd = location.lastIndexOf('/', rootDirEnd - 2) + 1;
    }
    // 将 prefixEnd 的值赋给 rootDirEnd,也即是冒号后一位
    if (rootDirEnd == 0) {
        rootDirEnd = prefixEnd;
    }
    // 截取根目录
    return location.substring(0, rootDirEnd);
}

   看看几个例子就可以大概知道这方法的作用了:

原路径 根路径
classpath*:spring/a*/*-service.xml classpath*:spring/
classpath*:spring/a/*-service.xml classpath*:spring/a/
classpath*:*-service.xml classpath*:

 

4.6.5.2、doFindPathMatchingXxxResources

  这个指的是特定类型资源的全部加载,只要是 findPathMatchingResources 方法中用到的这3个

  • doFindPathMatchingJarResources(Resource rootDirResource, URL rootDirURL, String subPattern)
  • doFindPathMatchingFileResources(Resource rootDirResource, String subPattern)
  • VfsResourceMatchingDelegate.findMatchingResources(URL rootDirURL, String locationPattern, PathMatcher pathMatcher)

  对于这几个方法的分析,可以参考以下文章:

 

5、总结

  行文至此,Spring 的资源加载策略和加载过程已基本分析完毕,相信读者也知道了资源比较常见的用途(初始化 Application),以及平常在配置中为何可以写各种带 * 的配置了。Spring 的资源加载策略和加载过程总结如下:

  • Spring 提供了 Resource 和 ResourceLoader 来进行资源的统一抽象及其统一定位加载,并且提供了合适的 Default 类或者 Abstract 类来处理一些比较常用的预实现,同时使得自定义实现更加清晰便捷。
  • AbstractResource 作为 Resource 的默认抽象实现,提供了大部分接口方法的典型预实现,子类继承该类后只需要根据特定的资源特性覆盖相应的方法即可;对于自定义的 Resource 我们也是继承这个抽象类,然后根据实际来覆盖相关方法。
  • Spring 提供了丰富的资源实现类,如 文件( FileSystemResource ) 、 Classpath 资源( ClassPathResource )、 URL 资源( UrlResource )、 InputStream 资源( InputStreamResource ) 、Byte 数组( ByteArrayResource )等,用于表示各种底层资源。
  • DefaultResourceLoader 则提供了 ResourceLoader 的默认实现,可用于默认情况下的资源加载。自定义加载策略,除了直接继承 ResourceLoader 外,还可以直接通过实现 ProtocolResolver 接口,只需要提供自定义的资源加载策略。
  • Spring 还提供了 ResourcePatternResolver 接口,内部的 Resource[] getResources(String locationPattern) 方法可用于一次性返回多个资源,弥补了 DefaultResourceLoader 每次只能返回单一资源的不足,其实现类 PathMatchingResourcePatternResolver 既实现了 Resource getResource(String location) 方法,也实现了 Resource[] getResources(String locationPattern) 方法,是 ApplicationContext 的资源默认加载策略类。
  • Resource 和 ResourceLoader 相关接口和实现类都是在 spring-core 包里面,是比较核心的基础功能。

 

6、测试案例参考

  spring-core 模块的 test 案例中的 org.springframework.core.io.support.PathMatchingResourcePatternResolverTests 中包含了一些相关的测试案例,可以进行参考,有兴趣的也可自行测试验证。

  另外,本文源码相关注释可参考github:https://github.com/wpbxin/spring-framework

 

7、参考

 

8、声明

  本文仅用于学习使用,如有雷同,纯属借鉴,如有问题,可留言反馈~~~

 

posted @ 2020-06-07 18:01  心明谭  阅读(1174)  评论(0编辑  收藏  举报