在学 Java的时候,我们学习了一个标准类 java.net.URL,该类在 Java SE 中的定位为统一资源定位器(Uniform Resource Locator),但是我们知道它的实现基本只限于网络形式发布的资源的查找和定位。然而,实际上资源的定义比较广泛,除了网络形式的资源,还有以二进制形式存在的、以文件形式存在的、以字节流形式存在的等等。而且它可以存在于任何场所,比如网络、文件系统、应用程序中。所以 java.net.URL 的局限性迫使 Spring 必须实现自己的资源加载策略,该资源加载策略需要满足如下要求:
org.springframework.core.io.Resource 为 Spring 框架所有资源的抽象和访问接口,它继承 org.springframework.core.io.InputStreamSource接口。作为所有资源的统一抽象,Resource 定义了一些通用的方法,由子类 AbstractResource 提供统一的默认实现。定义如下:
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 java.nio.channels.Channels.newChannel(getInputStream()); } /** * 资源内容的长度 */ long contentLength() throws IOException; /** * 资源最后的修改时间 */ long lastModified() throws IOException; /** * 根据资源的相对路径创建新资源 */ Resource createRelative(String relativePath) throws IOException; /** * 资源的文件名 */ @Nullable String getFilename(); /** * 资源的描述 */ String getDescription(); }
类结构图如下:
从上图可以看到,Resource 根据资源的不同类型提供不同的具体实现,如下:
org.springframework.core.io.AbstractResource ,为 Resource 接口的默认抽象实现。它实现了 Resource 接口的大部分的公共实现,作为 Resource 接口中的重中之重,其定义如下:
public abstract class AbstractResource implements Resource { /** * 判断文件是否存在,若判断过程产生异常(因为会调用SecurityManager来判断),就关闭对应的流 */ @Override public boolean exists() { try { // 基于 File 进行判断 return getFile().exists(); } catch (IOException ex) { // Fall back to stream existence: can we open the stream? // 基于 InputStream 进行判断 try { InputStream is = getInputStream(); is.close(); return true; } catch (Throwable isEx) { return false; } } } /** * 直接返回true,表示可读 */ @Override public boolean isReadable() { return true; } /** * 直接返回 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"); } /** * 根据 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[255]; // 每次最多读取 255 字节 int read; while ((read = is.read(buf)) != -1) { size += read; } return size; } finally { try { is.close(); } catch (IOException ex) { } } } /** * 返回资源最后的修改时间 */ @Override 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(); } /** * 抛出 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 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(); } }
如果我们想要实现自定义的 Resource ,记住不要实现 Resource 接口,而应该继承 AbstractResource 抽象类,然后根据当前的具体资源特性覆盖相应的方法即可。
Resource 的子类,例如 FileSystemResource、ByteArrayResource 等等的代码非常简单。感兴趣的同学,自己去研究。
一开始就说了 Spring 将资源的定义和资源的加载区分开了,Resource 定义了统一的资源,那资源的加载则由 ResourceLoader 来统一定义。
org.springframework.core.io.ResourceLoader 为 Spring 资源加载的统一抽象,具体的资源加载则由相应的实现类来完成,所以我们可以将 ResourceLoader 称作为统一资源定位器。其定义如下:
public interface ResourceLoader { String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; // CLASSPATH URL 前缀。默认为:"classpath:" Resource getResource(String location); ClassLoader getClassLoader(); }
与 AbstractResource 相似,org.springframework.core.io.DefaultResourceLoader 是 ResourceLoader 的默认实现。
它接收 ClassLoader 作为构造函数的参数,或者使用不带参数的构造函数。
@Nullable private ClassLoader classLoader; public DefaultResourceLoader() { // 无参构造函数 this.classLoader = ClassUtils.getDefaultClassLoader(); } public DefaultResourceLoader(@Nullable ClassLoader classLoader) { // 带 ClassLoader 参数的构造函数 this.classLoader = classLoader; } public void setClassLoader(@Nullable ClassLoader classLoader) { this.classLoader = classLoader; } @Override @Nullable public ClassLoader getClassLoader() { return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader()); }
ResourceLoader 中最核心的方法为 #getResource(String location) ,它根据提供的 location 返回相应的 Resource 。而 DefaultResourceLoader 对该方法提供了核心实现(因为,它的两个子类都没有提供覆盖该方法,所以可以断定 ResourceLoader 的资源加载策略就封装在 DefaultResourceLoader 中),代码如下:
// DefaultResourceLoader.java @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. return getResourceByPath(location); } } }
其次,若 location 以 "/" 开头,则调用 #getResourceByPath() 方法,构造 ClassPathContextResource 类型资源并返回。代码如下:
protected Resource getResourceByPath(String path) { return new ClassPathContextResource(path, getClassLoader()); }
org.springframework.core.io.ProtocolResolver ,用户自定义协议资源解决策略,作为 DefaultResourceLoader 的 SPI:它允许用户自定义资源加载协议,而不需要继承 ResourceLoader 的子类。
在介绍 Resource 时,提到如果要实现自定义 Resource,我们只需要继承 AbstractResource 即可,但是有了 ProtocolResolver 后,我们不需要直接继承 DefaultResourceLoader,改为实现 ProtocolResolver 接口也可以实现自定义的 ResourceLoader。
ProtocolResolver 接口,仅有一个方法 Resource resolve(String location, ResourceLoader resourceLoader) 。代码如下:
/** * 使用指定的 ResourceLoader ,解析指定的 location 。 * 若成功,则返回对应的 Resource 。 * * 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);
在 Spring 中你会发现该接口并没有实现类,它需要用户自定义,自定义的 Resolver 如何加入 Spring 体系呢?调用 DefaultResourceLoader#addProtocolResolver(ProtocolResolver) 方法即可。代码如下:
/** * ProtocolResolver 集合 */ private final SetprotocolResolvers = new LinkedHashSet<>(4); public void addProtocolResolver(ProtocolResolver resolver) { Assert.notNull(resolver, "ProtocolResolver must not be null"); this.protocolResolvers.add(resolver); }
下面示例是演示 DefaultResourceLoader 加载资源的具体策略,代码如下(该示例参考《Spring 揭秘》 P89):
ResourceLoader resourceLoader = new DefaultResourceLoader(); Resource fileResource1 = resourceLoader.getResource("D:/Users/chenming673/Documents/spark.txt"); System.out.println("fileResource1 is FileSystemResource:" + (fileResource1 instanceof FileSystemResource)); Resource fileResource2 = resourceLoader.getResource("/Users/chenming673/Documents/spark.txt"); System.out.println("fileResource2 is ClassPathResource:" + (fileResource2 instanceof ClassPathResource)); Resource urlResource1 = resourceLoader.getResource("file:/Users/chenming673/Documents/spark.txt"); System.out.println("urlResource1 is UrlResource:" + (urlResource1 instanceof UrlResource)); Resource urlResource2 = resourceLoader.getResource("http://www.baidu.com"); System.out.println("urlResource1 is urlResource:" + (urlResource2 instanceof UrlResource));
运行结果:
fileResource1 is FileSystemResource:false fileResource2 is ClassPathResource:true urlResource1 is UrlResource:true urlResource1 is urlResource:true
从上面的示例,我们看到,其实 DefaultResourceLoader 对#getResourceByPath(String) 方法处理其实不是很恰当,这个时候我们可以使用 org.springframework.core.io.FileSystemResourceLoader 。它继承 DefaultResourceLoader ,且覆写了 #getResourceByPath(String) 方法,使之从文件系统加载资源并以 FileSystemResource 类型返回,这样我们就可以得到想要的资源类型。代码如下:
@Override protected Resource getResourceByPath(String path) { // 截取首 / if (path.startsWith("/")) { path = path.substring(1); } // 创建 FileSystemContextResource 类型的资源 return new FileSystemContextResource(path); }
FileSystemContextResource ,为 FileSystemResourceLoader 的内部类,它继承 FileSystemResource 类,实现 ContextResource 接口。代码如下:
/** * FileSystemResource that explicitly expresses a context-relative path * through implementing the ContextResource interface. */ private static class FileSystemContextResource extends FileSystemResource implements ContextResource { public FileSystemContextResource(String path) { super(path); } @Override public String getPathWithinContext() { return getPath(); } }
org.springframework.core.io.ClassRelativeResourceLoader ,是 DefaultResourceLoader 的另一个子类的实现。和 FileSystemResourceLoader 类似,在实现代码的结构上类似,也是覆写 #getResourceByPath(String path) 方法,并返回其对应的 ClassRelativeContextResource 的资源类型。
ResourceLoader 的 Resource getResource(String location) 方法,每次只能根据 location 返回一个 Resource 。当需要加载多个资源时,我们除了多次调用 #getResource(String location) 方法外,别无他法。org.springframework.core.io.support.ResourcePatternResolver 是 ResourceLoader 的扩展,它支持根据指定的资源路径匹配模式每次返回多个 Resource 实例,其定义如下:
public interface ResourcePatternResolver extends ResourceLoader { String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; Resource[] getResources(String locationPattern) throws IOException; }
org.springframework.core.io.support.PathMatchingResourcePatternResolver ,为 ResourcePatternResolver 最常用的子类,它除了支持 ResourceLoader 和 ResourcePatternResolver 新增的 "classpath*:" 前缀外,还支持 Ant 风格的路径匹配模式(类似于 "**/*.xml")。
PathMatchingResourcePatternResolver 提供了三个构造函数,如下:
/** * 内置的 ResourceLoader 资源定位器 */ 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); }
@Override public Resource getResource(String location) { return getResourceLoader().getResource(location); } public ResourceLoader getResourceLoader() { return this.resourceLoader; }
该方法,直接委托给相应的 ResourceLoader 来实现。所以,如果我们在实例化的 PathMatchingResourcePatternResolver 的时候,如果未指定 ResourceLoader 参数的情况下,那么在加载资源时,其实就是 DefaultResourceLoader 的过程。
其实在下面介绍的 Resource[] getResources(String locationPattern) 方法也相同,只不过返回的资源是多个而已。
@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, // 通常只在这里的前缀后面查找模式 // and on Tomcat only after the "*/" separator for its "war:" protocol. 而在 Tomcat 上只有在 “*/ ”分隔符之后才为其 “war:” 协议 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)}; } } }
当 locationPattern 以 "classpath*:" 开头但是不包含通配符,则调用 #findAllClassPathResources(...) 方法加载资源。该方法返回 classes 路径下和所有 jar 包中的所有相匹配的资源。
protected Resource[] findAllClassPathResources(String location) throws IOException { String path = location; // 去除首个 / if (path.startsWith("/")) { path = path.substring(1); } // 真正执行加载所有 classpath 资源 Setresult = doFindAllClassPathResources(path); if (logger.isTraceEnabled()) { logger.trace("Resolved classpath location [" + location + "] to resources " + result); } // 转换成 Resource 数组返回 return result.toArray(new Resource[0]); }
真正执行加载的是在 #doFindAllClassPathResources(...) 方法,代码如下:
protected SetdoFindAllClassPathResources(String path) throws IOException { Set result = new LinkedHashSet<>(16); ClassLoader cl = getClassLoader(); // <1> 根据 ClassLoader 加载路径下的所有资源 Enumeration resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path)); // <2> while (resourceUrls.hasMoreElements()) { URL url = resourceUrls.nextElement(); // 将 URL 转换成 UrlResource result.add(convertClassLoaderURL(url)); } // <3> 加载路径下得所有 jar 包 if ("".equals(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() 方法,代码如下:
// java.lang.ClassLoader.java public EnumerationgetResources(String name) throws IOException { @SuppressWarnings("unchecked") Enumeration [] tmp = (Enumeration []) new Enumeration>[2]; if (parent != null) { tmp[0] = parent.getResources(name); } else { tmp[0] = getBootstrapResources(name); } tmp[1] = findResources(name); return new CompoundEnumeration<>(tmp); }
<2> 处,遍历 URL 集合,调用 #convertClassLoaderURL(URL url) 方法,将 URL 转换成 UrlResource 对象。代码如下:
protected Resource convertClassLoaderURL(URL url) { return new UrlResource(url); }
<3> 处,若 path 为空(“”)时,则调用 #addAllClassLoaderJarRoots(...)方法。该方法主要是加载路径下得所有 jar 包,方法较长也没有什么实际意义就不贴出来了。
通过上面的分析,我们知道 #findAllClassPathResources(...) 方法,其实就是利用 ClassLoader 来加载指定路径下的资源,不论它是在 class 路径下还是在 jar 包中。如果我们传入的路径为空或者 /,则会调用 #addAllClassLoaderJarRoots(...) 方法,加载所有的 jar 包。
当 locationPattern 中包含了通配符,则调用该方法进行资源加载。代码如下:
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException { // 确定根路径、子路径 String rootDirPath = determineRootDir(locationPattern); String subPattern = locationPattern.substring(rootDirPath.length()); // 获取根据路径下的资源 Resource[] rootDirResources = getResources(rootDirPath); // 遍历,迭代 Setresult = 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(String location) 方法,一个是 #doFindPathMatchingXXXResources(...) 等方法。
2.5.5.1 determineRootDir
determineRootDir(String location) 方法,主要是用于确定根路径。代码如下
/** * Determine the root directory for the given location. *Used for determining the starting point for file matching, * resolving the root directory location to a {@code java.io.File} * and passing it into {@code retrieveMatchingFiles}, with the * remainder of the location as pattern. *
Will return "/WEB-INF/" for the pattern "/WEB-INF/*.xml", * for example. * @param location the location to check * @return the part of the location that denotes the root directory * @see #retrieveMatchingFiles */ 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/ |
2.5.5.2 doFindPathMatchingXXXResources
#doFindPathMatchingXXXResources(...) 方法,是个泛指,一共对应三个方法:
因为本文重在分析 Spring 统一资源加载策略的整体流程。相对来说,上面几个方法的代码量会比较多。所以本文不再追溯
至此 Spring 整个资源记载过程已经分析完毕。下面简要总结下: