本文已收录【修炼内功】跃迁之路
阅读源码是一件极其枯燥无比的事情,对于使用频率较高的组件,如果能做到知其然且知其所以然,这对日常工作中不论是问题排查、代码优化、功能扩展等都是利大于弊的,如同老司机开车(对,就是开车),会让你有一种参与感,而不仅仅把它当成一种工具,若能习之精髓、学以致用,那便再好不过!
从工作之初便开始接触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 资源