Spring详解 —— IoC Container(上)

872 阅读11分钟

前言

Spring最早的时候,只是特指Spring Framework,但是经过这么多年的发展,Spring已经膨胀为了一个庞然大物,我们谈起Spring,最多指的是Spring的生态圈,里面包含了整个Spring家族,但是它虽然庞大,却不臃肿,使用者可以在Spring的生态圈中按需选择,而不必将它所有的依赖全部引入。而Spring可以做到这一点,全部依赖于它优秀的设计。
这个系列文章我们主要来讲讲Spring Framework中的一些优秀的代码、设计以及设计目的。

Spring Framework 设计原则

我们在学习一个框架前,首先要了解它的设计理念,或者说设计原则,Spring Framework的设计原则有以下5条:

  • Provide choice at every level. Spring lets you defer design decisions as late as possible. For example, you can switch persistence providers through configuration without changing your code. The same is true for many other infrastructure concerns and integration with third-party APIs.

提供各个级别的选择。Spring让你可以尽量延迟你的设计决策,比如不改变你的代码,而不通过修改配置文件来切换持久化功能。其它基础功能和集成的三方框架也是一样。

  • Accommodate diverse perspectives. Spring embraces flexibility and is not opinionated about how things should be done. It supports a wide range of application needs with different perspectives.

多角度兼容。Spring拥抱变化(具有灵活性)且不会强制要求你应该怎么做。支持不同视角的广泛的兼容性。

  • Maintain strong backward compatibility. Spring’s evolution has been carefully managed to force few breaking changes between versions. Spring supports a carefully chosen range of JDK versions and third-party libraries to facilitate maintenance of applications and libraries that depend on Spring.

持续且强大的向后兼容性。Spring精心管理它的演进(升级)来避免不同版本间的重大差异。Spring支持一部分精心挑选出来的JDK版本和三方库以便于维护依赖于Spring的应用和库。

  • Care about API design. The Spring team puts a lot of thought and time into making APIs that are intuitive and that hold up across many versions and many years.

重视API设计。Spring的团队花了大量的时间和精力来设计直观且可以经过多个版本和长时间考验的API。

  • Set high standards for code quality. The Spring Framework puts a strong emphasis on meaningful, current, and accurate javadoc. It is one of very few projects that can claim clean code structure with no circular dependencies between packages.

代码质量高标准。Spring框架强调有意义、最新以及含义准确的javadoc。Spring框架可以自豪的说,它是极少具有代码结构简洁并且包之间没有循环依赖的项目之一。

简单的来说,Spring的设计原则主要就五点:可扩展性、兼容性、可维护性、用户友好的API设计、高质量的代码。整个Spring Framework都是基于以上五个原则来设计和实现的。

IoC Container

IoC ContainerSpring Framework的核心、基石,没有它,Spring Framework将失去自己的骨架和灵魂。

Spring Framework的核心是什么?其实就是Bean,就和Java是一门面向对象的语言一样,Spring Framework是一个面向Bean的框架。它所有的功能几乎都是通过对Bean的管理、增强等操作来实现。想想Spring Framework最初推出时的核心功能是什么? 是依赖反转/依赖注入机制(EJB中也有实现),它解决了一个很关键的问题,让对象间的依赖关系通过配置文件来关联。就像上面的设计原则第一条所说的,将你的设计决策延迟,最终使用哪种实现方式,不需要在写代码时就确定。 而这些Bean是如何进行管理的呢? 就是通过IoC Container,来管理Bean之间的各种关系和状态。

从代码结构上来说的话,IoC Container主要功能在:

  • srping-beans
  • spring-context
  • spring-core 这三个模块中体现,这三个模块也是Spring Framework最初设计时最核心的三个包

spring-beans

上面说到Bean是Spring Framework的核心,而这个模块下就主要解决了三件事情:Bean的定义、创建和解析。

BeanFactory

Bean的创建采用典型的工厂方法模式,它的核心接口大家应该耳熟能详: BeanFactory

上面是它的核心类图(剔除了ApplictionContext相关的接口和类,这块后续再说),可以看到最终的实现类是DefaultListableBeanFactory,为什么要定义这么多的接口和抽象类? 实现类和顶层接口BeanFactory之间隔了4层,简单一点不好吗? 通过接口名称和对应的注释,我们可以看到不同的接口实际上对应着不同的场景,比如ListableBeanFactory意味着实现了此接口的工厂可以遍历它所有的Bean,HierarchicalBeanFactory代表实现它的工厂是存在继承关系的,即可能有Parent FactoryAutowireCapableBeanFactory代表它具备自动装配Bean的功能。这么多类它们其实是共同定义了Bean的集合、关系以及行为。

