【Spring 笔记】资源加载策略相关整理
2019-09-14 本文已影响0人
58bc06151329
文前说明
作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种方式记录自己的学习之旅。
本文仅供学习交流使用,侵权必删。
不用于商业目的,转载请注明出处。
1. 概述
- Java SE 中有一个标准类 java.net.URL,该类定位为统一资源定位器(Uniform Resource Locator),但是它的实现基本只限于网络形式发布的资源的查找和定位。
- 实际上资源的定义比较广泛,除了网络形式的资源,还有以二进制形式存在的、以文件形式存在的、以字节流形式存在的等。
- 实际上资源可以存在于任何场所,比如网络、文件系统、应用程序中。
- 因为 java.net.URL 的局限性,所以 Spring 实现了自己的资源加载策略,满足了如下要求。
- 职能划分清晰,资源的定义 和 资源的加载 有一个清晰的界限。
- 有着统一的资源定义和资源加载策略,资源加载后返回统一的抽象给客户端,客户端对资源的处理由抽象资源接口界定。
2. 原理
2.1 统一的资源定义(Resource)
-
org.springframework.core.io.Resource 是 Spring 框架所有资源的抽象和访问接口,它继承 org.springframework.core.io.InputStreamSource 接口。
- 作为所有资源的统一抽象,
Resource
定义了一些通用的方法,由子类AbstractResource
提供统一的默认实现。
- 作为所有资源的统一抽象,
public interface Resource extends InputStreamSource {
/**
* 资源是否存在
*/
boolean exists();
/**
* 资源是否可读
*/
boolean isReadable();
/**
* 资源所代表的句柄是否被一个 stream 打开了
*/
boolean isOpen();
/**
* 返回资源的 URL 的句柄
*/
URL getURL() throws IOException;
/**
* 返回资源的 URI 的句柄
*/
URI getURI() throws IOException;
/**
* 返回资源的 File 的句柄
*/
File getFile() throws IOException;
/**
* 资源内容的长度
*/
long contentLength() throws IOException;
/**
* 资源最后的修改时间
*/
long lastModified() throws IOException;
/**
* 根据资源的相对路径创建新资源
*/
Resource createRelative(String var1) throws IOException;
/**
* 资源的文件名
*/
String getFilename();
/**
* 资源的描述
*/
String getDescription();
}
2.1.1 类结构图

