前言

  1. Spring框架管理这些Bean的创建工作,即由用户管理Bean转变为框架管理Bean,这个就叫控制反转 - Inversion of Control (IoC)
  2. Spring 框架托管创建的Bean放在哪里呢? 这便是Spring容器(IoC Container)
  3. Spring 框架为了更好让用户配置Bean,必然会引入不同方式来配置Bean? 这便是xml配置,Java配置,注解配置等支持
  4. Spring 框架既然接管了Bean的生成,必然需要管理整个Bean的生命周期
  5. 应用程序代码从Ioc Container中获取依赖的Bean,注入到应用程序中,这个过程叫 依赖注入(Dependency Injection,DI) ; 所以说控制反转是通过依赖注入实现的,其实它们是同一个概念的不同角度描述。通俗来说就是IoC是设计思想,DI是实现方式
  6. 在依赖注入时,有哪些方式呢?这就是构造器方式,**@Autowired, @Resource, @Qualifier**… 同时Bean之间存在依赖(可能存在先后顺序问题,以及循环依赖问题等)

如何理解IoC

Spring Bean是什么

IoC Container管理的是Spring Bean, 那么Spring Bean是什么呢?

Spring里面的bean就类似是定义的一个组件,而这个组件的作用就是实现某个功能的,这里所定义的bean就相当于给了你一个更为简便的方法来调用这个组件去实现你要完成的功能。

IoC是什么

Ioc—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。

  • 谁控制谁,控制什么?
    传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建。
    谁控制谁?当然是IoC 容器控制了对象。
    控制什么?那就是主要控制了外部资源获取(不只是对象,包括比如文件等)。
  • 为何是反转,哪些方面反转了?
    有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象。
    为何是反转?因为由容器帮我们查找及注入依赖对象,程序只是被动的接受容器中的依赖对象,所以是反转。
    哪些方面反转了?依赖对象的获取方式被反转了。
  • 图例说明
    • 传统程序设计
    • IoC DI

Ioc能做什么

IoC 不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。

传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试。

有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。

IoC和DI是什么关系

控制反转是通过依赖注入实现的,其实它们是同一个概念的不同角度描述。通俗来说就是IoC是设计思想,DI是实现方式。

DI—Dependency Injection,即依赖注入:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。
依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

  • 谁依赖于谁?
    应用程序依赖于IoC容器。
  • 为什么需要依赖?
    应用程序需要IoC容器注入应用程序依赖的对象。
  • 谁注入谁?
    IoC容器将应用程序所依赖的对象注入应用程序中。
  • 注入了什么?
    注入某个对象所需要的外部资源(包括对象、资源、常量数据)。
  • IoC和DI有什么关系?
    “依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。
    通俗来说就是IoC是设计思想,DI是实现方式。

IoC和DI的使用

IoC配置的三种方式

总体上目前的主流方式是 注解 + Java 配置。

xml配置 - 古老

就是将bean的信息配置.xml文件里,通过Spring加载文件为我们创建bean。
这种方式出现很多早前的SSM项目中,将第三方类库或者一些配置工具类都以这种方式进行配置,主要原因是由于第三方类不支持Spring注解。

Java配置

将类的创建交给我们配置的JavcConfig类来完成,Spring只负责维护和管理,采用纯Java创建方式。
其本质上就是把在XML上的配置声明转移到Java配置类中。

  • 创建一个配置类, 添加@Configuration注解声明为配置类
  • 创建方法,方法上加上@bean,该方法用于创建实例并返回,该实例创建后会交给spring管理,方法名建议与实例名相同(首字母小写)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class BeansConfig {

/**
* @return user dao
*/
@Bean("userDao")
public UserDaoImpl userDao() {
return new UserDaoImpl();
}

/**
* @return user service
*/
@Bean("userService")
public UserServiceImpl userService() {
UserServiceImpl userService = new UserServiceImpl();
userService.setUserDao(userDao());
return userService;
}
}

注解配置

通过在类上加注解的方式,来声明一个类交给Spring管理。
Spring会自动扫描带有@Component、@Controller、@Service、@Repository这四个注解的类,然后帮我们创建并管理,前提是需要先配置Spring的注解扫描器@ComponentScan。

