百摩网
当前位置: 首页 生活百科

spring ioc原理与源码(一文讲清楚SpringIoC)

时间:2023-06-06 作者: 小编 阅读量: 5 栏目名: 生活百科

了解了自定义扩展Bean之后,再理解SpringIoC的过程相信会更加清楚。也可以说控制反转是最终目的,依赖注入是实现这个目的的具体方法。简单来看,结果就是开发者不需要关心new对象的操作了。这样一来,控制权由开发者转变成了第三方框架,这就叫做控制反转。Container负责实例化,配置和组装Bean,并将其注入到依赖调用者类中。Container是管理Spring项目中Bean整个生命周期的管理者,包括Bean的创建、注册、存储、获取、销毁等等。

之前发了一个 Spring IoC 的预热篇 「想要理解 Spring IoC,先要知道如何扩展 Spring 自定义 Bean」,有兴趣的可以看一看,如何在 Spring 中扩展自定义的 Bean,比如 标签中有属性 id 和 name,是如何实现的,我们怎么样能够扩展出一个和 功能类似的标签,但是属性却不一样的功能呢?

了解了自定义扩展 Bean 之后,再理解 Spring IoC 的过程相信会更加清楚。

好了,正文开始。

Spring IoC,全称 Inversion of Control - 控制反转,还有一种叫法叫做 DI( Dependency Injection)-依赖注入。也可以说控制反转是最终目的,依赖注入是实现这个目的的具体方法。

什么叫控制反转

为什么叫做控制反转呢。

在传统的模式下,我想要使用另外一个非静态对象的时候会怎么做呢,答案就是 new 一个实例出来。

举个例子,假设有一个 Logger 类,用来输出日志的。定义如下:

public class Logger {public void log(String text){System.out.println("log:"text);}}复制代码

那现在我要调用这个 log 方法,会怎么做呢。

Logger logger = new Logger();logger.log("日志内容");复制代码

对不对,以上就是一个传统的调用模式。何时 new 这个对象实例是由调用方来控制,或者说由我们开发者自己控制,什么时候用就什么时候 new 一个出来。

而当我们用了 Spring IoC 之后,事情就变得不一样了。简单来看,结果就是开发者不需要关心 new 对象的操作了。还是那个 Logger 类,我们在引入 Spring IoC 之后会如何使用它呢?

public class UserController {@Autowiredprivate Logger logger;public void log(){logger.log("please write a log");}}复制代码

开发者不创建对象,但是要保证对象被正常使用,不可能没有 new 这个动作,这说不通。既然如此,肯定是谁帮我们做了这个操作,那就是 Spring 框架做了,准确的说是 Spring IoC Container 帮我们做了。这样一来,控制权由开发者转变成了第三方框架,这就叫做控制反转。

什么叫依赖注入

依赖注入的主谓宾补充完整,就是将调用者所依赖的类实例对象注入到调用者类。拿前面的那个例子来说,UserController 类就是调用者,它想要调用 Logger 实例化对象出来的 log 方法,logger 作为一个实例化(也就是 new 出来的)对象,就是 UserController 的依赖对象,我们在代码中没有主动使用 new 关键字,那是因为 Spring IoC Container 帮我们做了,这个对于开发者来说透明的操作就叫做注入。

注入的方式有三种:构造方法的注入、setter 的注入和注解注入,前两种方式基本上现在很少有人用了,开发中更多的是采用注解方式,尤其是 Spring Boot 越来越普遍的今天。我们在使用 Spring 框架开发时,一般都用 @Autowired,当然有时也可以用 @Resource

@Autowiredprivate IUserService userService;@Autowiredprivate Logger logger;复制代码

Spring IoC Container

前面说了注入的动作其实是 Spring IoC Container 帮我们做的,那么 Spring IoC Container 究竟是什么呢?

本次要讨论的就是上图中的 Core Container 部分,包括 Beans、Core、Context、SpEL 四个部分。

Container 负责实例化,配置和组装Bean,并将其注入到依赖调用者类中。Container 是管理 Spring 项目中 Bean 整个生命周期的管理者,包括 Bean 的创建、注册、存储、获取、销毁等等。

先从一个基础款的例子说起。前面例子中的 @Bean 是用注解的方式实现的,这个稍后再说。既然是基础款,那就逃不掉 xml 的,虽然现在都用 Spring Boot 了,但通过原始的 xml 方式能更加清晰的观察依赖注入的过程,要知道,最早还没有 Spring Boot 的时候,xml 可以说是 Spring 项目的纽带,配置信息都大多数都来自 xml 配置文件。

首先添加一个 xml 格式的 bean 声明文件,假设名称为 application.xml,如果你之前用过 Spring MVC ,那大多数情况下对这种定义会非常熟悉。

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bean="http://www.springframework.org/schema/c"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd"><bean/></beans>复制代码