- 根据资源的不同类型提供不同的具体实现。
-
FileSystemResource
对 java.io.File 类型资源的封装,支持文件和 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
的实现类。
-
ServletContextResource
-
org.springframework.web.context.support.ServletContextResource 类继承
AbstractFileResolvingResource
类,并实现ContextResource
接口。- 是为了访问 web 容器上下文中的资源而设计的类,负责以相对于 web 应用程序根目录的路径加载资源,它支持以流和 URL 的方式访问。
- 在 war 解包的情况下,也可以通过 File 的方式访问,还可以直接从 jar 包中访问资源。
2.1.2 AbstractResource
-
org.springframework.core.io.AbstractResource 是
Resource
接口的默认抽象实现,实现了 Resource 接口的大部分的 公共实现。
public abstract class AbstractResource implements Resource {
public AbstractResource() {
}
/**
* 判断文件是否存在,若判断过程产生异常(因为会调用 SecurityManager 来判断),就关闭对应的流
*/
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;
}
}
}
/**
* 直接返回 true,表示可读
*/
public boolean isReadable() {
return true;
}
/**
* 直接返回 false,表示未被打开
*/
public boolean isOpen() {
return false;
}
/**
* 抛出 FileNotFoundException 异常,交给子类实现
*/
public URL getURL() throws IOException {
throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");
}
/**
* 基于 getURL() 返回的 URL 构建 URI
*/
public URI getURI() throws IOException {
URL url = getURL();
try {
return ResourceUtils.toURI(url);
} catch (URISyntaxException ex) {
throw new NestedIOException("Invalid URI [" + url + "]", ex);
}
}
/**
* 抛出 FileNotFoundException 异常,交给子类实现
*/
public File getFile() throws IOException {
throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
}
/**
* 获取资源的长度
*
* 这个资源内容长度实际就是资源的字节长度,通过全部读取一遍来判断
*/
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) {
}
}
}
/**
* 返回资源最后的修改时间
*/
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;
}
/**
* 抛出 FileNotFoundException 异常,交给子类实现
*/
public Resource createRelative(String relativePath) throws IOException {
throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());
}
/**
* 获取资源名称,默认返回 null ,交给子类实现
*/
public String getFilename() {
return null;
}
/**
* 返回资源的描述
*/
public String toString() {
return getDescription();
}
public boolean equals(Object obj) {
return obj == this || obj instanceof Resource && ((Resource)obj).getDescription().equals(this.getDescription());
}
public int hashCode() {
return this.getDescription().hashCode();
}
}
// java.io.File.java
public boolean exists() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(path);
}
if (isInvalid()) {
return false;
}
return ((fs.getBooleanAttributes(this) & FileSystem.BA_EXISTS) != 0);
}
- 想要实现自定义的
Resource
,继承AbstractResource
抽象类,根据当前的具体资源特性覆盖相应的方法即可。
FileSystemResource(文件类型资源)的定义。
public class FileSystemResource extends AbstractResource implements WritableResource {
private final File file;
private final String path;
public FileSystemResource(File file) {
Assert.notNull(file, "File must not be null");
this.file = file;
this.path = StringUtils.cleanPath(file.getPath());
}
public FileSystemResource(String path) {
Assert.notNull(path, "Path must not be null");
this.file = new File(path);
this.path = StringUtils.cleanPath(path);
}
/**
* 返回文件路径
*/
public final String getPath() {
return this.path;
}
/**
* 判断文件是否存在,若判断过程产生异常(因为会调用 SecurityManager 来判断),就关闭对应的流
*/
public boolean exists() {
return this.file.exists();
}
/**
* 返回 true,表示可读
*/
public boolean isReadable() {
return this.file.canRead() && !this.file.isDirectory();
}
/**
* 返回文件输入流
*/
public InputStream getInputStream() throws IOException {
return new FileInputStream(this.file);
}
/**
* 返回 true,表示可写
*/
public boolean isWritable() {
return this.file.canWrite() && !this.file.isDirectory();
}
/**
* 返回文件输出流
*/
public OutputStream getOutputStream() throws IOException {
return new FileOutputStream(this.file);
}
/**
* 返回资源的 URL 的句柄
*/
public URL getURL() throws IOException {
return this.file.toURI().toURL();
}
/**
* 返回资源的 URI 的句柄
*/
public URI getURI() throws IOException {
return this.file.toURI();
}
/**
* 返回资源的 File 的句柄
*/
public File getFile() {
return this.file;
}
/**
* 资源内容的长度
*/
public long contentLength() throws IOException {
return this.file.length();
}
/**
* 根据资源的相对路径创建新资源
*/
public Resource createRelative(String relativePath) {
String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
return new FileSystemResource(pathToUse);
}
/**
* 资源的文件名
*/
public String getFilename() {
return this.file.getName();
}
/**
* 资源的描述
*/
public String getDescription() {
return "file [" + this.file.getAbsolutePath() + "]";
}
public boolean equals(Object obj) {
return obj == this || obj instanceof FileSystemResource && this.path.equals(((FileSystemResource)obj).path);
}
public int hashCode() {
return this.path.hashCode();
}
}
-
ByteArrayResource
、UrlResource
、ClassPathResource
、InputStreamResource
不同类型资源的定义与其类似。
2.2 统一的资源加载(ResourceLoader)
- 资源的加载则由
ResourceLoader
来统一定义,org.springframework.core.io.ResourceLoader 为 Spring 资源加载的统一抽象,具体的资源加载则由相应的实现类来完成,可以将ResourceLoader
称作为 统一资源定位器。 -
ResourceLoader
(定义资源加载器)主要应用于根据给定的资源文件地址,返回对应的Resource
。
public interface ResourceLoader {
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; // CLASSPATH URL 前缀。默认为:"classpath:"
Resource getResource(String location);
ClassLoader getClassLoader();
}
-
getResource(String location)
方法根据所提供资源的路径 location 返回Resource
实例,但是不确保该Resource
一定存在,需要调用Resource
的exist()
方法来判断。- 该方法支持 URL 位置资源(如:file:C:/test.dat),ClassPath 位置资源(如:classpath:test.dat),相对路径资源(如:WEB-INF/test.dat)几种模式的资源加载。
- 该方法的主要实现是在其子类
DefaultResourceLoader
中实现。
-
getClassLoader()
方法,返回ClassLoader
实例,可以直接获取ResourceLoader
中使用的ClassLoader
。-
ClassPathResource
这个类就可以根据指定的ClassLoader
来加载资源。
-
2.2.1 类结构图

