Resource
在Spring中,对资源进行了抽象,从而屏蔽了资源类型和来源的区别,使得内部对于操作这些资源的API更加统一。下图为Spring中Resource
的继承体系图:
根据继承关系图可以看到,整个Resource
体系中,进行了一定层级的抽象,通过顶层的借口定义资源常见的操作,然后给出一个通用的抽象实现,最后各个不同类型的资源各自实现各自特殊的处理方法。
最顶层的Resource
接口继承自InputStreamSource
接口,而InputStreamSource
接口中仅定义了一个方法:getInputStream
。凡是可以获取输入流的资源都可以是一个InputStreamSource
类型。而对于大多数资源文件来说,不一定可写但一般是可读的。因此这里专门为可写的资源类型定义了WritableResource
接口,此接口中定义了两个和写操作相关的方法:
- isWritable
- getOutputStream
可写的资源一般也包含可读资源的各种特性,因此WritableResource
接口继承自Resource
接口。在Resource
接口中定义了一些通用的方法:
public interface Resource extends InputStreamSource {
// 判断资源是否存在
boolean exists();
// 判断资源是否可读,只有在返回true的时候,getInputStream方法才可用
boolean isReadable();
// 判断资源是否已打开,如果已打开则资源不能多次读写,资源应该在读完成之后关闭。
boolean isOpen();
// 获取资源对象的URL,如果该资源不能表示为URL形式则抛出异常
URL getURL() throws IOException;
// 获取资源对象的URI,如果该资源不能表示为URI形式则抛出异常
URI getURI() throws IOException;
// 获取资源的File表示对象,如果资源不能表示为File对象则抛出异常
File getFile() throws IOException;
// 获取资源内容的长度,如果资源无法解析则抛出异常
long contentLength() throws IOException;
// 获取资源最后修改时间戳,如果资源无法解析则抛出异常
long lastModified() throws IOException;
// 相对当前资源创建新的资源对象,如果相对的资源无法解析则抛出异常
Resource createRelative(String relativePath) throws IOException;
// 获取当前资源的文件名,如果当前资源没有文件名则返回null
String getFilename();
// 获取当对资源的描述信息
String getDescription();
}
在Resource
接口中定义的方法,并不需要每一种实际资源类型都必须实现,各个实际资源类型根据自身的情况决定要实现哪些方法。例如基于文件的资源一般会实现getFile
方法,而不是基于文件的资源则一般不实现getFile
方法。
在具体资源实现之前,Spring还提供了一个抽象的公共实现AbstractResource
,在AbstractResource
中实现一些跟资源类型无关的通用逻辑,和底层资源有关的则留给具体的资源类型去实现。
public abstract class AbstractResource implements Resource {
public boolean exists() {
// Try file existence: can we find the file in the file system?
try {
return getFile().exists();
} catch (IOException ex) {
// Fall back to stream existence: can we open the stream?
try {
InputStream is = getInputStream();
is.close();
return true;
} catch (Throwable isEx) {
return false;
}
}
}
public boolean isReadable() {
return true;
}
public boolean isOpen() {
return false;
}
public URL getURL() throws IOException {
// 默认认为资源无法表示为URL,子类可覆写此方法
throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");
}
public URI getURI() throws IOException {
URL url = getURL();
try {
// 通过getURL方法的返回值来进行转换
return ResourceUtils.toURI(url);
} catch (URISyntaxException ex) {
throw new NestedIOException("Invalid URI [" + url + "]", ex);
}
}
public File getFile() throws IOException {
// 默认认为资源无法表示为File对象,子类可覆写
throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
}
public long contentLength() throws IOException {
InputStream is = this.getInputStream();
Assert.state(is != null, "resource input stream must not be null");
try {
// 默认实现为读取inputStream中的所有数据来获取长度
long size = 0;
byte[] buf = new byte[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;
}
protected File getFileForLastModifiedCheck() throws IOException {
return getFile();
}
public Resource createRelative(String relativePath) throws IOException {
// 默认不支持创建相对路径资源
throw new FileNotFoundException("Cannot create a relative resource for " + getDescription());
}
public String getFilename() {
return null; // 默认返回null,即认为资源五文件名
}
@Override
public String toString() {
return getDescription();
}
@Override
public boolean equals(Object obj) {
return (obj == this ||
(obj instanceof Resource && ((Resource) obj).getDescription().equals(getDescription())));
}
@Override
public int hashCode() {
return getDescription().hashCode();
}
}
在AbstractResource
的实现中,默认认为资源不能够表示为URL
和File
的形式,这样的资源如ByteArrayResource
、InputStreamResource
等都可以适应,因为这些资源类型底层并不是基于文件而是包装了字节数组或输入流而成,因此正对这种类型的操作,一般只支持读取操作。具体资源类型可以通过覆写isOpen
方法,通过返回true
和false
来标识当前资源是否支持多次读取操作。
从上面的继承关系图中可以看到,FileSystemResource
不但继承了AbstractResource
还实现了WritableResource
接口,也就是基于文件系统的资源类型,一般可以支持读写操作,当然一般也会支持相对路径资源。如果有兴趣可以阅读FileSystemResource
类的源代码,可以发现大部分操作都是基于内部的File
对象来完成,如isReadable
方法和getInputStream
方法:
public boolean isReadable() {
return (this.file.canRead() && !this.file.isDirectory());
}
public InputStream getInputStream() throws IOException {
return new FileInputStream(this.file);
}
另外像是isWritable
、getOutputStream
和getURL
等方法均是通过内部的File
对象来完成。
从前面的继承关系图还可以看到,除了上面介绍的各种类型的资源实现,还有一个AbstractFileResolvingResouce
类型的资源,并且他还有两个子类:UrlResource
和ClassPathResouce
,其中ClassPathResource
类型是我们在Spring中非常常用的资源类型。AbstractFileResolvingResource
表示需要通过解析URL
来获取的资源,例如其exists
方法的实现:
public boolean exists() {
try {
URL url = getURL();
if (ResourceUtils.isFileURL(url)) {
// Proceed with file system resolution...
return getFile().exists(); // 如果是文件,则直接检测文件是否存在
} else {
// Try a URL connection content-length header...
URLConnection con = url.openConnection();
ResourceUtils.useCachesIfNecessary(con);
HttpURLConnection httpCon = (con instanceof HttpURLConnection ? (HttpURLConnection) con : null);
if (httpCon != null) {
// 如果是http url则检测url对应的资源是否存在
httpCon.setRequestMethod("HEAD");
int code = httpCon.getResponseCode();
if (code == HttpURLConnection.HTTP_OK) {
return true;
} else if (code == HttpURLConnection.HTTP_NOT_FOUND) {
return false;
}
}
if (con.getContentLength() >= 0) {
return true;
}
if (httpCon != null) {
// no HTTP OK status, and no content-length header: give up
httpCon.disconnect();
return false;
} else {
// Fall back to stream existence: can we open the stream?
InputStream is = getInputStream();
is.close();
return true;
}
}
} catch (IOException ex) {
return false;
}
}
从代码中可以看到,判断资源是否存在,均是通过判断URL
的类型来执行不同的检测方式,如果通过URL
无法判断资源是否存在,最终通过getInputStream
方法来判断,如果能够返回输入流,那么资源必然存在。
对于UrlResource
来说,基本上就是通过解析URL
来完成相关的操作,只要符合URL
规范的格式都可以表示为UrlResource
对象。这里我们主要来看下ClassPathResource
的实现吧。根据我们日常的使用经验和其名字透露的信息,我们知道ClassPathResource
表示的是类路径下面的资源。既然是类路径下面的资源,那么根据我们对java技术的了解,一般要获取类路径下的资源无非是通过Class
或ClassLoader
这两个类操作。带着这些疑问,我们看一下ClassPathResource
的源码吧:
public class ClassPathResource extends AbstractFileResolvingResource {
private final String path;
private ClassLoader classLoader;
private Class<?> clazz;
public ClassPathResource(String path) {
this(path, (ClassLoader) null);
}
public ClassPathResource(String path, ClassLoader classLoader) {
Assert.notNull(path, "Path must not be null");
String pathToUse = StringUtils.cleanPath(path);
if (pathToUse.startsWith("/")) { // ClassLoader方式不需要'/'
pathToUse = pathToUse.substring(1);
}
this.path = pathToUse;
// 如果ClassLoader为null则使用默认的ClassLoader
this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
}
public ClassPathResource(String path, Class<?> clazz) {
Assert.notNull(path, "Path must not be null");
this.path = StringUtils.cleanPath(path);
this.clazz = clazz; // 使用Class来加载资源,也可以使用ClassLoader加载
}
protected ClassPathResource(String path, ClassLoader classLoader, Class<?> clazz) {
this.path = StringUtils.cleanPath(path);
// 同时使用Clss和ClassLoader
this.classLoader = classLoader;
this.clazz = clazz;
}
public final String getPath() {
return this.path;
}
public final ClassLoader getClassLoader() {
return (this.classLoader != null ? this.classLoader : this.clazz.getClassLoader());
}
@Override
public boolean exists() {
URL url;
// 资源是否存在通过Class和ClassLoader来判断
if (this.clazz != null) {
url = this.clazz.getResource(this.path);
} else {
url = this.classLoader.getResource(this.path);
}
return (url != null);
}
public InputStream getInputStream() throws IOException {
InputStream is;
// InputStream的获取也是通过Class和ClassLoader来判断
if (this.clazz != null) {
is = this.clazz.getResourceAsStream(this.path);
} else {
is = this.classLoader.getResourceAsStream(this.path);
}
if (is == null) {
throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
}
return is;
}
@Override
public URL getURL() throws IOException {
URL url;
if (this.clazz != null) {
url = this.clazz.getResource(this.path);
} else {
url = this.classLoader.getResource(this.path);
}
if (url == null) {
throw new FileNotFoundException(getDescription() + " cannot be resolved to URL because it does not exist");
}
return url;
}
@Override
public Resource createRelative(String relativePath) {
String pathToUse = StringUtils.applyRelativePath(this.path, relativePath);
return new ClassPathResource(pathToUse, this.classLoader, this.clazz);
}
@Override
public String getFilename() {
return StringUtils.getFilename(this.path);
}
public String getDescription() {
StringBuilder builder = new StringBuilder("class path resource [");
String pathToUse = path;
if (this.clazz != null && !pathToUse.startsWith("/")) {
builder.append(ClassUtils.classPackageAsResourcePath(this.clazz));
builder.append('/');
}
if (pathToUse.startsWith("/")) {
pathToUse = pathToUse.substring(1);
}
builder.append(pathToUse);
builder.append(']');
return builder.toString();
}
}
从代码中可以看到,和我们前面分析的一样,ClassPathResource
确实是通过java提供的Class
和ClassLoader
机制来完成对底层资源的处理的。
总结:Spring中对资源进行抽象,从而统一对资源操作的API,屏蔽不同资源之间的差异。使得其他组件可以不关心具体资源类型的实现,使用统一的API进行操作,并且通过不同的接口来分别定义资源的不同行为,然后通过抽象类的形式给出一个通用的实现,底层具体的实现只需要继承这个抽象类,并覆写跟当前资源类型需要特殊处理的方法即可。另外,在定义接口时,通过给出一对方法(如:isReadable
和getInputStream
)来分离条件检测和执行逻辑,这种设计方式在我们日常的开发过程中也是可以应用的。例如发短信操作,就可以将检测是否满足发短信条件和执行短信发送这两个逻辑分开定义(permit和semd),这样可以使得逻辑更加清晰,同时可以提高代码复用率(比如部分业务只需要检测是否满足发短信条件而不一定需要发短信)。
ResourceLoader
前面学习了Spring中的Resource
体系,只是对资源类型进行了抽象,统一了接口,但是如果需要使用不同类型的Resource的时候,还是得通过new
具体资源类型的方式来获取。Spring中为了简化对Resource
的查找和加载,提供了ResourceLoader
来专门负责加载Resource
,使用这不需要关心如何加载具体的资源,只需要给出资源的定义(schame),剩下的就交由ResourceLoader
来处理了。
先来看下ResourceLoader
的继承结构:
Spring通过ResourceLoader
接口来定义其基本的行为:
public interface ResourceLoader {
// ClassPathResource对应的Url的协议头(前缀)
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
// 根据给出的资源Url获取对应的Resource对象
Resource getResource(String location);
// 获取当前ResourceLoader所使用的ClassLoader
ClassLoader getClassLoader();
}
从接口的定义来看,ResourceLoader
最主要的行为就是getResource
操作。getResource
方法接收一个字符串类型参数,通过对字符串参数的解析和处理返回对应的Resource
对象,这里的location
参数可以包含一定的协议头或前缀来表明参数的来源信息,这些可以使用的协议头和前缀定义在ResourceUtils
类中:
/** Pseudo URL prefix for loading from the class path: "classpath:" */
public static final String CLASSPATH_URL_PREFIX = "classpath:";
/** URL prefix for loading from the file system: "file:" */
public static final String FILE_URL_PREFIX = "file:";
/** URL protocol for a file in the file system: "file" */
public static final String URL_PROTOCOL_FILE = "file";
/** URL protocol for an entry from a jar file: "jar" */
public static final String URL_PROTOCOL_JAR = "jar";
/** URL protocol for an entry from a zip file: "zip" */
public static final String URL_PROTOCOL_ZIP = "zip";
/** URL protocol for an entry from a JBoss jar file: "vfszip" */
public static final String URL_PROTOCOL_VFSZIP = "vfszip";
/** URL protocol for a JBoss VFS resource: "vfs" */
public static final String URL_PROTOCOL_VFS = "vfs";
/** URL protocol for an entry from a WebSphere jar file: "wsjar" */
public static final String URL_PROTOCOL_WSJAR = "wsjar";
/** URL protocol for an entry from an OC4J jar file: "code-source" */
public static final String URL_PROTOCOL_CODE_SOURCE = "code-source";
/** Separator between JAR URL and file path within the JAR */
public static final String JAR_URL_SEPARATOR = "!/";
例如对一个类路径下的资源spring.xml
的表示为:classpath:spring.xml
。可以使用前缀,也可以使用标准URL中的协议头(如:http://)的形式。
Spring提供了一个默认的实现DefaultResourceLoader
,它可以使用ClassLoader
作为参数进行创建。其getResource
方法实现如下:
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
if (location.startsWith(CLASSPATH_URL_PREFIX)) { // classpath:前缀
// ClassPathResource需要使用Class或ClassLoader,这里传入ClassLoader
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
} else {
try {
// Try to parse the location as a URL...
URL url = new URL(location);
return new UrlResource(url);
} catch (MalformedURLException ex) {
// No URL -> resolve as resource path.
return getResourceByPath(location); // 如果不符合URL规范,则当做普通路径(如:test/resource.xml)处理
}
}
}
从代码中可以看到,对于传入的location
参数,显示判断了是否包含classpath:
前缀,如果包含则返回ClassPathResource
对象,如果不是则通过解析URL的方式解析location
参数,如果参数符合URL规范,则创建一个UrlResource
,如果资源既不包含classpath:
特殊前缀,也不是URL形式,那么就将其当做普通的资源路径传递给getResourceByPath
进行处理:
protected Resource getResourceByPath(String path) {
return new ClassPathContextResource(path, getClassLoader());
}
private static class ClassPathContextResource extends ClassPathResource implements ContextResource {
public ClassPathContextResource(String path, ClassLoader classLoader) {
super(path, classLoader);
}
public String getPathWithinContext() {
return getPath();
}
@Override
public Resource createRelative(String relativePath) {
String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath);
return new ClassPathContextResource(pathToUse, getClassLoader());
}
}
getResourceByPath
返回ClassPathContextResource
类型,实际上也可以也就是ClassPathResource
。可以看出,ResourceLoader
中并不判断资源是否真实存在和是否可读写,而仅仅通过解析出传入的location
参数返回不同的Resource
实现而已,资源是否存在,是否可读,需要调用方在拿到Resource
对象后通过Resource
提供的方法自行判断。
DefaultResourceLoader
的子类大多都只是覆写了getResourceByPath
方法,用于在给定的参数既不是classpath:
资源也不是UrlResource
时需要返回的默认Resource类型。如FileSystemResourceLoader
中的getResourceByPath
返回FileSystemContextResource
对象(继承自FileSystemResource
);ClassPathRelativeResourceLoader
的getResourceByPath
方法返回ClassPathRelativeContextResource
(继承自ClassPathResource
)。
注意:虽然ClassPathContextResource
和ClassPathRelaticeContextResource
都是继承自ClassPathResource
,但是前者使用的ClassLoader
来加载资源,后者使用的是Class
来加载资源,二者还是有区别的:
- ClassLoader来加载资源,路径以类路径的根目录为基准(如WEB-INF/classes为基准)。
- Class来架子资源,资源以Class缩在的包路径为基准(如Web-INF/classes/com/test/resource/)
切记不要搞混淆了。
ResourcePatternResolver
Spring除了提供ResourceLoader
之外,还提供了ResourcePatternResolver
来扩充ResourceLoader
。引进了一个新的前缀:classpath*:
。和classpath:
的差别就是,classpath*:
可以搜索class path下所有满足条件的资源(包括同名的资源),而classpath:
则只能返回一个资源(即使存在多个)。
同时,Spring也提供了默认的实现:PathMatchingResourcePatternResource
。在它的内部包含一个DefaultResourceLoader
的实例,所有ResourceLoader
中定义的方法它都委托给内部的DefaultResourceLoader
实例进行处理,而自己只负责处理getResources
方法的实现。
getResources(locationPattern)
-> findPathMatchingResources(locationPattern)
-> doFindPathMatchingFileResources(rootDirResource, subPattern)
-> doFindMatchingFileSystemResources(rootDir, subPattern)
-> retrieveMatchingFiles(rootDir, subPattern)
-> doRetrieveMatchingFiles(fullPattern, rootDir, result)
// classpath*:非模式匹配路径
-> findAllClassPathResources(location)
// 非classpath*:非模式匹配路劲
-> getResourceLoader().getResource(locationPattern)
上面列出了getResources
方法的大致的执行流程,其中只列举了从文件系统匹配资源的过程。
大概的执行流程如下:
- 判断
locationPattern
是否包含匹配模式:
- 如果包含则调用
findPathMatchingResources
方法进行查找。 - 否则判断
locationPattern
是否包含classpath*:
前缀:
- 如果包含
classpath*:
前缀,则调用findAllClassPathResources
方法获取所有类路径下对应的资源 - 否则,调用
getResourceLoader().getResource()
进行查找
- 如果包含
- 如果包含则调用
- 进入
findPathMatchingResources(locationPattern)
方法后:
- 首先确定
rootDirPath
,即locationPattern
中除开前缀的部分中,截至第一个模式匹配字符为止的目录路径。如:classpath*:spring/test/spring-*.xml
,则对应的rootDirPath
为:spring/test
。 - 递归调用
getResources
方法获取rootDirPath
下的所有资源。此时会进入到findAllClassPathResources
方法中,获取rootDirPath
下的所有有效的资源 - 遍历上一步返回的资源列表,然后调用
doFindPathMatchingFileResources(rootDirResource, subPattern)
方法对当前资源和剩余的模式路劲进行匹配,并将返回结果加入到最终的结果集中
- 首先确定
- 在
doFindPathMatchingFileResources(rootDirResource, subPattern)
方法中获取到rootDirResource
对应的文件系统目录File
对象,然后调用doFindMatchingFileSystemResources(rootDir, subPattern)
方法进行匹配并返回匹配结果。 - 在
doFindMatchingFileSystemResources(rootDir, subPattern)
中调用retrieveMatchingFiles(rootDir, subPattern)
方法获取到所有匹配成功的File
集合,然后遍历生成FileSystemResource
对象并加入到返回结果集中。 - 在
retrieveMatchingFiles(rootDir, subPattern)
方法中,检测rootDir
表示的目录是否有效,无效则忽略。有效则拿到rootDir
对应的absolutePath
进行处理之后加上subPattern
组合成完整的模式匹配路径(如:/home/luojing/webapp/spring/test/spring-*.xml),即最终的fullPattern
是包含模式匹配符号的绝对路径。最后调用doRetrieveMatchingFiles(fullPattern, rootDir, result)
进行处理。 - 在
doRetrieveMatchingFiles(fullPattern, rootDir, result)
中获取到rootDir
目录下的所有内容(资源),并进行遍历,如果是目录则递归调用doRetrieveMatchingFiles(fullPattern, content, result)
方法,如果是普通文件,则调用getPathMatcher().match(fullPattern, currPath)
对当前文件的绝对路径(会进行一些格式化处理)和前面生成的fullPattern
进行匹配,如果匹配成功则当前文件添加到匹配成功的列表中。
protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
File[] dirContents = dir.listFiles();
if (dirContents == null) {
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);
}
}
if (getPathMatcher().match(fullPattern, currPath)) { // 真正进行匹配
result.add(content);
}
}
}
PathMatcher
这里又引入了一个新的组件:PathMatcher
。PathMatcher
主要用于对基于字符串的路径和指定的模式符号进行匹配。Spring中提供了一个默认的实现:AntPathMatcher`,主要使用 **Ant-style path patterns进行路径匹配:
The mapping matches URLs using the following rules:
?
matches one character*
matches zero or more characters**
matches zero or more directories in a pathExamples
com/t?st.jsp
— matchescom/test.jsp
but alsocom/tast.jsp
orcom/txst.jsp
com/*.jsp
— matches all.jsp
files in thecom
directorycom/**/test.jsp
— matches alltest.jsp
files underneath thecom
pathorg/springframework/**/*.jsp
— matches all.jsp
files underneath theorg/springframework
pathorg/**/servlet/bla.jsp
— matchesorg/springframework/servlet/bla.jsp
but alsoorg/springframework/testing/servlet/bla.jsp
andorg/servlet/bla.jsp
关于AntPathMatcher
的实现原理和细节,等以后有时间了在详细分析。这里先推理猜想一下,根据前面对PathMatchingResourcePatternResolver
的分析,后面可以看到在进行对比的时候,实际上是将前面不包含模式匹配符的部分替换成了对应的绝对路径,然后跟遍历获取到的资源文件进行匹配,如:
fullPath:/home/luojing/webapp/spring/test/spring-*.xml
curPath:/home/luojing/webapp/spring/test/spring-redis.xml
可以根据/
符号将路径和模式匹配符分割为数组,然后一段一段的进行匹配,只要有一段匹配不成功即认为不匹配。