Spring Resource源码分析
- 一、构建Demo项目
- 二、模块拆解
- 三、ResourceLoader接口详解
- 四、Resource接口详解
- 五、Resource和ResourceLoader的使用
- 六、DefaultResourceLoader的实现
- 七、ResourcePatternResolver详解
- 八、ApplicationContext与ResourceLoader
- 九、PathMatchingResourcePatternResolver详解
- 十、总结
- 参考
转载请注明出处即可。
上篇文章描述了阅读Spring源码的相关思考阅读Spring Frameworks源码的思考,在这里就按照文章中所描述的思维方式来进行分析和拆解,并进行一些核心类的解析。
一、构建Demo项目
我们先来构建一个简单的项目,
pom.xml引入了Spring boot,里面的Spring是5.1.9的版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>study</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.8.RELEASE</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project> |
然后在项目的resources目录下编写一个bean.xml
1 2 3 4 5 6 7 | <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="myTestBean" class="org.study.MyTestBean"/> </beans> |
在编写一个Bean
1 2 3 4 5 6 7 8 9 10 11 | public class MyTestBean { private String testStr = "testStr"; public String getTestStr() { return testStr; } public void setTestStr(String testStr) { this.testStr = testStr; } } |
最后补充一个main
1 2 3 4 5 6 7 | public class Run { public static void main(String[] args) throws Exception { ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml"); MyTestBean bean = (MyTestBean) context.getBean("myTestBean"); System.out.println(bean.getTestStr()); } } |
这应该是使用Spring的最简单的方式之一。如果到这里项目可以执行了,那么就已经可以进入源码阅读的历程了,并不需要去clone框架的源码。
二、模块拆解
先看下Run类中的3行代码,目标是搞清楚,这三行的底层是怎么实现的。那么在理清之前,优先应该关注的就是"接口"。因为接口作为框架"对外"提供的"功能描述",很容易通过接口所提供的方法来知晓可以利用的框架功能。并且通过接口还可以在接口的继承链中,或者该接口实现类的属性中(对象组合)找到"模块"的划分,进而根据不同的模块阅读来分别理解Spring的某一项功能的实现原理,整理出模块内部类与类之间的关系,最后在整合出模块与模块之间的关系,并了解他们是如何配合
在Java中一般只有两种方式来扩展自身接口或者类的功能,一种是继承另一种是组合,
看到相对较繁琐的继承关系可能已经抓不住头脑了。不过没关系,我们对源码的拆解是一步一步的不会步子跨的太大,可以先对这张图有个基本的概念,后面我们会发现,对于找到对应的实现,这张图还是很有作用的。前面刚说可以在接口的继承链或者,实现类的属性中(对象)找到模块的划分。
如图所示,把整个
我们在这篇先看下
三、ResourceLoader接口详解
通过接口的名称资源加载器,我们就能知晓接口的功能,无非就是加载资源而已。那么问题来了,一个是什么是资源? 另一个是如何加载资源。要解答这两个问题,接口上的注释和接口的方法其实就是答案了。
注释上首先提供了3个最重要的信息
(1) 加载的资源可能在classpath下,也能可能只是文件系统中的一个资源。那么这里其实就可以知晓,所谓的资源就是某一个文件,只是随着加载策略的不同,读取的文件的路径也不同。
(2) 这个接口的功能是
(3) 通过继承于
在这里也可以看到单一职责原则的好处了吧。注释上涵盖的几条信息就已经描述清楚了接口的职责。
注释还没解释完,但是这里还是要在对上面3个信息多说一些。
所谓资源就是某一个文件,对于这个描述,如果看过源码的人可能就会喷我了。但是如果没有完整的把所有细节都看完,只是看注释来得出这个结论也无伤大雅。随着阅读量的增加必然的会修正早期阅读理解的错误。
其他两段注释描述了两个信息
(1)
(2) 在
在看下
1 2 3 4 5 6 7 8 9 10 | public interface ResourceLoader { String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX; Resource getResource(String location); @Nullable ClassLoader getClassLoader(); } |
先看下
其实到这里,就可以大概猜出来
在调用
四、Resource接口详解
对于Resource接口,其实不用多说,毕竟注释已经写得非常清楚了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | /** * Interface for a resource descriptor that abstracts from the actual * type of underlying resource, such as a file or class path resource. * * <p>An InputStream can be opened for every resource if it exists in * physical form, but a URL or File handle can just be returned for * certain resources. The actual behavior is implementation-specific. * */ public interface Resource extends InputStreamSource { boolean exists(); default boolean isReadable() { return exists(); } default boolean isOpen() { return false; } default boolean isFile() { return false; } URL getURL() throws IOException; URI getURI() throws IOException; File getFile() throws IOException; default ReadableByteChannel readableChannel() throws IOException { return Channels.newChannel(getInputStream()); } long contentLength() throws IOException; long lastModified() throws IOException; Resource createRelative(String relativePath) throws IOException; @Nullable String getFilename(); String getDescription(); } |
通过注释总结下来的有几点。
(1)
(2) 资源不一定是一个文件,可以是个网页,也可以只是远程对象存储的一个对象(Spring的实现里面没有,但不妨碍我们自己封装一个Resource的子类)。
(3) 大部分资源都可以获取资源的
1 2 3 4 5 | public interface InputStreamSource { InputStream getInputStream() throws IOException; } |
我们已经解释了2个接口,并且提到了一个
五、Resource和ResourceLoader的使用
先看下
我们随便找几个实现类试下,具体实现类不用解释,类型表述的很清楚了。
1 2 3 4 5 6 | Resource fileSystemResource = new FileSystemResource("/Users/wangzedong/Documents/java-project/study/src/main/resources/bean.xml"); assert fileSystemResource.exists(); Resource classPathResource = new ClassPathResource("bean.xml"); assert classPathResource.exists(); Resource urlResource = new UrlResource("https://www.baidu.com"); assert urlResource.exists(); |
其实在使用Spring框架的过程中,如果也有读取资源的情况,不妨用
我们来写段代码试下
1 2 3 4 5 6 7 8 9 10 | DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); Resource resource = resourceLoader.getResource("/Users/wangzedong/Documents/java-project/study/src/main/resources/bean.xml"); assert resource instanceof FileSystemResource; assert resource.exists(); resource = resourceLoader.getResource("bean.xml"); assert resource instanceof ClassPathResource; assert resource.exists(); resource = resourceLoader.getResource("https://www.baidu.com"); assert resource instanceof UrlResource; assert resource.exists(); |
前两个断言是执行不过去的。后四个是没有问题的,这和
六、DefaultResourceLoader的实现
下面是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | @Override public Resource getResource(String location) { Assert.notNull(location, "Location must not be null"); // 第一步 for (ProtocolResolver protocolResolver : this.protocolResolvers) { Resource resource = protocolResolver.resolve(location, this); if (resource != null) { return resource; } } // 第二步 if (location.startsWith("/")) { return getResourceByPath(location); } else if (location.startsWith(CLASSPATH_URL_PREFIX)) { return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); } 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) { // No URL -> resolve as resource path. return getResourceByPath(location); } } } |
(1) 第一步中,先循环协议解析器,默认情况下协议解析器的集合虽然由容量但是里面并没有相关对象
1 | private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4); |
所以可以通过
1 2 3 4 | public void addProtocolResolver(ProtocolResolver resolver) { Assert.notNull(resolver, "ProtocolResolver must not be null"); this.protocolResolvers.add(resolver); } |
其实
1 2 3 4 5 6 7 | @FunctionalInterface public interface ProtocolResolver { @Nullable Resource resolve(String location, ResourceLoader resourceLoader); } |
(2) 第二步判断如果是"/"开头,则使用
1 2 3 | protected Resource getResourceByPath(String path) { return new ClassPathContextResource(path, getClassLoader()); } |
如果是
在这里在看下
1 2 3 | public ClassPathContextResource(String path, @Nullable ClassLoader classLoader) { super(path, classLoader); } |
(3) 第三步比较简单了,判断下是不是url,然后在判断url指向的是文件,还是远程的地址。并创建相应的对象。url解析失败了还会尝试用
到此为止,我们已经把
七、ResourcePatternResolver详解
其实不看注释,只看方法,也能知道是做了读取多个Resource的扩展。并且locationPattern是可以传通配符的。
1 2 3 4 5 6 7 | public interface ResourcePatternResolver extends ResourceLoader { String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; Resource[] getResources(String locationPattern) throws IOException; } |
但为了理解源码作者的意图,略过注释可不是一个好习惯。
注释中没有隐含的信息,所以不做解释。
八、ApplicationContext与ResourceLoader
到了这一步,先不去看
我们把思路回到最初决策模块拆分的那个类图。
到目前为止,已经理解了
我们先写一个demo
1 2 3 | ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml"); Resource resource = context.getResource("bean.xml"); Resource[] resources = context.getResources("bean.xml"); |
因为
通过idea可以看到,具体逻辑在
1 2 3 4 | @Override public Resource[] getResources(String locationPattern) throws IOException { return this.resourcePatternResolver.getResources(locationPattern); } |
在继续在
1 2 3 4 5 6 | /** * Create a new AbstractApplicationContext with no parent. */ public AbstractApplicationContext() { this.resourcePatternResolver = getResourcePatternResolver(); } |
1 2 3 | protected ResourcePatternResolver getResourcePatternResolver() { return new PathMatchingResourcePatternResolver(this); } |
根据构造器和方法,发现
再看下
1 2 3 4 | public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) { Assert.notNull(resourceLoader, "ResourceLoader must not be null"); this.resourceLoader = resourceLoader; } |
看到这里说明
到这里,我们画下相关的类图,理解下到目前为止的源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @startuml interface ResourceLoader interface ResourcePatternResolver interface ApplicationContext interface ConfigurableApplicationContext class DefaultResourceLoader abstract class AbstractApplicationContext { private ResourcePatternResolver resourcePatternResolver; } class PathMatchingResourcePatternResolver ResourceLoader <|.. DefaultResourceLoader: 实现 DefaultResourceLoader <|-- AbstractApplicationContext: 继承 ResourceLoader <|-- ResourcePatternResolver: 继承 ResourcePatternResolver <|-- ApplicationContext: 继承 ApplicationContext <|-- ConfigurableApplicationContext: 继承 ConfigurableApplicationContext <|.. AbstractApplicationContext: 实现 ResourcePatternResolver <|.. PathMatchingResourcePatternResolver: 实现 PathMatchingResourcePatternResolver .. AbstractApplicationContext: 引用 @enduml |
九、PathMatchingResourcePatternResolver详解
具体的关系理清后,我们最后再看下具体的实现类。
直接看
刚进入代码阅读就会发现一个新家伙。
简单来看其实就是一个路径匹配器,因为方法含义很清楚,并且注释也写的很详细。在这就简单描述下每个方法的作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public interface PathMatcher { // 验证路径是否是一个需要匹配的字符串,比如 /**/*.xml。如果不是,只是一个静态路径只需要直接读取即可,无需在判断match boolean isPattern(String path); // 验证path 和模式(patten)字符串是否匹配 boolean match(String pattern, String path); // 这里代表着前缀匹配,和match方法区别是,如果只是字符串后面匹配了,但是前缀不匹配依然会返回false boolean matchStart(String pattern, String path); // 这个方法是提取出匹配的部分字符串 String extractPathWithinPattern(String pattern, String path); // 这个方法是用于提取uri变量的, 直接用注释里的例子 : pattern 为 "/hotels/{hotel}" , // 路径为 "/hotels/1", 则该方法会返回一个 map为 : "hotel"->"1". Map<String, String> extractUriTemplateVariables(String pattern, String path); // 通过path返回一个Comparator, 可以用于排序 Comparator<String> getPatternComparator(String path); // 合并两个模式 String combine(String pattern1, String pattern2); } |
然后我们看下子类
| 通配符 | 描述 |
|---|---|
| ? | 匹配任何单字符 |
| * | 匹配0或者任意数量的字符 |
| ** | 匹配0或者更多的目录 |
| 路径 | 描述 |
|---|---|
| 匹配(Matches)所有在app路径下的.x文件 | |
| 匹配(Matches) /app/pattern 和 /app/pXttern,但是不包括/app/pttern | |
| 匹配(Matches) /app/example, /app/foo/example, 和 /example | |
| 匹配(Matches) /app/dir/file.jsp, /app/foo/dir/file.html,/app/foo/bar/dir/file.pdf, 和 /app/dir/file.java | |
| 匹配(Matches)任何的.jsp 文件 |
了解到这里,我们在回头看下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | @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) // 判断是否有?和*,以及{},根据前面的描述,如果存在模式,则需要对根目录下的所有资源的path进行match 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 /** * 执行到这说明可能是静态字符串,直接找具体的资源就好,或者可能是classpath*:,要找jar的目录 * 当然方法名称中还隐含了其他信息,因为如果仅仅是在当前项目下的查找方法名称直接叫做findClassPathResources就好了 * 我们可以看下doFindAllClassPathResources的实现,其实也包含jar包。 */ return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length())); } } else { // 以下逻辑和上面类似,不在多说 // Generally only look for a pattern after a prefix here, // and on Tomcat only after the "*/" separator for its "war:" protocol. 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 // 这里的ResourceLoader,其实就是AbstractApplicationContext return new Resource[] {getResourceLoader().getResource(locationPattern)}; } } } |
然后我们在看下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | protected Resource[] findPathMatchingResources(String locationPattern) throws IOException { // 获取根路径, 也就是classpath*: String rootDirPath = determineRootDir(locationPattern); // 获取子路径, 也就是*.xml String subPattern = locationPattern.substring(rootDirPath.length()); // 通过根路径在此调用getResources, 其实也就是逻辑了jar里面的根路径, 因为isPattern执行是false了 Resource[] rootDirResources = getResources(rootDirPath); // 结果集合 Set<Resource> result = new LinkedHashSet<>(16); // 遍历所有的路径 for (Resource rootDirResource : rootDirResources) { // 这步什么都没错,只是一个return, 但是方法是protected的,也就是说这里是个模板方法的模式 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); } // 判读是否是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); } return result.toArray(new Resource[0]); } |
看到这里我们基本已经把核心逻辑捋清了,当然还有部分实现逻辑没有往下继续写,但肯定少不了getPathMatcher().match的调用。比较简单的逻辑就不在这里完全把细节写完了。
十、总结
到这里基本已经就把
参考
Spring MVC的路径匹配规则 Ant-style