通过 <bean> 元素来声明一个 Bean 对象,并指定 id 和 class,这是 xml 方式声明 bean 对象的标准方式,如果你自从接触 Java 就用 Spring Boot 了,那其实这种方式还是有必要了解一下的。

之后通过通过一个控制台程序来测试一下,调用 Logger 类的 log 方法。

public class IocTest {public static void main(String[] args){ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml");Logger logger = (Logger) ac.getBean("logger");logger.log("hello log");}}复制代码

ApplicationContext是实现容器的接口类, 其中 ClassPathXmlApplicationContext就是一个 Container 的具体实现,类似的还有 FileSystemXmlApplicationContext,这两个是都是解析 xml 格式配置的容器。我们来看一下 ClassPathXmlApplicationContext 的继承关系图。

有没有看起来很复杂的意思,光是到 ApplicationContext 这一层就经过了好几层。

这是我们在控制台中主动调用 ClassPathXmlApplicationContext,一般在我们的项目中是不需要关心 ApplicationContext的,比如我们使用的 Spring Boot 的项目,只需要下面几行就可以了。

@SpringBootApplicationpublic class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}}复制代码

但是,这几行并不代表 Spring Boot 就不做依赖注入了,同样的,内部也会实现 ApplicationContext,具体的实现叫做 AnnotationConfigServletWebServerApplicationContext,下面看一下这个实现类的继承关系图,那更是复杂的很,先不用在乎细节,了解一下就可以了。

注入过程分析

继续把上面那段基础款代码拿过来,我们的分析就从它开始。

