1. Spring Framework中的资源Resource
1.1 Resource的源码
在org.springframework.core.io包中的Resource接口,作为所有资源的统一抽象,它继承 org.springframework.core.io.InputStreamSource接口,在Resource 定义了一些通用的方法,由子类 AbstractResource 提供统一的默认实现。Resource 的代码如下:
public interface Resource extends InputStreamSource {
/**
* 资源是否存在
*/
boolean exists();
/**
* 资源是否可读
*/
default boolean isReadable() {
return exists();
}
/**
* 指示此资源是否表示具有开放流的句柄。如果为true,则不能多次读取InputStream,
* 必须读取并关闭InputStream以避免资源泄漏。对于典型的资源描述符,将为false。
*/
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;
default ReadableByteChannel readableChannel() throws IOException {
return Channels.newChannel(getInputStream());
}
/**
* 资源内容的长度
*/
long contentLength() throws IOException;
/**
* 资源的最后修改时间
*/
long lastModified() throws IOException;
/**
* 根据相对路径创建资源
*/
Resource createRelative(String relativePath) throws IOException;
/**
* 确定此资源的文件名,即通常路径的最后一部分:例如“myfile.txt”。如果此类型的资源没有文件名,则返回null
*/
@Nullable
String getFilename();
/**
* 资源的描述
*/
String getDescription();
1.2 Resource相关的类图
从上面类图可以看到,Resource 根据资源的不同类型提供不同的具体实现,如下:
- FileSystemResource :对 java.io.File 类型资源的封装,只要是跟 File 打交道的,基本上与 FileSystemResource 也可以打交道;
- ByteArrayResource :对字节数组提供的数据的封装。如果通过 InputStream 形式访问该类型的资源,该实现会根据字节数组的数据构造一个相应的 ByteArrayInputStream;
- UrlResource :对 java.net.URL类型资源的封装。内部委派 URL 进行具体的资源操作;
- ClassPathResource :class path 类型资源的实现。使用给定的 ClassLoader 或者给定的 Class 来加载资源。
- InputStreamResource : 将给定的 InputStream 作为一种资源的 Resource 的实现类。
1.3 AbstractResource的源码
AbstractResource是个抽象类,为 Resource 接口的默认抽象实现。它实现了 Resource 接口的大部分的公共实现,是Resource接口最重要的实现,源码如下:
public abstract class AbstractResource implements Resource {
/**
* 此实现检查文件是否可以打开,若判断过程产生错误或者异常,就关闭对应的流
*/
@Override
public boolean exists() {
// 基于 File 文件系统进行判断
if (isFile()) {
try {
return getFile().exists();
}
catch (IOException ex) {
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Could not retrieve File for existence check of " + getDescription(), ex);
}
}
}
//若判断过程产生错误或者异常,就关闭对应的流
try {
getInputStream().close();
return true;
}
catch (Throwable ex) {
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Could not retrieve InputStream for existence check of " + getDescription(), ex);
}
return false;
}
}
/**
* 同exists()方法一致
*/
@Override
public boolean isReadable() {
return exists();
}
/**
* 直接返回 false,表明没有打开
*/
@Override
public boolean isOpen() {
return false;
}
/**
* 直接返回false,表明不是 File
*/
@Override
public boolean isFile() {
return false;
}
/**
* 获取URL,直接抛出异常
*/
@Override
public URL getURL() throws IOException {
throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");
}
/**
* 基于 getURL() 返回的 URL 构建 URI
*/
@Override
@SuppressWarnings("deprecation")
public URI getURI() throws IOException {
URL url = getURL();
try {
return ResourceUtils.toURI(url);
}
catch (URISyntaxException ex) {
throw new org.springframework.core.NestedIOException("Invalid URI [" + url + "]", ex);
}
}
/**
* 获取File,直接抛出异常
*/
@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[256];
int read;
while ((read = is.read(buf)) != -1) {
size += read;
}
return size;
}
finally {
try {
is.close();
}
catch (IOException ex) {
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.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;
}
protected File getFileForLastModifiedCheck() throws IOException {
return getFile();
}
/**
* 根据相对路径创建资源,直接抛出异常
*/
@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 boolean equals(@Nullable Object other) {
return (this == other || (other instanceof Resource &&
((Resource) other).getDescription().equals(getDescription())));
}
@Override
public int hashCode() {
return getDescription().hashCode();
}
/**
* 返回资源的描述
*/
@Override
public String toString() {
return getDescription();
}
}
通过源码我们可以看到,AbstractResource实现了较大部分的Resource接口的内容,并且将一些非通用的实现交给子类去实现。所以如果我们想要实现自定义的 Resource ,记住不要实现 Resource 接口,而应该继承 AbstractResource 抽象类,然后根据当前的具体资源特性覆盖相应的方法即可。
1.4 AbstractResource的其他子类
从类图上我们可以看到,FileSystemResource、InputStreamResource、ByteArrayResource、ClassPathResource、UrlResource这些都是AbstractResource的子类。根据名称,我们基本就能猜出各个子类所代表的资源类型,相关的源码这里不再一一进行分析了。
2. Spring Framework中的资源加载器ResourceLoader
Spring Framework将资源的定义和资源的加载区分开了,Resource 定义了统一的资源,而资源的加载则由 ResourceLoader 来统一定义。ResourceLoader 主要用于根据给定的资源文件地址,返回对应的 Resource 。
2.1 ResourceLoader的源码
ResourceLoader接口在org.springframework.core.io包下,源码如下:
public interface ResourceLoader {
/** CLASSPATH URL 前缀。默认为:"classpath:"*/
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
/**
* 根据资源路径返回资源对象,它不确保该 Resource 一定存在,需要调用 Resource#exist() 方法来判断。
* 该方法支持以下模式的资源加载:
* URL位置资源,如 "file:C:/test.dat" 。
* ClassPath位置资源,如 "classpath:test.dat 。
* 相对路径资源,如 "WEB-INF/test.dat"
* 返回的Resource 实例,根据实现不同而不同。
* 主要实现是在子类 DefaultResourceLoader 中实现,具体过程我们在分析 DefaultResourceLoader 时做详细说明。
*/
Resource getResource(String location);
/**
*返回 ClassLoader 实例,对于想要获取 ResourceLoader 使用的 ClassLoader 用户来说,
*可以直接调用该方法来获取。在分析 Resource 时,提到了一个类 ClassPathResource ,
*这个类是可以根据指定的 ClassLoader 来加载资源的。
*/
@Nullable
ClassLoader getClassLoader();
}
2.2 ResourceLoader的类图
2.3 DefaultResourceLoader的源码
DefaultResourceLoader是ResourceLoader的默认实现类,它实现了 ResourceLoader接口的大部分的公共实现,是ResourceLoader接口最重要实现类,源码如下:
public class DefaultResourceLoader implements ResourceLoader {
@Nullable
private ClassLoader classLoader;
private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);
private final Map<Class<?>, Map<Resource, ?>> resourceCaches = new ConcurrentHashMap<>(4);
/**
* 无参构造函数
*/
public DefaultResourceLoader() {
}
/**
* 带 ClassLoader 参数的构造函数
*/
public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
}
/**
* 设置ClassLoader
*/
public void setClassLoader(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
}
/**
* 获取ClassLoader
*/
@Override
@Nullable
public ClassLoader getClassLoader() {
return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
}
/**
* 添加自定义的协议解析器
*/
public void addProtocolResolver(ProtocolResolver resolver) {
Assert.notNull(resolver, "ProtocolResolver must not be null");
this.protocolResolvers.add(resolver);
}
/**
* 返回自定义解析器的集合
*/
public Collection<ProtocolResolver> getProtocolResolvers() {
return this.protocolResolvers;
}
@SuppressWarnings("unchecked")
public <T> Map<Resource, T> getResourceCache(Class<T> valueType) {
return (Map<Resource, T>) this.resourceCaches.computeIfAbsent(valueType, key -> new ConcurrentHashMap<>());
}
public void clearResourceCaches() {
this.resourceCaches.clear();
}
/**
* 最核心的方法,获取资源对象。
*/
@Override
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
// 1、通过 自定义的ProtocolResolvers来加载资源
for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
// 2、如果通过自定义的ProtocolResolvers没有获得资源,就尝试 以 / 开头,返回 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 {
//最后.根据是否为文件 URL ,是则返回 FileUrlResource 类型的资源,否则返回 UrlResource 类型的资源
URL url = new URL(location);
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// 最后,如果出现异常,返回 ClassPathContextResource 类型的资源
return getResourceByPath(location);
}
}
}
protected Resource getResourceByPath(String path) {
return new ClassPathContextResource(path, getClassLoader());
}
protected static class ClassPathContextResource extends ClassPathResource implements ContextResource {
public ClassPathContextResource(String path, @Nullable ClassLoader classLoader) {
super(path, classLoader);
}
@Override
public String getPathWithinContext() {
return getPath();
}
@Override
public Resource createRelative(String relativePath) {
String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath);
return new ClassPathContextResource(pathToUse, getClassLoader());
}
}
}
2.4 ProtocolResolver的源码
在2.3 DefaultResourceLoader源码中我们看到,getResource这个方法中,首先使用的是自定义的协议解析器来获取资源对象。所以如果我们要实现自己的资源获取方式,可以不需要继承DefaultResourceLoader,可以通过实现ProtocolResolver接口来获取Resource资源对象。ProtocolResolver接口的源码如下:
@FunctionalInterface
public interface ProtocolResolver {
/**
* 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);
}
这个接口比较简单,就一个resolve方法。如果我们自定义一个ProtocolResolver实现后,直接调用 DefaultResourceLoader#addProtocolResolver(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.5 DefaultResourceLoader的其他子类
从类图上我们可以看到,FileSystemResourceLoader、ClassRelativeResourceLoader这些都是DefaultResourceLoader的子类。根据名称,我们基本就能猜出各个子类所代表的加载器类型。相关的源码这里不再一一进行分析了。
3.小结
Spring Framework整个资源加载过程就已经基本分析完成了。
- Resource 和 ResourceLoader 来统一抽象整个资源及其加载方式。使得资源与资源的定位有了一个更加清晰的界限,并且提供了合适的 Default 类,使得自定义实现更加方便和清晰。
- AbstractResource 为 Resource 的默认抽象实现类,它对 Resource 接口做了一个统一的实现,子类继承该类后只需要覆盖相应的方法即可,同时对于自定义的 Resource 我们也是继承该类。
- DefaultResourceLoader 同样也是 ResourceLoader 的默认实现,在自定 ResourceLoader 的时候我们除了可以继承该类外还可以实现 ProtocolResolver 接口来实现自定资源加载协议。