二、spring统一资源加载策略

spring统一资源加载策略
org.springframework.core.io.Resource 为 Spring 框架所有资源的抽象和访问接口,它继承 org.springframework.core.io.InputStreamSource接口。作为所有资源的统一抽象,Source 定义了一些通用的方法,由子类 AbstractResource 提供统一的默认实现。

统一资源:Resource

org.springframework.core.io.Resource为Spring所有资源的抽象和访问接口,继承自org.springframework.core.io.InputStreamSourceResource定义的统一方法由其子类AbstractResource提供默认实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public interface Resource extends InputStreamSource {
//资源是否存在
boolean exists();
//资源是否可读
default boolean isReadable() {
return true;
}
//资源所代表的句柄是否被一个stream打开了
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();

}

Resource

  1. FileSystemResource:对 java.io.File 类型资源的封装,只要是跟 File 打交道的,基本上与 FileSystemResource 也可以打交道。支持文件和 URL 的形式,实现 WritableResource 接口。
  2. ByteArrayResource:对字节数组提供的数据的封装。如果通过 InputStream 形式访问该类型的资源,该实现会根据字节数组的数据构造一个相应的 ByteArrayInputStream。
  3. UrlResource:对 java.net.URL类型资源的封装。内部委派 URL 进行具体的资源操作。
  4. ClassPathResource:class path 类型资源的实现。使用给定的 ClassLoader 或者给定的 Class 来加载资源。
  5. InputStreamResource:将给定的 InputStream 作为一种资源的 Resource 的实现类。

AbstractResource 为 Resource 接口的默认实现,它实现了 Resource 接口的大部分的公共实现。

如果想要实现自定义的 Resource,记住不要实现 Resource 接口,而应该继承 AbstractResource 抽象类,然后根据当前的具体资源特性覆盖相应的方法即可。

统一资源定位:ResourceLoader

定义

org.springframework.core.io.ResourceLoader 为 Spring 资源加载的统一抽象,具体的资源加载则由相应的实现类来完成,所以我们可以将 ResourceLoader 称作为统一资源定位器。

1
2
3
4
5
6
public interface ResourceLoader {
//只定义了两个方法
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;//ResourceUtils.CLASSPATH_URL_PREFIX="classpath:"
Resource getResource(String var1);
ClassLoader getClassLoader();
}

getResource根据所提供资源的路径 location 返回 Resource 实例,但是它不确保该 Resource 一定存在,需要调用 Resource.exist()方法判断。该方法的主要实现是在其子类 DefaultResourceLoader 中实现,支持以下模式的资源加载:

  1. URL位置资源,如”file:C:/test.dat”。
  2. ClassPath位置资源,如”classpath:test.dat”。
  3. 相对路径资源,如”WEB-INF/test.dat”,此时返回的Resource实例根据实现不同而不同。

getClassLoader 返回 ClassLoader 实例,对于想要获取 ResourceLoader 使用的 ClassLoader 用户来说,可以直接调用该方法来获取。

ResourceLoader

DefaultResourceLoader

org.springframework.core.io.DefaultResourceLoader 是 ResourceLoader 的默认实现。

构造函数
1
2
3
4
5
6
7
8
9
/*
* 在访问资源时,使用当前线程的,一般 Thread.currentThread().getContextClassLoader()
*/
public DefaultResourceLoader() {
}

public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
}

当然,后续也可以使用setClassLoader进行设置。

1
2
3
public void setClassLoader(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
}
getResource()方法

ResourceLoader 中最核心的方法为public Resource getResource(String location)DefaultResourceLoader提供了具体实现,他的两个子类并未覆盖此方法,所以ResourceLoader的资源加载策略就封装在 DefaultResourceLoader 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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());
}
//根据是否为文件 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) {
// 返回 ClassPathContextResource 类型的资源
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
}
}
  • 首先,通过 ProtocolResolver 来加载资源,成功返回 Resource

  • 否则,若 location"/" 开头,则调用 #getResourceByPath() 方法,构造 ClassPathContextResource 类型资源并返回。代码如下:

    1
    2
    3
    protected Resource getResourceByPath(String path) {
    return new ClassPathContextResource(path, getClassLoader());
    }
  • 再否则,若 location"classpath:" 开头,则构造 ClassPathResource 类型资源并返回。在构造该资源时,通过 #getClassLoader() 获取当前的 ClassLoader。

  • 再否则,构造 URL ,尝试通过它进行资源定位,若没有抛出 MalformedURLException 异常,则判断是否为 FileURL , 如果是则构造 FileUrlResource 类型的资源,否则构造 UrlResource 类型的资源。

  • 最后,若在加载过程中抛出 MalformedURLException 异常,则委派 #getResourceByPath() 方法,实现资源定位加载。该过程和第二步 location"/" 开头相同。

其子类可以通过重写getResourceByPath(String path)来返回和自身相关的资源类型

ProtocolResolver

org.springframework.core.io.ProtocolResolver(是个函数式接口),用户自定义协议资源解决策略,如上介绍Resource所说:如果想要实现自定义的 Resource,应该继承 AbstractResource 抽象类,但是有了 ProtocolResolver 后,不需要直接继承 DefaultResourceLoader,改为实现 ProtocolResolver 接口也可以实现自定义的 ResourceLoader。

