本文已收录【修炼内功】跃迁之路
阅读源码是一件极其枯燥无比的事情,对于使用频率较高的组件,如果能做到知其然且知其所以然,这对日常工作中不论是问题排查、代码优化、功能扩展等都是利大于弊的,如同老司机开车(对,就是开车),会让你有一种参与感,而不仅仅把它当成一种工具,若能习之精髓、学以致用,那便再好不过!
从工作之初便开始接触Spring框架,时至今日也没有认真地正视过它的实现细节,今日开拔,希望能够坚持下来~
对于Spring如此“庞大”(至少与我而言)的框架,不想一上来就将level提的很高,以上帝的视角将整个Spring框架的架构图或者类图之类抛出来,对于并不特别了解的人来说,除了膜拜Spring的“宏伟”之外别无他法,依然不清楚应该如何下手
这里,希望能够循序渐进,将Spring的几个核心组件各个击破,再将各组件串联起来,以点至面
Spring系列文章
言归正传,本篇就Spring的资源Resource聊起
Resource(资源)是进入Spring生态的第一道门,不敢说它是Spring的基石,但绝对是Spring的核心组件之一
Resource主要负责资源的(读写)操作,最为常见地出现在系统各初始化阶段,如
ApplicationContext
new ClassPathXmlApplicationContext("classpath:spring/application.xml");
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/application.xml</param-value> </context-param>
@Configuration @ComponentScan("com.manerfan") public class AppConfiguration {}
<context:component-scan base-package="com.manerfan"></context:component-scan>
mybatis.config-location=classpath:/mybatis/mybatis-config.xml
@Component public class SomeComponent { @Value("classpath:in18/zh-cn.properties") private Resource in18ZhCn; }
<bean id="someComponent" class="...SomeComponent"> <property name="in18ZhCn" value="classpath:in18/zh-cn.properties"/> </bean>
application[-env].properties
application[-env].yml
,加载 META-INF
配置文件等 Java中的 URL
通过不同的前缀(协议)已经实现了一套资源的读取,如磁盘文件 file:///var/log/system.log
、网络文件 https://some.host/some.file.txt
甚至jar中的class jar:file:///spring-core.jar!/org/springframework/core/io/Resource.class
,然而Spring并没有采用 URL
的方案,其官方文档给出了一定的解释
Java’s standard java.net.URL
class and standard handlers for various URL prefixes, unfortunately, are not quite adequate enough for all access to low-level resources. For example, there is no standardized URL
implementation that may be used to access a resource that needs to be obtained from the classpath or relative to a ServletContext
. While it is possible to register new handlers for specialized URL
prefixes (similar to existing handlers for prefixes such as http:
), this is generally quite complicated, and the URL
interface still lacks some desirable functionality, such as a method to check for the existence of the resource being pointed to.
其一, URL
扩展复杂;其二, URL
功能有限
Spring将不同类型的资源统一抽象成了 Resource
,这有点类似Linux系统的“一切皆文件”(磁盘文件、目录、硬件设备、套接字、网络等),资源的抽象屏蔽了不同类型资源的差异性,统一了操作接口
⇪Resource
的定义非常简洁明了,方法的命名已经足够清晰,不再统一解释
public interface Resource extends InputStreamSource { boolean exists(); boolean isReadable(); boolean isOpen(); boolean isFile() URL getURL() throws IOException; URI getURI() throws IOException; File getFile() throws IOException; ReadableByteChannel readableChannel() throws IOException; long contentLength() throws IOException; long lastModified() throws IOException; Resource createRelative(String relativePath) throws IOException; String getFilename(); String getDescription(); // ... }
Resource
继承自更为抽象的 ⇪InputStreamSource
public interface InputStreamSource { String CLASSPATH_URL_PREFIX = "classpath:"; InputStream getInputStream() throws IOException; }
其只有一个方法 getInputStream
,用于获取资源的 InputStream
对于 Resouce
的具体实现可参考下图(莫被错综复杂的类关系扰乱了思路),一层层剥离解析
⇪WritableResource
派生自 Resource
,其在 Resource
的基础上增加了'写'相关的能力
public interface WritableResource extends Resource { boolean isWritable(); OutputStream getOutputStream() throws IOException; WritableByteChannel writableChannel() throws IOException; }
这里重点关注 Resource
'读'能力的实现
⇪AbstractResource
实现了大部分 Resource
中公共的、无底层差异的逻辑,实现较为简单,不再详述
AbstractResource
的具体实现类则是封装了不同类型的资源类库,使用具体的类库函数实现 Resource
定义的一系列接口
⇪FileSystemResource
封装了 java.io.File
(或 java.io.Path
)的能力实现了 Resource
的一些细节
public class FileSystemResource extends AbstractResource implements WritableResource { @Override public InputStream getInputStream() throws IOException { // ... // 将File/Path封装为InputStream return Files.newInputStream(this.filePath); // ... } @Override public OutputStream getOutputStream() throws IOException { // 将File/Path封装为OutputStream return Files.newOutputStream(this.filePath); } @Override public URL getURL() throws IOException { return (this.file != null ? this.file.toURI().toURL() : this.filePath.toUri().toURL()); } @Override public URI getURI() throws IOException { return (this.file != null ? this.file.toURI() : this.filePath.toUri()); } // ... }
同理, ⇪ByteArrayResource
封装了 ByteArray
的能力, ⇪InputStreamResource
封装了 InputSream
的能力,等等,不再一一介绍
⇪AbstractFileResolvingResource
则把中心放在 java.net.URL
上,其使用 URL
的能力重写了其父类 AbstractResource
的大部分实现
AbstractFileResolvingResource
的实现类只有两个, ⇪UrlResource
及 ⇪ClassPathResource
UrlResource
同样简单地封装了 URL
的能力来实现 Resource
中定义的接口
public class UrlResource extends AbstractFileResolvingResource { @Override public InputStream getInputStream() throws IOException { URLConnection con = this.url.openConnection(); ResourceUtils.useCachesIfNecessary(con); try { return con.getInputStream(); } catch (IOException ex) { // Close the HTTP connection (if applicable). if (con instanceof HttpURLConnection) { ((HttpURLConnection) con).disconnect(); } throw ex; } } // ... }
ClassPathResource
则是借助 ClassLoader
的能力来实现 Resource
中定义的接口
public class ClassPathResource extends AbstractFileResolvingResource { @Override public InputStream getInputStream() throws IOException { InputStream is; if (this.clazz != null) { is = this.clazz.getResourceAsStream(this.path); } else if (this.classLoader != null) { is = this.classLoader.getResourceAsStream(this.path); } else { is = ClassLoader.getSystemResourceAsStream(this.path); } if (is *** null) { throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist"); } return is; } }
细心的可能会发现,这里还有一个 ⇪EncodedResource
,其在 Resource
的基础上加入了编码信息,并提供了额外的 getReader
接口
public class EncodedResource implements InputStreamSource { public Reader getReader() throws IOException { if (this.charset != null) { return new InputStreamReader(this.resource.getInputStream(), this.charset); } else if (this.encoding != null) { return new InputStreamReader(this.resource.getInputStream(), this.encoding); } else { return new InputStreamReader(this.resource.getInputStream()); } } }
这对于获取编码格式有要求的资源来讲十分受用
@Component public class MyComponent { private final Properties properties; public MyComponent(@Value("classpath:/config/my-config.properties") Resource resource) { // Properties读取配置默认编码为ISO-8859-1 this.properties = PropertiesLoaderUtils.loadProperties(new EncodedResource(resource, "utf-8")); } // ... }
⇪Resource
将资源的操作抽象,屏蔽不同类型资源差异性,统一操作接口 ⇪FileSystemResource
、 ⇪ByteArrayResource
、 ⇪InputStreamResource
、 ⇪UrlResource
及 ⇪ClassPathResource
等,借助相应的资源类库能力,实现 Resource
中定义的接口
FileSystemResource
→ File
or Path
ByteArrayResource
→ ByteArray
InputStreamResource
→ InputStream
UrlResource
→ Url
ClassPathResource
→ ClassLoader
使用上,针对不同的资源类型创建不同的 Resource
即可
new FileSystemResource("/var/log/system.log"); // 文件系统中的文件 new ClassPathResource("/config/my-config.properties"); // classpath中的文件 new UrlResource("http://oss.manerfan.com/config/my-config.properties"); // 网络上的文件
“ 针对不同的资源类型创建不同的 Resource
”,如上例中的硬编码并不符合开闭原则,对于开发者来说其实并不那么友好,还好Spring提供了 ⇪ResourceLoader
(接下来,你会发现Spring中提供了各种各样的 Loader 、 Resolver 、 Aware 等等)
public interface ResourceLoader { Resource getResource(String location); }
ResourceLoader
中定义了 getResource
方法用于创建 合适 类型的 Resource
,至于应该创建哪种类型以及如何创建,则交由 ResourceLoader
处理
ResourceLoader
的实现类主要有两种,其一为 ⇪DefaultResourceLoader
,其二为 ⇪PathMatchingResourcePatternResolver
⇪DefaultResourceLoader
为 ResourceLoader
的默认实现,其实现极为简单
public class DefaultResourceLoader implements ResourceLoader { @Override public Resource getResource(String location) { Assert.notNull(location, "Location must not be null"); // 1. 如果存在自定义的解析器,优先使用自定义解析器 for (ProtocolResolver protocolResolver : getProtocolResolvers()) { Resource resource = protocolResolver.resolve(location, this); if (resource != null) { return resource; } } // 2. 如果没有自定义解析器,或者自定义解析器无法解析,则使用默认实现 if (location.startsWith("/")) { // 构造ClassPathResource return getResourceByPath(location); } else if (location.startsWith(CLASSPATH_URL_PREFIX)) { // 取"classpath:"后的内容,构造ClassPathResource return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); } else { try { // 尝试构造URLResource URL url = new URL(location); return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); } catch (MalformedURLException ex) { // 降级到ClassPathResource return getResourceByPath(location); } } } protected Resource getResourceByPath(String path) { return new ClassPathContextResource(path, getClassLoader()); } }
首先会尝试使用自定义解析器解析(通过 addProtocolResolver
方法添加),如果没有或者解析失败才会使用默认实现
默认实现逻辑中,如果是以 /
或 classpath:
开头的,会直接构造 ClassPathResource
,否则会尝试构造为 UrlResource
了解 ClassPathResource
实现的会注意到,如果在所有的classpath路径中同时存在多个文件匹配(如,同时在 a.jar 及 b.jar 中存在 /config/my-config.properties 文件),则只会返回首个匹配到的(取决于JVM加载顺序),这也是有别于 PathMatchingResourcePatternResolver
的一个地方
⇪PathMatchingResourcePatternResolver
实现自 ⇪ResourcePatternResolver
接口
public interface ResourcePatternResolver extends ResourceLoader { String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; Resource[] getResources(String locationPattern) throws IOException; }
ResourcePatternResolver
在 ResourceLoader
的基础上,增加了批量获取的接口 getResources
默认情况下, PathMatchingResourcePatternResolver
的 getResource
实现其实是使用了 DefaultResourceLoader
(当然你也可以自己指定默认的 ResourceLoader
实现)
public class PathMatchingResourcePatternResolver implements ResourcePatternResolver { public PathMatchingResourcePatternResolver() { this.resourceLoader = new DefaultResourceLoader(); } @Override public Resource getResource(String location) { return getResourceLoader().getResource(location); } }
PathMatchingResourcePatternResolver
的一大特点在于 PathMatching (默认使用 AntPathMatcher
,也可以指定),对于类似 classpath:/config/my-*.properties 或 classpath*:/config/my-*.properties 等Ant风格的资源进行匹配,其基本思路大致为
⇪findPathMatchingResources
方法) classpath: 与 classpath*: 的区别主要在于父目录的查找逻辑
父目录的查找借助 DefaultResourceLoader
的能力(归根结底使用了 ClassLoader.getResource
),上文也有提到,这里只会返回首个匹配到的目录资源
@Override public Resource[] getResources(String locationPattern) throws IOException { // ... 各种 if-else 之后 // a single resource with the given name return new Resource[] {getResourceLoader().getResource(locationPattern)}; }
父目录的查找则直接使用 ClassLoader.getResources
,返回所有classpath中的目录资源
@Override public Resource[] getResources(String locationPattern) throws IOException { // ... 各种 if-else 之后 // all class path resources with the given name return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); } protected Resource[] findAllClassPathResources(String location) throws IOException { // ... Set<Resource> result = doFindAllClassPathResources(path); // ... } protected Set<Resource> doFindAllClassPathResources(String path) throws IOException { // ... Enumeration<URL> resourceUrls = (cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path)); // ... }
所以, classpath:/config/my-*.properties 只会返回首个匹配到的 /config/ 目录中所有的 my-*.properties 资源,而 classpath*:/config/my-*.properties 则会返回所有匹配到的 /config/ 目录中所有的 my-*.properties 资源
AbstractApplicationContext
同时实现了 ResourcePatternResolver
接口并继承了 DefaultResourceLoader
,
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext { public AbstractApplicationContext() { this.resourcePatternResolver = getResourcePatternResolver(); } protected ResourcePatternResolver getResourcePatternResolver() { return new PathMatchingResourcePatternResolver(this); } }
所以,如开篇的几个例子里,在 ApplicationContext
中获取 Resource
资源,大多数情况下使用的都是 PathMatchingResourcePatternResolver
ResourceLoader
根据不同的前缀(协议)生成相对应的 Resource
(Spring中提供了各种各样的 Loader 、 Resolver 、 Aware 等等) DefaultResourceLoader
只能获取单个资源,且只能获取classpath中首次匹配到的资源( ClassLoader.getResource
) PathMatchingResourcePatternResolver
可以使用Ant风格匹配并返回多个资源
ClassLoader.getResource
)所有的满足给定Ant规则的资源 ClassLoader.getResources
)所有的满足给定Ant规则的资源 ApplicationContext
默认使用 PathMatchingResourcePatternResolver
获取 Resource
资源 ⇪Resource
将资源的操作抽象,屏蔽不同类型资源差异性,统一操作接口 Resource
的不同实现,均借助相应的资源类库能力,来实现 Resource
中定义的接口 ResourceLoader
根据不同的前缀(协议)生成相对应的 Resource
DefaultResourceLoader
只能获取单个资源,且只能获取classpath中首次匹配到的资源 PathMatchingResourcePatternResolver
可以使用Ant风格匹配并返回多个资源, classpath: 与 classpath*: 的区别在于如何获取 根目录 以在其中查找匹配Ant风格的资源 ApplicationContext
默认使用 PathMatchingResourcePatternResolver
获取 Resource
资源