一说到Spring Ioc,我们很多小伙伴很本能的想到了在开发时候,我们在一个类上加上诸如@Component
之类的注解,然后再在另外一个同样加着注解的类中用@Autowired之类的注解去引用就好了。那么这样子编程有什么好处呢?我们一起看看下面的代码,注意看注释部分
image
image
BService
,和CService
的代码和AService
的代码一样,绿色下划线的不用管,那是作者自己配置的sonarLint在提示field从未被使用过。再看看有了Spring IOC的吧,虽然大家都知道,但是我还是贴出来:
image
其他的类也就加上了@Component注解而已,代码很简单,大家应该一下就能看明白,引入Ioc之后,我们不再需要关心对象和对象之间的依赖关系,开发的时候只需要在需要的类上标上注解,然后使用的时候用注解引用即可。我们称这种好处为解耦,即不再需要关注对象和对象之间耦合的关系。同时,我们的代码也变得简洁很多,比如上面的图中,doSomeThing这个方法只需要关注自己的业务逻辑即可,无需再关注复杂的对象创建过程。
很多人依然不知道这个不需要关注对象和对象之间耦合关系意味着什么。面向对象编程的痛点其实就是对象和对象之间的耦合关系,在早些时候的大型单体项目里面,类的扩展往往是通过设计模式来搞,但是引入了设计模式之后,类和类的关系变得复杂,这导致了别人在用的时候,需要先理清楚这个类和它所依赖的类之间的关系,然后才可以去创建对象,而且一旦依赖关系发生了改变,很多地方其他人使用你这玩意的代码也要变,这就是个很悲剧的事情。当然,也不一定是因为设计模式导致类和类关系变的复杂,也可能是别的原因。现在因为人们都在玩微服务了,业务类之间的依赖关系由于服务拆分的原因,一般都不会太复杂。
但是这背后的,Spring为我们做了什么呢?这是一个很复杂的流程,也是本系列文章要为小伙伴们讲清楚的东西,除此之外,还有在这个加载过程中一些可以扩展的点,争取能让大家对spring有更深入的认识。
本系列文章是基于spring 4.0
版本,本文中所使用的源码在这里https://github.com/BillBillLi/spring-framework-study
.
这是我fork的spring-framework
的项目,只不过在里面都加上了我自己的注释,如果看文章里的代码块看不明白,你可以把它clone
到本地,然后对着里面的代码来看,当然你应该切换到4.0.x
的分支上。
在开始之前,我们先来看一段熟悉的代码:
image
application-context
文件内容如下:
image
整个过程就是创建了一个Ioc 容器,然后根据类型获取了xml里面定义的对象而已。接下来,我们先以使用xml配置的这种场景(注解的会随后讲),来讲述下xml是怎么被加载,解析。画外音:xml这种配置方式现在确实已经不怎么使用,但是我们先顺着这一条路下去讲通,然后其他的场景也会容易理解很多。
我们很容易可以想到,使用xml配置的情况下,Spring需要先加载解析我们的xml,在Spring的世界里,把类似于xml这样的东西定义为Resource
,对应的接口为Resource.java
,代表了Spring对资源的统一抽象,我们来看看它的代码:
image
画外音
:后边还会贴很多这种最顶层的接口,目的是为了让大家理解,这个接口的实现类的最核心的行为,比如说对于Resource,最核心的行为就是getFile。我们通过方法名字可以可以看出这些方法是想让它的实现类来做什么的,在这里我就不一一解释。然后我们一起来看看Resource类及其实现类的结构图(来自网络):
我们可以看出来,根据资源类型的不同,Spring为我们提供了不同的实现。其中,AbstractResource是Resource的默认实现,它实现了Resource的大部分接口,这个类也是Resource体系中的重中之重,大部分的子类也是继承的它的方法。如果我们想要定义一种资源,那么一定是继承AbstractResource类,然后根据我们的资源的特点,去覆盖它的方法。
Resource这里说完了,然后我们来说Resource的加载。在Spring里,Resource和它的加载是分开的,我想这么设计的道理大家肯定都懂,一方面是单一职责的原则嘛,各个部分只需要专注于自己的事情就好,另外一方面其实就是面向对象思想,比如说吃饭这个过程按照面向对象的思想来设计的话,那就是饭和吃两个接口,饭就像Resource,而吃就像加载Resource的接口。在Spring中,这个加载Resource的接口是ResourceLoader
,还是按照惯例,贴一下它的源码:
image
这个接口的代码非常简单,只有两个方法,其中最重要的当然是getResource这个方法,其实现类往往是通过这个方法定位到具体的资源实例。它的默认实现是DefaultResourceLoader,当然最核心的方法依然是getResource这个方法,我们一起来看看:
image
我们通过上面的代码可以看出来一个问题,就是如果localtion
是诸如D:/abc/cde/fgh这种格式的时候,它会被解析成一个ClassPathContextResource
(因为显然location不属于Url,在 URL url = new URL(location)
的时候会抛MalFormedURLException),这个显然不是我们想要的,我们更希望,这个可以被解析成一个FileSystemResource。这时候,我们可以使用FileSystemResourceLoader中的getResource方法来获取,具体用法就不在这里提了。
上面的ResourceLoader中的getResource方法每次只能加载一个Resource,如果要同时加载某个文件夹下面的所有Resource的话,需要使用到ResourcePatternReslover相关的的实现类,我们还是先来看看ResourcePatternReslover的源码:
image
我们可以看到这里只有一个getResources方法,可以根据传入的路径来匹配,返回多个资源。注意看哈,其实这个接口还继承了ResourceLoader
这个接口,所以其实它的子类,也是具备ResourceLoader
的功能的。同时,ResourcePatternReslover
还增加了一种新的协议前缀classpath*,对这一点的支持也是由相应的子类来实现的。它最常用的一个子类是PathMatchingResourceReslover
,它能够像ResourceLoader
一样加载资源(但是注意,这里它具备ResourceLoader的功能,完全是通过一个委派来实现的)。这个类还支持基于Ant风格的路径匹配模式(**/*.xml之类的,也就是匹配某个路径下的资源,然后加载)。关于PathMatchingResourceReslover
的getResources
方法,由于篇幅原因,就不具体讲了,大家可以直接去我的github里面的代码看源码,还能锻炼自己阅读复杂代码的能力,这一点是很重要的。大家不用害怕,这个方法即使不看不会影响大家阅读后边的文章~?
在看到这块之前,希望大家是可以搞明白上面到底讲了什么的(你最少要明白Resource
、ResourceLoader
、PathMatchingResourceReslover
是干嘛的),如果还不明白,建议先滚动到之前的地方看一看,如果没看明白的话,接下来的东西看了也不会理解的。我们这篇文章的题目是统一资源的加载,那么是谁的统一资源的加载呢?答案其实就是ApplicationContext
,之所以没有在开始就说这玩意,是因为这ApplicationContext
的东西太多了,不想让大家过多关注了这个,现在我们可以来看看部分源码了:
image
What??说好的源码你就让我看个类名?别急?,我们看这个接口所继承的接口的最后一个,原来这个ApplicationContext
继承了ResourcePatternReslover
接口了啊,而我们前面的文章里面提过ResourcePatternReslover
它又继承了ResourceLoader
接口。这里需要多提一个关于ApplicationContext
的点是一般来说,各种各样场景的AplicationContext
比如最开始代码的ClassPathXmlApplicationContext
都是直接或者间接的继承AbstractApplicationContext
,而这个AbstractApplicationContext
它又继承了DefaultResourceLoader
,所以说ApplicationContext
子类的getResource
方法,其实就是DefaultResourceLoader
方法,然后需要加载多个资源时,AbstractApplicationContext
里面还有一个ResourcePatternReslover
,而它的实例就是PathMatchingResourcePatternReslover
。也就是说,在需要进行资源加载的时候,ApplicationContext
的实现类们完全就可以作为一个ResourcePatternReslover
或者是ResourceLoader
来进行资源的加载,只不过在干活的时候,还是交给具体的ResourceLoader
以及ResourcePatternReslover
的实例来的。我们可以看一看UML图(来自揭秘Spring):
image