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

Posted wpbxin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 类型的资源
            //以上是关于Spring5源码分析(005)——IoC篇之统一资源加载Resource和ResourceLoader的主要内容,如果未能解决你的问题,请参考以下文章

Spring5源码分析(021)——IoC篇之bean加载

Spring5源码分析(006)——IoC篇之核心类DefaultListableBeanFactory和XmlBeanDefinitionReader

Spring5源码分析(007)——IoC篇之加载 BeanDefinition(的大致流程)

Spring5源码分析(004)——IoC篇之理解Ioc

Spring5源码分析(003)——IoC篇之 spring IoC 容器体系总览(占坑待更新)

spring5源码分析系列——IOC容器的初始化