1
2
3
4
5
6
7
@FunctionalInterface
public interface ProtocolResolver {
//需要用户自定义实现
@Nullable
Resource resolve(String location, ResourceLoader resourceLoader);

}

自定义的resolve添加到spring中:

1
2
3
4
5
//在此资源加载器中注册给定的解析器,从而允许处理其他协议,它也可以覆盖任何默认规则。
public void addProtocolResolver(ProtocolResolver resolver) {
Assert.notNull(resolver, "ProtocolResolver must not be null");
this.protocolResolvers.add(resolver);
}
FileSystemResourceLoader

org.springframework.core.io.FileSystemResourceLoader重写了getResourceByPath,使之从文件系统加载资源并以 FileSystemResource 类型返回:

1
2
3
4
5
6
7
protected Resource getResourceByPath(String path) {
if (path.startsWith("/")) {
path = path.substring(1);
}
//创建 FileSystemContextResource 类型的资源,FileSystemContextResource为内部类。
return new FileSystemContextResource(path);
}
FileSystemContextResource

为 FileSystemResourceLoader 的内部类,它继承 FileSystemResource 类,实现 ContextResource 接口。

1
2
3
4
5
6
7
8
9
10
private static class FileSystemContextResource extends FileSystemResource implements ContextResource {
public FileSystemContextResource(String path) {
super(path);
}

@Override
public String getPathWithinContext() {
return getPath();
}
}
ClassRelativeResourceLoader

org.springframework.core.io.ClassRelativeResourceLoader是DefaultResourceLoader的另一个子类,和FileSystemResourceLoader一样,都是重新写了getResourceByPath()方法,然后有个内部类ClassRelativeContextResource

ClassRelativeResourceLoader扩展的功能是可以根据给定的class所在包或者所在包的子包下加载资源。

ResourcePatternResolver

ResourceLoader 的 Resource getResource(String location) 方法,每次只能根据 location 返回一个 Resource 。当需要加载多个资源时,我们除了多次调用 getResource(String location) 方法外,别无他法。org.springframework.core.io.support.ResourcePatternResolver 是 ResourceLoader 的扩展,它支持根据指定的资源路径匹配模式每次返回多个 Resource 实例。

1
2
3
4
5
public interface ResourcePatternResolver extends ResourceLoader {
//注:默认是"classpath:";
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}

ResourcePatternResolverResourceLoader的基础上增加了getResources方法:

1
2
//注:父类是getResource
Resource[] getResources(String locationPattern) throws IOException;
PathMatchingResourcePatternResolver

ResourcePatternResolver的子类,在父类的基础上额外还支持 Ant 风格的路径匹配模式(类似于 "/*.xml")。

构造函数

提供了三个构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//内置的 ResourceLoader 资源定位器
private final ResourceLoader resourceLoader;

//Ant 路径匹配器,默认为 AntPathMatcher 对象,用于支持 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);
}
getResource
1
2
3
4
5
6
7
public Resource getResource(String location) {
return getResourceLoader().getResource(location);
}

public ResourceLoader getResourceLoader() {
return this.resourceLoader;
}

该方法,直接委托给相应的 ResourceLoader 来实现。如果我们在实例化的 PathMatchingResourcePatternResolver 的时候,如果未指定 ResourceLoader 参数的情况下,那么在加载资源时,其实就是 DefaultResourceLoader 的过程。

getResources
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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()));
}
}
else {
// Generally only look for a pattern after a prefix here,
// and on Tomcat only after the "*/" separator for its "war:" protocol.
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 。
findAllClassPathResources

locationPattern"classpath*:" 开头但是不包含通配符,则调用 findAllClassPathResources(...) 方法加载资源。该方法返回 classes 路径下和所有 jar 包中的所有相匹配的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
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(...) 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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));
while (resourceUrls.hasMoreElements()) {
//<2> 将 URL 转换成 UrlResource
URL url = resourceUrls.nextElement();
result.add(convertClassLoaderURL(url));
}
// <3> 加载路径下得所有 jar 包
if (!StringUtils.hasLength(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() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

如果当前父类加载器不为 null ,则通过父类向上迭代获取资源,否则调用BootLoader.findResources(name);

<2> 处,遍历 URL 集合,调用 convertClassLoaderURL(URL url) 方法,将 URL 转换成 UrlResource 对象。

1
2
3
protected Resource convertClassLoaderURL(URL url) {
return new UrlResource(url);
}

<3> 处,若 path 为空(“”)时,则调用 addAllClassLoaderJarRoots(...)方法。该方法主要是加载路径下得所有 jar 包。

由上可以看出,#findAllClassPathResources(...) 方法,其实就是利用 ClassLoader 来加载指定路径下的资源,不论它是在 class 路径下还是在 jar 包中。如果我们传入的路径为空或者 /,则会调用 addAllClassLoaderJarRoots(...) 方法,加载所有的 jar 包。

findPathMatchingResources

locationPattern 中包含了通配符,则调用该方法进行资源加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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(String location) 方法,一个是 doFindPathMatchingXXXResources(...) 等方法。

determineRootDir(String location) 方法,主要是用于确定根路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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/
总结

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) 方法。

二、spring统一资源加载策略
http://www.muzili.ren/2022/06/11/统一资源加载策略/
作者
jievhaha
发布于
2022年6月11日
许可协议