依赖注入的三种方式

常用的注入方式主要有三种:构造器注入(Construct Injection),Setter方法注入,字段注入(Field Injection)

  • 构造函数注入:
    • 适合于强制依赖,适合在创建对象的同时必须要初始化的属性,但是要注入的依赖多了会导致构造器相对臃肿;
    • 构造器注入时,循环依赖问题无法有效解决,会在启动的时候报错。
  • Setter方法注入:
    • 适合于可选依赖,当没有提供相关依赖时,类能够正常工作;
    • 相对更加灵活,可以多次调用,循环依赖问题(Setter注入的单例bean)可以通过Spring的单例Bean的三级缓存解决。
  • 字段注入:
    • 构造函数注入和字段注入发生在对象实例化的早期,而setter方法注入发生在对象实例化之后。
    • 所以在 Spring 中,构造函数注入和字段注入存在循环依赖时,Spring 无法直接解决这些循环依赖问题,而只能通过特定的机制(单例的三级缓存)解决 setter 方法注入的循环依赖。

构造函数注入

在Spring4.x版本中推荐的注入方式就是这种。

1
2
3
4
5
6
7
8
9
10
11
@Service
public class MyClass {
private MyDependency dependency;

@Autowired
public MyClass(MyDependency dependency) {
this.dependency = dependency;
}

// 其他代码...
}

为什么推荐构造器注入方式?
构造器注入的方式能够保证注入的组件不可变,并且确保需要的依赖不为空。此外,构造器注入的依赖总是能够在返回客户端(组件)代码的时候保证完全初始化的状态。

  • 依赖不可变:其实说的就是final关键字。
  • 依赖不为空(省去了我们对其检查):当要实例化UserServiceImpl的时候,由于自己实现了有参数的构造函数,所以不会调用默认构造函数,那么就需要Spring容器传入所需要的参数,所以就两种情况:1、有该类型的参数->传入,OK 。2:无该类型的参数->报错。
  • 完全初始化的状态:这个可以跟上面的依赖不为空结合起来,向构造器传参之前,要确保注入的内容不为空,那么肯定要调用依赖组件的构造方法完成实例化。而在Java类加载实例化的过程中,构造方法是最后一步(之前如果有父类先初始化父类,然后自己的成员变量,最后才是构造方法),所以返回来的都是初始化之后的状态。

Setter方法注入

在Spring3.x刚推出的时候,推荐使用注入的就是这种,但是这种方式比较麻烦,所以在Spring4.x版本中推荐构造函数注入。

1
2
3
4
5
6
7
8
9
10
11
@Service
public class MyClass {
private MyDependency dependency;

@Autowired
public void setDependency(MyDependency dependency) {
this.dependency = dependency;
}

// 其他代码...
}

如果使用setter注入,缺点显而易见,对于IOC容器以外的环境,除了使用反射来提供它需要的依赖之外,无法复用该实现类。而且将一直是个潜在的隐患,因为你不调用将一直无法发现NPE的存在。

字段注入

1
2
3
4
5
6
7
8
9
public class A {
@Autowired
private B b;
}

public class B {
@Autowired
private A a;
}

循环依赖的问题:使用field注入可能会导致循环依赖,即A里面注入B,B里面又注入A。
如果使用构造器注入,在spring项目启动的时候,就会抛出:BeanCurrentlyInCreationException:Requested bean is currently in creation: Is there an unresolvable circular reference?从而提醒你避免循环依赖,如果是field注入的话,启动的时候不会报错,在使用那个bean的时候才会报错。

实际上,Spring 5.x中使用@Autowired属性注入,发生循环依赖时,在SpringBootApplication启动时就会报循环依赖的错误。

@Autowired、@Resource、@Inject等注解注入有何区别?
Spring对于@Autowired、@Resource注解使用不同的后置处理器进行处理。
Spring使用AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor类分别处理@Autowired注解@Resource注解,它们都实现了InstantiationAwareBeanPostProcessor类,所以标注了@Autowired、@Resource会在第六次调用bean的后置处理器的时候完成属性注入。

