我准备战斗到最后,不是因为我勇敢,是我想见证一切。 --双雪涛《猎人》
[TOC]
Thinking
- 一个技术,为什么要用它,解决了那些问题?
- 如果不用会怎么样,有没有其它的解决方法?
- 对比其它的解决方案,为什么最终选择了这种,都有何利弊?
- 你觉得项目中还有那些地方可以用到,如果用了会带来那些问题?
- 这些问题你又如何去解决的呢?
本文主要基于 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
实现。
类结构图
-
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);
}
}
}
上述源代码。逻辑非常清晰:
- 首先
spring
会尝试从资源策略上加载resource
,没有的话进行下面的判断。 - 若
location
以"/"
开头,则调用#getResourceByPath()
方法,构造ClassPathContextResource
类型资源并返回。代码如下:
protected Resource getResourceByPath(String path) {
return new ClassPathContextResource(path, getClassLoader());
}
- 若
location
以"classpath:"
开头,则构造ClassPathResource
类型资源并返回。在构造该资源时,通过#getClassLoader()
获取当前的 ClassLoader。 - 尝试构造
URL
,尝试通过解析的URL
来对资源进行定位。若失败则抛出MalformedURLException
,若解析成功,会判断类型是否为文件系统资源(文件资源类型包括三种类型:File
,vfs
,vfsfile
)。是:则返回FileUrlResource
,否则返回UrlResource
类型。 - 解析报错,就会当作 第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
在本地文件系统中仅支持三种协议以File
,vfs
,vfsfile
开头的。所以在第一个文件判断中,绝对路径返回的对象类型应该是在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]);
}
方法有点儿长,但是思路还是很清晰的,主要分两步:
- 确定目录,获取该目录下得所有资源。
- 在所获得的所有资源后,进行迭代匹配获取我们想要的资源。
在这个方法里面,我们要关注两个方法,一个是 #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 统一资源加载策略的整体流程。相对来说,上面几个方法的代码量会比较多。所以本文不再追溯,推荐阅读如下文章:
-
《Spring源码情操陶冶-PathMatchingResourcePatternResolver路径资源匹配溶解器》 ,主要针对
#doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPatter)
方法。 -
《深入 Spring IoC 源码之 ResourceLoader》 ,主要针对
#doFindPathMatchingFileResources(rootDirResource, subPattern)
方法。 - 《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 ,进行操作调试。
本文仅供笔者本人学习,一起进步!
加油!