设计模式的六大原则里面有一条:接口隔离原则(interface segregation principle):建立单一的接口,客户端不应该依赖它不需要的接口并且 类之间的依赖应该建立在最小的接口上。
怎么理解呢? 即我们在创建一个接口时,不要创建那种大而全的接口,而应该根据功能它他们拆分为多个独立且简单的接口,客户端在使用时只需要依赖它所需要的接口即可。

接口的设计粒度越小,系统就会越灵活,但是相应的,系统结构的复杂度也会越高,开发成本和维护成本会增加

bean注册

我们知道在配置Bean时,可能会有多种方式,有可能是通过xml配置,也可能是通过注解或其它方式进行配置。IoC Container在读取这些配置时,肯定是采取不同的工具,但是这些工具读取出来的结果必须是统一的,这样才可以交给BeanFactory进行初始化。

BeanFactory只有一个作用,就是生产Bean,它并不关心获取Bean之间的差异以及它们来自哪里。

这个统一的结果就是BeanDefiniton,不管通过什么方式获取到Bean,最终都需要转换成BeanDefinition,再交给BeanFactory进行处理。 DefaultListableBeanFactory同时也实现了BeanDefinitionRegistry接口,不同的ApplicationContext最终都需要生成BeanDefinition对象,然后通过BeanDefinitionRegistryregisterBeanDefinition来注册Bean,实际上就是把beanName作为key,BeanDefinition作为value存在了一个map中。然后BeanFactory再通过getBean方法创建bean并放入容器中。

上面这种实现方式是不是和适配器模式有点类似? 目标接口只接收BeanDefinition,每种不同的配置方式都实现自己的适配器,将它要处理的内容转换为BeanDefinition的实现类。之后再增加新的配置方式,也只需要增加新的适配器即可,不需要对原代码进行修改。

知道了Bean的注册过程,那我们在使用Spring Framework过程中,如果想要动态的创建一个Bean就十分简单了:

    @Resource
    private ApplicationContext ctx;

    public DynamicBean create() {
        ((BeanDefinitionRegistry)ctx).registerBeanDefinition("dynamicBean",
                             BeanDefinitionBuilder
                                .rootBeanDefinition(DynamicBean.class)
                                .addConstructorArgValue("something...")
                                .setRole(BeanDefinition.ROLE_APPLICATION)
                                .setScope(BeanDefinition.SCOPE_SINGLETON)
                                .getBeanDefinition());
        return (DynamicBean) ctx.getBean("dynamicBean");
    }

也可以通过BeanProcessor:

public class DynamicBeanCreator implements BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor {

    /***
    * 通过实现BeanFactoryPostProcessor,将factory转换为BeanDefinitionRegistry来注册bean
    */
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        BeanDefinitionRegistry registry = (BeanDefinitionRegistry) configurableListableBeanFactory;
        registry.registerBeanDefinition("dynamicBean", BeanDefinitionBuilder...getBeanDefinition());
    }

    /**
    * 通过实现BeanDefinitionRegistryPostProcessor, 可以直接获取BeanDefinitionRegistry来注册bean
    */
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        beanDefinitionRegistry.registerBeanDefinition("dynamicBean", BeanDefinitionBuilder...getBeanDefinition());
    }
}

bean的初始化

