概述
资源加载策略需要满足如下要求:
- 职能划分清楚,资源的定义和资源的加载应该有一个清晰的界限
- 同意的抽象,同意的资源定义和资源加载策略。资源加载后要返回同意的抽象给客户端,客户端要对资源进行怎样的处理,应该由抽象资源接口来界定。
Resource:统一资源
Resource接口
org.springframework.core.io.Resource是Spring框架所有资源的抽象和访问接口,它继承org.springframework.core.io.InputStreamSource接口,Source定义了一些通用的方法,由子类AbstractResource提供统一的默认实现。
public interface Resource extends InputStreamSource {
// 资源是否存在
boolean exists();
// 资源是否可读
default boolean isReadable() {
return exists();
}
// 资源所代表的句柄是否被一个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;
// 资源的文件名
String getFilename();
// 资源的描述
String getDescription();
}
Resource根据不同的资源类型提供不同的具体实现,例如:
- FileSystemResource:对
java.io.File类型的资源的封装,只要是和 File 有关联,基本上与 FileSystemResource 也有关联。支持解析为 File 和 URL,实现扩展 WritableResource 接口。 - ClassPathResource:class path 类型资源的实现。使用给定的 ClassLoader 或者给定的 Class 来加载资源。
- ServletContextResource:为访问 Web 容器上下文中的资源而设计的类,负责以相对于Web应用根目录的路径加载资源,它支持以流和URL的方式访问。ServletContextResource 持有一个 ServletContext 的引用 ,其底层是通过 ServletContext 的 getResource()方法和 getResourceAsStream()方法来获取资源的。
AbstractResource默认实现
AbstractResource为Resource的默认实现,它实现了Resource接口的大部分的公共实现。
public abstract class AbstractResource implements Resource {
// 判断文件是否存在,若判断过程产生异常,就关闭对应的流
@Override
public boolean exists() {
// 首先判断文件是否存在
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;
}
}
// 当文件存在时,永远返回True
public boolean isReadable() {
return exists();
}
// 直接返回false,表示未打开
@Override
public boolean isOpen() {
return false;
}
// 直接返回false,表示部位File
@Override
public boolean isFile() {
return false;
}
// 抛出FileNotFoundException异常
@Override
public URL getURL() throws IOException {
throw new FileNotFoundException(getDescription() + " cannot be resolved to URL");
}
// 基于getURL()返回的URL构建URI
@Override
public URI getURI() throws IOException {
URL url = getURL();
try {
return ResourceUtils.toURI(url);
}
catch (URISyntaxException ex) {
throw new NestedIOException("Invalid URI [" + url + "]", ex);
}
}
// 抛出 FileNotFoundException 异常,交给子类实现
@Override
public File getFile() throws IOException {
throw new FileNotFoundException(getDescription() + " cannot be resolved to absolute file path");
}
@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();
}
// 抛出 FileNotFoundException 异常,交给子类实现
@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();
}
}
ResourceLoader:统一资源定位
ResourceLoader接口
Spring将资源的定义和资源的加载区分开了,Resource定义了统一的资源,ResourceLoader定义了统一的资源加载。
ResourceLoader是Spring资源加载的统一抽象,具体的资源加载则由相应的实现类来完成,ResourceLoader称作统一资源定位器。
public interface ResourceLoader {
// CLASSPATH_URL_PREFIX = "classpath:"
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
Resource getResource(String location);
@Nullable
ClassLoader getClassLoader();
}
getResource()根据所提供的路径location返回Resource实例,该方法的实现主要在其子类DefaultResourceLoader中实现,支持以下模式的资源加载:
- URL位置资源,例如"file:D/app/bean.xml"
- ClassPath位置资源,例如"classpath:bean.xml"
- 相对路径资源,例如"/conf/bean.xml"
getClassLoader()返回ResourceLoader使用的ClassLoader。
DefaultResourceLoader默认实现
DefaultResourceLoader是ResourceLoader的默认实现,它有两个构造函数。
// 默认构造函数
public DefaultResourceLoader() {
}
// 指定ClassLoader构造函数
public DefaultResourceLoader(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
}
// 设置ClassLoader
public void setClassLoader(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
}
// 获取ClassLoader,如果为null,使用默认的ClassLoader
@Override
@Nullable
public ClassLoader getClassLoader() {
return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
}
ResourceLoader最核心的方法是getResource(),它根据提供的location返回相应的Resource,DefaultResource对该方法提供了核心实现。
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;
}
}
// 处理以/开头的location
if (location.startsWith("/")) {
return getResourceByPath(location);
}
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
// 处理以classpath:开头的location,且使用getClassLoader()获取当前的ClassLoader
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// 构造URL,尝试通过它进行资源定位
URL url = new URL(location);
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// 加载出现异常时处理
return getResourceByPath(location);
}
}
}
// 返回ClassPathContextResource资源
protected Resource getResourceByPath(String path) {
return new ClassPathContextResource(path, getClassLoader());
}
getResource()使用的ProtocolResolver叫做"协议解析器",允许我们自己定义协议资源加载策略,而不需要继承ResourceLoader的子类。在Resource中,如果需要自定义Resource,我们只需要继承AbstractResource即可。在ResouceLoader中,则不需要直接继承DefaultResourceLoader,而是实现ProtocolResolver接口也可以实现自定义的ResourceLoader,而该接口只有一个方法,可以根据传入的location解析出对应的资源。
public interface ProtocolResolver {
@Nullable
Resource resolve(String location, ResourceLoader resourceLoader);
}
ResourcePatternResolver加载多个Resouce
ResourLoader的Resource getResource(String location)每次根据location返回一个Resource,当需要加载多个资源时,必须要调用多次getResource()方法才行。而ResourcePatternResolver可以根据指定的资源路径匹配模式每次返回多个Resource实例,该接口继承自ResourceLoader。
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
ResourcePatternResolver在ResourceLoader的基础上增加了getResources(String locationPattern),以支持根据路径匹配模式返回多个Resource实例,同时也增加了一种新的协议前缀classpath*:。
PathMatchingResourcePatternResolver
PathMatchingResourcePatternResolver为ResourcePatternResolver的实现类,它除了支持ResourceLoader和ResourcePatternResolver新增的classpath*:前缀外,还支持Ant风格的路径匹配模式。PathMatchingResourcePatternResolver提供了三个构造方法。
// 默认构造方法,使用的ResourceLoader为DefaultResourceLoader
public PathMatchingResourcePatternResolver() {
this.resourceLoader = new DefaultResourceLoader();
}
// 指定ResourceLoader的构造方法
public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
Assert.notNull(resourceLoader, "ResourceLoader must not be null");
this.resourceLoader = resourceLoader;
}
// 指定Classloader的构造方法
public PathMatchingResourcePatternResolver(@Nullable ClassLoader classLoader) {
this.resourceLoader = new DefaultResourceLoader(classLoader);
}
getResource()获取单个resource
getResource()方法直接委托相应的ResourceLoader来实现
public Resource getResource(String location) {
return getResourceLoader().getResource(location);
}
getResources()获取多个resource
PathMatchingResourcePatternResolver支持Ant风格的路径匹配,该实现是通过AntPathMatcher类中的实现isPattern()方法实现。
// PathMatchingResourcePatternResolver中支持Ant路径匹配的pathMatcher
private PathMatcher pathMatcher = new AntPathMatcher();
public PathMatcher getPathMatcher() {
return this.pathMatcher;
}
// AntPathMatcher类中的isPattern方法
public boolean isPattern(@Nullable String path) {
// 主要判断location中是否包含*和?
if (path == null) {
return false;
}
boolean uriVar = false;
for (int i = 0; i < path.length(); i++) {
char c = path.charAt(i);
if (c == '*' || c == '?') {
return true;
}
if (c == '{') {
uriVar = true;
continue;
}
if (c == '}' && uriVar) {
return true;
}
}
return false;
}
getResources()实现主要依据不同的路径模式进行不同的处理,逻辑为
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
return findPathMatchingResources(locationPattern);
}
else {
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}
else {
int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
locationPattern.indexOf(':') + 1);
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
return findPathMatchingResources(locationPattern);
}
else {
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}
- 以
classpath:*开头但是不包含通配符*和?的使用findAllClassPathResources()加载路径下全部的资源 - 以
classpath:*开头或者以war:开头,且包含通配符的,使用findPathMatchingResources()加载路径下全部的资源 - 其他情况使用resourceLoader加载一个resource
findAllClassPathResources方法
当以classpath:* 开头但是不包含通配符,使用findAllClassPathResources()方法加载资源,该方法返回class路径下和jar包中的相匹配的资源。
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]);
}
// 使用classLoader加载资源
protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
Set<Resource> result = new LinkedHashSet<>(16);
ClassLoader cl = getClassLoader();
Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path));
while (resourceUrls.hasMoreElements()) {
URL url = resourceUrls.nextElement();
result.add(convertClassLoaderURL(url));
}
// 如果path为空,则加载路径下所有jar包
if (!StringUtils.hasLength(path)) {
addAllClassLoaderJarRoots(cl, result);
}
return result;
}
classLoader的getResource()方法如下,主要通过父类委托来加载资源
public Enumeration<URL> getResources(String name) throws IOException {
@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
if (parent != null) {
tmp[0] = parent.getResources(name);
} else {
tmp[0] = getBootstrapResources(name);
}
tmp[1] = findResources(name);
return new CompoundEnumeration<>(tmp);
}
findPathMatchingResources方法
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();
if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
if (resolvedUrl != null) {
rootDirUrl = resolvedUrl;
}
rootDirResource = new UrlResource(rootDirUrl);
}
if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
}
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);
}
return result.toArray(new Resource[0]);
}
总结
- Spring提供了Resource和ResourceLoader来统一抽象整个资源以及资源获取,使得资源与资源获取有了更加清晰地界限,并且提供了合适地Default类,使得自定义实现更加方便和清晰
- AbstractResource为Resource地默认实现类,它对Resource接口做了统一的实现,子类继承该类后只需要覆盖相应的方法即可,对于自定义的Resource也是继承该类
- DefaultResourceLoader为ResourceLoader的默认实现,且每次只能返回单一的资源,在自定义ResourceLoader时,除了可以继承该类外还可以实现ProtocolResolver接口来实现自定义资源加载协议
- ResourcePatternResolver是一个可以返回多个资源的接口,其子类PathMatchingResourcePatternResolver即实现了
getResource()可以获取单一资源,也实现了getResources()可以获取多个资源