对于static属性,如果标注了@Resource的是static静态的属性或方法,那么会直接报错;如果标注了@Autowired的是static静态的属性或方法,那么Spring会直接忽略,但不会报错。

  • @Autowired

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Autowired {
    boolean required() default true;
    }

    // @Autowired可以用在这些地方
    @Target(ElementType.CONSTRUCTOR) #构造函数
    @Target(ElementType.METHOD) #方法
    @Target(ElementType.PARAMETER) #方法参数
    @Target(ElementType.FIELD) #字段、枚举的常量
    @Target(ElementType.ANNOTATION_TYPE) #注解

    @Autowired是Spring自带的注解,通过AutowiredAnnotationBeanPostProcessor类实现的依赖注入
    @Autowired可以作用在CONSTRUCTOR、METHOD、PARAMETER、FIELD、ANNOTATION_TYPE
    @Autowired默认是根据类型(byType )进行自动装配的
    如果有多个类型一样的Bean候选者,需要指定按照名称(byName )进行装配,则需要配合@Qualifier。

  • @Resource

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Target({TYPE, FIELD, METHOD})
    @Retention(RUNTIME)
    public @interface Resource {
    String name() default "";
    // 其他省略
    }

    @Target(ElementType.TYPE) #接口、类、枚举、注解
    @Target(ElementType.FIELD) #字段、枚举的常量
    @Target(ElementType.METHOD) #方法

    @Resource是JSR250规范的实现,在javax.annotation包下
    @Resource可以作用TYPE、FIELD、METHOD上
    @Resource是默认根据属性名称进行自动装配的,如果有多个类型一样的Bean候选者,则可以通过name进行指定进行注入

  • @Inject

    1
    2
    3
    4
    5
    6
    7
    8
    @Target({ METHOD, CONSTRUCTOR, FIELD })
    @Retention(RUNTIME)
    @Documented
    public @interface Inject {}

    @Target(ElementType.CONSTRUCTOR) #构造函数
    @Target(ElementType.METHOD) #方法
    @Target(ElementType.FIELD) #字段、枚举的常量

    @Inject是JSR330 (Dependency Injection for Java)中的规范,需要导入javax.inject.Inject jar包,才能实现注入
    @Inject可以作用CONSTRUCTOR、METHOD、FIELD上
    @Inject是根据类型进行自动装配的,如果需要按名称进行装配,则需要配合@Named

站在设计者的角度考虑设计IOC容器

如果让你来设计一个IoC容器,你会怎么设计?我们初步的通过这个问题,来帮助我们更好的理解IOC的设计。

在设计时,首先需要考虑的是IOC容器的功能(输入和输出),初步的画出IOC容器的整体功能。

在此基础上,我们初步的去思考,如果作为一个IOC容器的设计者,主体上应该包含哪几个部分:

  • 加载Bean的配置(比如xml配置、注解配置等)
    • 比如不同类型资源的加载,解析成生成统一Bean的定义
  • 根据Bean的定义加载生成Bean的实例,并放置在Bean容器中
    • 比如Bean的依赖注入,Bean的嵌套,Bean存放(缓存)等
  • 除了基础Bean外,还有常规针对企业级业务的特别Bean
    • 比如国际化Message,事件Event等生成特殊的类结构去支撑
  • 对容器中的Bean提供统一的管理和调用
    • 比如用工厂模式管理,提供方法根据名字/类的类型等从容器中获取Bean

IOC体系结构设计

BeanFactory和BeanRegistry:IOC容器功能规范和Bean的注册

Spring Bean的创建是典型的工厂模式,这一系列的Bean工厂,也即IOC容器为开发者管理对象间的依赖关系提供了很多便利和基础服务,在Spring中有许多的IOC容器的实现供用户选择和使用,这是IOC容器的基础。
在顶层的结构设计主要围绕着BeanFactory和xxxRegistry进行:

  • BeanFactory: 工厂模式定义了IOC容器的基本功能规范
  • BeanRegistry: 向IOC容器手工注册 BeanDefinition 对象的方法

BeanFactory定义了IOC容器基本功能规范?
BeanFactory作为最顶层的一个接口类,它定义了IOC容器的基本功能规范。
BeanFactory 有三个子类:ListableBeanFactory、HierarchicalBeanFactory 和AutowireCapableBeanFactory。

