前言:
在Spring当中的资源和加载是分开定义的,为什么会这样做呢?
- 各干各的活也就是职能划分问题,换句话说怎么加载资源,跟我是个什么资源不搭边
- 接口各自能做一个规范,也就是我提供大体上的框架,谁也不能超纲
那么为什么Spring 需要单独实现处理资源的方式呢?我们看下官方文档怎么描述的
英语不好,大概齐的意思是我想用java.net.url
但是他资源加载太拉跨了,各种想要的加载方式他都不太行,然后这忍了就算了,但是功能有残缺就忍不了了
资源:Resource
我们看一下 Resource
的整体结构,和一些比较重要的方法
public interface Resource extends InputStreamSource {
/**
* 返回一个 boolean 指示此资源是否以物理形式实际存在
*/
boolean exists();
/**
* 资源是否可读
*/
boolean isReadable();
/**
返回一个 boolean ,指示此资源是否表示具有打开流的句柄。
如果为 ,则 true InputStream 无法多次读取,必须仅读取一次,然后关闭以避免资源泄漏。
返回 false 所有常用资源实现,但 除外 InputStreamResource
*/
boolean isOpen();
/**
* 是否为 File
*/
boolean isFile();
/**
* 返回资源的 URL
*/
URL getURL() throws IOException;
/**
* 返回资源的 URL
*/
URI getURI() throws IOException;
/**
* 返回资源的 File句柄
*/
File getFile() throws IOException;
/**
* 返回一个可以读取字节的通道
*/
ReadableByteChannel readableChannel() throws IOException;
long contentLength() throws IOException;
long lastModified() throws IOException;
/**
* 相对路径中获取资源
*/
Resource createRelative(String relativePath) throws IOException;
/**
* 获得资源名称
*/
String getFilename();
/**
*返回此资源的说明,用于在处理资源时输出错误。这通常是资源的完全限定文件名或实际 URL
*/
String getDescription();
}
可以看到继承了 InputStreamSource
接口,而 InputStreamSource
接口只有一个方法:getInputStream
, 方法是 Resource
的核心功能,用于获取资源的内容。
public interface InputStreamSource {
/**
*查找并打开资源,返回 for InputStream 从资源读取。预计每次调用都会返回一个新的 InputStream .调用方负责关闭流。
*/
InputStream getInputStream() throws IOException;
}
为什么会有每次调用都会返回一个新的InputStream的说明呢,因为大部分InputStream 是没有实现的reset();方法的
继承关系
接着看下他的继承关系
然后看下具体实现:
UrlResource
:包装了java.net.URL
并可用于访问通常可通过 URL 访问的任何对象ClassPathResource
:从类路径获取的资源FileSystemResource
:对java.io.File
类型资源的封装,支持nio2 Api的支持ServletContextResource
: Web 应用程序根目录中的相对路径的资源InputStreamResource
:将给定的 InputStream 作为一种资源的 Resource 的实现类ByteArrayResource
:字节数组提供的数据的封装。内部会new 一个ByteArrayInputStream
很明显,AbstractResource
是默认实现,他的内部提供的大量的公共已实现的公共方法,要不然咋能抽象出来它吗?
比较典型的exists()
方法:子类想要实现的就自己动手实现,懒的实现的只要把关键方法实现了就行
@Override
public boolean exists() {
// 看看是不是文件,能不能在文件系统中找到文件
// 基于 File 进行判断
if (isFile()) {
try {
return getFile().exists();
}
catch (IOException ex) {
debug(() -> "Could not retrieve File for existence check of " + getDescription(), ex);
}
}
// // 基于 InputStream 进行判断
try {
getInputStream().close();
return true;
}
catch (Throwable ex) {
debug(() -> "Could not retrieve InputStream for existence check of " + getDescription(), ex);
return false;
}
}
再继续来看下哪些方法提供了默认实现:
@Override
public URI getURI() throws IOException {
URL url = getURL();
try {
return ResourceUtils.toURI(url);
}
catch (URISyntaxException ex) {
throw new IOException("Invalid URI [" + url + "]", ex);
}
}
/**
* 根据 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) {
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;
}
看到这,如果我们想要扩展Resource
,那么最合适的继承AbstractResource
去根据资源的不同覆写方法,而不是直接实现Resource
我们同时看到 getInputStream()
的到处使用,如果是没有实现reset()
方法的InputStream
,大概是使用一次就不能用了,这也就是为什么Spring强调 每次调用getInputStream()
都会返回一个新的InputStream
的原因了
其他子类基本都是根据自身资源类型的不同去实现,感兴趣的朋友可以去仔细看一下
资源加载 ResourceLoader
资源加载器顾名思义用来加载资源的嘛!
public interface ResourceLoader {
/**
* 根据所提供资源的路径 location 返回 Resource 实例,但是它不确保该 Resource 一定存在,需要调用
* Resource#exist() 方法来判断。
* 默认实现中支持、
* url 如 file:C/xx
* classPath位置资源 classpath:xxx
* 相对路径的资源 WEB-INF/xxx
* 如果指定的位置路径没有特定前缀,则会返回适合该特定应用程序上下文 Resource 的类型(这个我们后边会说)
*/
Resource getResource(String location);
/**
* 对于想要获取 ResourceLoader 使用的 ClassLoader 用户来说,可以直接调用该方法来获取。
* 在分析 Resource 时,提到了一个类 ClassPathResource ,
* 这个类是可以根据指定的 ClassLoader 来加载资源的。
*/
ClassLoader getClassLoader();
}
继承关系
DefaultResourceLoader
它是ResourceLoader
的默认实现,能接收 ClassLoader
作为构造函数的参数,或者使用不带参数的构造函数。
- 当无参构造的时候使用的是默认的类加载器
- 有参构造接收一个
ClassLoader
- 除此以外我们还可以使用
setClassLoader();
去设置类加载器
/**
* Create a new DefaultResourceLoader.
* <p>ClassLoader access will happen using the thread context class loader
* at the time of actual resource access (since 5.3). For more control, pass
* a specific ClassLoader to {@link #DefaultResourceLoader(ClassLoader)}.
* @see java.lang.Thread#getContextClassLoader()
*
* 默认的 ClassLoader
*/
public DefaultResourceLoader() {
}
/**
* 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 actual resource access (since 5.3).
*/
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.
* @see ClassPathResource
*/
@Override
@Nullable
public ClassLoader getClassLoader() {
return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
}
接下来看一下它的核心实现
/**
* 核心实现
* @param location
* @return {@link Resource}
*/
@Override
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
// 通过协议解析器解析资源
// ProtocolResolver是一个策略接口,可以用于自定义协议解析,
// 比如spring就有一个 “classpath:”开头的特定协议(但是spring并不是自定义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 类型的资源
URL url = ResourceUtils.toURL(location);
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
//这会真没招没招了,寻思从哪儿来的资源,哪儿解析,要是子类本身就没招就返回ClassPathContextResource
return getResourceByPath(location);
}
}
}
之前我们说:如果指定的位置路径没有特定前缀,则会返回适合该特定应用程序上下文 Resource
的类型 就是由getResourceByPath(location);
去完成的
/**
* 由子类实现实现
*/
protected Resource getResourceByPath(String path) {
return new ClassPathContextResource(path, getClassLoader());
}
我们现在看一下协议解析器,ProtocolResolver
ProtocolResolver:
用户自定义协议资源解决策略,作为 DefaultResourceLoader
的 SPI
,允许用户自定义资源加载协议,而不需要继承ResourceLoader
的子类。
接口定义:
@FunctionalInterface
public interface ProtocolResolver {
@Nullable
Resource resolve(String location, ResourceLoader resourceLoader);
}
比如,我们现在想实现一个资源加载方式,只要是abc:
开头的路径,都去classPath
下加载
public class CustomProtocolResolver implements ProtocolResolver {
@Override
public Resource resolve(String location, ResourceLoader resourceLoader) {
System.out.println("My custom resolver has started executing");
if (location.startsWith("abc:")) {
return resourceLoader.getResource("classpath:" + location.substring("abc:".length()));
}
// 如果无法解析或找不到资源,则返回null
return null;
}
public static void main(String[] args) {
DefaultResourceLoader defaultResourceLoader = new DefaultResourceLoader();
defaultResourceLoader.addProtocolResolver(new CustomProtocolResolver());
Resource resource = defaultResourceLoader.getResource("abc:Spring-config.xml");
System.out.println("Does the resource exist ? " + resource.exists());
}
}
执行结果如下:
My custom resolver has started executing
My custom resolver has started executing
Does the resource exist ? true
可以看到是正常加载到了资源! My custom resolver has started executing 为什么我们打印的这段话执行了两边呢?
// 默认先走我们的自定义的协议解析器
defaultResourceLoader.getResource("abc:Spring-config.xml");
// 然后又在自定义的协议解析器调用了
resourceLoader.getResource("classpath:" + location.substring("abc:".length()))
ResourcePatternResolver
如果说DefaultResourceLoader
是默认实现,那么这个接口就是一个扩展接口,定义了将位置模式(例如,Ant 样式路径模式)解析为对象的策略
public interface ResourcePatternResolver extends ResourceLoader {
// 配置Classpath 下所有资源的特殊前缀
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
// 为什么说是扩展,因为它支持根据指定的资源路径匹配模式每次返回多个 Resource 实例
Resource[] getResources(String locationPattern) throws IOException;
}
继承关系
这次了我们先不看ApplicationContext
,这家伙逮谁认谁爹,要不然咋说富二代呢!
先看下PathMatchingResourcePatternResolver
最常用的一个子类
PathMatchingResourcePatternResolver
它不仅能够支持 ResourceLoader
里头的加载方式,也支持ResourcePatternResolver
中定义的classpath*:
还能支持Ant风格的匹配
比如:
*
用于匹配路径中的任何字符(不包括路径分隔符/
)。**
用于递归匹配路径中的任何字符,包括路径分隔符/
。
/**
* 内置的 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);
}
- 内置了一个资源定位器虽然直接也能实现
ResourceLoader
但毕竟DefaultResourceLoader
都实现的差不多了 - 路径匹配器:我们也可以去实现PathMatcher接口调用setPathMatcher();去自定义实现我们的路径匹配器
PathMatchingResourcePatternResolver
在实例化的时候,可以指定一个ResourceLoader
,如果不指定的话,它会在内部构造一个DefaultResourceLoader
我们知道ResourcePatternResolver
作为ResourceLoader
的扩展接口,那必然是要实现getResource();
方法的
@Override
public Resource getResource(String location) {
return getResourceLoader().getResource(location);
}
public ResourceLoader getResourceLoader() {
return this.resourceLoader;
}
它的实现则来源于内部维护的ResourceLoader
,委托给相应的ResourceLoader
来实现,我们实例化PathMatchingResourcePatternResolver
时如果未指定ResourceLoader
,走的还是DefaultResourceLoader
的getResourceLoader()
那一套
getResources
对于扩展的ResourceLoader
扩展的核心实现
@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)) {
// 去掉前缀 "classpath*:",获取真实的模式路径
String locationPatternWithoutPrefix = locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length());
// 首先搜索模块路径下的资源
Set<Resource> resources = findAllModulePathResources(locationPatternWithoutPrefix);
// 接下来搜索类路径下的资源。路径包含通配符
if (getPathMatcher().isPattern(locationPatternWithoutPrefix)) {
// 类路径资源模式,执行路径匹配搜索,(认真的按照通配符匹配)
Collections.addAll(resources, findPathMatchingResources(locationPattern));
}
else {
// 路径不包含通配符,获取具有给定名称的所有类路径资源,(路径我有了,我一把梭哈了)
Collections.addAll(resources, findAllClassPathResources(locationPatternWithoutPrefix));
}
// 将资源集合转换为数组并返回
return resources.toArray(new Resource[0]);
}
else {
// 通常只在前缀后查找模式,例如 "war:" 协议的情况下查找 "*/" 分隔符之后的模式
int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
locationPattern.indexOf(':') + 1);
// 如果路径包含通配符
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
// 是文件模式,执行路径匹配搜索(认真的按照通配符匹配)
return findPathMatchingResources(locationPattern);
}
else {
// 路径不包含通配符,返回一个单一资源,使用给定名称加载,(又不包含classpath*:,又不包含通配符的,统一按照单个算)
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}
-
有
classpath*
- 支持通配符:认真的按照通配符匹配
- 不支持通配符:全给加载了
-
无
classpath*
- 路径包含通配符:认真的按照通配符匹配
- 不支持通配符:统一按照单个算
然后,我们一个一个方法看:
findAllModulePathResources():
主要功能是在 Java 9+ 以后的模块化项目中查找符合给定资源位置模式的资源。 只要是模块化项目,甭管是不是通配符模式,它都好使
protected Set<Resource> findAllModulePathResources(String locationPattern) throws IOException {
// 创建一个 LinkedHashSet 用于存储结果资源集合
Set<Resource> result = new LinkedHashSet<>(16);
// 如果应用程序正在运行在原生映像(Native Image)环境中,直接返回空结果
/*
Native Image 是一种将 Java 代码提前编译为独立可执行文件(称为原生映像)的技术。
此可执行文件包括应用程序类、其依赖项中的类、运行时库类以及来自 JDK 的静态链接本机代码。
它不在 Java VM 上运行,但包含来自不同运行时系统(称为“Substrate VM”)的必要组件,
如内存管理、线程调度等。Substrate VM 是运行时组件(如去优化器、垃圾收集器、线程调度等)的名称。
与 JVM 相比,生成的程序具有更快的启动时间和更低的运行时内存开销。感兴趣的同学可以了解一下
*/
if (NativeDetector.inNativeImage()) {
return result;
}
// 去掉资源位置模式中的前导斜杠:/*.xml转 *.xml
String resourcePattern = stripLeadingSlash(locationPattern);
// 创建一个谓词,用于检查资源路径是否与资源位置模式匹配
Predicate<String> resourcePatternMatches = (getPathMatcher().isPattern(resourcePattern) ?
path -> getPathMatcher().match(resourcePattern, path) :
resourcePattern::equals);
try {
// 获取启动模块层(ModuleLayer.boot())的配置信息,并对每个非系统模块进行处理
ModuleLayer.boot().configuration().modules().stream()
.filter(isNotSystemModule) // 过滤掉系统模块
.forEach(resolvedModule -> {
// NOTE: ModuleReader 和 ModuleReader.list() 返回的 Stream 必须被关闭。
try (ModuleReader moduleReader = resolvedModule.reference().open();
Stream<String> names = moduleReader.list()) {
// 过滤出与资源位置模式匹配的资源路径,并通过模块读取器查找资源
names.filter(resourcePatternMatches)
.map(name -> findResource(moduleReader, name))
.filter(Objects::nonNull)
.forEach(result::add);
}
catch (IOException ex) {
// 如果读取模块内容失败,记录日志并抛出 UncheckedIOException
if (logger.isDebugEnabled()) {
logger.debug("Failed to read contents of module [%s]".formatted(resolvedModule), ex);
}
throw new UncheckedIOException(ex);
}
});
}
catch (UncheckedIOException ex) {
// 捕获并解包 UncheckedIOException 以符合该方法的异常抛出要求
throw ex.getCause();
}
// 如果日志级别是 TRACE,记录已解析的模块路径资源
if (logger.isTraceEnabled()) {
logger.trace("Resolved module-path location pattern [%s] to resources %s".formatted(resourcePattern, result));
}
return result;
}
这个在非模块化的项目中其实没有意义。而在模块化项目中会让findPathMatchingResources()
,findAllClassPathResources()
变得没有意义,因为资源已经被他加载完毕了
findPathMatchingResources();
这是非模块化项目中使用的通配符匹配加载资源的方法
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();
// 如果是 Equinox OSGi 环境并且 URL 协议以 "bundle" 开头,则使用 Equinox 方法解析资源
if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
if (resolvedUrl != null) {
rootDirUrl = resolvedUrl;
}
rootDirResource = new UrlResource(rootDirUrl);
}
// 根据 URL 协议执行不同的资源匹配方法
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
// 处理 VFS 协议(例如 JBoss VFS)
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
// 处理 Jar URL 或 Jar 资源
result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
}
else {
// 处理普通文件系统路径
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
}
// 如果日志级别是 TRACE,记录已解析的资源
if (logger.isTraceEnabled()) {
logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
}
// 将结果转换为数组并返回
return result.toArray(new Resource[0]);
}
这一段主要是确定下来根目录是什么,然后迭代获取资源
然后我们再来看下获得根目录的方法
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;
}
// 如果没有斜杠,将根目录的末尾位置设为协议部分的末尾
if (rootDirEnd == 0) {
rootDirEnd = prefixEnd;
}
// 返回确定的根目录部分
return location.substring(0, rootDirEnd);
}
假设现在有这么一个路径:classpath*:com/example/**/*.xml
处理完毕我们会得到 classpath*:com/example
不同 URL 协议的资源匹配:
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
// 处理 VFS 协议(例如 JBoss VFS)
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {
// 处理 Jar URL 或 Jar 资源
result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
}
else {
// 处理普通文件系统路径
result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
}
这部分逻辑根据资源的 URL 协议执行不同的资源匹配方法:
- 如果资源的 URL 协议以
ResourceUtils.URL_PROTOCOL_VFS
开头,那么它可能是使用 VFS(Virtual File System)协议表示的资源,例如在 JBoss 环境中。在这种情况下,使用VfsResourceMatchingDelegate
类来查找匹配的资源。 - 如果资源的 URL 协议被识别为 Jar URL(例如
jar:
或zip:
),或者资源被确定为 Jar 资源(使用isJarResource
方法判断),则认为它是 Jar 文件中的资源。在这种情况下,使用doFindPathMatchingJarResources
方法来查找匹配的 Jar 资源。 - 如果上述两种情况都不匹配,那么默认情况下认为资源是普通的文件系统路径,然后使用
doFindPathMatchingFileResources
方法来查找匹配的文件系统资源。
展开来说,代码量比较大,后期我们专门开一篇文章详细介绍
我们在来看下以 "classpath*:"
开头但是不包含通配符的方法匹配(非模块化项目)
findAllClassPathResources()
protected Resource[] findAllClassPathResources(String location) throws IOException {
// 去除路径字符串中可能存在的前导斜杠'/'
String path = stripLeadingSlash(location);
// 调用特定方法查找所有位于给定类路径位置的资源
Set<Resource> result = doFindAllClassPathResources(path);
// 如果启用了日志的跟踪级别,记录已解析的类路径位置和找到的资源
if (logger.isTraceEnabled()) {
logger.trace("已解析类路径位置 [" + path + "],找到的资源为:" + result);
}
// 将资源集合转换为资源数组并返回
return result.toArray(new Resource[0]);
}
主要的逻辑执行就是doFindAllClassPathResources();
doFindAllClassPathResources()
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
// 创建一个存储资源的有序集合(LinkedHashSet)
Set<Resource> result = new LinkedHashSet<>(16);
// 获取当前类的类加载器
ClassLoader cl = getClassLoader();
// 使用类加载器获取所有位于给定路径的资源的URL枚举
Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
// 遍历资源URL枚举
while (resourceUrls.hasMoreElements()) {
// 获取下一个资源的URL
URL url = resourceUrls.nextElement();
// 将URL转换为Resource对象并添加到结果集合中
result.add(convertClassLoaderURL(url));
}
// 如果路径为空,通常表示查找根类路径,这可能不足够
if (!StringUtils.hasLength(path)) {
// 上面的结果可能不完整,可能只包含文件系统引用。
// 我们还需要指向类路径上每个JAR文件的指针添加到result
addAllClassLoaderJarRoots(cl, result);
}
// 返回包含资源的集合
return result;
}
ClassLoader
如果在PathMatchingResourcePatternResolver
实列化的时候没有传入,则是使用的ClassLoader.getSystemResources(path)
// 是不是熟悉的双亲委派机制呢?
public Enumeration<URL> getResources(String name) throws IOException {
Objects.requireNonNull(name);
@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
if (parent != null) {
tmp[0] = parent.getResources(name);
} else {
tmp[0] = BootLoader.findResources(name);
}
tmp[1] = findResources(name);
return new CompoundEnumeration<>(tmp);
}
addAllClassLoaderJarRoots(cl, result);
这个方法呢主要是把jar文件的指针添加资源的集合当中,感兴趣的朋友可以看一下
总结:
- Spring框架为了更好地处理资源及其定位问题,引入了
Resource
和ResourceLoader
的抽象概念 AbstractResource
,是Resource
接口的默认实现。通过继承这个类,我们可以更轻松地创建自定义的资源类型。我们只需要覆盖必要的方法即可DefaultResourceLoader
是ResourceLoader
接口的默认实现。如果需要自定义ResourceLoader
,可以继承这个类。此外,我们还可以通过实现ProtocolResolver
接口来定义自己的资源加载协议。- 为了扩展
ResourceLoader
每次只能返回单个资源,引入了ResourcePatternResolver
接口 PathMatchingResourcePatternResolver
允许根据指定的通配符模式(locationPattern)返回多个资源,并且增加了 Java 9+ 以后的模块化项目的支持