参考文章:
资源描述:Resource
统一的接口
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 getFile() throws IOException;
/**
* byte读取通道
*/
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();
}
类图
抽象类
org.springframework.core.io.AbstractResource,为 Resource 接口的默认抽象实现。它实现了 Resource 接口的大部分的公共实现,自定义资源的时候,最好是继承AbstractResource而不是实现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 的实现类。
资源加载:ResourceLoader
统一接口
public interface ResourceLoader {
/** Pseudo URL prefix for loading from the class path: "classpath:". */
// CLASSPATH URL 前缀。默认为:"classpath:"
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
//根据所提供资源的路径 location 返回 Resource 实例
Resource getResource(String location);
@Nullable
ClassLoader getClassLoader();
}
类图
子类
- DefaultResourceLoader:
org.springframework.core.io.DefaultResourceLoader是 ResourceLoader 的默认实现 - FileSystemResourceLoader:它继承 DefaultResourceLoader ,且覆写了
#getResourceByPath(String)方法,使之从文件系统加载资源并以 FileSystemResource 类型返回。 - ClassRelativeResourceLoader: DefaultResourceLoader 的另一个子类的实现。和 FileSystemResourceLoader 类似,在实现代码的结构上类似,也是覆写
#getResourceByPath(String path)方法,并返回其对应的 ClassRelativeContextResource 的资源类型 - ResourcePatternResolver:ResourceLoader 的扩展,它支持根据指定的资源路径匹配模式每次返回多个 Resource 实例
- PathMatchingResourcePatternResolver:除了支持 ResourceLoader 和 ResourcePatternResolver 新增的
"classpath*:"前缀外,还支持 Ant 风格的路径匹配模式
实现类
DefaultResourceLoader
org.springframework.core.io.DefaultResourceLoader是 ResourceLoader 的默认实现
构造函数
/**
* 使用的 ClassLoader 为默认的 ClassLoader(一般 Thread.currentThread()#getContextClassLoader() )
*/
public DefaultResourceLoader() {
this.classLoader = ClassUtils.getDefaultClassLoader();
}
/**
* 在使用带参数的构造函数时,可以通过 ClassUtils#getDefaultClassLoader()获取。
*/
public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
}
getResource 方法
ResourceLoader 中最核心的方法为
#getResource(String location),它根据提供的 location 返回相应的 Resource 。而 DefaultResourceLoader 对该方法提供了核心实现
@Override
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
// 首先,通过 ProtocolResolver 来加载资源
for (ProtocolResolver protocolResolver : this.protocolResolvers) {
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.
// location.startsWith("/") 其实又回到最初的判断了
return getResourceByPath(location);
}
}
}
ProtocolResolver
用户自定义协议资源解决策略,作为 DefaultResourceLoader 的 SPI:它允许用户自定义资源加载协议,而不需要继承 ResourceLoader 的子类
ProtocolResolver 接口,仅有一个方法 Resource resolve(String location, ResourceLoader resourceLoader)
@Nullable
Resource resolve(String location, ResourceLoader resourceLoader);
FileSystemResourceLoader
它继承 DefaultResourceLoader ,且覆写了
#getResourceByPath(String)方法,使之从文件系统加载资源并以 FileSystemResource 类型返回,这样我们就可以得到想要的资源类型
@Override
protected Resource getResourceByPath(String path) {
//截取第一个/分界线
if (path.startsWith("/")) {
path = path.substring(1);
}
//// 创建 FileSystemContextResource 类型的资源
return new FileSystemContextResource(path);
}
FileSystemContextResource
/**
* 实现 ContextResource 接口,并实现对应的 #getPathWithinContext() 接口方法
*/
private static class FileSystemContextResource extends FileSystemResource implements ContextResource {
public FileSystemContextResource(String path) {
super(path);
}
@Override
public String getPathWithinContext() {
return getPath();
}
}
ClassRelativeResourceLoader
DefaultResourceLoader 的另一个子类的实现。和 FileSystemResourceLoader 类似,在实现代码的结构上类似,也是覆写
#getResourceByPath(String path)方法,并返回其对应的 ClassRelativeContextResource 的资源类型ClassRelativeResourceLoader 扩展的功能是,可以根据给定的
class所在包或者所在包的子包下加载资源。
ResourcePatternResolver
ResourceLoader 的扩展,它支持根据指定的资源路径匹配模式每次返回多个 Resource 实例
public interface ResourcePatternResolver extends ResourceLoader {
/**
* 新的路径前缀
*/
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
/**
* ResourcePatternResolver 在 ResourceLoader 的基础上增加了 #getResources(String locationPattern) 方法,
* 以支持根据路径匹配模式返回多个 Resource 实例
* @param locationPattern
* @return
* @throws IOException
*/
Resource[] getResources(String locationPattern) throws IOException;
}
PathMatchingResourcePatternResolver
除了支持 ResourceLoader 和 ResourcePatternResolver 新增的
"classpath*:"前缀外,还支持 Ant 风格的路径匹配模式
构造函数
/**
* 可以指定一个 ResourceLoader,如果不指定的话,它会在内部构造一个 DefaultResourceLoader
*/
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);
}
getResource 方法
@Override
public Resource getResource(String location) {
return getResourceLoader().getResource(location);
}
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,
// Tomcat 上只有在 “*/ ”分隔符之后才为其 “war:” 协议
// 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)};
}
}
}
findAllClassPathResources
地址不含通配符的时候
/**
* 该方法返回 classes 路径下和所有 jar 包中的所有相匹配的资源。
* "classpath*:" 开头但是不包含通配符
*/
protected Resource[] findAllClassPathResources(String location) throws IOException {
String path = location;
if (path.startsWith("/")) {
path = path.substring(1);
}
//真正执行加载的地方
Set<Resource> result = doFindAllClassPathResources(path);
if (logger.isTraceEnabled()) {
logger.trace("Resolved classpath location [" + location + "] to resources " + result);
}
return result.toArray(new Resource[0]);
}
/**
* Find all class location resources with the given path via the ClassLoader.
* Called by {@link #findAllClassPathResources(String)}.
* @param path the absolute path within the classpath (never a leading slash)
* @return a mutable Set of matching Resource instances
* @since 4.1.1
*/
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
Set<Resource> result = new LinkedHashSet<>(16);
//根据 ClassLoader 加载路径下的所有资源
ClassLoader cl = getClassLoader();
//如果当前父类加载器不为 null ,则通过父类向上迭代获取资源,否则调用 #getBootstrapResources()
Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
while (resourceUrls.hasMoreElements()) {
URL url = resourceUrls.nextElement();
// 将 URL 转换成 UrlResource
result.add(convertClassLoaderURL(url));
}
//加载所有jar包 传入的为/的时候
if ("".equals(path)) {
addAllClassLoaderJarRoots(cl, result);
}
return result;
}
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();
//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
主要是用于确定根路径
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);
}