看下BeanFactory接口:

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
public interface BeanFactory {    
//用于取消引用实例并将其与FactoryBean创建的bean区分开来。例如,如果命名的bean是FactoryBean,则获取将返回Factory,而不是Factory返回的实例。
String FACTORY_BEAN_PREFIX = "&";

//根据bean的名字或者Class类型等来得到bean实例
Object getBean(String name) throws BeansException;
Object getBean(String name, Class requiredType) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

//返回指定bean的Provider
<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);

//检查工厂中是否包含给定name的bean,或者外部注册的bean
boolean containsBean(String name);

//检查所给定name的bean是否为单例/原型
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

//判断所给name的类型与type是否匹配
boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;

//获取给定name的bean的类型
@Nullable
Class<?> getType(String name) throws NoSuchBeanDefinitionException;

//返回给定name的bean的别名
String[] getAliases(String name);
}

BeanFactory为何要定义这么多层次的接口?定义了哪些接口?
主要是为了区分在 Spring 内部在操作过程中对象的传递和转化过程中,对对象的数据访问所做的限制

  • ListableBeanFactory
    该接口定义了访问容器中 Bean 基本信息的若干方法,如查看Bean 的个数、获取某一类型 Bean 的配置名、查看容器中是否包括某一 Bean 等方法;
  • HierarchicalBeanFactory
    父子级联 IoC 容器的接口,子容器可以通过接口方法访问父容器。
    通过 HierarchicalBeanFactory 接口, Spring 的 IoC 容器可以建立父子层级关联的容器体系,子容器可以访问父容器中的 Bean,但父容器不能访问子容器的 Bean。
    Spring 使用父子容器实现了很多功能,比如在 Spring MVC 中,展现层 Bean 位于一个子容器中,而业务层和持久层的 Bean 位于父容器中。这样,展现层 Bean 就可以引用业务层和持久层的 Bean,而业务层和持久层的 Bean 则看不到展现层的 Bean。
  • ConfigurableBeanFactory
    是一个重要的接口,增强了 IoC 容器的可定制性,它定义了设置类装载器、属性编辑器、容器初始化后置处理器等方法;
  • ConfigurableListableBeanFactory
    ListableBeanFactory 和 ConfigurableBeanFactory的融合;
  • AutowireCapableBeanFactory
    定义了将容器中的 Bean 按某种规则(如按名字匹配、按类型匹配等)进行自动装配的方法;

如何将Bean注册到BeanFactory中?BeanRegistry
Spring 配置文件中每一个<bean>节点元素在 Spring 容器里都通过一个 BeanDefinition 对象表示,它描述了 Bean 的配置信息。而 BeanDefinitionRegistry 接口提供了向容器手工注册 BeanDefinition 对象的方法。

BeanDefinition:各种Bean对象及其相互的关系

Bean对象存在依赖嵌套等关系,所以设计者设计了BeanDefinition,它用来对Bean对象及关系定义。
我们在理解时只需要抓住如下三个要点:

  • BeanDefinition 定义了各种Bean对象及其相互的关系
  • BeanDefinitionReader 这是BeanDefinition的解析器
  • BeanDefinitionHolder 这是BeanDefination的包装类,用来存储BeanDefinition、name以及aliases等。

SpringIOC容器管理了我们定义的各种Bean对象及其相互的关系,在Spring实现中是以BeanDefinition来描述的,其继承体系如下:

ApplicationContext:IOC接口设计和实现

IoC容器的接口类是ApplicationContext,很显然它必然继承BeanFactory对Bean规范(最基本的ioc容器的实现)进行定义。

ApplicationContext表示的是应用的上下文,除了对Bean的管理外,还至少应该包含了

  • 访问资源: 对不同方式的Bean配置(即资源)进行加载。(实现ResourcePatternResolver接口)
  • 国际化: 支持信息源,可以实现国际化。(实现MessageSource接口)
  • 应用事件: 支持应用事件。(实现ApplicationEventPublisher接口)