public class IocTest {public static void main(String[] args){ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml");Logger logger = (Logger) ac.getBean("logger");logger.log("hello log");}}复制代码

注入过程有好多文章都进行过源码分析,这里就不重点介绍源码了。

简单介绍一下,我们如果只分析 ClassPathXmlApplicationContext 这种简单的容器的话,其实整个注入过程的源码很容易读,不得不说,Spring 的源码写的非常整洁。我们从 ClassPathXmlApplicationContext的构造函数进去,一步步找到 refresh() 方法,然后顺着读下去就能理解 Spring IoC 最基础的过程。以下代码是 refresh 方法的核心方法:

@Override public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {// Prepare this context for refreshing.prepareRefresh();// Tell the subclass to refresh the internal bean factory.ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// Prepare the bean factory for use in this context.prepareBeanFactory(beanFactory);try {// Allows post-processing of the bean factory in context subclasses.postProcessBeanFactory(beanFactory);// Invoke factory processors registered as beans in the context.invokeBeanFactoryPostProcessors(beanFactory);// Register bean processors that intercept bean creation.registerBeanPostProcessors(beanFactory);// Initialize message source for this context.initMessageSource();// Initialize event multicaster for this context.initApplicationEventMulticaster();// Initialize other special beans in specific context subclasses.onRefresh();// Check for listener beans and register them.registerListeners();// Instantiate all remaining (non-lazy-init) singletons.finishBeanFactoryInitialization(beanFactory);// Last step: publish corresponding event.finishRefresh();}catch (BeansException ex) {}destroyBeans();cancelRefresh(ex);throw ex;}finally {resetCommonCaches();}} }复制代码

注释都写的非常清楚,其中核心注入过程其实就在这一行:

ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();复制代码

我把这个核心部分的逻辑调用画了一个泳道图,这个图只列了核心方法,但是已经能够清楚的表示这个过程了。

题外话:关于源码阅读 大部分人都不太能读进去源码,包括我自己,别说这种特别庞大的开源框架,就算是自己新接手的项目也看不进去多少。读源码最关键的就是细节,这儿说的细节不是让你抠细节,恰恰相反,千万不能太抠细节了,谁也不能把一个框架的所有源码一行不落的全摸透,找关键的逻辑关系就可以了,不然的话,很有可能你就被一个细节搞到头疼、懊恼,然后就放弃阅读了。

有的同学一看图或者源码会发现,怎么涉及到这么多的类啊,这调用链可真够长的。没关系,你就把它们当做一个整体就可以了(理解成发生在一个类中的调用),通过前面的类关系图就看出来了,继承关系很复杂,各种继承、实现,所以到最后调用链变得很繁杂。

简单概括

那么简单来概括一下注入的核心其实就是解析 xml 文件的内容,找到 元素,然后经过一系列加工,最后把这些加工后的对象存到一个公共空间,供调用者获取使用。

而至于使用注解方式的 bean,比如使用 @Bean、@Service、@Component 等注解的,只是解析这一步不一样而已,剩下的操作基本都一致。

所以说,我们只要把这里面的几个核心问题搞清楚就可以了。

BeanFactory 和 ApplicationContext 的关系

上面的那行核心代码,最后返回的是一个 ConfigurableListableBeanFactory对象,而且后面多个方法都用这个返回的 beanFactory 做为参数。

BeanFactory 是一个接口,ApplicationContext 也是一个接口,而且,BeanFactory 是 ApplicationContext的父接口,有说 BeanFactory才是 Spring IoC 的容器。其实早期的时候只有 BeanFactory,那时候它确实是 Spring IoC 容器,后来由于版本升级扩展更多功能,所以加入了 ApplicationContext。它们俩最大的区别在于,ApplicationContext 初始化时就实例化所有 Bean,而BeanFactory 用到时再实例化所用 Bean,所以早期版本的 Spring 默认是采用懒加载的方式,而新版本默认是在初始化时就实例化所有 Bean,所以 Spring 的启动过程不是那么快,这是其中的一个原因。

BeanDefinition 保存在哪儿

上面概括里提到保存到一个公共空间,那这个公共空间在哪儿呢?其实是一个 Map,而且是一个 ConcurrentHashMap ,为了保证并发安全。它的声明如下,在 DefaultListableBeanFactory 中。

private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256)复制代码

其中 beanName 作为 key,也就是例子中的 logger,value 是 BeanDefinition 类型,BeanDefinition 用来描述一个 Bean 的定义,我们在 xml 文件中定义的 元素的属性都在其中,还包括其他的一些必要属性。

向 beanDefinitionMap 中添加元素,叫做 Bean 的注册,只有被注册过的 Bean 才能被使用。

Bean 实例保存在哪儿

另外,还有一个 Map 叫做 singletonObjects,其声明如下:

/** Cache of singleton objects: bean name to bean instance. */private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);复制代码

在 refresh() 过程中,还会将 Bean 存到这里一份,这个存储过程发生在 finishBeanFactoryInitialization(beanFactory) 方法内,它的作用是将非 lazy-init 的 Bean 放到singletonObjects 中。

除了存我们定义的 Bean,还包括几个系统 Bean。

例如我们在代码中这样调用:

ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml");StandardEnvironment env = (StandardEnvironment) ac.getBean("environment");复制代码

使用已注册的 Bean

在这个例子中,我们是通过 ApplicationContext 的 getBean() 方法显示的获取已注册的 Bean。前面说了我们定义的 Bean 除了放到 beanDefinitionMap,还在 singletonObjects 中存了一份,singletonObjects 中的就是一个缓存,当我们调用 getBean 方法的时候,会先到其中去获取。如果没找到(对于那些主动设置 lazy-init 的 Bean 来说),再去 beanDefinitionMap 获取,并且加入到 singletonObjects 中。

获取 Bean 的调用流程图如下

以下是 lazy-init 方式设置的 Bean 的例子。

<beanlazy-init="true"/>复制代码

如果不设置的话,默认都是在初始化的时候注册。

注解的方式

现在已经很少项目用 xml 这种配置方式了,基本上都是 Spring Boot,就算不用,也是在 Spring MVC 中用注解的方式注册、使用 Bean 了。其实整个过程都是类似的,只不过注册和获取的时候多了注解的参与。Srping 中 BeanFactory和ApplicationContext都是接口,除此之外,还有很多的抽象类,使得我们可以灵活的定制属于自己的注册和调用流程,可以认为注解方式就是其中的一种定制。只要找到时机解析好对应的注解标示就可以了。

但是看 Spring Boot 的注册和调用过程没有 xml 方式的顺畅,这都是因为注解的特性决定的。注解用起来简单、方便,好处多多。但同时,注解会割裂传统的流程,传统流程都是一步一步主动调用,只要顺着代码往下看就可以了,而注解的方式会造成这个过程连不起来,所以读起来需要额外的一些方法。

Spring Boot 中的 IoC 过程,我们下次有机会再说。

    推荐阅读
  • 妙可蓝多是真的吗(妙可蓝多问题多)

    目前,上述股权转让已完成股份交割变更登记工作。与此同时,妙可蓝多正陷入监管风波。妙可蓝多表示,若监管部门对上述资金占用问题启动纪律处分,则此次发行存在终止风险。根据公告,该预案也将于4月10日举办的临时股东大会上审议。虽然妙可蓝多对此予以否认,但并未能完全消除市场质疑。柴琇家族生意的问题在2019年集中爆发。根据wind数据显示,截至目前,柴琇未解押股权质押数量为7200万股,占其持有股份96.43%。虽未能获悉柴琇家族

  • 徐诗晓个人资料简介(徐诗晓个人介绍)

    徐诗晓个人资料简介徐诗晓,1992年2月16日出生,中国皮划艇运动员,效力于江西省水上运动管理中心。2021年,徐诗晓入选2020年东京奥运会中国体育代表团皮划艇项目运动员名单。2017年第十三届全运会女子200米单人划艇、女子500米双人划艇双料冠军。2019年,被国家体育总局授予国际级运动健将称号。2021年8月7日,2020东京奥运会女子500米双人划艇半决赛中,中国组合徐诗晓/孙梦雅以2分01秒369的成绩获小组第一,强势挺进决赛。

  • 财务杠杆原理的七个公式(杠杆原理-运用财务杠杆带来大收益)

    杠杆常常用“倍”来表示大小。在股票、房价疯涨的时候,许多人恨不得把杠杆用到100倍以上,这样才能回报快,一本万利;而当股票、房价大幅下跌的时候,杠杆的放大效应迫使很多人把股票和房子以低价卖出。而当人们把股票和房子低价卖出时,则造成了更多的家庭资不抵债,被迫将资产以更低价出售,从而造成恶性循环,导致严重的经济危机。

  • 室内养花技巧与方法(室内养花技巧与方法介绍)

    室内养花技巧与方法了解所养花草习性。对于高温天气,不要选择在下午两三点钟浇水,最好是选择在清晨浇水,花草才更容易吸收到充足的水份。另外,最好不要用喷头喷水,因为这样根本不能让植物根部吸收水分,时间一长,反而容易枯萎。花草的成长,是离不开阳光的,万物生长靠太阳,但是花草有喜阳和喜阴之区分。而对于月季花来说,虽然对土壤要求不高,但要经常施肥才可。

  • 杭州暑假带小孩去哪里(在杭州带小孩玩一天去哪里)

    沉浸式航空航天科普体验——埋下星辰大海的种子前段时间,中国航天员登上自己的空间站了,看得小编那叫一个心潮澎湃!在杭州也有一个可以探索太空的地儿,那就是北航杭州创新研究院。

  • 女生日常护肤知识大全(美容护肤知识大全护肤小常识)

    护肤小窍门二:酸奶蜂蜜保湿将酸奶和蜂蜜取同样的分量混合均匀,均匀的涂抹在脸部,15分钟后洗净,肌肤会变得非常光滑水嫩,还能持久锁住肌肤角质层的水分。

  • 油菜苗怎么做好吃(油菜苗怎样做好吃)

    油菜苗怎么做好吃把菜花苗的根部给一一掐掉,当然用剪刀剪掉也可以,用剪刀剪去根部的。把摘干净的菜花,泡在清水里清洗几遍,放在一个盆子里面备用,舀一勺面粉倒进盆子里面,加食盐和菜花苗一起抓均匀。先用手轻轻地把面粉和菜花苗抓摸均匀,然后再撒上一层面粉,再多抓几遍。起锅添上清水,我们再把抓好的菜花苗铺在蒸笼上面,盖上锅盖蒸20分钟,蒸菜的时间不能一概而论,看蒸菜花苗的多少,不过一般20分钟就可以了。

  • 专利与商业秘密差异到底有怎样的体现

    商业秘密等知识产权可申请紧急保全。专利是国家授予的一项合法垄断权,通过《专利法》保护,目的侧重于鼓励创新。),核心技术信息所有人自行采取保密措施。此外,商业秘密没有期限限制,只要拥有者一直持续把某信息当商业秘密对待,该信息从产生之日起立即生效,而且可以一直持续拥有商业秘密的身份,而无需去向任何机构申请登记也无需缴纳任何费用。(五)公开后果不同专利的取得是以公开为前提,是通过公开后取得权利保护。

  • 新媒体文案(新媒体文案是指一种广告表现形式还是一种职业称呼?)

    接下来我们就一起去了解一下吧!新媒体文案新媒体文案是广告的一种表现形式:文案来源于广告行业,是“广告文案”的简称,也是企业为达成商业目的的表现形式。新媒体文案又是一种职业的称呼:作为职业出现,文案的英文词为copy-writer,译作文案写手,指的是专门创作广告文字的工作者。新媒体平台包括:社交类手机应用:QQ、微信、微博等。新闻资讯类应用:今日头条、网易新闻、腾讯新闻等。视频娱乐类应用:优酷、哔哩哔哩、爱奇艺等。

  • 微博小号怎么注册(注册微博小号的方法)

    下面希望有你要的答案,我们一起来看看吧!微博小号怎么注册注册微博小号跟注册普通的微博id是一样的。具体的方法如下:打开微博网页版,点击“立即注册”,可以使用手机号注册,一个手机号只能注册一个微博账号。完成信息填写后,点击“立即注册”。