- Spring 统一的资源加载器,提供了统一的抽象,具体的实现则由相应的子类来负责实现。
2.2.2 DefaultResourceLoader
-
org.springframework.core.io.DefaultResourceLoader 是
ResourceLoader
的默认实现。 - 在使用无参构造函数时,使用默认的
ClassLoader
(ClassUtils.getDefaultClassLoader()
)。
@Nullable
private ClassLoader classLoader;
public DefaultResourceLoader() { // 无参构造函数
this.classLoader = ClassUtils.getDefaultClassLoader();
}
public DefaultResourceLoader(@Nullable ClassLoader classLoader) { // 带 ClassLoader 参数的构造函数
this.classLoader = classLoader;
}
public void setClassLoader(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
@Nullable
public ClassLoader getClassLoader() {
return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
}
- 也可以调用
setClassLoader()
方法进行设置。
getResource 方法实现
// DefaultResourceLoader.java
@Override
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
// 1. 通过 ProtocolResolver 加载资源
for (ProtocolResolver protocolResolver : this.protocolResolvers) {
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());
// 4. 根据是否为文件 URL ,是则返回 FileUrlResource 类型的资源,否则返回 UrlResource 类型的资源
} else {
try {
// 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) {
// 5. 返回 ClassPathContextResource 类型的资源
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
}
}
- 方法的调用流程。
-
步骤 1,通过
ProtocolResolver
加载资源,成功返回Resource
。 -
步骤 2,如果 location 以 " / " 开头,则调用
getResourceByPath()
方法,构造ClassPathContextResource
类型资源并返回。 -
步骤 3,如果 location 以 " classpath: " 开头,则构造
ClassPathResource
类型资源并返回。- 在构造该资源时,通过
getClassLoader()
方法获取当前ClassLoader
。
- 在构造该资源时,通过
-
步骤 4,构造 URL ,尝试通过它进行资源定位,若没有抛出
MalformedURLException
异常,则判断是否为 FileURL , 如果是则构造FileUrlResource
类型的资源,否则构造UrlResource
类型的资源。 -
步骤 5,如果在加载过程中抛出
MalformedURLException
异常,则委派getResourceByPath()
方法,实现资源定位加载,与 <步骤 2> 的调用相同。
-
步骤 1,通过
ProtocolResolver(用户自定义协议资源解决策略)
- org.springframework.core.io.ProtocolResolver 允许用户自定义资源加载协议。
-
ProtocolResolver
接口,仅有一个方法Resource resolve(String location, ResourceLoader resourceLoader)
。
/**
* 使用指定的 ResourceLoader ,解析指定的 location 。
* 若成功,则返回对应的 Resource 。
*
* 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 指定的加载器 ResourceLoader
* @return a corresponding {@code Resource} handle if the given location
* matches this resolver's protocol, or {@code null} otherwise 返回为相应的 Resource
*/
@Nullable
Resource resolve(String location, ResourceLoader resourceLoader);
- 自定义的
Resolver
调用DefaultResourceLoader#addProtocolResolver(ProtocolResolver)
方法即可使用该策略。
/**
* ProtocolResolver 集合
*/
private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);
public void addProtocolResolver(ProtocolResolver resolver) {
Assert.notNull(resolver, "ProtocolResolver must not be null");
this.protocolResolvers.add(resolver);
}
2.2.3 FileSystemResourceLoader
- 继承
DefaultResourceLoader
,且覆写了getResourceByPath(String)
方法,使之从文件系统加载资源并以FileSystemResource
类型返回,获得文件系统资源类型。 -
FileSystemContextResource
,是FileSystemResourceLoader
的内部类,继承了FileSystemResource
类,并实现了ContextResource
接口。
@Override
protected Resource getResourceByPath(String path) {
// 截取首 /
if (path.startsWith("/")) {
path = path.substring(1);
}
// 创建 FileSystemContextResource 类型的资源
return new FileSystemContextResource(path);
}
2.2.4 ClassRelativeResourceLoader
-
org.springframework.core.io.ClassRelativeResourceLoader 也是
DefaultResourceLoader
的子类实现。和FileSystemResourceLoader
类似,也覆写了getResourceByPath(String path)
方法,并返回其对应的ClassRelativeContextResource
资源类型。 -
ClassRelativeContextResource
对象表示上下文相对路径,可以根据给定的 class 所在包或者所在包的子包下加载资源,如下,启动服务器,可以加载资源成功。
@Controller
public class LoginController {
@RequestMapping(value="/index.html")
public String loginPage() throws IOException {
ResourceLoader resourceLoader=new ClassRelativeResourceLoader(this.getClass());
Resource resource=resourceLoader.getResource("test.xml");
System.out.println(resource.getFile().getPath());
return "index";
}
}
2.2.5 ResourcePatternResolver
-
ResourceLoader
的getResource(String location)
方法,每次只能根据 location 返回一个Resource
。 -
org.springframework.core.io.support.ResourcePatternResolver 是
ResourceLoader
的扩展,支持根据指定的资源路径匹配模式每次返回多个Resource
实例。
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
-
ResourcePatternResolver
在ResourceLoader
的基础上增加了getResources(String locationPattern)
方法,以支持根据路径匹配模式返回 多个Resource
实例。 - 新增了一种新的协议前缀 " classpath*: ",该协议前缀由其子类负责实现。
2.2.6 PathMatchingResourcePatternResolver
-
org.springframework.core.io.support.PathMatchingResourcePatternResolver 为
ResourcePatternResolver
最常用的子类,除了支持ResourceLoader
和ResourcePatternResolver
新增的 " classpath*: " 前缀外,还支持 Ant 风格 的路径匹配模式(类似于 " **/*.xml ")。
/**
* 内置的 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
。 - 默认的
AntPathMatcher
对象,用于支持 Ant 类型的路径匹配。
getResource 方法实现
- 如果在实例化
PathMatchingResourcePatternResolver
时,未指定ResourceLoader
参数,在加载资源时,则执行DefaultResourceLoader
过程。
@Override
public Resource getResource(String location) {
return getResourceLoader().getResource(location);
}
public ResourceLoader getResourceLoader() {
return this.resourceLoader;
}
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)};
}
}
}
- 方法的调用流程。
-
步骤 1,非 " classpath*: " 开头,且路径不包含通配符,直接委托给相应的
ResourceLoader
实现。 -
步骤 2,其他情况,调用
findAllClassPathResources()
、或findPathMatchingResources()
方法,返回多个Resource
。
-
步骤 1,非 " classpath*: " 开头,且路径不包含通配符,直接委托给相应的
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;
}
protected Resource convertClassLoaderURL(URL url) {
return new UrlResource(url);
}
// 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);
}
- 方法的调用流程。
-
步骤 1,根据
ClassLoader
加载路径下的所有资源。在加载资源过程时,如果在构造PathMatchingResourcePatternResolver
实例时传入ClassLoader
,则调用该ClassLoader
的getResources()
方法,否则调用ClassLoader#getSystemResources(path)
方法。 -
步骤 2,遍历 URL 集合,调用
convertClassLoaderURL(URL url)
方法,将 URL 转换成UrlResource
对象。 -
步骤 3,若 path 为空字符串时,则调用
addAllClassLoaderJarRoots()
方法,加载路径下得所有 jar 包。
-
步骤 1,根据
-
findAllClassPathResources()
方法利用ClassLoader
加载指定路径下的资源,不论它是在 class 路径下还是在 jar 包中。如果传入的路径为空或者 " / ",则调用addAllClassLoaderJarRoots()
方法,加载所有的 jar 包。
findPathMatchingResources 方法实现
- 当 locationPattern 中包含了通配符,则调用该方法进行资源加载。
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
// 确定根路径、子路径
String rootDirPath = determineRootDir(locationPattern);
String subPattern = locationPattern.substring(rootDirPath.length());
// 获取根据路径下的资源
Resource[] rootDirResources = getResources(rootDirPath);
// 遍历,迭代
Set<Resource> result = new LinkedHashSet<>(16);
for (Resource rootDirResource : rootDirResources) {
rootDirResource = resolveRootDirResource(rootDirResource);
URL rootDirUrl = rootDirResource.getURL();
// bundle 资源类型
if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
if (resolvedUrl != null) {
rootDirUrl = resolvedUrl;
}
rootDirResource = new UrlResource(rootDirUrl);
}
// vfs 资源类型
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
// jar 资源类型
} else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
// 其它资源类型
} else {
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
}
if (logger.isTraceEnabled()) {
logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
}
// 转换成 Resource 数组返回
return result.toArray(new Resource[0]);
}
- 方法的调用流程。
- 步骤 1,确定目录,获取该目录下得所有资源。
- 步骤 2,在获得所有资源后,进行迭代匹配获取想要的资源。
determineRootDir 方法实现
- 主要用于确定根路径。
protected String determineRootDir(String location) {
// 找到冒号的后一位
int prefixEnd = location.indexOf(':') + 1;
// 根目录结束位置
int rootDirEnd = location.length();
// 在从冒号开始到最后的字符串中,循环判断是否包含通配符,如果包含,则截断最后一个由”/”分割的部分。
// 再循环判断剩下的部分,直到剩下的路径中都不包含通配符。
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/)
doFindPathMatchingJarResources 方法实现
- 方法的调用流程。
-
步骤 1,计算当前
Resource
在 jar 文件中的根路径 rootEntryPath。 -
步骤 2,遍历 jar 文件中所有 entry,如果当前 entry 名以 rootEntryPath 开头,并且之后的路径信息和之前从 patternLocation 中截取出的 subPattern 使用
PathMatcher
匹配,若匹配成功,则调用rootDirResource#createRelative
方法创建一个Resource
,将新创建的Resource
添加入结果集中。
-
步骤 1,计算当前
doFindPathMatchingFileResources 方法实现
- 方法的调用流程。
- 步骤 1,获取要查找资源的根路径(根路径全名)。
-
步骤 2,递归获得根路径下的所有资源,使用
PathMatcher
匹配,如果匹配成功,则创建FileSystemResource
,并将其加入到结果集中。- 在递归进入一个目录前首先调用
PathMatcher#matchStart()
方法,用以先简单的判断是否需要递归,以提升性能。
- 在递归进入一个目录前首先调用
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
if (logger.isDebugEnabled()) {
logger.debug("Searching directory [" + dir.getAbsolutePath() +
"] for files matching pattern [" + fullPattern + "]");
}
//从根目录开始罗列文件集合
File[] dirContents = dir.listFiles();
if (dirContents == null) {
//查找到没有了则直接返回
if (logger.isWarnEnabled()) {
logger.warn("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
}
return;
}
//遍历
for (File content : dirContents) {
//获取当前文件路径
String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
//查找到的子文件仍是目录且以根目录为开头
if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
if (!content.canRead()) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
"] because the application is not allowed to read the directory");
}
}
else {
//递归调用查找所有的文件
doRetrieveMatchingFiles(fullPattern, content, result);
}
}
//查看当前文件路径是否满足**/*.class格式,满足则添加
if (getPathMatcher().match(fullPattern, currPath)) {
result.add(content);
}
}
}
-
需要注意的是,由于
ClassLoader#getResources()
方法存在的限制,当传入一个空字符串时,只能从 classpath 的文件目录下查找,而不会从 jar 文件的根目录下查找,因而对 " classpath*: " 前缀的资源来说,会找不到 jar 根路径下的资源。- 如果类似定义 " classpath*:*.xml ",并且只有在 jar 文件的根目录下存在 XML 配置文件,那么这个 pattern 将返回空的
Resource
数组。 - 解决方法是不要在 jar 文件的根目录中放配置文件,可以将这些配置文件放到 jar 文件中的resources、config 等目录下。
- 如果类似定义 " classpath*:*.xml ",并且只有在 jar 文件的根目录下存在 XML 配置文件,那么这个 pattern 将返回空的
2.2.7 ServletContextResourcePatternResolver
-
ServletContextResourcePatternResolver
类继承PathMatchingResourcePatternResolver
类,重写了文件查找逻辑,对ServletContextResource
资源使用ServletContext.getResourcePaths()
方法来查找参数目录下的文件,而不是File#listFiles()
方法。
protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException {
if (rootDirResource instanceof ServletContextResource) {
ServletContextResource scResource = (ServletContextResource)rootDirResource;
ServletContext sc = scResource.getServletContext();
String fullPattern = scResource.getPath() + subPattern;
Set<Resource> result = new LinkedHashSet(8);
this.doRetrieveMatchingServletContextResources(sc, fullPattern, scResource.getPath(), result);
return result;
} else {
return super.doFindPathMatchingFileResources(rootDirResource, subPattern);
}
}
2.3 小结
- Spring 提供
Resource
和ResourceLoader
统一抽象整个资源及其定位,使资源与资源的定位有了一个清晰的界限,提供了合适的默认类,使自定义实现更加方便和清晰。-
AbstractResource
是Resource
的默认抽象实现,对Resource
接口做了统一的实现,子类继承该类后只需要覆盖相应的方法即可,同时对于自定义的Resource
也需要继承该类。 -
DefaultResourceLoader
是ResourceLoader
的默认实现,自定义ResourceLoader
时除了可以继承该类以外还可以通过实现ProtocolResolver
接口来实现自定资源加载协议。 -
DefaultResourceLoader
每次只能返回单一的资源,而ResourcePatternResolver
接口,根据指定的 locationPattern 返回多个资源,其子类PathMatchingResourcePatternResolver
即实现了getResource(String location)
方法,也实现了getResources(String locationPattern)
方法。
-
-
Resource
和ResourceLoader
的核心都包含在 spring-core 项目中。