【Spring源码】IoC之Spring统一资源加载策略
在学 Java SE 的时候,我们学习了一个标准类java.net.URL,该类在 Java SE中定位为统一资源定位器(Uniform Resource Locator),但是我们知道它的实现基本只限于网络形式发布的资源的查找和定位。然而,实际上资源的定义比较广泛,除了网络形式的资源,还有以二进制形式存在的、以文件形式存在的、以字节流形式存在的等等。而且它可以存在于任何场所,比如网络、文件系统、应用程序中。所以java.net.URL的局限性迫使 Spring 必须实现自己的资源加载策略,该资源加载策略需要满足如下要求:
- 职能划分清楚。资源的定义和资源的加载应该要有一个清晰地界限;
- 统一的抽象。统一的资源定义和资源加载策略。资源加载后要返回统一的抽象给客户端,客户端要对资源进行怎样的处理,应该由抽象资源接口来界定。
前言
在学 Java SE 的时候,我们学习了一个标准类java.net.URL,该类在 Java SE中定位为统一资源定位器(Uniform Resource Locator),但是我们知道它的实现基本只限于网络形式发布的资源的查找和定位。然而,实际上资源的定义比较广泛,除了网络形式的资源,还有以二进制形式存在的、以文件形式存在的、以字节流形式存在的等等。而且它可以存在于任何场所,比如网络、文件系统、应用程序中。所以java.net.URL的局限性迫使 Spring 必须实现自己的资源加载策略,该资源加载策略需要满足如下要求:
- 职能划分清楚。资源的定义和资源的加载应该要有一个清晰地界限;
- 统一的抽象。统一的资源定义和资源加载策略。资源加载后要返回统一的抽象给客户端,客户端要对资源进行怎样的处理,应该由抽象资源接口来界定。
统一资源:Resource
org.springframework.core.io.Resource 为 Spring 框架所有资源的抽象和访问接口,它继承 org.springframework.core.io.InputStreamSource 接口。作为所有资源的统一抽象,Resource 定义了一些通用的方法,由子类 AbstractResource 提供统一的默认实现。定义如下:
1 | public interface Resource extends InputStreamSource { |
子类结构

从类结构图可以看到,Resource 根据资源的不同类型提供不同的具体实现
- FileSystemResource:对
java.io.File类型资源的封装。只要是跟 File 打交道的,基本上与 FileSystemResource 也可以打交道。支持文件和 URL 的形式,实现 WritableResource 接口,且从 Spring Framework 5.0 开始,FileSystemResource 使用 NIO2 API 进行读、写交互。 - ByteArrayResource:对字节数组提供的数据的封装。如果通过 InputStream 形式访问该类型的资源,该实现会根据字节数组的数据构造一个相应的 ByteArrayInputStream。
- UrlResource:对
java.net.URL类型资源的封装。内部委派 URL 进行具体的资源操作。 - ClassPathResource:class path 类型资源的实现。使用给定的 ClassLoader 或者给定的 Class 来加载资源。
- InputStreamResource:将给定的 InputStream 作为一种资源的 Resource 的实现类。
AbstractResource
org.springframework.core.io.AbstractResource,为 Resource 接口的默认抽象实现。它实现了 Resource 接口的大部分的公共实现,作为 Resource 接口中德重中之重,其定义如下:
1 | public abstract class AbstractResource implements Resource { |
如果我们想要实现自定义的Resource,记住不要实现 Resource 接口,而应该继承 AbstractResource 抽象类,然后根据当前的具体资源特性覆盖相应的方法即可。
统一资源定位:ResourceLoader
一开始就说了 Spring 将资源的定义和资源的加载区分开了,Resource 定义了统一的资源,那资源的加载则由 ResourceLoader 来统一定义。
org.springframework.core.io.ResourceLoader 为 Spring 资源加载的统一抽象,具体的资源加载则由相应的实现类来完成,所以我们可以将 ResourceLoader 称作为统一资源定位器。其定义如下:
ResourceLoader,定义资源加载器,主要应用于根据给定的资源文件地址,返回对应的 Resource。
——《Spring 源码深度解析》P16
1 | public interface ResourceLoader { |
getResource(String location)方法,根据所提供资源的路径 loaction 返回 Resource 实例,但是它不确保该 Resource 一定存在,需要调用Resource.exist()方法来判断。- 该方法支持以下模式的资源加载:
- URL 位置资源,如
"file:C:/test.dat"。 - ClassPath 位置资源,如
classpath:test.dat。 - 相对路径资源,如
WEB-INF/test.dat,此时返回的 Resource 实例,根据实现不同而不同。
- URL 位置资源,如
- 该方法的主要实现是在其子类 DefaultResourceLoader 中实现,具体过程我们在分析 DefaultResourceLoader 时做详细说明。
- 该方法支持以下模式的资源加载:
getClassLoader()方法,返回 ClassLoader 实例,对于想要获取 ResourceLoader 使用的 ClassLoader 用户来说,可以直接调用该方法来获取。在分析 Resource 时,提到了一个类 ClassPathResource,这个类是可以根据指定的 ClassLoader 来加载资源的。
子类结构
作为 Spring 统一的资源加载器,它提供了统一的抽象,具体的实现则由相应的子类来负责实现,其类的类结构图如下:

DefaultResourceLoader
与 AbstractResource 相似,org.springframework.core.io.DefaultResourceLoader 是 ResourceLoader 的默认实现。
构造函数
它接收 ClassLoader 作为构造函数的参数,或者使用不带参数的构造函数。
- 在使用不带参数的构造函数时,使用的 ClassLoader 为默认的 ClassLoader (一般
Thread.currentThread()#getContextClassLoader())。 - 在使用带参数的构造函数时,可以通过
ClassUtils#getDefaultClassLoader()获取
代码如下:
1 |
|
- 另外,也可以调用
#setClassLoader()方法进行后续设置。
getResource 方法
ResourceLoader 中最核心的方法为 #getResource(String location),它根据提供的 location 返回相应的 Resource。而 DefaultResourceLoader 对该方法提供了核心实现(因为,它的两个子类都没有提供覆盖该方法,所以可以断定 ResourceLoader 的资源加载策略就封装在 DefaultResourceLoader 中),代码如下:
1 | // DefaultResourceLoader.java |
- 首先,通过
ProtocolResolver来加载资源,成功返回Resource。 - 其次,若
locaation以/开头,则调用#getResourceByPath()方法,构造ClassPathContextResource类型资源并返回。代码如下:
1 | protected Resource getResourceByPath(String path) { |
- 再次,若
location以classpath:开头,则构造ClassPathResource类型资源并返回。在构造该资源时,通过#getClassLoader()获取当前的ClassLoader。 - 然后,构造 URL,尝试通过它进行资源定位,若没有抛出
MalformedURLException异常,则判断是否为FileURL,如果是则构造FileUrlResource类型,否则构造UrlResource类型的资源。 - 最后,若在加载过程中抛出
MalformedURLException异常,则委派#getResourceByPath()方法,实现资源定位加载。实际上,和「其次」相同。
ProtocolResolver
org.springframework.core.io.ProtocolResolver,用户自定义协议资源解决策略,作为 DefaultResourceLoader 的 SPI:它允许用户自定义资源加载协议,而不需要继承 ResourceLoader 的子类。在介绍 Resource 时,提到如果要实现自定义 Resource,我们只需要继承 AbstractResource 即可,但是有了 ProtocolResolver 后,我们不需要直接继承 DefaultResourceLoader,改为实现 ProtocolResolver 接口也可以实现自定义的 ResourceLoader。
ProtocolResolver 接口,仅有一个方法 Resource resolve(String location, ResourceLoader resourceLoader)。代码如下:
1 | /** |
在 Spring 中你会发现该接口并没有实现类,它需要用户自定义,自定义的 Resolver 如何加入 Spring 体系呢?调用 DefaultResourceLoader#addProtocolResolver(ProtocolResolver) 方法即可。代码如下:
1 | /** |
示例
下面示例是演示 DefaultResourceLoader 加载资源的具体策略,代码如下(该示例参考《Spring 揭秘》 P89):
1 | ResourceLoader resourceLoader = new DefaultResourceLoader(); |
运行结果:
1 | fileResource1 is FileSystemResource:false |
- 其实对于
fileResource1,我们更加希望是FileSystemResource资源类型。但是,事与愿违,它是ClassPathResource类型。为什么呢?在DefaultResourceLoader#getResource()方法的资源加载策略中,我们知道D:/Users/von/Documents/spark.txt地址,其实在该方法中没有相应的资源类型,那么它就会在抛出MalformedURLException异常时,通过DefaultResourceLoader#getResourceByPath(...)方法,构造一个ClassPathResource类型的资源。 - 而
urlResource1和urlResource2,指定有协议前缀的资源路径,则通过 URL 就可以定义,所以返回的都是 UrlResource 类型。
FileSystemResourceLoader
从上面的示例,我们看到,其实 DefaultResourceLoader 对 #getResourceByPath(String) 方法处理其实并不是很恰当,这个时候我们可以使用 org.springframework.core.io.FileSystemResourceLoader。它继承 DefaultResourceLoader,且覆写了 #getResourceByPath(String) 方法,使之从文件系统加载资源并以 FileSystemResource 类型返回,这样我们就可以得到想要的资源类型。代码如下:
1 |
|
FileSystemContextResource
FileSystemContextResource, 为FileSystemResourceLoader 的内部类,它继承 FileSystemResource 类,实现 ContextResource 接口。代码如下:
1 | /** |
- 在构造器中,也是调用
FileSystemResource的构造函数来构造FileSystemResource的。 - 为什么要有
FileSystemContextResource类的原因是,实现ContextResource接口,并实现对应的#getPathWithinContext()接口方法。
示例
再回过头看上一节的示例,如果将 DefaultResourceLoader 改为 FileSystemResourceLoader,则 fileResource1 则为 FileSystemResource 类型的资源。
ClassRelativeResourceLoader
org.springframework.core.io.ClassRelativeResrouceLoader,是 DefaultResourceLoader 的另一个子类的实现。和 FileSystemResourceLoader 类似,在实现代码的结构上类似,也是覆写 #getResourceByPath(String path) 方法,并返回其对应的 ClassRelativeContextResource 的资源类型。
感兴趣的话可以看Spring5:就这一次,搞定资源加载器之ClassRelativeResourceLoader
ClassRelativeResourceLoader扩展功能是,可以根据给定的class所在包或者所在包的子包下加载资源。
ResourcePatternResolver
ResourceLoader 的 Resource getResource(String location) 方法,每次只能根据 location 返回一个 Resource 。当需要加载多个资源时,我们除了多次调用 #getResource(String location) 方法外,别无他法。org.springframework.core.io.support.ResourcePatternResolver 是 ResourceLoader 的扩展,它支持根据指定的资源路径匹配模式每次返回多个 Resource 实例,其定义如下:
1 | public interface ResourcePatternResolver extends ResourceLoader { |
- ResourcePatternResolver 在 ResourceLoader 的基础上增加了
#getResources(String locationPattern)方法,以支持根据路径匹配模式返回多个 Resource 实例。 - 同时,也新增了一种新的协议前缀
"classpath*:",该协议前缀由其子类负责实现。
PathMatchingResourcePatternResolver
org.springframework.core.io.support.PathMatchingResourcePatternResolver ,为 ResourcePatternResolver 最常用的子类,它除了支持 ResourceLoader 和 ResourcePatternResolver 新增的 "classpath*:" 前缀外,还支持 Ant 风格的路径匹配模式(类似于 "**/*.xml")。
构造函数
PathMatchingResourcePatternResolver 提供了三个构造函数,如下:
1 | /** |
- PathMatchingResourcePatternResolver 在实例化的时候,可以指定一个 ResourceLoader,如果不指定的话,它会在内部构造一个 DefaultResourceLoader 。
pathMatcher属性,默认为 AntPathMatcher 对象,用于支持 Ant 类型的路径匹配。
getResource
1 |
|
该方法,直接委托给相应的 ResourceLoader 来实现。所以,如果我们在实例化的 PathMatchingResourcePatternResolver 的时候,如果未指定 ResourceLoader 参数的情况下,那么在加载资源时,其实就是 DefaultResourceLoader 的过程。
其实在下面介绍的 Resource[] getResources(String locationPattern) 方法也相同,只不过返回的资源是多个而已。
getResources
1 |
|
- 非
"classpath*:"开头,且路径不包含通配符,直接委托给相应的 ResourceLoader 来实现。 - 其他情况,调用
#findAllClassPathResources(...)、或#findPathMatchingResources(...)方法,返回多个 Resource 。下面,我们来详细分析。
findAllClassPathResources
当 locationPattern 以 "classpath*:" 开头但是不包含通配符,则调用 #findAllClassPathResources(...) 方法加载资源。该方法返回 classes 路径下和所有 jar 包中的所有相匹配的资源。
1 | protected Resource[] findAllClassPathResources(String location) throws IOException { |
真正执行加载的是在 #doFindAllClassPathResources(...) 方法,代码如下:
1 | protected Set<Resource> doFindAllClassPathResources(String path) throws IOException { |
<1>处,根据 ClassLoader 加载路径下的所有资源。在加载资源过程时,如果在构造 PathMatchingResourcePatternResolver 实例的时候如果传入了 ClassLoader,则调用该 ClassLoader 的#getResources()方法,否则调用ClassLoader#getSystemResources(path)方法。另外,ClassLoader#getResources()方法,代码如下:
1 | // java.lang.ClassLoader.java |
- 看到这里是不是就已经一目了然了?如果当前父类加载器不为
null,则通过父类向上迭代获取资源,否则调用#getBootstrapResources()。这里是不是特别熟悉。 <2>处,遍历 URL 集合,调用#convertClassLoaderURL(URL url)方法,将 URL 转换成 UrlResource 对象。代码如下:
1 | protected Resource convertClassLoaderURL(URL url) { |
<3>处,若path为空(“”)时,则调用#addAllClassLoaderJarRoots(...)方法。该方法主要是加载路径下得所有 jar 包,方法较长也没有什么实际意义就不贴出来了。感兴趣的胖友,自己可以去看看。当然,可能代码也比较长哈。
通过上面的分析,我们知道 #findAllClassPathResources(...) 方法,其实就是利用 ClassLoader 来加载指定路径下的资源,不论它是在 class 路径下还是在 jar 包中。如果我们传入的路径为空或者 /,则会调用 #addAllClassLoaderJarRoots(...) 方法,加载所有的 jar 包。
findPathMatchingResources
当 locationPattern 中包含了通配符,则调用该方法进行资源加载。代码如下:
1 | protected Resource[] findPathMatchingResources(String locationPattern) throws IOException { |
方法有点儿长,但是思路还是很清晰的,主要分两步:
- 确定目录,获取该目录下得所有资源。
- 在所获得的所有资源后,进行迭代匹配获取我们想要的资源。
在这个方法里面,我们要关注两个方法,一个是 #determineRootDir(String location) 方法,一个是 #doFindPathMatchingXXXResources(...) 等方法。
determineRootDir
determineRootDir(String location) 方法,主要是用于确定根路径。代码如下:
1 | /** |
方法比较绕,效果如下示例:
| 原路径 | 确定根路径 |
|---|---|
classpath*:test/cc*/spring-*.xml |
classpath*:test/ |
classpath*:test/aa/spring-*.xml |
classpath*:test/aa/ |
doFindPathMatchingXXXResources
#doFindPathMatchingXXXResources(...) 方法,是个泛指,一共对应三个方法:
#doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPatter)方法#doFindPathMatchingFileResources(rootDirResource, subPattern)方法VfsResourceMatchingDelegate#findMatchingResources(rootDirUrl, subPattern, pathMatcher)方法
因为本文重在分析 Spring 统一资源加载策略的整体流程。相对来说,上面几个方法的代码量会比较多。所以本文不再追溯,感兴趣的胖友,推荐阅读如下文章:
- 《Spring源码情操陶冶-PathMatchingResourcePatternResolver路径资源匹配溶解器》 ,主要针对
#doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPatter)方法。 - 《深入 Spring IoC 源码之 ResourceLoader》 ,主要针对
#doFindPathMatchingFileResources(rootDirResource, subPattern)方法。 - 《Spring 源码学习 —— 含有通配符路径解析(上)》 😈 貌似没有下
小结
至此 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 ,进行操作调试。
参考
摘要: 原创出处 http://cmsblogs.com/?p=2656 「小明哥」,略作修改及补充