在BeanDefinition被注册之后,就可以初始化这个bean了,调用BeanFactory的getBean方法会返回对应的bean实例,如果不存在,则在getBean时会尝试初始化这个bean,下面我们看一下具体的实现

  1. 我们前面知道BeanFactory的最终实现是DefaultListableBeanFactory,调用getBean方法,经过多层重载后,会调用resolveBean方法,可以看到如果从当前的beanFactory中获取不到bean会从parent或objectProvider实现中尝试获取。 下图精简了部分代码

  2. 接下来就到resolveNamedBean方法,这里主要是实现了确定唯一bean的逻辑,如果只有一个唯一的bean,那么会进入真正的创建bean的逻辑,如果有多个,那么会先根据 @Primary 来查找唯一的bean;如果不存在 @Primary,则会根据 @Order 优先级来取优先级最高的bean;如果都不存在,那么就会抛出NoUniqueBeanDefinitionException异常:我们一般见到的"expected single matching bean but found 2...异常就是在这里产生的。 下图精简了部分代码

  3. 下面进入到创建bean的逻辑,resolveNamedBean方法中调用了父类AbstractBeanFactorygetBean方法,这里主要实现了被@DependsOn注解标识的依赖递归初始化,同时通过三个map缓存来解决了循环依赖的问题,后续会详细解释。 最终不管是singleton还是prototype,最终都会进入到createBean方法。 下图精简了部分代码

  4. createBean方法中给出了bean初始化前的一个扩展点,可以能通过实现InstantiationAwareBeanPostProcessor接口来实现自己的初始化bean逻辑,然后进入doCreateBean下图精简了部分代码

  5. doCreateBean实现了bean的最终初始化和发布,主要做了三件事:

    1. 实例化bean对象
    2. 初始化bean的属性并赋值
    3. 初始化bean,主要是执行一系列的扩展点,包括我们经常用的InitialazingBeaninit方法,各种BeanAware接口以及BeanProcessor下图精简了部分代码

上面一系列的扩展点的执行,有点像是callback、listener和observer模式的混合使用(前两者不属于GoF23),只要一个bean实现了对应的接口,那么这个bean就可以在某些特定的时间点被IoC Container通过 callback 方法通知到。比如在bean的属性被设置之后,会扫描获取所有实现了InitialazingBean接口的bean,然后调用他们的 callback 方法afterPropertiesSet(), 还有各种Aware接口等等。 Spring Framework中大量的扩展点都是通过这种方式来实现的。

循环依赖处理

上面说到了spring的循环依赖是依靠三个map缓存来处理的,分别是:

  1. singletonObjects 存放已经初始化完成的bean对象
  2. earlySingletonObjects 存在未初始化完成的bean对象(polulateBean还未执行完成)
  3. singletonFactories 存在bean的工厂对象

那么spring是如何依赖这三个map来解决循环依赖的问题的呢?假设A、B两个对象互相依赖 A -> B -> A

  1. 在初始化A时(getBean(A)),在初始化属性(populateBean())之前,会先将自己的工厂对象放入singletonFactories中(addSingletonFactory()方法)
  2. populateBean时发现依赖了A,开始初始化A(getBean(A))
  3. 在初始化A时,先从缓存中获取:
    1. 先从singletonObjects中获取,如果存在,直接返回
    2. 如果A处于创建状态中(InCreation),则会依次从earlySingletonObjectssingletonFactories中获取
    3. 发现在singletonFactories中存在,则通过工厂bean获取半成品bean,将它放入earlySingletonObjects中,并从singletonFactories中移除
  4. 初始化A成功(半成品),将A引用赋值给B,B创建成功,将B放入singletonObjects
  5. A创建成功,将A放入singletonObjects中,并从earlySingletonObjects中移除

便于理解,我简化了一下getBean的过程,并且只使用一个缓存(这是有问题的,这里只是为了演示循环依赖的处理过程):

FactoryBean

FactoryBean和普通bean不同的地方在于,它是一个工厂bean,它的getObject方法可以返回或创建某个类型的bean实例。简单点来说,在初始化bean完成之后,如果是一个普通bean,就直接返回了,如果是一个实现了FactoryBean接口的bean,那么它会返回getObject方法返回的对象。

那么这东西有什么用? 为什么要多此一举,直接返回不就行了吗?
它其实是提供了一个让我们可以自定义bean实现的方式,你可以在getObject方法的实现中,返回你自己创建的bean,同时它也可以对bean来做增强,就是代理。

举个栗子: Spring Framework本身实现了很多FactoryBean对象,比如TransactionProxyFactoryBean,返回的是一个被事务包裹的代理类, ListFactoryBean在返回对象前会根据泛型对value做类型转换(需要有必要的话)。

总结

上面主要讲到了BeanFactory创建bean的主要流程,以及它涉及到的一些设计模式和方法。在阅读Spring Framework的代码的时候,可能会不太理解某些设计,但是如果我们能够换个角度,以使用者的角度去看的话,可能会有不同的理解。 或者我们也可以按自己的方式来思考或实现那段逻辑,也许在思考或实现的过程中,可以真正的理解它的设计目标。

问题1: 为什么prototype模式和构造器无法处理循环依赖?
问题2: 处理循环依赖时,为什么需要三个缓存map,一个可以吗? 二个可以吗?

如果你喜欢这篇文章,请点个赞支持一下,谢谢~