《包你懂系列》一文讲清楚 Spring IoC 实现原理和过程

我是风筝,公众号「古时的风筝」,一个不仅有技术的技术公众号,一个在程序圈混迹多年,主业 Java,另外 Python、React 也玩儿的 6 的斜杠开发者。 Spring Cloud 系列文章已经完成,能够到 个人github 上查看系列完整内容。也能够在公众号内回复「pdf」获取我精心制做的 pdf 版完整教程。git

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

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

好了,正文开始。spring

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

什么叫控制反转

为何叫作控制反转呢。安全

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

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

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

那如今我要调用这个 log 方法,会怎么作呢。app

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

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

而当咱们用了 Spring IoC 以后,事情就变得不同了。简单来看,结果就是开发者不须要关心 new 对象的操做了。仍是那个 Logger 类,咱们在引入 Spring IoC 以后会如何使用它呢?

public class UserController {
 @Autowired private 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

@Autowired
private IUserService userService;
 @Autowired private 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/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
 <bean id="logger" class="org.kite.spring.bean.Logger" /> </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 的项目,只须要下面几行就能够了。

@SpringBootApplication
public 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 也是一个接口,并且,BeanFactoryApplicationContext的父接口,有说 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 的例子。

<bean id="lazyBean" lazy-init="true" class="org.kite.spring.bean.lazyBean" />
复制代码

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

注解的方式

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

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

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

获取本文高清泳道图请在公众号内回复「矢量图」

感受还好给个赞吧,老是被白嫖,身体吃不消!

参考文档:

https://docs.spring.io/spring/docs

我是风筝,公众号「古时的风筝」,一个在程序圈混迹多年,主业 Java,另外 Python、React 也玩儿的很 6 的斜杠开发者。能够在公众号中加我好友,进群里小伙伴交流学习,好多大厂的同窗也在群内呦。

技术交流还能够加群或者直接加我微信。

相关文章
相关标签/搜索