ApplicationContext接口的设计

  • HierarchicalBeanFactory 和 ListableBeanFactory
    ApplicationContext 继承了 HierarchicalBeanFactory 和 ListableBeanFactory 接口,在此基础上,还通过多个其他的接口扩展了 BeanFactory 的功能:
  • ApplicationEventPublisher
    让容器拥有发布应用上下文事件的功能,包括容器启动事件、关闭事件等。
    实现了 ApplicationListener 事件监听接口的 Bean 可以接收到容器事件,并对事件进行响应处理。
    在 ApplicationContext 抽象实现类AbstractApplicationContext 中,我们可以发现存在一个 ApplicationEventMulticaster,它负责保存所有监听器,以便在容器产生上下文事件时通知这些事件监听者。
  • MessageSource
    为应用提供 i18n 国际化消息访问的功能;
  • ResourcePatternResolver
    所有 ApplicationContext 实现类都实现了类似于PathMatchingResourcePatternResolver 的功能,可以通过带前缀的 Ant 风格的资源文件路径装载 Spring 的配置文件。
  • LifeCycle
    该接口是 Spring 2.0 加入的,该接口提供了 start()和 stop()两个方法,主要用于控制异步处理过程。
    在具体使用时,该接口同时被 ApplicationContext 实现及具体 Bean 实现,ApplicationContext 会将 start/stop 的信息传递给容器中所有实现了该接口的 Bean,以达到管理和控制 JMX、任务调度等目的。

ApplicationContext接口的实现
在考虑ApplicationContext接口的实现时,关键的点在于,不同Bean的配置方式(比如xml、groovy、annotation等)有着不同的资源加载方式,这便衍生出了众多ApplicationContext的实现类。

  • 第一,从类结构设计上看,围绕着是否需要Refresh容器衍生出两个抽象类
    1. GenericApplicationContext
      初始化的时候就创建容器,往后的每次refresh都不会更改。
    2. AbstractRefreshableApplicationContext
      AbstractRefreshableApplicationContext及子类的每次refresh都是先清除已有(如果不存在就创建)的容器,然后再重新创建;
      AbstractRefreshableApplicationContext及子类无法做到GenericApplicationContext混合搭配从不同源头获取bean的定义信息
  • 第二,从加载的源来看(比如xml、groovy、annotation等),衍生出众多类型的ApplicationContext
    • FileSystemXmlApplicationContext
      从文件系统下的一个或多个xml配置文件中加载上下文定义,也就是说系统盘符中加载xml配置文件
    • ClassPathXmlApplicationContext
      类路径下的一个或多个xml配置文件中加载上下文定义,适用于xml配置的方式。
    • AnnotationConfigApplicationContext
      从一个或多个基于java的配置类中加载上下文定义,适用于java注解的方式。
    • ConfigurableApplicationContext
      扩展于 ApplicationContext,它新增加了两个主要的方法: refresh()和 close(),让 ApplicationContext 具有启动、刷新和关闭应用上下文的能力。
      在应用上下文关闭的情况下调用 refresh()即可启动应用上下文,在已经启动的状态下,调用 refresh()则清除缓存并重新装载配置信息,而调用close()则可关闭应用上下文。
      这些接口方法为容器的控制管理带来了便利,但作为开发者,我们并不需要过多关心这些方法。

设计者在设计时AnnotationConfigApplicationContext为什么是继承GenericApplicationContext
因为基于注解的配置,是不太会被运行时修改的,这意味着不需要进行动态Bean配置和刷新容器,所以只需要GenericApplicationContext。

而基于XML这种配置文件,这种文件是容易修改的,需要动态性刷新Bean的支持,所以XML相关的配置必然继承AbstractRefreshableApplicationContext; 且存在多种xml的加载方式(位置不同的设计),所以必然会设计出AbstractXmlApplicationContext, 其中包含对XML配置解析成BeanDefination的过程。

为什么AnnotationWebConfigApplicationContext继承自AbstractRefreshableApplicationContext呢?
因为用户可以通过ApplicationContextInitializer来设置contextInitializerClasses(context-param / init-param),在这种情况下用户倾向于刷新Bean的,所以设计者选择让AnnotationWebConfigApplicationContext继承了AbstractRefreshableApplicationContext。

把之前的设计要点和设计结构结合起来看: