本文已收录 【修炼内功】跃迁之路
阅读源码是一件极其枯燥无比的事情,对于使用频率较高的组件,若是能作到知其然且知其因此然,这对平常工做中不管是问题排查、代码优化、功能扩展等都是利大于弊的,如同老司机开车(对,就是开车),会让你有一种参与感,而不只仅把它当成一种工具,若能习之精髓、学以至用,那便再好不过!html
从工做之初便开始接触Spring框架,时至今日也没有认真地正视过它的实现细节,今日开拔,但愿可以坚持下来~java
对于Spring如此“庞大”(至少与我而言)的框架,不想一上来就将level提的很高,以上帝的视角将整个Spring框架的架构图或者类图之类抛出来,对于并不特别了解的人来讲,除了膜拜Spring的“宏伟”以外别无他法,依然不清楚应该如何下手git
这里,但愿可以按部就班,将Spring的几个核心组件各个击破,再将各组件串联起来,以点至面github
Spring系列文章web
- 默认您对Spring框架有必定的了解及使用经验
- 既然是源码分析便不可避免地会贴一些源码,会尽可能以精简代码、伪代码、额外注释等方式呈现,以减小源码所占的篇幅
言归正传,本篇就Spring的资源Resource聊起spring
Resource(资源)是进入Spring生态的第一道门,不敢说它是Spring的基石,但绝对是Spring的核心组件之一segmentfault
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中的classjar:file:///spring-core.jar!/org/springframework/core/io/Resource.class
,然而Spring并无采用URL
的方案,其官方文档给出了必定的解释mybatis
https://docs.spring.io/spring...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 standardizedURL
implementation that may be used to access a resource that needs to be obtained from the classpath or relative to aServletContext
. While it is possible to register new handlers for specializedURL
prefixes (similar to existing handlers for prefixes such ashttp:
), this is generally quite complicated, and theURL
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
资源