您现在的位置是:首页 >学无止境 >Spring核心-IoC控制反转详解 (典藏版)网站首页学无止境

Spring核心-IoC控制反转详解 (典藏版)

JavGo6 2023-05-31 16:00:03
简介Spring核心-IoC控制反转详解 (典藏版)

文章目录

既然你能鼓起勇气打开这篇 “不短” 的博客,想必你是想对 Spring IoC 进行更加深入或者全面的了解,或者准备将其作为字典随时查阅。不管是怎样,这篇基于 Spring 官方文档和学习经典书籍而来的博客或多或少都能让你对 Spring IoC 有一个新的认知。

创作不易,如果觉得对你有用,请记得一键三连嗷(👍 + ⭐+ 💗),后续内容请关注主页 Spring Boot 专栏,获取最新文章。

控制反转 (Inversion of Control, IoC) 与面向切面编程 (Aspect Oriented Programming,AOP)是 Spring Framework 中最重要的两个概念。在本篇博客中,笔者将介绍 Spring Framework 中的 IoC 容器及其背后的主要思想。

1.IoC容器和Bean介绍

IoC(控制反转)是一种编程思想,通过它可以更加灵活和可控地管理代码之间的关系,而实现 IoC 这一思想的一个最好体现就是依赖注入(dependency injection,DI)。

传统的编程方式中,对象需要主动去获取它所依赖的其他对象,并且这些依赖关系通常是硬编码的。而在 IoC 中,对象的依赖关系由 IoC 容器来负责维护和注入,从而实现了对象之间的松耦合和更好的可维护性。

举个例子来说,假设我们现在有一个电商网站,需要实现一个购物车模块。购物车模块依赖于商品模块和用户模块,我们传统的做法是在购物车模块里直接实例化商品模块和用户模块的对象,然后在代码中直接调用它们的方法。但是,如果商品模块或用户模块的实现发生了变化,我们就需要修改购物车模块的代码,这样维护性就变得很差。

而使用 IoC 的方法,我们可以将商品模块和用户模块的对象注入到购物车模块中,而这些对象的实例化则由 IoC 容器来负责。这样,我们只需要在 IoC 容器中配置一下商品模块和用户模块的依赖关系,购物车模块就可以通过 IoC 容器来获取这些对象了。这样,当商品模块或用户模块的实现发生变化时,我们只需要修改 IoC 容器的配置即可,购物车模块的代码不需要做任何改动,从而提高了代码的可维护性。

而这些由 IoC 容器管理的对象便称为 bean,这些 bean 是由容器负责实例化、组装和管理的,这个过程基本上是 bean 本身的逆过程,因此得名”控制反转“。如果没有被容器管理,bean 只是应用程序中的一个普通 Java 对象。这些 bean 以及它们之间的依赖关系都反映在容器使用的配置元数据中。

2.Spring 中的 IoC 容器

在 Spring Framework 中,BeanFactory 和 ApplicationContext 是两个经常被提到的概念。它们是 Spring Framework 中最重要的核心组成部分之一,为开发人员提供了丰富而灵活的依赖注入和控制反转机制。本节将详细介绍 BeanFactory 和 ApplicationContext 这两个概念,让胖友全面了解它们之间的区别和联系,并能够在实际项目中更好地使用它们。

2.1 BeanFactory和ApplicationContext概述

BeanFactory 和 ApplicationContext 是 Spring Framework 中的两个关键组件。它们都是实现依赖注入(DI)和控制反转(IoC)的原理的容器,但在实现的过程中有所不同。

BeanFactory 接口是 Spring Framework 中最基本的容器,它负责管理应用程序中的所有对象。ApplicationContext 是 BeanFactory 的一个子接口,它在 BeanFactory 父接口的基础上增加了更多的功能,如国际化支持、应用程序事件发布机制、应用程序上下文级别的 Bean 定义继承等。

2.2 BeanFactory

BeanFactory 是 Spring Framework 中最基本的 IOC 容器,它负责管理应用程序中的所有对象。它是 Spring Framework 中容器的基础实现,也是所有容器的祖先。BeanFactory 的主要责任就是管理所有的 Java 对象,包括创建、配置、装配、销毁。这些对象也被称为“Bean”或“组件”。

BeanFactory 接口的常用基本 API 如下表:

API描述
Object getBean(String name)根据bean的名称,从容器中获取一个bean
T getBean(String name, Class<T> requiredType)根据bean的名称和类型,从容器中获取一个bean
<T> T getBean(Class<T> requiredType)根据bean的类型,从容器中获取一个bean
boolean containsBean(String name)判断容器中是否包含指定名称的bean
boolean isSingleton(String name)判断指定名称的bean是否是单例的
boolean isPrototype(String name)判断指定名称的bean是否是原型的
Class<?> getType(String name)获取指定名称的bean的Class类型
String[] getAliases(String name)获取指定名称的bean的所有别名

BeanFactory 在应用程序中的作用是:

  1. 实例化对象:BeanFactory 负责创建应用程序中所有的 Bean 对象。
  2. 装配Bean:BeanFactory 负责为 Bean 注入所有的依赖,包括其他 Bean 对象,以及来自外部的值。
  3. 控制Bean的声命周期:BeanFactory 在应用程序的开始与结束时负责创建和销毁 Bean 对象。
  4. 提供配置元数据:BeanFactory 负责维护应用程序中所有 Bean 对象的配置元数据,包括 Bean 的类型、作用域、属性值等等。
  5. 提供容器内Bean的访问:BeanFactory 为应用程序中的其他组件提供访问容器内 Bean 的接口。

BeanFactory 在实现时采用了“延迟初始化”的策略。也就是说,在加载 Spring 配置文件时,BeanFactory 并不会立即实例化所有的 Bean 对象,而是等到应用程序真正要使用这些 Bean 时再一次性初始化所有的 Bean 对象。这种策略可大大减少应用程序的启动时间和内存消耗。

BeanFactory 的典型使用示例笔者会在 2.6.1 小结详细展开。

2.3 ApplicationContext

ApplicationContext 是 BeanFactory 的一个子接口,它增加了更多的功能。ApplicationContext 在 BeanFactory 的基础上增加了以下特性:

新特性描述
AOP(面向切面编程)支持提供了AOP的实现,可以使用切面来分离应用程序的关注点。
事件的发布与监听支持应用程序事件机制,可以让你在应用程序中发布事件,并且响应这些事件。
多国语言支持支持国际化功能,可以在应用程序中提供多种语言的支持。
继承Bean定义支持Bean定义的继承,这样可以减少Bean定义的冗余。
运行时注入支持在运行时动态地注入Bean。
Spring表达式语言(SpEL)支持SpEL,可以更灵活地配置Bean属性。

ApplicationContext 通常是 BeanFactory 的首选。在实际的开发场景中,如果只是需要简单的 IOC 容器,使用 BeanFactory 是足够了。但是,如果需要使用 Spring 的高级功能,例如 AOP、事件机制、国际化等,就应该使用 ApplicationContext。

ApplicationContext 有多种实现方式,下面是比较常用的三个:

  • AnnotationConfigApplicationContext:基于注解的方式配置应用程序上下文;
  • ClassPathXmlApplicationContext:基于 CLASSPATH 配置文件的方式配置应用程序上下文;
  • FileSystemXmlApplicationContext:基于文件系统配置文件的方式配置应用程序上下文;

对应的类图如下:

ApplicationContext 的典型使用示例笔者会在 2.6.1 小结详细展开。

2.4 BeanFactory vs ApplicationContext

BeanFactory 和 ApplicationContext 都可以作为 Spring 的 IOC 容器,但它们之间有着很明显的不同点。

主要区别在以下三个方面:

  1. 配置文件的读取

    BeanFactory 使用延迟初始化,而 ApplicationContext 在容器启动时就会实例化 Bean。因此,BeanFactory 在读取配置文件时只进行语法解析和部分地属性设置,只有在调用 getBean() 方法时,才会进行 Bean 的初始化。相反,ApplicationContext 在读取配置文件时,就会实例化所有的 Bean,并完成所有的依赖注入和装配工作。(即容器中 Bean 的初始化时机不同)

  2. 提供的额外功能

    ApplicationContext 提供了更多的功能,如AOP、事件机制、国际化、Bean定义继承等。而 BeanFactory 只是简单地实现了依赖注入和控制反转。

  3. 性能和资源消耗

    因为 ApplicationContext 在启动时就加载了所有的 Bean,并完成了所有的依赖注入和装配,所以在应用程序运行时,ApplicationContext 的性能比 BeanFactory 更高。但是,ApplicationContext 也会比 BeanFactory 占用更多的系统资源,因为它需要提前加载所有的 Bean。

2.5 容器的初始化

在 Spring Framework 的官方文档中有一幅图 Spring 工作原理的高级视图,它非常直观地表达了 Spring IoC 容器的初始化过程。

从上图来打,大致意思是将业务对象 (也就是组件,在 Spring 中这些组件被称为 Bean) 和关于组件的配置元数据 (比如依赖关系、属性) 输入 Spring 容器中,在 ApplicationContextBeanFactory(即 IoC 容器)创建和初始化之后,就能拥有一个完全配置且可执行的系统或应用程序。

2.6 配置元数据

如上图所示,Spring IoC 容器需要使用某种形式来配置元数据。所谓元数据(Configuration Metadata),就是用来描述应用程序中组件(业务对象)的基本信息,包括组件之间的关系、依赖、属性和行为等。配置元数据传统上以简单直观的 XML 格式进行配置,但基于 XML 的元数据不是唯一允许的配置元数据形式。

Spring IoC 容器本身与实际写入配置元数据的格式完全分离。如今,一共支持以下三种配置方式:

  1. 基于 XML 配置元数据:传统上简单直观的 XML 文件配置方式。
  2. 基于注解配置元数据:Spring Framework 2.5 引入了对基于注解的配置元数据的支持。
  3. 基于 Java 类配置元数据:Spring Framework 3.0 开始,可以通过 Spring JavaConfig 而不是 XML 文件来定义应用程序类外部的 beans。

接下来我们来试着体验一下使用 Spring 的 IoC 容器的基本过程。比如,我们有一个的业务对象,它会返回一个字符串:

/**
 * @Author: JavGo
 * @DateTime: 2023/4/17 22:47
 **/
public class Hello {
    public String sayHello(String name) {
        return "Hello " + name;
    }
}

在没有 IoC 容器时,我们需要自己管理 Hello 实例的生命周期,通常是在代码中用 new 关键字新建一个实例,然后把它传给具体要调用它的对象(此处没有涉及)。

/**
 * @Author: JavGo
 * @DateTime: 2023/4/17 22:49
 **/
public class Test01 {
    public static void main(String[] args) {
        Hello hello = new Hello();
        hello.sayHello("JavGo");
    }
}

如果是把实例交给 Spring IoC 容器托管,我们首先要挑选一种形势来配置元数据。通过配置元数据来表示你作为应用程序开发人员告诉 Spring 容器如何在你的应用程序中实例化、配置和组装对象。

下面的三种方式目的在告诉胖友,我们可以使用不同的方式来进行元数据的配置(其实准确来说应该叫做配置容器),即重在方式的讲解,而具体配置的细节我们将在后面的小节中详细解释。

2.6.1 基于XML的容器配置

使用 XML 的方式配置元数,我们可以将它配置到一个 applicationContext.xml 中,这个文件建议放在工程的 src/main/resources 目录下,对于该文件的名字和数量没有特殊的要求,但建议遵守 applicationContext-*.xml 的方式进行命名。

一个最普通的基于 XML 的配置元数据的基本文件结构如下:

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

    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions go here -->
</beans>

可以看到,XML 配置的 bean 定义需要放在顶级 <beans/> 元素内的 <bean/> 元素中,其中涉及到两个最基本的属性:

  • id 属性:一个字符串,用于标识单个 bean 定义。id 属性的值还可以被其他 bean 引用时直接使用。

    对于 id 的名称,我们可以任意指定,如果不指定则默认为类名首字母小写,如 StudentDao 的 bean id 名就为“studentDao”。

  • class 属性:定义 bean 的类型并使用完全限定的类名(不能是接口)。

基于此,我们上面示例中的 cn.javgo.ch1springboothelloworld.helloworld.common.Hello 类的配置应该如下:

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

    <bean id="hello" class="cn.javgo.ch1springboothelloworld.helloworld.common.Hello"/>
</beans>

如果你在使用 Spring Framework 开发项目,那么想要使用 Spring 的 IoC 容器管理 Bean,还需要在 pom.xml 文件中引入 org.springframework:spring-beans 依赖。(Spring Boot 中默认引入了该依赖)

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>x.x.x</version>
</dependency>

org.springframework:spring-beans 是 Spring 框架的核心模块之一,包含了 Spring 框架的 Bean 容器功能。

接下来我们只需要将配置文件载入容器,然后通过容器拿到对象即可。一般情况下,都是使用 BeanFactoryApplicationContext 实现作为容器。但是为了加深理解,我们下面先采用最基本的方式拿到容器,然后从容器中取出 Bean。

一个最基本的方式拿到容器对象的示例代码如下:

/**
 * @Author: JavGo
 * @DateTime: 2023/4/17 22:49
 **/
public class Test01 {
    public static void main(String[] args) {
        // 1.通过实现类 DefaultListableBeanFactory 创建 BeanFactory
        BeanFactory beanFactory = new DefaultListableBeanFactory();
        // 2.通过 BeanFactory 创建 XMLBeanDefinitionReader 对象,用于加载 Bean 配置文件
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader((DefaultListableBeanFactory) beanFactory);
        // 3.通过 XMLBeanDefinitionReader 对象加载 Bean 配置文件
        reader.loadBeanDefinitions("classpath:applicationContext.xml");
        // 4.通过 BeanFactory 获取 Bean 对象
        Hello hello = beanFactory.getBean("hello", Hello.class);
        // 5.调用 Bean 对象的方法
        System.out.println(hello.sayHello("JavGo"));
    }

上面我们用到了两个新的类,下面分别简单介绍一下:

  1. DefaultListableBeanFactory 类

    DefaultListableBeanFactory 类是 Spring 框架中最常用的 BeanFactory 实现之一,它是一个可扩展的 BeanFactory,可以通过添加 BeanPostProcessor、BeanFactoryPostProcessor 和其他自定义扩展来增强其功能。DefaultListableBeanFactory 类的主要职责是管理 Bean 的定义和实例化,并提供了许多方法来注册、获取和删除 Bean。

  2. XmlBeanDefinitionReader 类

    XmlBeanDefinitionReader 类是一个用于读取 XML 配置文件的工具类,它可以将 XML 配置文件中的 Bean 定义读取到 DefaultListableBeanFactory 中。XmlBeanDefinitionReader 类的主要职责是解析 XML 文件,并将解析结果转换为 BeanDefinition 对象,然后将这些对象注册到 DefaultListableBeanFactory 中。XmlBeanDefinitionReader 类还可以与 ResourceLoader 接口配合使用,以支持从不同类型的资源(例如文件系统、类路径或 Web 应用程序上下文)加载 XML 配置文件。

了解了这两个类之后,上面的代码逻辑也就一目了然了。我们通过使用 DefaultListableBeanFactory 类和 XmlBeanDefinitionReader 类,可以轻松地实现从 XML 配置文件中加载 Bean 定义,并将这些 Bean 定义注册到 BeanFactory 中,从而实现依赖注入和控制反转。

复习:BeanFactory 在实现时采用了“延迟初始化”的策略。也就是说,在加载 Spring 配置文件时,BeanFactory 并不会立即实例化所有的 Bean 对象,而是等到应用程序真正要使用这些 Bean 时(调用 getBean() 方法时)再一次性初始化所有的 Bean 对象。这种策略可大大减少应用程序的启动时间和内存消耗。

在得到 BeanFactory 容器后,我们就可以使用该接口提供的 API 进行一系列操作, 上面我们通过接口提供的 T getBean(String name, Class<T> requiredType) 方法从容其中获取一个 id 名为“hello”,Hello 类型的 Bean 对象。这意味着,通过调用 getBean() 方法,程序员可以在不知道 Bean 具体实现的情况下,从容器中直接获取 Bean 对象,而无需关心 Bean 的具体实现。

对于 getBean() 方法,BeanFactory 接口提供了很多不同的参数列表,上面示例的就是最常用的一种,也是我们推荐使用的。即通过 bean id 和 bean Type 从容器中获取指定的 bean,由于提供了具体的类型,我们便可以直接使用对应的类型去接受其返回值,如果容器中有多个名称为 “hello” 的 Bean,但类型不同,则会抛出 BeansException 异常。

下面是另外两种需要知道的 getBean() 方法:

  • Object getBean(String name):通过指定 bean 的名称(id)从容器中获取 bean 对象;由于没有给出具体类型,该方法会返回一个 Object 类型的 bean,程序员需要进行强制转换为目标类型。

    使用此种形式获取 bean,无法保证获取的 Bean 是正确的类型,如果容器中存在多个名称为 “hello” 的 Bean,则可能会获取到错误的 Bean。

  • T getBean(Class<T> requiredType):通过指定 bean 的类型从容器中获取 bean 对象。

除了上面的方式获取容器对象外,更多情况下,我们都是使用 BeanFactory 的 ApplicationContext 实现作为容器。通过使用不同场景下 ApplicationContext 的具体实现来灵活获取容器对象。

注意:Spring Boot 已经默认引入!

如果你使用的是 Spring Framework 开发项目,想要使用 ApplicationContext 的容器功能,需要做一些小小改动,那就是将之前在 pom.xml 文件中引入的 org.springframework:spring-beans 依赖修改为org.springframework:spring-context ,删除原有的 org.springframework:spring-beans 依赖。该依赖提供了 Spring 应用上下文(ApplicationContext)的各种实现,其中就包括了我们下面即将使用的 ClassPathXmlApplicationContext。

所以,在使用 Spring Framework 框架时,如果需要使用 Spring 的 ApplicationContext 容器来管理 Bean 的生命周期和依赖关系,就必须引入如下依赖:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>x.x.x</version>
</dependency>

该依赖默认包含了以下依赖:

  • org.springframework:spring-aop:提供了 Spring 面向切面编程(AOP)的功能。
  • org.springframework:spring-beans:提供了 Spring Bean 的定义和管理的功能。
  • org.springframework:spring-core:提供了 Spring 框架的核心功能,如控制反转(IoC)、依赖注入(DI)等。
  • org.springframework:spring-expression:提供了 Spring 表达式语言(SpEL)的功能。

下面是一个等效的使用案例:

/**
 * @Author: JavGo
 * @DateTime: 2023/4/18 10:15
 **/
public class Test02 {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        Hello hello = context.getBean("hello", Hello.class);
        hello.sayHello("JavGo");
    }
}

怎么样,使用了 ApplicationContext 之后是不是感觉瞬间清爽了许多呢?在上面的代码中我们通过实现类 ClassPathXmlApplicationContext 基于 CLASSPATH 配置文件的方式配置应用程序上下文(容器),然后便可直接从容器中取出 bean。

在这个例子的 sayHello() 方法中,我们完全不用关心 Hello 这个类的实例是如何创建的,只需从容器中获取实例对象然后使用即可。虽然看起来比传统方式代码的行数要多,但当工程复杂度增加之后 IoC 托管 Bean 生命周期的优势就体现出来了。

复习:ApplicationContext 在读取配置文件时,就会实例化所有的 Bean,并完成所有的依赖注入和装配工作。所以在应用程序运行时,ApplicationContext 的性能比 BeanFactory 更高。但是,ApplicationContext 也会比 BeanFactory 占用更多的系统资源,因为它需要提前加载所有的 Bean。

上面我们只加载了一个配置文件 applicationContext.xml,当有多个配置文件的时候诸如 ClassPathXmlApplicationContext 的实现也提供了如 ClassPathXmlApplicationContext(String... configLocations) 的构造,即我们可以一次传入多个配置文件。如:

ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml","classpath:applicationContext-dao.xml","classpath:applicationContext-services.xml");

当然,除了这种方式以外我们还可以通过在配置文件中使用 <import> 标签导入另外一个配置文件。例如,假设我们有两个配置文件 applicationContext.xmlapplicationContext-dataSource.xml,其中 applicationContext-dataSource.xml 配置了数据源相关的信息,我们可以在 applicationContext.xml 中通过以下方式导入 applicationContext-dataSource.xml

<beans>
  <!-- 导入 dataSource.xml -->
  <import resource="applicationContext-dataSource.xml" />

  <!-- 其他 bean 的声明 -->
  <bean id="myBean" class="com.example.MyBean" />
</beans>

2.6.2 基于注解的容器配置

基于注解配置的出现引发了一个问题,即这种方式是否比 XML 更好。简短的答案是“取决于情况”。长话短说,每种方式都有其优缺点,通常由开发人员决定哪种策略更适合他们。由于它们的定义方式不同,注解在声明时提供了大量的上下文信息,导致配置更短、更简洁。然而,XML 在不触及源代码或重新编译组件的情况下,优于连接组件。一些开发人员更喜欢将连接放在源代码附近,而其他人则认为注解类不再是 POJO,并且配置变得分散且难以控制。

值得注意的使,Spring 可以适应 XML 和注解两种配置容器的方式,甚至可以混合使用。

基于注解的方式我们可以直接在类上使用注解,即可将该类作为 Bean 注册到容器中进行统一管理,而不需要在 XML 文件中进行 Bean 的配置。

下面是一个等效的示例代码:

@Component("hello")
public class Hello {
    public String sayHello(String name) {
        return "Hello " + name;
    }
}

可以看到,通过在 Hello 类上添加一个 @Component("hello") 注解并指定 bean id(当然也可以不指定),即可将 Hello 作为 Bean 交由容器进行管理。当然这里面还涉及到很多细节,在后面深入了解 Bean 之后,我们还会着重讲解。

如果你使用的是 Spring Framework 项目,那么想要开启注解功能需要在 Spring 配置文件,即 applicationContext.xml 中开启注解支持。(Spring Boot 则不需要手动开启)

<!--  开启注解支持  -->
<context:component-scan base-package="teach.nocode"/>

<context:component-scan>context 是命名空间)是 Spring XML 配置文件中的一个元素,它用于结合 base-package 属性指示 Spring 容器扫描指定包及其子包中的类,自动识别并注册带有特定注解的类作为 Spring Bean。这些特定的注解包括 @Component@Service@Repository@Controller 等。这些注解后面会进行详细讨论。

2.6.3 基于Java类的容器配置

从 Spring Framework 3.0 开始,我们可以使用 Java 类代替 XML 文件,使用 @Configuration@Bean@Componentscan 等一系列注解,基本可以满足日常所需。与 XML 配置方式相比,基于 Java 类的配置方式更灵活,更具可读性,并且可以使用 Java 代码的特性,如代码提示和代码检查。

上面提到过,基于注解的方式配置 bean 通常需要在源代码中添加注解,增加了代码的侵入性。而基于 Java 类的配置是非侵入性的,它可以在不修改目标组件源代码的情况下,使用 Java 类来配置 Bean,这个 Java 类中使用注解来完成配置。这样做的好处是,我们可以将配置和目标组件的源代码分离,使得配置更加清晰和易于维护。

此时我们可以不使用 XML 配置文件,而是通过创建一个 Java 类并通过 @Configuration 注解标记该类作为配置类,代理原本的 XML 配置文件功能,这个类将包含创建和初始化 Bean 的方法。在配置类中,通过 @Bean 注解标记一个方法,该方法返回一个对象,该对象将作为 Spring 容器中的 Bean 实例。@Bean 注解的方法名默认为 Bean 的名称,但也可以通过 name 参数指定自定义名称。

下面是一个等效的示例代码:

@Configuration
public class CommonConfig {
    @Bean(name = "hello")
    public Hello hello() {
        return new Hello();
    }
}

需要获取容器对象时,我们只需要将前面的 XML 配置文件修改为配置类即可:

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(CommonConfig.class);

其中的 CommonConfig 类就是上面加了 @Configuration 注解的 Java 类,这与之前通过 XML 配置文件的方式加载 applicationContext.xml 配置文件道理上是大致相同的。

OK,到这里我们就介绍完了容器的三种配置方式了,在了解了 Bean 之后,我们会对这三种配置方式再次进行深入探讨。

2.7 容器的继承关系

Java 类之间有继承(child extends parent)的关系,子类能够继承父类的属性和方法。同样地,Spring 的容器之间也存在类似的继承关系,子容器可以继承父容器中配置的组件。

在使用 Spring MVC 时就会涉及到容器的继承,此处提前了解。

2.7.1 容器继承概述

容器继承是指通过父级和子级容器之间的关系来构建 Bean 定义的过程。在 Spring Framework 中,容器继承可以通过使用 parent 属性来实现。一个 Bean 的父 Bean 可以设置为另一个包含了该父 Bean 的应用上下文。在这种情况下,父 Bean 作为模板,子 Bean 将继承父 Bean 的配置信息,然后可以对这些信息进行修改或扩展,以满足更具体的需求。

下面是一个简单的例子:

<bean id="parentBean" class="com.example.ParentBean" abstract="true">
    <property name="property1" value="value1"/>
    <property name="property2" value="value2"/>
</bean>

<bean id="childBean" class="com.example.ChildBean" parent="parentBean">
    <property name="property1" value="newValue1"/>
</bean>

在上面的配置文件中,父 Bean 的 class 属性设置为 “com.example.ParentBean”,abstract 属性设置为 true,表示该 Bean 是一个抽象 Bean,不能被实例化。子 Bean 的 class 属性设置为 “com.example.ChildBean”,parent 属性设置为 “parentBean”,表示该 Bean 继承了父 Bean 的配置信息,并且覆盖了父 Bean 中的 “property1” 属性。

2.7.2 parent 属性

在 Spring Framework 中,Bean 定义的 parent 属性指定了另一个 Bean 定义作为当前 Bean 父亲。在 Spring 中,一个 Bean 只能有一个父亲,而一个父亲可以有多个子 Bean。这与 Java 的单继承机制是有异曲同工之妙的。

如果一个 Bean 没有指定 parent 属性,那么它将没有父 Bean,称之为 root Bean。每个 Bean 都需要一个唯一的 ID 作为它的标识符。在一个父 Bean 中,就算子级没有指定 ID,也可以通过其定义位置来唯一标识该 Bean,而在一个没有父 Bean 的子级中,ID 变得十分重要。

2.7.3 容器继承的应用场景

当应用的组件需要分层次组织时,容器继承的应用场景就很多了。但在大多数情况下,应该尝试避免使用继承,并在可能的情况下使用依赖注入代替它,因为它比继承更加灵活。

但是,在以下情况下使用继承是有用的:

  • 当用一个父 Bean 定义作为模板来配置一个或多个子 Bean 时,使用继承。
  • 当有一个 Bean 层次结构,其中某些 Bean 通常只定义模板方法或属性,而其它 Bean 定义则扩展这些模板以定义具体实现时,使用继承。

下面通过一个例子来更好的理解容器继承的应用场景。

2.7.4 应用案例

假设我们想要创建一个 Web 应用程序,其中包括一种通用的日志记录机制。该机制要求在特定的情况下输出调试信息和用户交互,并可以配置以便在不同级别(如警告、错误等)下输出不同类型的消息。

我们可以首先定义一个基础的 Bean,作为父 Bean,定义默认的日志记录级别。如下所示:

<bean id="parentLogger" class="org.springframework.beans.TestLogger">
    <property name="level" value="DEBUG"/>
</bean>

定义一个子 Bean,继承父 Bean 的配置,并增加特定于 Web 应用程序的配置,如打开用户交互日志记录等。如下所示:

<bean id="webLogger" parent="parentLogger" class="org.springframework.beans.TestLogger">
    <property name="webInteractionsEnabled" value="true"/>
</bean>

定义一个子 Bean,继承 webLogger Bean 的配置,但取消用户交互日志记录。如下所示:

<bean id="specializedLogger" parent="webLogger" class="org.springframework.beans.TestLogger">
    <property name="webInteractionsEnabled" value="false"/>
</bean>

现在,我们创建了一个简单但功能强大的日志记录机制,可以在我们的 Web 应用程序中灵活配置。我们还可以轻松地创建其他类型的日志记录 Bean,例如数据库日志记录器或文件日志记录器,而不必重复我们在基本日志记录器中定义的约定。

下表将 Spring 容器的继承与 Java 继承机制进行了简单对比,加深理解:

容器继承Java继承
子上下文可以看到父上下文中定义的 Bean,反之则不行子类可以看到父类的 protected 和 public 属性和方法,父类看不到子类的
子上下文中可以定义与父上下文同 ID 的 Bean,各自都能获取自己定义的 Bean子类可以覆盖(Override)父类定义的属性和方法

总结起来,容器继承提供了一种定义 Bean 的层次结构的方式,从而可以组织和管理 Bean 的配置信息。这使得 Bean 配置更加模块化,更容易维护和重用。但需要注意的是,继承关系的深度不要过深,否则会影响程序的性能。

3.Bean概述

Bean 是 Spring 容器中的重要概念,本节就让我们来着重了解一下 Bean 的概念、如何注入 Bean 的依赖,以及如何在容器中进行 Bean 的配置。

3.1 Bean 的概念

Java 中有个比较重要的概念叫做“JavaBeans”,维基百科中有如下描述:

JavaBeans 是 Java 中一种特殊的类,可以将多个对象封装到一个对象(Bean)中。特点是可序列化,提供无参构造器,提供 Getter 方法和 Setter 方法访问对象的属性。名称中的“Bean”是用于 Java 的可重用软件组件的惯用叫法。

从中可以看到 “Bean 是指 Java 中的可重用软件组件”,Spring 容器也遵循这一惯例,因此将容器中管理的可重用组件称为 Bean。一个 Spring IoC 容器可以管理一个或多个 Bean。容器会根据所提供的配置元数据(例如 XML、Java注解、Java类)来创建并管理这些 Bean,其中也包括它们之间的依赖关系。

Spring IoC 容器对 Bean 并没有太多的要求,无须实现特定接口或依赖特定库,只要是最普通的 Java 对象即可,这类对象也被称为 POJO (Plain Old Java Object)。

POJO (Plain Old Java Object) 指的是一种没有实现任何框架或库特定接口或继承特定父类的简单 Java 对象。它们简单地定义了属性、 gettersetter 方法,以及通常的 toStringequalshashCode 方法。

3.2 Bean 的属性

在 Spring IoC 容器中,这些 Bean 的定义被表示为 BeanDefinition 对象。 org.springframework.beans.factory.config.BeanDefinition 接口是描述一个 Bean 的元数据信息的核心对象,它包含了一个 Bean 的所有属性、依赖关系和行为。BeanDefinition 对象用于指导 IoC 容器如何创建、初始化和管理 Bean 实例。

一个 BeanDefinition 对象通常包含以元数据:

  • Bean的标识信息:Bean 的名称和别名等标识信息,也就是 Bean 的 id;
  • Bean的类信息:通常是被定义的 Bean 的实际实现类的全路径名,不能是接口;
  • Bean的作用域,是单例 (singleton) 还是原型 (prototype) ;
  • Bean的依赖信息:Bean 对其他 Bean 的依赖信息,例如依赖注入的属性或构造函数参数等。
  • Bean的构造方法信息:Bean 的构造方法以及构造方法参数的信息;
  • Bean的属性信息:Bean 的属性值、属性引用以及其他 Bean 的信息等;
  • Bean的初始化和销毁方法信息:Bean 的初始化和销毁方法名称,以及方法的参数等信息。

上面提到的这些元数据,被定义为构成 Bean 的一组位于 <bean> 元素中的属性,如下表:

属性名值类型说明常用属性值
id字符串Bean的唯一标识符
name字符串Bean的别名(可指定多个)
class字符串Bean的类型(不能是接口)
scope字符串Bean的作用域singleton(单例)、prototype(原型)、request(请求)、session(会话)、globalSession(全局会话)等
abstractboolean是否为抽象Beantrue 或 false
lazy-initboolean是否在IoC容器启动时进行初始化true 或 false
autowire字符串自动注入模式byName、byType、constructor、autodetect等
autowire-candidateboolean是否参与自动注入true 或 false
primaryboolean是否为首选Bean(解决自动注入时的歧义)true 或 false
init-method字符串Bean的初始化方法
destroy-method字符串Bean的销毁方法
factory-method字符串工厂方法
factory-bean字符串工厂Bean

对于 Bean 的注册顺序,Spring 官方建议优先注册单例的 Bean,官方建议如下:

大致意思就是,Bean 的元数据和手动提供的单例实例(singleton)需要尽可能早地注册,以便容器在自动装配和其他自检步骤期间正确地推断它们。虽然在某种程度上支持覆盖现有元数据和现有单例实例,但在运行时注册新Bean (与对工厂的实时访问同时进行) 不受官方支持,并且可能导致并发访问异常、Bean 容器中的不一致状态,或者两者都有。

3.3 Bean 的命名规则

每个 Bean 都有一个或多个标识符,这些标识符在托管 Bean 的 IoC 容器中必须是唯一的,且一个 Bean 通常只有一个标识符。但是,如果需要多个,则多出的可以视为别名(alias)。

在基于 XML 的配置元数据中,我们可以使用 idname 属性来指定 Bean 标识符。

  • id 属性:Bean 的唯一标识符,用于在 IoC 容器中唯一标识一个 Bean。
  • name 属性:是 Bean 的别名,用于为 Bean 指定多个名称。

如果没有为 Bean 指定 id 属性,则 IoC 容器会使用 name 属性的第一个值作为 Bean 的 id 属性值。

例如,下面的 <bean> 元素中,id 为 “myBean”,name 为 “bean1” 和 “bean2”,表示该 Bean 的唯一标识符为 “myBean”,别名为 “bean1” 和 “bean2”:

<bean id="myBean" name="bean1,bean2" class="com.example.MyBean"/>

上面的方式是在 <bean> 中直接定义了别名,如果你需要为别处定义的 Bean 引入别名,即在 <bean> 元素外为某个 Bean 指定别名。在基于 XML 的配置元数据中,就可以使用 <alias/> 元素来完成此操作。

以下示例显示了如何执行此操作:

<alias name="myBean" alias="bean3"/>

在这种情况下,在使用此别名定义后,命名的 Bean(在同一容器中)myBean 也可以称为 bean3。这样,我们就可以使用 “myBean” 或 “bean3” 引用同一个 Bean 实例,例如:

MyBean myBean = applicationContext.getBean("myBean", MyBean.class);
MyBean myBean2 = applicationContext.getBean("bean3", MyBean.class);

在上述代码中,“myBean” 和 “bean3” 都指向同一个 MyBean 实例。

当然,Spring 并没有强制你为 Bean 提供 nameid。如果你没有显式地提供 nameid,容器将为该 Bean 自动生成一个唯一的名称。但如果涉及到依赖注入,且希望通过使用 ref 元素来按名称引用该 Bean,则必须提供名称。

下面是 Spring 官方的命名约定:

即,在命名 Bean 时使用标准 Java 约定作为实例字段名称。也就是说,Bean 名称以小写字母开头,并从那里开始使用驼峰式大小写。此类名称的示例如 accountManageraccountServiceuserDaologinController 等。

4.实例化 Bean

在实际开发中,我们一般不会直接 new 一个 Bean 对象,而是通过 Spring 容器来管理和获取 Bean 对象。而要想使用 Spring 容器来管理 Bean 对象,首先就需要对 Bean 进行实例化。本节将详细介绍 Bean 实例化的几种方式,并针对每种方式给出详细的示例代码。

4.1 BeanDefinition

首先,我们需要了解一下 BeanDefinition 在 Spring 中的概念。BeanDefinition 是 Spring 框架中的一个接口,它用于描述一个 Bean 的定义信息。它包含了 Bean 的名称、类型、作用域、构造方法、属性、依赖等等的信息。在 Spring 容器启动时,如果配置文件中存在 Bean 的定义信息,Spring 容器就会按照这些信息来实例化 Bean 对象。

4.2 使用构造函数实例化

使用构造方法来实例化 Bean 是最常用的方式之一。其实现方式相较于其他方式也比较简单,只需要在 Bean 定义中指定一个或多个构造器参数即可。

下面是一个简单的示例代码。

public class Hello02 {
    private String name;
    
    public Hello02(String name) {
        this.name = name;
    }
        
    public String sayHello() {
        return "Hello " + name;
    }
}

在这个示例中,我们定义了一个 Hello02 类,它只有一个构造函数,该构造函数接受一个 String 类型的 name 参数。下面是一个 Spring 配置文件的示例,它将 Hello02 定义为一个 Bean,并使用了构造方法来实例化 Bean。

<bean id="hello02" class="cn.javgo.ch1springboothelloworld.helloworld.common.Hello02">
    <constructor-arg name="name" value="javgo"/>
</bean>

在这个示例中,我们首先指定了一个 Bean 的 id 为 hello02,并将其类型指定为cn.javgo.ch1springboothelloworld.helloworld.common.Hello02。接着,我们将一个值为“javgo”的字符串常量传递给它的构造函数。这样,当 Spring 容器启动时,它将根据这些信息来实例化一个新的 Hello02 对象。

4.3 使用静态工厂方法实例化

除了使用构造方法来实例化 Bean,我们还可以使用静态工厂方法来实现 Bean 的实例化。同样的,这也是一种相对简单的方式。我们只需要在 Bean 定义中指定一个工厂类,并调用它的静态方法来创建 Bean 对象即可。

静态工厂方法通常具有以下特点:

  • 静态工厂方法是一个静态方法,不需要实例化对象即可调用;
  • 静态工厂方法可以返回任意类型的对象,包括该类的子类、接口的实现类等;
  • 静态工厂方法可以根据参数的不同返回不同的对象实例,从而实现对象的灵活创建和管理。

在 Spring 中,静态工厂方法常用于创建和管理 Bean 实例。通过将 Bean 的创建逻辑封装在一个专门的工厂类的静态方法中,我们可以使 Bean 的创建和管理更加灵活和可维护。

下面是一个示例代码:

public class HelloFactory {
    private static Hello03 hello03 = new Hello03();

    private HelloFactory() {
    }

    public static Hello03 getHello03(String name) {
        hello03.setName(name);
        return hello03;
    }
}

在这个示例中,我们定义了一个 HelloFactory 工厂类,它包含一个静态方法 getHello03(String name),用于创建一个新的 Hello03 对象。

对应的 Hello03 代码如下:

public class Hello03 {
    private String name;

    public Hello03() {
    }

    public Hello03(String name) {
        this.name = name;
    }

    public String sayHello() {
        return "Hello " + name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

下面是一个 Spring 配置文件示例,它使用了 HelloFactory 工厂类来实例化 Bean:

<bean id="hello03" class="cn.javgo.ch1springboothelloworld.helloworld.factory.HelloFactory" factory-method="getHello03">
    <constructor-arg name="name" value="javgo"/>
</bean>

在这个示例中,我们首先定义了一个 id 为hello03 的 Bean,并指定它的类型为 HelloFactory。接着,我们将工厂方法 getHello03 指定为创建 Bean 的方式,并传递了一个参数 javgo。这样,当 Spring 容器启动时,它将调用 HelloFactory 的 getHello03 方法来创建一个名为 hello03 的 Bean 对象。

静态工厂方法的使用场景

  1. 工厂方法需要隐藏对象的创建细节,提供一个更加简单的接口,从而简化应用程序的使用和维护。
  2. 工厂方法需要根据需求动态地创建和返回对象实例,从而使应用程序更加灵活和可扩展。
  3. 工厂方法需要实现对象的单例模式、多例模式、懒加载等高级创建模式,从而满足不同场景下的需求。

4.4 使用实例工厂方法实例化

除了使用静态工厂方法来实例化 Bean,我们还可以使用实例工厂方法来实现 Bean 的实例化。这种方式比较特殊,需要在 Bean 定义中指定一个工厂 Bean,并调用它的实例方法来创建 Bean 对象。

下面是一个示例代码:

public class HelloFactory02 {
    private static Hello03 hello03 = new Hello03();
    
    public Hello03 getHello03(String name) {
        hello03.setName(name);
        return hello03;
    }
}

在这个示例中,我们定义了一个 HelloFactory02 工厂类,它包含一个实例方法 getHello03(String name),用于创建一个新的 Hello03 对象。与静态工厂方法不同的是,每次调用 getHello03 方法时,我们都会创建一个新的 HelloFactory02 对象。

下面是一个 Spring 配置文件示例,它使用了 HelloFactory02 工厂类来实例化 Bean:

<bean id="helloFactory02" class="cn.javgo.ch1springboothelloworld.helloworld.factory.HelloFactory02"/>

<bean id="hello3" factory-bean="helloFactory02" factory-method="getHello03">
    <constructor-arg name="name" value="javgo"/> 
</bean>

在这个示例中,我们首先定义了一个 id 为 helloFactory02 的 Bean,并指定它的类型为 HelloFactory02。接着,我们将工厂 Bean 指定为 helloFactory02,并传递了一个参数 javgo 以调用 getHello03 方法。这样,当 Spring 容器启动时,它将调用 HelloFactory02 的 getHello03 方法来创建名为 hello3 的 Bean 对象。

可见,要使用此机制,需要将使用实例工厂方法对应的 <bean> 元素的 class 属性留空,并在 factory-bean 属性中指定当前(或父级或祖先)容器中的工厂 Bean 名称,要求被指定的工厂 Bean 包含要调用以创建对象的实例方法。最后,使用 factory-method 属性指定对应的工厂方法名即可。

实例工厂方法必须在一个实例上调用,而不是在类上调用。因此,在使用实例工厂方法实例化 Bean 时,必须先实例化工厂 Bean,并将其注册到 Spring IoC 容器中。

实例工厂方法的使用场景与静态工厂方法区别不大。

4.5 确定 Bean 的运行时类型

确定特定 bean 的运行时类型并不容易。在 bean 元数据定义中指定的类只是一个初始类引用,可能与声明的工厂方法(静态工厂方法或实例工厂方法)结合使用或是 FactoryBean 类,这可能导致 bean 的不同运行时类型,或者在实例级别工厂方法的情况下根本没有设置 class 属性(这是通过 factory-bean 属性指定工厂 bean 名称解决的)。此外,对于使用了 AOP 等增强功能的 Bean(AOP 下一篇文章会详细讨论),Spring IoC 容器会将其包装在代理对象中,从而屏蔽了实际的 Bean 实例类型。在这种情况下,我们很难确定 Bean 的运行时类型,因为代理对象的类型与实际的 Bean 实例类型不同。

了解特定 bean 的实际运行时类型的推荐方法是使用 BeanFactory.getType 调用指定的 bean 名称。这考虑到了上述所有情况,并返回与 BeanFactory.getBean 调用相同 bean 名称的对象类型。

在使用 getType() 方法时,需要注意的是,如果 Bean 实例是被代理过的,则 getType() 方法会返回代理对象的类型,而不是实际 Bean 实例的类型。

除了使用 getType() 方法外,我们还可以通过调用 Bean 实例的 getClass() 方法来获取 Bean 的运行时类型。但是,这种方式也有一定的局限性,因为在某些情况下,我们无法直接访问 Bean 实例,例如在使用 AOP 等增强功能时,我们只能访问代理对象,无法直接访问实际的 Bean 实例。

5.Bean的依赖关系

在 Spring 框架中,Bean 的依赖关系是一个非常核心的概念。理解 Bean 的依赖关系将会帮助胖友更好地理解 Spring 的 IoC(Inverse of Control)特性,同时也能够帮助解决应用程序中的一些设计问题。在本节中,我们将介绍 Bean 的依赖关系以及如何管理这些依赖关系。

5.1 什么是Bean依赖关系?

Bean 的依赖关系,指的是在应用程序中,一个 Bean 可能会依赖于另一个 Bean,也就是说一个 Bean 在其生命周期内将会依赖于另外一个 Bean 的一个实例来完成一些操作。在此过程中,我们需要为这些依赖关系提供支持,同时也需要保证这些依赖关系的正确性。

依赖关系是通过依赖注入(Dependency injection,DI)来实现的。我们需要明白的是,依赖注入 (DI) 是一个过程。在这个过程中,对象仅通过构造函数参数、静态工厂方法的参数或实例工厂方法的参数创建并返回对象实例后,通过在对象实例上设置的属性来定义它们的依赖关系(即与它们一起工作的其他对象),然后容器在创建 Bean 时根据需要自动注入这些依赖项。

怎么理解上面这句话呢?

这句话的意思是,传统的对象实例化过程通常是由对象本身来控制的,它负责创建和管理自己所依赖的对象。在传统的对象实例化过程中,对象之间的依赖关系通常是硬编码在对象本身中的,这种方式会导致对象之间的耦合性很高,不利于对象的复用和维护。而在依赖注入过程中,对象的实例化和依赖项的注入由外部容器来控制,即容器会自动创建和注入对象所依赖的其他对象,从而实现了对象之间的解耦和简化。

总之,需要明确的一点是,控制反转(IoC)是一种设计思想,而依赖注入(DI)过程本质上就是一种控制反转的具体实现技术,它将对象的实例化和依赖项的注入从对象本身转移到了外部容器中,实现了对象之间的解耦和简化!!!

在 Spring 中,我们有多种方法可以实现 Bean 的依赖关系,包括基于构造函数的依赖注入、基于 Setter 的依赖注入、自动织入、方法注入。

5.2 基于构造函数的依赖注入

构造函数是 Java 中最基本也是最常用的初始化对象的方式。Spring 框架提供了基于构造函数的依赖注入,可以通过构造函数将 Bean 的依赖关系注入到目标 Bean 中,构造方法的每个参数就代表着一个依赖项。

在 XML 配置文件中,我们可以指定构造方法的参数类型,并从容器中获取这些参数所对应的 Bean 对象。然后,Spring 就可以自动调用对应的构造方法来创建 Bean 对象,并将这些依赖对象传入构造方法。这一切的前提是,你必须提供有对应的构造方法。

下面是一个用来演示的 Hello04 类:

/**
 * @Author: JavGo
 * @DateTime: 2023/4/18 20:43
 **/
public class Hello04 {
    // 依赖项
    private String name;
    // 依赖项
    private int age;

    public Hello04() {
    }
    
    // 提供对应的构造方法
    public Hello04(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String sayHello() {
        return "Hello " + name + ", age: " + age;
    }
}

基于构造函数的依赖注入需要在 Bean 的定义中进行配置。如下所示,我们可以使用 <constructor-arg> 来定义一个构造函数的参数:

<bean id="hello04" class="cn.javgo.ch1springboothelloworld.helloworld.common.Hello04">
    <constructor-arg name="name" value="javgo"/>
    <constructor-arg name="age" value="18"/>
</bean>

在这个例子中,我们定义了一个名为 hello04 的 Bean,并将其类定义为 cn.javgo.ch1springboothelloworld.helloworld.common.Hello04。在定义该 Bean 时,我们通过 <constructor-arg> 标签为其指定了两个构造函数参数,分别是 name 和 age。其中 name 属性用于指定构造函数参数名,value 属性用于指定需要传入的参数值。在实例化 Bean 时,Spring 框架将会自动查找适合的构造函数,并将其参数值注入到该函数中。

最终这些参数将会在实例化 Bean 时被传入如下 Bean 的构造函数中:

// 提供对应的构造方法
public Hello04(String name, int age) {
    this.name = name;
    this.age = age;
}

<constructor-arg> 中属可配置的属性,具体如下表:

属性作用
value要传给构造方法参数的值(基本数据类型+String)
ref要传给构造方法参数的 Bean ID(对象的引用)
type构造方法参数对应的类型
index构造方法参数对应的位置,从 0 开始计算
name构造方法参数对应的名称

基于构造方法的注入是 Spring 中一种常用的依赖注入方式,它可以确保在创建 Bean 对象时,所需的依赖对象必须被注入。如果没有提供必要的依赖对象,Spring 就会抛出异常。这对于保证代码正确性是很有帮助的。

5.3 基于Setter的依赖注入

5.3.1 原理概述

Setter 方法是用来设置一个对象属性的最常用方法。Spring 框架提供了基于 Setter 的依赖注入来实现 Bean 的依赖关系。基于 Setter 的 Dl 是通过容器在调用无参数构造函数或无参数静态工厂方法来实例化 Bean 之后调用 Bean 上对应的的 Setter 方法设置依赖项来实现的。

5.3.2 循环依赖问题

基于 Setter 方法注入的方式具有“延迟初始化”Bean 的特点,延迟初始化的优点在于,当容器需要创建一个 Bean 时,不需要立即调用 Bean 上的所有 Setter 方法,而是先将该 Bean 创建出来,只有当需要注入某个依赖项时才会调用相应的 Setter 方法。这样可以节省资源和时间,并且降低初始化的复杂度。

如果容器在创建 Bean 时直接调用所有 Setter 方法,可能会导致依赖项之间的循环依赖问题。例如,如果 Bean A 依赖于 Bean B,而 Bean B 又依赖于 Bean A,如果容器在创建 Bean A 时直接调用 Bean B 上的 Setter 方法,就会导致 Bean B 中的依赖项无法注入,从而导致程序出错,则 Spring IoC 容器会在运行时检测到此循环引用,并抛出一个 BeanCurrentlyInCreationException Bean 创建异常。

而采用延迟初始化的方式,容器会先调用无参数构造函数或无参数静态工厂方法创建所有 Bean 的实例,并完成所有 Bean 实例的依赖注入,在需要时再通过 Setter 进行属性赋值,这样就可以避免循环依赖问题。

5.3.3 案例演示

在配置文件中,我们可以通过 <property> 标签指定每个 Setter 方法的参数,并从容器中使用 ref 属性获取这些参数所对应的 Bean 对象的引用或者通过 value 属性直接传入普通类型的值(基本数据类型+String)。然后,Spring 就可以自动调用该 Setter 方法来注入所需的依赖对象。

当然,前提是你必须提供有对应的 Setter 方法:

/**
 * @Author: JavGo
 * @DateTime: 2023/4/18 21:09
 **/
public class Hello05 {
    // 依赖项
    private String name;
    // 依赖项
    private int age;

    public Hello05() {
    }

    // 提供 name 依赖项的 setter 方法
    public void setName(String name) {
        this.name = name;
    }

    // 提供 age 依赖项的 setter 方法
    public void setAge(int age) {
        this.age = age;
    }

    public String sayHello() {
        return "Hello " + name + ", age: " + age;
    }
}

如下所示,我们可以通过 <property> 标签定义一个 Bean 的属性:

<bean id="hello05" class="cn.javgo.ch1springboothelloworld.helloworld.common.Hello05">
    <property name="name" value="javgo"/>
    <property name="age" value="18"/>
</bean>

在这个例子中,我们定义了一个名为 hello05 的 Bean,并将其类定义为 cn.javgo.ch1springboothelloworld.helloworld.common.Hello05。在定义该 Bean 时,我们为其指定了两个属性,分别是 name 和 value。在实例化 Bean 时,Spring 框架将会自动调用对应的 Setter 方法,将属性值注入到该方法中。

最终这些属性将会在实例化 Bean 时被调用如下 Setter 方法设置:

// 提供 name 依赖项的 setter 方法
public void setName(String name) {
    this.name = name;
}

// 提供 age 依赖项的 setter 方法
public void setAge(int age) {
    this.age = age;
}

下面是一些其他情况的应用举例:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <!--  其他情况举例  -->
    <bean id="..." class="...">
        <property name="...">
            <!--   直接定义一个内部的 Bean   -->
            <bean class="..."/>
        </property>
    </bean>

    <bean id="..." class="...">
        <!--    定义依赖的 Bean    -->
        <property name="..." ref="..."/>
    </bean>

    <bean id="..." class="...">
        <property name="...">
            <!--    定义一个列表注入多个相同类型的简单类型对象    -->
            <list>
                <value>aaa</value>
                <value>bbb</value>
                <value>...</value>
            </list>
        </property>
    </bean>
    
    <bean id="..." class="...">
        <property name="...">
            <!--    定义一个列表注入多个相同类型的引用类型对象    -->
            <list>
                <ref bean="..."/>
                <ref bean="..."/>
                <ref bean="..."/>
            </list>
        </property>
    </bean>
</beans>

基于 Setter 方法的注入是 Spring 中常用的依赖注入方式,因为它允许在 Bean 对象创建之后再注入依赖对象。这对于动态地注入或替换依赖对象是很有帮助的。

不过,基于 Setter 方法的注入有一个缺点,就是不能保证在创建 Bean 对象时必须注入所需的依赖对象,因此代码的正确性可能会受到影响。

5.3.4 基于构造还是 Setter?

Spring 官方给出了一个问题:

那么,到底是基于构造函数还是基于 Setter 进行依赖注入呢(DI)?下面是 Spring 官方给出的大致建议:


在 Spring 中,我们可以使用构造函数注入和 Setter 方法注入来实现依赖注入。一般来说,我们应该使用构造函数注入来注入必需的依赖项,使用 Setter 方法或配置方法来注入可选的依赖项。虽然可以使用 @Required 注解来标记 Setter 方法的属性为必需依赖项,但是使用构造函数注入并通过程序进行参数验证是更好的选择。

Spring 团队通常推荐使用构造函数注入,因为这样可以将应用程序组件实现为不可变对象,并确保必需的依赖项不为 null。此外,通过构造函数注入的组件总是以完全初始化的状态返回给客户端代码。需要注意的是,如果构造函数参数过多,这通常意味着该类可能具有太多的责任,应该进行重构以更好地实现关注点分离。

Setter 方法注入主要应该用于可选的依赖项,这些依赖项可以在类内部分配合理的默认值。否则,代码在使用依赖项的任何地方都必须进行非空检查。Setter 方法的另一个好处是,可以使该类的对象易于在以后进行重新配置或重新注入。

应根据特定类的情况选择最合适的DI方式。有时,当使用第三方类时,你可能无法更改其源代码,这时候DI的方式就由第三方类决定了。例如,如果第三方类没有暴露任何Setter方法,则构造函数注入可能是唯一可用的DI方式。

5.4 自动织入

5.4.1 自动织入概述

前面的两种手动配置依赖方式在 Bean 少时还能接受,当 Bean 的数量变多后,这种配置就会变得非常繁琐。在合适的场合,可以让 Spring 容器替我们自动进行依赖注入,这种机制称为自动织入,也可以称为自动装配。当使用自动装配时,你只需在配置文件中声明 Bean,而不需要手动通过构造函数或者 Setter 方式显示配置它们的依赖关系。

自动织入是一种使用 AspectJ 注解来定义维度的方式,它可以非常方便地实现 Bean 的依赖注入。如果你使用基于注解的方式,我们可以使用 @Autowire 注解将一个 Bean 注入到另一个 Bean 中。在后面的小节中会进行详细讲解。

5.4.2 自动织入的优点

自动装配具有以下优点:

  • 自动装配可以显著减少指定属性或构造函数参数的需要。
  • 自动装配可以随着对象的演变而更新配置。例如,如果你需要再向类添加一个依赖项,那么该依赖项可以自动得到满足,而不需要修改配置。

5.4.3 四种自动装配策略

Spring 支持四种自动装配策略,具体见下表:

自动装配策略说明
no(默认值)不自动装配。Bean引用必须由 ref 元素定义。对于较大的部署,不建议更改默认设置,因为显式指定依赖对象可以提供更好的控制和提供更好的可读性。
byName通过属性名自动装配。Spring 在当前容器或父容器中寻找与需要自动装配的属性具有相同名称的 Bean。例如,如果一个 Bean 定义被设置为按名称自动装配,并且它包含一个主属性(也就是说,它有一个 setMaster(..) 方法),Spring 将寻找一个名为 master 的 Bean 定义并使用它来设置该属性。
byType通过属性类型自动装配。如果容器中仅存在一个属性类型的 Bean,则允许自动装配属性。如果存在多个,则抛出一个异常表明你不能对该 Bean 使用 byType 自动装配。如果没有匹配的 Bean,则什么都不会发生(属性未设置)。
constructor类似于 byType,但适用于构造函数参数。如果容器中没有一个构造函数参数类型的 Bean,则会引发致命的 Error。

当使用基于 XML 的方式配置元数据时,你可以使用 <bean/> 元素的 autowire 属性为 Bean 定义指定自动装配模式。

示例如下:

<bean id="hello" class="com.example.Hello" autowire="byType"/>

当然,你也可以在 <beans/> 根元素中设置 default-autowire 属性指定全局默认的自动织入方式(一般不推荐),示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
       default-autowire="byName"
>

</beans>

5.4.4 自动装配注意事项

在使用自动织入时,需要注意以下事项:

  • 开启自动织入后,仍可以手动设置依赖,手动设置的依赖优先级高于自动织入;
  • 自动织入无法注入基本类型和字符串,只能织入一个 Bean(引用类型);
  • 对于集合类型的属性,自动织入会把上下文里找到的 Bean 都放进去,但如果属性不是集合类型,有多个候选 Bean 就会有问题。

为了避免第三点中说到的问题,可以将 <bean/>autowire-candidate 属性设置为 false 来禁用自动装配,也可以在你所期望的候选 Bean 的 <bean/> 中将 primary 属性设置为 true,这就表明在多个候选 Bean 中该 Bean 是主要的,当有有多个候选 Bean 时就会优先使用该 Bean。

如果使用基于 Java 类的配置方式,我们可以通过使用 @Prmary 注解实现一样的功能。

5.5 方法注入

5.6 指定Bean的初始化顺序

一般情况下,Spring 容器会根据依赖情况自动调整 Bean 的初始化顺序。不过,有时 Bean 之间的依赖并不明显,容器可能无法按照我们的预期进行初始化,这时我们可以自己来指定 Bean 的依赖顺序。

5.6.1 通过设置 depends-on 属性

可以在 Bean 配置中设置 depends-on 属性,以指定 Bean 的初始化顺序,该属性可以指定当前 Bean 还要依赖哪些 Bean。

下面是一个简单示例:

<bean id="beanA" class="com.example.BeanA"/>
<bean id="beanB" class="com.example.BeanB" depends-on="beanA"/>
<bean id="beanC" class="com.example.BeanC" depends-on="beanB"/>

这意味着,BeanB 将在 BeanA 初始化后初始化,而 BeanC 将在 BeanB 初始化后初始化。

如果使用基于 Java 类的配置方式,@DependsOn 注解也能实现一样的功能。

@DependsOn("beanB") 
@Component 
public class BeanA { 
    // ... 
}

@Component 
public class BeanB { 
    // ...
}

5.6.2 通过实现 InitializingBean 接口

在 Spring 中,InitializingBean 是一个回调接口,用于**在 Bean 实例化后,属性注入完成之后,执行自定义初始化逻辑。**该接口只有一个方法 afterPropertiesSet(),在 Bean 实例化和属性注入完成后,Spring 会调用该方法完成 Bean 的初始化。

接口源码如下:

public interface InitializingBean {
   void afterPropertiesSet() throws Exception;
}

InitializingBean 接口的作用主要有以下几点:

  1. 统一管理Bean的初始化逻辑:通过实现 InitializingBean 接口,将 Bean 的初始化逻辑统一放在 afterPropertiesSet() 方法中,使得 Bean 的初始化逻辑更加清晰明了。
  2. 在Bean属性注入完成之后执行初始化逻辑:在 Bean 实例化之后,属性注入完成之后,Spring 会调用 afterPropertiesSet() 方法执行自定义的初始化逻辑,确保 Bean 的属性注入完成后再执行初始化操作。
  3. 可以通过实现该接口来检查Bean的完整性:在 afterPropertiesSet() 方法中,可以对 Bean 的属性进行检查,确保 Bean 的完整性。如果 Bean 的属性不完整或不合法,可以抛出异常,使 Bean 的初始化失败。

需要注意的是,InitializingBean 接口是 Spring 提供的一种扩展机制,如果我们需要在 Bean 初始化时执行一些自定义的逻辑,可以通过实现该接口来实现。但是,在实现该接口时,需要注意避免与其他组件产生依赖关系,以保证 Bean 的可移植性。同时,也可以通过配置文件中的 init-method 属性来指定 Bean 的初始化方法,而不是实现 InitializingBean 接口。

下面是一个简单示例:

public class BeanA implements InitializingBean {
   public void afterPropertiesSet() {
      // init code
      new BeanB(). afterPropertiesSet();
   }
}

public class BeanB implements InitializingBean {
   public void afterPropertiesSet() {
      // init code
   }
}

public class BeanC implements InitializingBean {
   public void afterPropertiesSet() {
      // init code
   }
}

这意味着,您可以在 BeanA 的 afterPropertiesSet() 方法中调用 BeanB 的 afterPropertiesSet() 方法,以确保 BeanB 在 BeanA 初始化后初始化。

5.6.7 通过实现 BeanPostProcessor 接口

在 Spring 中,BeanPostProcessor 接口是一个扩展点,用于在 Bean 实例化、属性注入和初始化完成之后,对 Bean 进行额外的处理。

该接口定义了两个方法:

  1. postProcessBeforeInitialization(Object bean, String beanName):在Bean初始化之前,对Bean进行额外的处理。bean 参数是要处理的 Bean 对象,在这个方法中可以修改它的属性值或做一些其它的操作。beanName 是要处理的 Bean 的名称,在 Spring 容器中,Bean 的名称是唯一的。可以在这个方法中根据 Bean 的名称做一些特定的处理。
  2. postProcessAfterInitialization(Object bean, String beanName):在Bean初始化之后,对Bean进行额外的处理。

BeanPostProcessor 接口的作用主要有以下几点:

  1. 扩展Spring的功能:通过实现 BeanPostProcessor 接口,可以扩展 Spring 的功能,对 Bean 进行额外的处理,如AOP、事务等。
  2. 实现自定义的初始化逻辑:通过实现 BeanPostProcessor 接口,可以在 Bean 初始化之前或之后,对 Bean 进行自定义的初始化逻辑,如通过反射修改 Bean 的属性值等。
  3. 实现自定义的销毁逻辑:除了初始化之外,BeanPostProcessor 接口还可以在 Bean 销毁之前或之后,对 Bean 进行自定义的销毁逻辑,如释放资源等。

需要注意的是,BeanPostProcessor 接口是 Spring 提供的一种扩展机制,可以通过实现该接口对 Bean 进行额外的处理。但是,在实现该接口时,需要注意避免与其他组件产生依赖关系,以保证 Bean 的可移植性。同时,也需要注意在对 Bean 进行处理时,不要破坏 Bean 的完整性和稳定性。

下面是一个简单的案例,用于在 Bean 初始化前后自定义一些检查逻辑来指定 Bean 的初始化顺序:

public class MyBeanPostProcessor implements BeanPostProcessor {
    // 定义一个 BEAN_NAMES_ORDER 列表指定 Bean 的初始化顺序
    private static final List<String> BEAN_NAMES_ORDER = Arrays.asList("beanA", "beanB", "beanC");

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException{
        // 输出初始化前的 bean 的名称
        System.out.println("postProcessBeforeInitialization: " + beanName);
        return bean;
    }
	 
    // 通过 BEAN_NAMES_ORDER 列表控制 Bean 的初始化顺序
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // 输出初始化后的 bean 的名称
        System.out.println("postProcessAfterInitialization: " + beanName);
        // 通过 BEAN_NAMES_ORDER 数组获取当前 bean 的前一个 bean 的名称的索引值
        int index = BEAN_NAMES_ORDER.indexOf(beanName);
        // 如果当前 bean 的前一个 bean 的索引值大于0,说明它存在
        if (index > 0) {
            // 获取当前 bean 的前一个 bean 的名称
            String previousBeanName = BEAN_NAMES_ORDER.get(index - 1);
            // 通过 bean 工厂获取前一个 bean 的单例实例
            Object previousBean = ((ConfigurableListableBeanFactory)beanFactory).getSingleton(previousBeanName);
            // 如果前一个 bean 的实例没有被初始化,则进行初始化
            if (!((MyBean)previousBean).isInitialized()) {
                ((MyBean)previousBean).initialize();
            }
        }
        // 返回当前 bean 的实例
        return bean;
    }
}

该示例中对于每个 Bean 都进行了判断,如果前一个 Bean 未初始化,则先初始化前一个 Bean。这样就可以控制 Bean 的初始化顺序了。

5.7 依赖解析过程

在 Spring 中,Bean 的依赖解析过程可以分为以下几个步骤:

  1. 加载Bean定义:Spring 通过读取配置文件或注解来加载 Bean 的定义,将其转化为 BeanDefinition 对象。

    在 Spring 中,Bean 的定义信息保存在 BeanDefinition 对象中,包括 Bean 的类名、属性、依赖关系等信息。Spring 通过 BeanDefinitionReader 接口的实现类来读取配置文件或注解,然后将 Bean 的定义信息转化为 BeanDefinition 对象。

  2. 实例化Bean:根据 BeanDefinition 中的信息,Spring 通过反射机制实例化 Bean 对象。

    在 Spring 中,Bean 的实例化是通过 BeanFactory 接口的实现类来完成的。BeanFactory 接口的实现类会根据 BeanDefinition 中的信息,使用反射机制实例化 Bean 对象。在实例化 Bean 对象时,Spring 还会判断是否需要代理,如果需要代理,则会使用动态代理技术生成代理对象。

  3. 设置Bean属性:Spring 通过反射机制将 Bean 的属性值设置到 Bean 对象中。

    在 Spring 中,Bean 的属性注入是通过 BeanWrapper 接口的实现类来完成的。BeanWrapper 接口主要用于对 Bean 对象进行包装,提供了设置和获取 Bean 属性值的方法。Spring 会根据 BeanDefinition 中的信息,使用反射机制将 Bean 的属性值注入到 Bean 对象中。

  4. 解析Bean依赖:Spring 通过 Bean 的依赖关系图,解析 Bean 之间的依赖关系。如果 Bean 之间存在依赖关系,则先实例化被依赖的 Bean。

    在 Spring 中,Bean 之间的依赖关系是通过 BeanDefinition 中的信息来确定的。如果 Bean 之间存在依赖关系,则先实例化被依赖的 Bean,然后再将依赖的 Bean 注入到需要依赖的 Bean 中。Spring 使用依赖注入的方式来解决 Bean 之间的依赖关系。

  5. 注入Bean依赖:Spring 通过反射机制将依赖的 Bean 注入到需要依赖的 Bean 中。

    在 Spring 中,依赖注入是通过 BeanWrapper 接口的实现类来完成的。Spring 会根据 BeanDefinition 中的信息,使用反射机制将依赖的 Bean 注入到需要依赖的 Bean 中。

  6. 初始化Bean:Spring 通过调用 Bean 实现了 InitializingBean 接口的 afterPropertiesSet() 方法或配置文件中配置的 init-method 方法来完成 Bean 的初始化操作。

    在 Spring 中,Bean 的初始化是通过 BeanPostProcessor 接口的实现类来完成的。BeanPostProcessor 接口提供了两个方法,分别是 postProcessBeforeInitialization() 和 postProcessAfterInitialization()。在 Bean 初始化之前和之后,Spring 会调用 BeanPostProcessor 接口的实现类的这两个方法来完成 Bean 的初始化操作。

  7. 返回Bean实例:完成以上步骤后,Spring将Bean实例返回给调用者。

    在 Spring 中,Bean 实例化完成后,Spring 会将 Bean 实例保存在 BeanFactory 中,供其他组件使用。调用者可以通过 BeanFactory 或 ApplicationContext 接口的 getBean() 方法来获取 Bean 实例。

6.Bean作用域

前面大篇幅的都是使用基于 XML 的方式讲解,下面换个口味使用基于注解的方式进行讲解,具体 Bean 的基于注解的装配方式我们在下一节就会详细讨论。

在 Spring Framework 中,Bean 的作用域决定了 Bean 的生命周期和可见范围。在本节中,我们将深入探讨 Spring Framework 中 Bean 的作用域,并详细介绍每种作用域的特点和使用场景。

6.1 Bean的作用域概述

在 Spring 配置文件定义 Bean 时,通过声明 scope 配置项,可以灵活定义 Bean 的作用范围。在 Spring Framework 6.x 中,Bean 的作用域分为以下6种:

作用域(Scope)描述
singleton(默认)单例模式,整个应用程序只存在一个Bean实例;
prototype原型模式,每次请求Bean时都会创建一个新的实例;
request请求作用域,每个HTTP请求都会创建一个新的Bean实例;
session会话作用域,每个HTTP会话都会创建一个新的Bean实例;
application将单个 bean 定义的范围限定为 ServletContext 的生命周期。
websocket将单个 bean 定义的范围限定为 WebSocket 的生命周期。

虽然官方文档的表格写着是六个作用域,但是很多时候都会被认为只有五个作用域。因为 applicationwebsocket 的作用差不多,通常会被认为是一个。

在 Spring Framework 中只有五种,分别是 Singleton、Prototype、Request、Session、Global session。

6.2 Singleton作用域

当你定义一个 bean 定义并将其限定为单例时,Spring IoC 容器会创建该 bean 定义定义的对象的一个实例。这个单个实例存储在此类单例 bean 的缓存中,并且对该命名 bean 的所有后续请求和引用都返回缓存的对象。

下图显示了单例作用域的工作原理:

这与我们所说的单例模式基本是相同的,在 Spring Framework 中,默认的 Bean 作用域为 Singleton,即整个应用程序中只存在一个 Bean 实例。这意味着无论在何处获取该 Bean 实例,都将返回同一个实例。Singleton 作用域是最常见的作用域,适用于那些不需要频繁创建 Bean 实例的情况。

下面是一个使用 Singleton 作用域的示例:

@Configuration
public class AppConfig {

    @Bean
    public UserService userService() {
        return new UserServiceImpl();
    }
}

在上面的示例中,userService() 方法返回一个 UserServiceImpl 实例,并且默认的作用域为 Singleton。因此,无论在何处获取 UserService 实例,都将返回同一个实例。

或者你可可以使用基于 XML 的配置方式,下面是 Spring 官方给出的案例:

<bean id="accountService" class="com.something.DefaultAccountService"/>

<!-- 下面是等价的, 但是冗余的 (singleton 作用域是默认的) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

Singleton 作用域的优点是:

  • 提高了应用程序的性能,因为不需要频繁创建和销毁Bean实例,而是直接从缓存中获取;
  • 简化了应用程序的配置,因为只需要在应用程序中创建一个Bean实例。

Singleton 作用域的缺点是:

  • 不适用于那些需要频繁创建和销毁Bean实例的情况;
  • 在多线程环境下可能会出现线程安全问题;
  • 不适用于那些需要动态修改Bean实例属性的情况。

6.3 Prototype作用域

在 bean部署中,非单例原型范围会在每次针对特定 bean的请求时创建一个新的 bean实例。也就是说,当 bean被注入到另一个 bean中或者您通过容器的 getBean() 方法调用来请求它时,都会创建新的实例。

一般而言,对于有状态的 bean(比如含有成员变量的 bean),应该使用 prototype 作用域,而对于无状态的 bean(比如只有方法的 bean),应该使用 singleton 作用域。这样能够使内容更加丰富、准确、优质。

举个例子,假设有一个用户管理的 bean,里面有用户列表的成员变量。由于不同的用户管理器应该拥有不同的用户列表(即有状态),所以应该使用 prototype 作用域。而另一个展示用户信息的 bean,它只需要根据传入的参数显示相应用户的信息,所以是无状态的,应该使用 singleton 作用域。

下图说明了 Spring 原型作用域工作原理:

DAO(数据访问对象)通常不被配置为原型,因为一个典型的 DAO 不会存储任何会话状态。

举个例子,假设有一个网站,用户需要使用该网站进行注册和登录。网站后端的程序设计可能使用DAO来管理用户数据。在这种情况下,DAO代表一个用户数据的访问对象,它可以读取、更新或删除该用户的数据并返回结果给网站。由于DAO不会存储任何会话状态,因此不需要将其配置为原型。相反,可以使用单例模式确保DAO在应用程序中只有一个实例。

下面是一个使用 Prototype 作用域的示例:

@Configuration
public class AppConfig {

    @Bean
    @Scope("prototype")
    public UserService userService() {
        return new UserServiceImpl();
    }
}

在上面的示例中,userService()方法返回一个 UserServiceImpl 实例,并且作用域为 Prototype。因此,每次请求 UserService 实例时都将创建一个新的实例。

或者你可可以使用基于 XML 的配置方式,下面是 Spring 官方给出的案例:

<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>

Prototype 作用域的优点是:

  • 适用于那些需要频繁创建和销毁 Bean 实例的情况;
  • 可以动态修改 Bean 实例属性。

Prototype 作用域的缺点是:

  • 可能会导致应用程序的性能下降,因为需要频繁创建和销毁 Bean 实例;
  • 可能会导致内存泄漏,因为每个 Bean 实例都需要手动销毁。

Request, Session, Application 和 WebSocket Scopes 作用域实际用得比较少,用到的时候再具体讨论。

7.Bean的三种装配方式

在 Spring Framework 中,装配 Bean 是一个非常重要的概念,前面在 2.6 小节配置元数据时,我们已经简单体验过了,现在进行详细讲解。Bean 的装配可以通过三种方式来实现:XML 配置、注解和 Java 配置。本节将详细介绍这三种装配方式,并通过示例代码来演示它们的用法。

7.1 基于 XML 文件的容器配置

XML 配置是使用 Spring 框架时最常见的方式之一,它可以用来配置 Spring 容器、解析 bean 定义以及定义 bean 之间的依赖关系。

7.1.1 XML 配置文件介绍

每个基于 XML 的 Spring 应用程序都必须至少包含一个 XML 配置文件,该文件声明了 Spring 容器需要使用的 bean,以及它们之间的依赖关系。这个 XML 配置文件通常被称为 Spring 配置文件。关于 Spring 配置文件的名称其实没有什么特别的要求,一般来说最基本的配置文件名为 applicationContext.xml,有时候为了更好的管理甚至可以有多个配置文件,如 applicationContext-dao.xmlapplicationContext-service.xmlapplicationContext-dataSources.xml 等。

在使用 Spring 容器时,我们需要在 Spring 配置文件中定义 bean。每个 bean 都需要通过 id 属性指定一个 ID,用来唯一地标识它。当然,我们前面也提到过,如果你没有显示给出,Spring 会默认给出一个以类名首字母小写开始遵守驼峰命名规则的 ID。一般情况下还需要使用 class 属性给出 bean 的类全路径名,但有时又也可以没有,比如使用实例工厂方法实例化 bean 时我们只需要使用 factory-bean 指定实例化工厂 bean 的引用,然后通过 factory-method 属性指定实例化工厂方法名。此外,还可以提供 bean 的属性和依赖项。

以下示例展示了一个简单的 Spring 配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">
 
    <bean id="exampleBean" class="com.example.ExampleBean">
        <property name="message" value="Hello World!"/>
    </bean>
 
</beans>

在这个配置文件中,我们使用 beans 标签作为根元素,并在其中定义了一个名为 exampleBean 的 bean。class 属性指定了该 bean 的全路径类名,使用 property 标签设置了一个名为 message 的属性,并将其值设置为 “Hello World!”。

上面涉及到的 <bean> 标签可配置属性总结如下:

属性描述
id一个字符串,用于标识单个 bean 定义。id 属性的值还可以被其他 bean 引用时直接使用。
class定义 bean 的类型并使用完全限定的类名(不能是接口) 。

上面涉及到的 <property> 标签可配置属性总结如下:

属性描述
name一个字符串,用于标识单个 bean 定义。id 属性的值还可以被其他 bean 引用时直接使用。
value定义 bean 的类型并使用完全限定的类名(不能是接口)。

7.1.2 bean 的定义和依赖关系配置

除了上面的例子中的示例之外,我们还可以使用以下方式定义 bean,并指定它们之间的依赖关系:

  • 构造器注入:使用构造器注入可以将一个或多个值注入到 bean 的构造函数中。

    <bean id="exampleBean" class="com.example.ExampleBean">
        <constructor-arg value="Hello World!"/>
    </bean>
    

    这意味着我们在实例化 ExampleBean 时,将使用值 “Hello World!” 作为构造函数的参数。上面是单个参数不会产生歧义,所以我们可以忽略参数名。如果存在多个参数,可以使用 indexname 标签指定参数的位置或名称。

  • 属性注入:除了使用构造函数注入之外,还可以通过 property 标签来设置 bean 的属性。

    <bean id="exampleBean" class="com.example.ExampleBean">
        <property name="message" value="Hello World!"/>
    </bean>
    

    这意味着我们创建了一个 ExampleBean 实例,并将其 message 属性设置为 “Hello World!”。

  • 依赖注入:我们可以使用 ref 属性来引用其他 bean,实现依赖关系注。

    <bean id="exampleBean" class="com.example.ExampleBean">
        <property name="anotherBean" ref="otherBean"/>
    </bean>
     
    <bean id="otherBean" class="com.example.OtherBean"/>
    

    这意味着我们在创建 ExampleBean 实例时设置了其 anotherBean 属性。该属性被设置为另一个名为 otherBean 的 bean 的引用。这是定义 bean 之间依赖关系的一种常见方式。

7.1.3 bean 的作用域配置

通过一个 XML 配置文件,Spring 容器能够管理并配置多个 bean。它也允许我们通过 scope 属性配置这些 bean 的范围。

主要有以下两种常用类型的作用域:

  • singleton:默认情况下,bean 都是单例的,也就是说,在 Spring 应用程序中只会存在一个该 bean 实例。

    <bean id="exampleBean" class="com.example.ExampleBean" scope="singleton">
         <property name="message" value="Hello World!"/>
    </bean>
    
  • prototype:原型作用域每次从容器中请求 bean 实例时都会得到一个新的实例。

    <bean id="exampleBean" class="com.example.ExampleBean" scope="prototype">
         <property name="message" value="Hello World!"/>
    </bean>
    

7.1.4 嵌套 bean 的配置

在 XML 配置文件中,我们可以使用嵌套 bean 来组成更复杂的对象。

以下是一个示例:

<bean id="exampleBean" class="com.example.ExampleBean">
    <property name="address">
        <bean class="com.example.Address">
            <property name="street" value="123 Main St."/>
            <property name="city" value="Anytown"/>
            <property name="state" value="CA"/>
            <property name="zip" value="12345"/>
        </bean>
    </property>
</bean>

此示例使用一个 Address bean 来作为 ExampleBean 的一个属性。在 Address bean 中,我们设置了 streetcitystatezip 属性。

7.1.5 通过 XML 文件引入其他 XML 文件配置

当一个应用程序需要配置大量的 bean 时,将它们全部放在一个 XML 文件中可能会使其变得难以管理。为了解决这个问题,Spring 允许使用一种将多个配置文件组合在一起的方法。

以下是一个示例:

<beans>
    <import resource="applicationContext-services.xml"/>
    <import resource="applicationContext-resources.xml"/>
    <import resource="applicationContext-controllers.xml"/>
</beans>

这个示例通过 <import> 标签导入三个其他的 XML 文件,该标签的 resource 属性用于指定配置文件的路径。

7.1.6 控制 Bean 的初始化行为

默认情况下,ApplicationContext 实现会急切地创建和配置所有单例 Bean 作为初始化过程的一部分。通常,这种预实例化是可取的,因为配置或周围环境中的错误会立即被发现,而不是数小时甚至数天之后。

当然,如果对于某些单例 Bean,您并不想让 Spring 如此着急的创建它,您可以通过将 Bean 定义标记为延迟初始化来防止预实例化单例 Bean。惰性初始化的 Bean 会告诉 IoC 容器在第一次请求时才创建一个 Bean 实例,而不是在启动时。

lazy-init 属性控制了 Bean 的初始化行为。如果设置为 true,则该 Bean 将在第一次访问时才被初始化;如果设置为 false,则该 Bean 将在应用程序启动时立即被初始化。默认值为 false。

例如,以下是一个声明了 lazy-init 属性为 true 的 Bean 的示例:

<bean id="exampleBean" class="com.example.ExampleBean" lazy-init="true">
  <property name="message" value="Hello World!"/>
</bean>

当惰性初始化 Bean 是未惰性初始化的单例 Bean 的依赖项时,ApplicationContext 在启动时就会强制创建惰性初始化 Bean,因为它必须满足单例的依赖项。

除了上面的方式,你甚至还可以通过使用 default-lazy-init 元素在 <beans> 容器级别上控制全局延迟初始化(一般不这样做)。如以下示例所示:

<beans default-lazy-init="true">
    
</beans>

7.1.7 可配置项汇总

下面我们对前面所有使用到的,以及暂时未使用到但是后面马上会介绍的的可配置项做一个简单汇总,方便胖友查找。

<bean> 标签的全部可配置项如下:

可配置项作用常用属性值
id为 bean 指定一个唯一的标识符字符串
class指定 bean 的类名类的全限定名
scope指定 bean 的作用域singleton、prototype、request、session、globalSession、application、websocket
init-method指定 bean 的初始化方法初始化方法名(后面会介绍)
destroy-method指定 bean 的销毁方法销毁方法名(后面会介绍)
lazy-init是否延迟初始化 beantrue、false
autowire指定自动装配的方式no、byName、byType、constructor、autodetect
autowire-candidate指定是否将该 bean 作为自动装配的候选者true、false
primary指定自动装配时优先选择该 beantrue、false
depends-on指定该 bean 依赖的其他 beanbean 的 id
factory-bean指定工厂 bean 的 idbean 的 id
factory-method指定工厂方法名静态工厂方法名或实例工厂方法名
parent指定该 bean 的父 beanbean 的 id

<bean> 标签的常用子标签 <constructor-arg> 的全部可配置项如下:

可配置项作用常用属性值
index指定构造函数参数的位置整数
type指定构造函数参数的类型类型的全限定名
value指定构造函数参数的值字面量
ref指定构造函数参数的引用对象bean 的 id

<bean> 标签的常用子标签 <property> 的全部可配置项如下:

可配置项作用常用属性值
name指定 bean 的属性名字符串
value为 bean 的属性注入字面量值字面量
ref为 bean 的属性注入引用对象bean 的 id
collection为 bean 的属性注入集合类型的值list、set、map、props
list为 bean 的属性注入 List 类型的值
set为 bean 的属性注入 Set 类型的值
map为 bean 的属性注入 Map 类型的值
props为 bean 的属性注入 Properties 类型的值
key指定 Map 或 Properties 类型的键字符串
key-ref指定 Map 或 Properties 类型的键引用对象bean 的 id
value-ref指定 Map 类型的值引用对象bean 的 id

<import> 标签的全部可配置项如下:

可配置项作用常用属性值
resource导入外部 XML 配置文件文件路径
classpath导入 classpath 下的 XML 配置文件文件路径
bean-name为导入的 bean 指定别名字符串
lazy-load指定是否延迟导入true、false

7.1.8 实战演示

接下来,我们将通过一个简单的示例演示如何在 Spring 中使用 XML 文件进行配置。假设我们有一个名为 User 的实体类和一个名为 UserDao 的数据访问对象类。

1)创建 User 类:
public class User {
    private String name;
    private String email;
 
    // Getter and Setter
}
2)创建 UserDao 类:
public class UserDao {
    public void save(User user) {
        System.out.println("User saved: " + user.getName());
    }
}
3)创建 Spring 配置文件:

我们可以使用以下 XML 配置文件来定义 UserUserDao bean:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">
 
    <bean id="user" class="com.example.User">
        <property name="name" value="JavGo"/>
        <property name="email" value="javgo@doe.com"/>
    </bean>
 
    <bean id="userDao" class="com.example.UserDao">
        <property name="user" ref="user"/>
    </bean>
 
</beans>

在这个配置文件中,我们定义了两个 bean:UserUserDaoUser bean 包含了一个名为 name 和一个名为 email 的属性。UserDao bean 依赖于 User bean,我们在这里使用 ref 引用了 User bean。

4)创建主应用程序:

创建一个主应用程序来加载 Spring 配置文件并获取 UserDao bean。以下是示例代码:

public class MainApp {
    public static void main(String[] args) {
 
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
 
        UserDao userDao = context.getBean("userDao",UserDao.class);
        User user = context.getBean("user",User.class);
        userDao.save(user);
    }
}

在这个示例中,我们使用 ClassPathXmlApplicationContext 类来加载 Spring 配置文件。然后,我们获取了名为 “userDao” 和 “user” 的 bean,并调用了 userDao.save(user) 方法。

5)运行应用程序:

运行这个应用程序,输出结果为:

User saved: JavGo

7.2 基于注解的容器配置

在 Spring 框架中,使用 XML 进行配置的方法比较老旧,它使得程序员需要在 XML 中描述他们想要创建对象和对象之间的依赖关系。

Spring 2.5 版本中引入了一个注解驱动的容器配置方法,这个新方法可以允许程序员使用注解来描述他们想要创建的对象和对象之间的依赖关系。基于注解的容器配置不但能够提高代码的可读性和可维护性,而且还可以允许程序员使用更加灵活的方式来组装对象。

注意:以上仅是个人观点!!!

7.2.1 XML 还是注解?

对此选择 XML 好还是使用注解的方式好,其实 Spring 官方也给了一些建议:

译文如下:


基于注解的配置的引入提出了这种方法是否比XML“更好”的问题。简短的回答是“视情况而定。”简单地说,每种方法都有其优缺点,通常由开发人员决定哪种策略更适合他们。由于它们的定义方式,注解在它们的声明中提供了大量上下文,从而导致更短、更简洁的配置。但是,XML擅长在不涉及源代码或重新编译的情况下将组件连接起来。一些开发人员更喜欢让连接靠近源代码,而另一些开发人员则认为带注释的类不再是 POJO,而且,配置变得分散且更难控制。

无论选择什么,Spring都可以适应这两种风格,甚至可以将它们混合在一起。值得指出的是,通过JavaConfig方式,Spring允许以一种非侵入式的方式使用注释,而不涉及目标组件的源代码,而且,就工具而言,所有配置样式都由Spring Tools for Eclipse支持。

7.2.2 使用前的说明

由于笔者本系列是直接围绕 SpringBoot 展开的,跳过了传统的 Spring Framework 方式,所以有需要时会进行不使用 SpringBoot 时的必要配置。该部分在 Spring Boot 项目中同样是不需要笔者关注的,因为它以及为我们准备好了。

如果你使用的是 Spring Framework 进行项目开发,在使用基于注解的方式之前,需要在项目的 pom.xml 文件中添加以下依赖:(当然,这在前面已经讲过)

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.0.RELEASE</version>
</dependency>

除此之外你还需要在 Spring 配置文件中手动开启注解扫描:

<?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:context="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 开启注解扫描,会扫描 base-package 指定包下的所有注解 -->
    <context:component-scan base-package="cn.javgo.ch1springboothelloworld.helloworld"/>
</beans>

基于上述配置 Spring 会扫描 base-package 属性指定的包内的类,这时只要在类上添了加对应的注解就能让 Spring 容器把它们配置为 Bean。

当然,

7.2.3 注册 Bean 的五个注解

在 Spring 框架中,使用 @Component 注解是一种常见的方式来描述一个 Bean 实例。程序员可以用 @Component 注解来告诉 Spring 框架,这个类是一个 Bean 实例。

开始之前,我们先来简单看看 @Componet 注解的源码(后面的几个注解除了注解名不同其余部分皆同 @Componet ),看看都有哪些可选属性:

@Documented
// 指定注解的生命周期为运行时
@Retention(RetentionPolicy.RUNTIME)
// 指定注解的作用目标为类、接口、Annotation类型、枚举
@Target(ElementType.TYPE)
public @interface Component {

    // 为 Bean 提供一个可选的名称。如果未指定,Spring 容器将为 Bean 自动生成一个默认的名称。
    String value() default "";
}

从源码中,我们知道可以使用 value 显示提供 Bean 的名称。当然,在只有一个可选属性的时候我们不必显示给出 value 属性名。同时我们还了解到,如果不指定 Bean 的名称,Spring 容器会自动生成一个名称(默认就是类名首字母小写 + 驼峰规则)。

下面是一个示例代码:

@Component("helloBean")
public class Hello
{
    public String hello(){
        return "Hello World!";
    }
}

这样,Spring 就会自动扫描到 Hello 类的 @Componet 注解,然后将 Hello 配置为 Bean 存放在 Spring 容器之中。

除此之外,Spring 框架还支持其他的注解来描述 Bean 实例。下面是一些使用频率较高的注解:

注解说明
@Component描述一个普通 Bean 实例(用于普通类)
@Controller描述一个 Controller Bean 实例(用于控制层)
@Service描述一个 Service Bean 实例(用于业务逻辑层)
@Repository描述一个 Repository Bean 实例(用于数据访问层)
@Configuration描述一个 Configuration Bean 实例(下一节会讲)

在一个基于注解的 Spring 应用程序中,需要适当的使用上面这些注解来在代码中创建 Bean 实例。

7.2.4 基于注解的依赖注入方式

在 Spring Framework 中,我们可以使用基于注解的依赖注入方式来实现依赖注入(Dependency Injection)。这种方式相比于 XML 配置的方式更加简便易行,能够提高开发效率。

可以使用如下的三个注解完成依赖的自动装配

注解说明
@Autowired根据类型(byType)注入依赖,可用于构造函数、属性、Setter方法、普通方法
@ResourceJSR-250 的注解,根据名称(byName)注入依赖,可用于属性、Setter方法
@InjectJSR-330 的注解,同 @Autowired,可用于构造函数、属性、Setter方法

JSR-250 和 JSR-330 是 Java 语言中的两个注解规范。

  • JSR-250(Java Specification Request 250)是 Java EE 5规范中定义的一组注解,其中包括了@Resource@PostConstruct@PreDestroy 等常用注解。这些注解主要用于资源管理、初始化和销毁等操作。
  • JSR-330(Java Specification Request 330)是 Java EE 6规范中定义的一组注解,其中包括了 @Inject@Named@Singleton 等常用注解。这些注解主要用于依赖注入、命名、单例管理等操作。
1)@Autowired

@Autowired 是 Spring Framework 中最常用的注解之一,它可以自动装配一个组件的依赖关系,不需要手动编写代码实现依赖注入。@Autowired 可以用于构造函数、属性、Setter 方法和普通方法上。

注解源码如下:

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
   boolean required() default true;
}

可以看到,该注解只有一个 boolean 类型的可选属性属性 required,该属性用于声名是否强制要求该注解作用的地方必须有对应的 Bean。如果为 true,则该注解作用的地方必须有对应的Bean,否则会抛出异常;如果为 false,则不强制要求该注解作用的地方有对应的 Bean,如果没有对应的 Bean,则该注解作用的地方的值为 null。默认值是 false

  • 构造函数注入

    构造函数注入是一种最常用的依赖注入方式。使用 @Autowired 注解可以将构造函数中需要的依赖对象自动注入到组件中,如下面的示例代码所示:

    @Service
    public class UserServiceImpl implements UserService {
        private final UserRepository userRepository;
    
        @Autowired
        public UserServiceImpl(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
    }
    

    在上述示例代码中,UserServiceImpl 依赖于 UserRepository,使用 @Autowired 注解将 UserRepository 自动注入到 UserServiceImpl 中。

  • 属性注入

    属性注入是一种简单的依赖注入方式。使用 @Autowired 注解可以将属性中需要的依赖对象自动注入到组件中,如下面的示例代码所示:

    @Service
    public class UserServiceImpl implements UserService {
        @Autowired
        private UserRepository userRepository;
    }
    

    在上述示例代码中,UserServiceImpl 依赖于 UserRepository,使用 @Autowired 注解将 UserRepository 自动注入到 UserServiceImpl 中的属性 userRepository 中。

    在属性注入中,@Autowired 注解不能指定 bean 的名称,如果有多个类型匹配的 Bean,则可以配合使用 @Qualifier 注解来指定要使用的 Bean。

    @Service
    public class UserServiceImpl implements UserService {
        @Autowired
        @Qualifier("userRepositoryA")
        private UserRepository userRepository;
    }
    
  • Setter方法注入

    Setter 方法注入是一种常用的依赖注入方式。使用 @Autowired 注解可以将 Setter 方法中需要的依赖对象自动注入到组件中,如下面的示例代码所示:

    @Service
    public class UserServiceImpl implements UserService {
        private UserRepository userRepository;
    
        @Autowired
        public void setUserRepository(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
    }
    

    在上述示例代码中,UserServiceImpl 依赖于 UserRepository,使用 @Autowired 注解将 UserRepository 自动注入到 UserServiceImpl 中的 Setter 方法 setUserRepository 中。

  • 普通方法注入

    普通方法注入是一种不太常用的依赖注入方式。使用 @Autowired 注解可以将普通方法中需要的依赖对象自动注入到组件中,如下面的示例代码所示:

    @Service
    public class UserServiceImpl implements UserService {
        private UserRepository userRepository;
    
        @Autowired
        public void init(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
    }
    

    在上述示例代码中,UserServiceImpl 依赖于 UserRepository,使用 @Autowired 注解将 UserRepository 自动注入到 UserServiceImpl 中的普通方法 init 中。

最后,我们一起来看看 @Autowired 注解底层是如何实现自动装配的。

@Autowired 注解实现自动装配的核心机制是通过 BeanPostProcessor 的实现类 AutowiredAnnotationBeanPostProcessor 这个后置处理器实现的。这个后置处理器会在容器中扫描所有被 @Autowired 注解标注的依赖项,并为其找到合适的 Bean 进行注入。

具体来说,AutowiredAnnotationBeanPostProcessor 会在 Bean 的初始化之前以及 Bean 初始化之后进行处理。在 Bean 初始化之前,AutowiredAnnotationBeanPostProcessor 会扫描 Bean 中所有的字段、Setter 方法以及构造方法参数是否被标注为 @Autowired 注解,如果被标注了,就会将这些依赖项记录下来,并在 Bean 初始化之后找到相应的 Bean 进行注入。

AutowiredAnnotationBeanPostProcessor 中,主要的注入方法是 postProcessPropertyValues 方法,该方法会获取当前 Bean 中所有被 @Autowired 注解标记的依赖项,然后通过 BeanFactory 接口按 Bean 的 class 属性(ByType)查找相应的 Bean 进行注入。

如果 @Autowired 标记的依赖项有多个实例符合条件,会通过 @Qualifier 注解进一步指定具体的 Bean 实例名称进行注入。此时,AutowiredAnnotationBeanPostProcessor 会通过与 @Qualifier 注解标记的值进行匹配,找到对应的 Bean 进行注入。如果所有符合条件的 Bean 与 @Qualifier 注解的值不匹配,会抛出相关的异常。

2)@Resource

@Resource 是 Java EE 5 规范中定义的注解,用于在类中引用外部资源,定义在 jakarta.annotation 包下。在 Spring 中,它也可以实现依赖注入和自动装配。@Resource 可以用于属性和 Setter 方法上,但不能用于构造函数和普通方法上。

我们同样先来看看该注解的关键部分源码:

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Resource {
	// 定义注解的 name 属性,默认值为""
	String name() default "";
	// 定义注解的 type 属性,默认值为 Object.class
	Class<?> type() default Object.class;
    
   // 省略其他源码 
}

可见虽然,上面提到该注解主要是通过 name 属性通过 bean 名称进行自动转配,但是其也提供了一个 type 属性用于指定具体的类型。

  • 属性注入

    使用 @Resource 注解可以将属性中需要的依赖对象自动注入到组件中,如下面的示例代码所示:

    @Service
    public class UserServiceImpl implements UserService {
        @Resource
        private UserRepository userRepository;
    }
    

    在上述示例代码中,UserServiceImpl 依赖于 UserRepository,使用 @Resource 注解将 UserRepository 自动注入到 UserServiceImpl 中的属性 userRepository 中。

    @Resource 注解默认会按照参数名称进行查找,当有多个名称匹配的 Bean 时,可以使用 name 属性指定要使用的 Bean 的名称。例如:

    @Service
    public class UserServiceImpl implements UserService {
        @Resource(name="userRepositoryA")
        private UserRepository userRepository;
    }
    

    当然,如果需要按类型查找,唯一确定一个 bean,则可以使用 type 属性指定目标类型,例如:

    @Service
    public class UserServiceImpl implements UserService {
        @Resource(name="userRepositoryA",type=UserRepository.class)
        private UserRepository userRepository;
    }
    
  • Setter方法注入

    使用 @Resource 注解可以将 Setter 方法中需要的依赖对象自动注入到组件中,如下面的示例代码所示:

    @Service
    public class UserServiceImpl implements UserService {
        private UserRepository userRepository;
    
        @Resource
        public void setUserRepository(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
    }
    

    在上述示例代码中,UserServiceImpl 依赖于 UserRepository,使用 @Resource 注解将 UserRepository 自动注入到 UserServiceImpl 中的 Setter 方法 setUserRepository 中。

最后,一起来看看 @Resource 注解底层是如何实现自动装配的。

@Resource 注解实现自动装配的核心机制是通过 BeanPostProcessor 的实现类 CommonAnnotationBeanPostProcessor 后置处理器实现的。这个后置处理器会在容器中扫描所有被 @Resource 注解标注的依赖项,并为其找到合适的 Bean 进行注入。

具体来说,CommonAnnotationBeanPostProcessor 会在 Bean 的初始化之前以及 Bean 初始化之后进行处理。在 Bean 初始化之前,CommonAnnotationBeanPostProcessor 会扫描 Bean 中所有的字段和 Setter 方法是否被标注为 @Resource 注解,如果被标注了,就会将这些依赖项记录下来,并在 Bean 初始化之后根据属性名或 name 属性指定的 bean 名称找到相应的 Bean 进行注入。

CommonAnnotationBeanPostProcessor 中,主要的注入方法是 postProcessPropertyValues 方法,该方法会获取当前 Bean 中所有被 @Resource 注解标记的依赖项,然后通过 BeanFactory 接口查找相应的 Bean 进行注入。

@Autowired 注解的区别在于,在注入 Bean 实例时,@Resource 注解需要同时指定 Bean 的名称和类型。如果只指定了名称,则会自动根据名称查找指定的 Bean,并将其注入到目标对象中。如果同一个名称对应多个 Bean 实例,则会抛出 NoUniqueBeanDefinitionException 异常。如果名称无法匹配对应的 Bean 实例,则会抛出 NoSuchBeanDefinitionException 异常。

3)@Inject

Spring 提供了 Java EE 6 中对 JSR-330 标准注释(依赖注入)的支持,这些注释的扫描方式与 Spring 注释相同。要使用它们,你需要在类路径中包含相关的 jar。这里我们使用的是 Maven,故可以将以下依赖项添加到文件 pom.xml 中以开启对 JSR-330 标准注释的支持:

<dependency>
    <groupId>jakarta.inject</groupId>
    <artifactId>jakarta.inject-api</artifactId>
    <version>2.0.0</version>
</dependency>

@Inject 注解虽然是 Java EE6 规范中提供的注解,但和 @Autowired 注解在 Spring 中是等价的,都是用于自动装配依赖的注解,位于 jakarta.inject 包(需要导入 jakarta.inject 包才能使用)。它们具有相同的功能和特性,都可以作用在字段、Setter 方法、构造方法等上面。

在 Spring 容器解析 @Inject 注解时,使用的后置处理器和 @Autowired 也是一样的,都是 AutowiredAnnotationBeanPostProcessor。

下面是 Spring 官方给出的等价示例:

import jakarta.inject.Inject;

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    public void listMovies() {
        this.movieFinder.findMovies(...);
        // ...
    }
}

如果您想为应该注入的依赖项使用限定名称,可以配合使用 @Named 注释,如以下示例所示:

import jakarta.inject.Inject;
import jakarta.inject.Named;

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

值得注意的是,@Inject 注解没有 @Autowired 中的 required 属性来指定是否依赖项必须存在。这可能会导致程序在运行时出现异常,因为如果依赖项不存在,则会出现空指针异常。

为了解决这个问题,我们可以使用 Spring 提供的 @Nullable 注解或是 Java 中自带的 Optional 类来处理缺失的依赖项。

@Nullable 注解表示依赖项可以是 null,这样即使依赖项不存在,程序也不会出现异常:

关于 @Nullable 注解,胖友可阅读:Java开发中与Null相关的注解有哪些?

public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        // ...
    }
}

或者,我们可以使用 Java 8 中新增的 Optional 类来处理缺失的依赖项:

关于 Optional 类,胖友可阅读:Java 解决和预防空指针异常都有哪些方法?

public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(Optional<MovieFinder> movieFinder) {
        // 如果movieFinder存在,可以调用如下方法:
        dao.ifPresent(d -> {
            // ...
        });
    }
}

这两种方法都可以解决 @Inject 注解没有 required 属性的问题,可以在处理依赖注入时更加优雅地处理缺失的依赖项。

7.2.5 @Value 注解的使用

1)@Value注解的概述

在使用 Spring Framework 进行开发时,我们经常需要在代码中使用配置文件中的属性值。然而,手动获取这些属性值并且将其注入到 Java 类中是一项繁琐的任务。为了简化这个过程,Spring Framework 提供了一个方便的注解 @Value,它可以将配置文件中的属性值自动注入到 Java 类中。

使用 @Value 注解时,我们需要指定属性的名称,然后 Spring Framework 会自动将该属性的值注入到 Java 类中。

以下是一个使用 @Value 注解的示例:

@Service
public class MyService {
    
    @Value("${my.property}")
    private String myProperty;
    
    // ...
}

在上面的示例中,我们使用了 @Value 注解将配置文件中的 my.property 属性值注入到了 MyService 类中的 myProperty 字段中。

2)@Value注解的使用方法

@Value 注解通常用于注入配置文件(Properties 或 YAML)中的属性值,但是它也可以用于注入其他类型的值,例如系统属性、环境变量和 SpEL 表达式的计算结果等。

  • 注入配置文件中的属性值

    在注入配置文件中的属性值时,我们需要使用 ${} 来引用属性值的名称。例如:

    @Value("${my.property}")
    private String myProperty;
    

    在上面的示例中,我们使用 ${} 引用了 my.property 属性的值。

    如果配置文件中的值不存在,则可以指定默认值,其中 key 是要注入的变量名,defaultValue 是指定的默认值:

    @Value("${key:defaultValue}")
    private String value;
    
  • 注入系统属性

    要注入系统属性,我们可以使用如下语法:

    @Value("#{systemProperties['my.property']}")
    private String myProperty;
    

    在上面的示例中,我们使用了 #{systemProperties['my.property']} 来引用系统属性 my.property 的值。

  • 注入环境变量

    要注入环境变量,我们可以使用如下语法:

    @Value("#{systemEnvironment['my.property']}")
    private String myProperty;
    

    在上面的示例中,我们使用了 #{systemEnvironment['my.property']} 来引用环境变量 my.property 的值。

  • 注入SpEL表达式

    除了上述方式之外,@Value 注解还支持注入 SpEL 表达式的值。例如:

    @Value("#{ T(java.lang.Math).random() * 100.0 }")
    private double randomNumber;
    

    在上面的示例中,我们使用了 SpEL 表达式来生成一个随机数,并将其注入到 randomNumber 字段中。(SpEL 表达式的相关知识感兴趣的可以自行查阅官方文档)

  • 注入集合类型

    我们可以使用 @Value 注解来注入集合类型,例如 List、Set、Map 等。例如:

    @Value("#{'${my.list.property}'.split(',')}")
    private List<String> myList;
    

    在上面的示例中,我们使用了 ${} 引用了 my.list.property 属性的值,并将其转换为 List 类型。

  • 注入对象类型

    我们还可以使用 @Value 注解来注入对象类型。例如:

    @Value("#{myObject}")
    private MyObject myObject;
    

    在上面的示例中,我们使用了 #{myObject} 来引用一个对象,并将其注入到 MyObject 类型的字段中。

3)注册 PropertySourcesPlaceholderConfigurer

需要注意的是,根据实际情况的不同,你可能要去考虑是否应该注册 PropertySourcesPlaceholderConfigurer,目的是使 @Value 注解能够地进行解析。在较早的 Spring 版本中,PropertySourcesPlaceholderConfigurer 的注册是必须的。在基于 Java 的配置中,可以这样做:

@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
    return new PropertySourcesPlaceholderConfigurer();
}

在基于 XML 的配置中,可以这样做:

<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer"/>

然而,从 Spring Framework 4.3 开始,如果你使用 @Configuration 注解创建基于 Java 的配置,那么可以省略 PropertySourcesPlaceholderConfigurer 的注册,因为 Spring 默认会注册一个 PropertySourcesPlaceholderConfigurer Bean。

所以,如果你使用的是 Spring 4.3 及更高版本,以及基于 Java 的配置(使用 @Configuration 注解),那么可以省略 PropertySourcesPlaceholderConfigurer 的注册。

下面给出一个使用示例:

@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig {

    @Value("${app.name}")
    private String appName;

    @Value("${app.version}")
    private String appVersion;
	
    // ......
}

在上面的示例中,@PropertySource("classpath:application.properties") 告诉 Spring 从类路径中加载 application.properties 文件,并将其属性添加到 Spring 环境中以保证 @Value 注解能够正确拿到对应的属性值。

4)@PropertySource注解介绍

@PropertySource 注解的作用是在基于 Java 类配置容器时指定一个或多个资源文件(通常是 properties 文件),以便将这些文件中的属性添加到 Spring 的环境中。这使得可以在 Spring 配置中使用 @Value 注解或 Environment 接口来访问这些属性。

如果不使用 @PropertySource 注解,可以使用以下两种方法之一来以传统方式加载资源文件:

  • 在基于 XML 的配置文件中,可以使用 <context:property-placeholder> 元素来加载 properties 文件。

    <context:property-placeholder location="classpath:application.properties" />
    
  • 在基于 Java 的配置中,可以使用 PropertyPlaceholderConfigurer Bean 来加载 properties 文件。这是 Spring 3.1 之前的方法,但仍然可以使用:(不常用,了解即可)

    @Configuration
    public class AppConfig {
    
        @Bean
        public static PropertyPlaceholderConfigurer propertyPlaceholderConfigurer() {
            PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer();
            configurer.setLocation(new ClassPathResource("application.properties"));
            return configurer;
        }
    
        // ...
    }
    

对于 Spring Boot 应用程序,无论使用哪种配置方式,PropertySourcesPlaceholderConfigurer 都会自动注册,无需手动添加。

总之,在某些情况下,可以省略 PropertySourcesPlaceholderConfigurer 的注册,但这取决于你使用的 Spring 版本、配置方式(Java 或 XML)以及是否使用了 Spring Boot。

7.3 基于 Java 的容器配置

从 Spring Framework 3.0 开始,我们可以使用 Java 类代替 XML 文件,使用 @Configuration@Bean@Componentscan 等一系列注解,基本可以满足日常所需

与 XML 配置方式相比,基于 Java 类的配置方式更灵活,更具可读性,并且可以使用 Java 代码的特性,如代码提示和代码检查。

7.3.1 创建一个 Java 配置类

在使用 Java 配置方式创建容器之前,我们需要创建一个 Java 配置类。

Java 配置类是一个普通的 Java 类,它使用了 @Configuration 注释来告诉 Spring 框架这是一个配置类。

@Configuration  // 声名为一个配置类
public class AppConfig { // 相当于原本的 XML 配置文件
	// 配置Bean的方法
}

7.3.2 创建一个简单的 Bean

在 Java 配置类中,我们可以使用 @Bean 注解来定义和配置组件。@Bean 注释将方法声明为一个 bean,并且 Spring 框架会自动将这些 bean 添加到应用程序上下文中。@Bean 注解的方法名默认为 bean 的名称,但也可以通过 namevalue 参数指定自定义名称。

例如,我们可以定义一个返回一个 HelloWorld 对象的方法:

@Configuration
public class AppConfig {
    @Bean
    public HelloWorld helloWorld() {
        return new HelloWorld();
    }
}

这个方法的返回值将被 Spring 框架自动添加到应用程序上下文中,并且我们可以在其他组件中自动装配它。

下表列出了 Spring 中 @Bean 注解的全部可配置项:

可配置项作用默认值
valueBean的名称,如果不指定,则使用方法名(等效于 name){}
nameBean的名称,可以指定多个名称(等效于 value){}
initMethodBean的初始化方法名称(后面会将)“”
destroyMethodBean的销毁方法名称(后面会将)“”
autowireCandidate是否允许该Bean作为其他Bean的依赖true

7.3.3 加载配置类拿到容器对象

我们可以通过 ApplicationContext 接口的一个实现类 AnnotationConfigApplicationContext 类构建一个支持基于注解和 Java 类的 Spring 上下文。该实现类主要用于读取并解析注解配置类,以创建和管理 Bean。

下面是一个简单的示例:

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

其中的 AppConfig 类就是上面加了 @Configuration 注解的 Java 配置类,这与之前通过 XML 配置文件的方式加载 applicationContext.xml 配置文件道理上是大致相同的。

7.3.4 自动注入一个 Bean

在 Java 配置类中,我们可以使用自动注入来获取其他组件。

例如,假设我们有一个 MessageService 接口和一个 MessageServiceImpl 类实现这个接口:

public interface MessageService {
    String getMessage();
}

public class MessageServiceImpl implements MessageService {
    public String getMessage() {
        return "Hello World!";
    }
}

我们可以在 Java 配置类中使用自动注入来获取这个组件:

@Configuration
public class AppConfig {

    @Bean
    public HelloWorld helloWorld() {
        // 通过直接调用 messageService() 方法进行注入
        return new HelloWorld(messageService());
    }

    @Bean
    public MessageService messageService() {
        return new MessageServiceImpl();
    }
}

在这个例子中,我们创建了一个在 HelloWorld bean 中使用的 MessageService bean。我们使用 messageService() 方法来获取这个组件,并将它作为参数传递给 HelloWorld 构造函数。


前面的示例虽然可以工作,但过于简单。但在大多数实际场景中,不同的 bean 之间存在跨越配置类的依赖关系是很常见的。当使用 XML 时,这不是问题,因为没有涉及编译器,而且你可以声明 ref="someBean",并且相信 Spring 会在容器初始化期间处理它。

但是,当使用 @Configuration 类时,Java 编译器会对配置模型施加限制,因为对其他 bean 的引用必须是有效的 Java 语法。这意味着,如果你在一个 @Configuration 类中定义了两个 bean,其中一个 bean 依赖另一个,你必须用 Java 语法引用被依赖的 bean。

这个问题的解决方法非常简单。借助于 @Bean 方法可以有任意数量的参数这个特点,我们就可以通过参数来描述 bean 之间的依赖关系。

@Configuration
public class ServiceConfig {
	 // 依赖 RepositoryConfig 配置类中的 AccountRepository Bean
    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }
}

@Configuration
public class RepositoryConfig {
	
    // 依赖 SystemTestConfig 配置类中的 DataSource Bean
    @Bean
    public AccountRepository accountRepository(DataSource dataSource) {
        return new JdbcAccountRepository(dataSource);
    }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class}) // 导入外部配置类
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return new DataSource
    }
}

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    // everything wires up across configuration classes...
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}

还有另一种方法可以达到相同的结果,因为配置类中的 Bean 最终都会被注册到容器中,因此我们可以使用 @Autowired@Value 注解来进行依赖项的自动注入。

下面是一个等效的示例代码:

@Configuration
public class ServiceConfig {

    // 自动装配
    @Autowired
    private AccountRepository accountRepository;

    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl(accountRepository);
    }
}

@Configuration
public class RepositoryConfig {

    private final DataSource dataSource;

    // 通过构造器自动注入
    public RepositoryConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return new DataSource
    }
}

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    // everything wires up across configuration classes...
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}

上面示例中用到的 @Configuration 配置类中通过构造器自动注入依赖的方式是从 Spring Framework 4.3 开始受支持的。此外,如果目标 Bean 只定义一个构造函数,则无需指定 @Autowired 注解。

什么意思呢?在 Spring 中,如果一个类只有一个构造函数,那么 Spring 会自动将其作为默认的构造函数,也就是说我们即使不标注 @Autowired 注解,Spring 也会自动进行注入。但是,如果一个类中有多个构造函数,那么我们就需要使用 @Autowired 注解来明确指定需要注入哪个构造函数。

值得注意的是,请确保你通过这种方式注入的依赖项仅为最简单的类型。因为 @Configuration 类在上下文初始化的早期阶段就会被处理,强制通过这种方式注入依赖项可能会导致被依赖的 Bean 被意外的提前初始化。所以应该尽可能使用基于参数的注入,就像前面的例子一样。

此外,要特别小心通过 @Bean 定义的 BeanPostProcessor 和 BeanFactoryPostProcessor。通常应将它们声明为静态的 @Bean 方法。如果我们将 BeanPostProcessor 或 BeanFactoryPostProcessor 定义为非静态的 @Bean 方法,那么这些方法可能在配置类实例化之前就被 Spring 容器实例化了。这样会导致 @Autowired 和 @Value 无法在配置类本身上工作,因为 AutowiredAnnotationBeanPostProcessor 还没有来得及处理这些注解。因此,我们需要使用静态的 @Bean 方法来定义 BeanPostProcessor 和 BeanFactoryPostProcessor,避免在配置类实例化前就将其创建成 bean 实例,以确保正确的注入和处理。

让我们来看一个例子。假设我们有一个配置类 MyConfig,其中定义了一个 BeanPostProcessor:

@Configuration
public class MyConfig {
    
    @Bean
    public MyBeanPostProcessor myBeanPostProcessor() {
        return new MyBeanPostProcessor();
    }

    // ...
}

这个 MyBeanPostProcessor 可以在 Bean 实例化后进行一些额外的处理。但是,在上面的代码中,我们使用了非静态的 @Bean 方法来定义这个 PostProcessor。这可能会导致问题,因为当容器启动时会自动实例化这个 MyConfig 类以及其中的所有 @Bean 方法,包括 myBeanPostProcessor() 方法,这样可能会在后面的依赖注入过程中出现问题,因为这个 Bean 其实是想在容器中的 Bean 实例化后完成一些处理的,但却被一同提前进行实例化了。

为了避免这种问题,我们应该使用静态的 @Bean 方法来定义 MyBeanPostProcessor:

@Configuration
public class MyConfig {
    
    @Bean
    public static MyBeanPostProcessor myBeanPostProcessor() {
        return new MyBeanPostProcessor();
    }

    // ...
}

在这个例子中,我们将 myBeanPostProcessor() 方法定义为静态的,这样 Spring 容器启动时就不会实例化 MyConfig 类,而是在后续处理过程中使用这个静态方法来创建 BeanPostProcessor 实例。

这个方法的静态定义确保了我们可以在 AutowiredAnnotationBeanPostProcessor(处理自动装配注解的后处理器)被实例化后再创建 BeanPostProcessor 实例,这样就可以避免出现依赖注入的问题。

7.3.5 开启扫描组件

我们也可以在 Java 配置类中使用 @ComponentScan 注释,自动扫描并装配 basePackages 指定包中的组件并注册到容器中。作用与 <context:component-scan base-package="xxx.xxx.xxx"/> 类似。

假设我们有一个 HelloService 类,在其中使用了 @Service 注释:

@Service
public class HelloService {
    public void sayHello() {
        System.out.println("Hello World!");
    }
}

我们可以在 Java 配置类中使用 @ComponentScan 注释来扫描并装配这个组件:

@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {

}

在这个例子中,我们使用 @ComponentScan 注释并设置 basePackages 属性为 com.example,这将告诉 Spring 框架扫描这个包及其子包下的所有组件。Spring 框架将自动创建并装配这些组件。

除此之外,在 @ComponentScan 注解中,我们还可以使用 includeFiltersexcludeFilters 来指定包含和排除的组件,例如:

@Configuration
@ComponentScan(
    basePackages = "com.example",
    includeFilters = @Filter(type = FilterType.REGEX,pattern = ".*Stub.*Repository"),
    excludeFilters = @Filter(Repository.class)
)
public class AppConfig {
    
}

如果 @Configuration 注解的配置类没有通过 @ComponentScan 注解指定扫描的基础包路径或者类,默认就从该配置类所在的包开始扫描。

7.3.6 配置注解驱动

在 Java 配置类中,我们可以使用 @Enable* 注释来启用一些注解驱动的功能。

例如,我们可以使用 @EnableAspectJAutoProxy 注释来启用 AspectJ 注解的支持:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig { 

}

在这个例子中,我们使用了 @EnableAspectJAutoProxy 注释来启用 AspectJ 注解的支持。这将允许我们使用 AspectJ 注解来声明和管理切面。

7.3.7 使用@Import注解导入外部配置类

就像 <import/> 元素在 Spring XML 文件中用于帮助模块化配置一样,@Import 注解允许从另一个配置类加载 @Bean 定义。

示例代码:

@Configuration
public class ConfigA {

    @Bean
    public A a() {
        return new A();
    }
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

    @Bean
    public B b() {
        return new B();
    }
}

现在,在实例化上下文时不需要同时指定 ConfigA.classConfigB .class,只需要显式地提供 ConfigB.class

示例代码:

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

    // now both beans A and B will be available...
    A a = ctx.getBean(A.class);
    B b = ctx.getBean(B.class);
}

这种方法简化了容器实例化,因为只需要处理一个类,而不需要在构造过程中记住大量的 @Configuration 类。

7.3.8 结合使用 Java 和 XML 配置

Spring 的 @Configuration 类支持并不意味着要完全取代 Spring XML。某些功能,如 Spring XML 命名空间,仍然是配置容器的理想方式。

在 XML 方便或必要的情况下,您可以选择以下两种方式之一:

1)使用“XML为中心”的方式实例化容器,例如使用 ClassPathXmlApplicationContext。这种方式允许您直接使用 XML 配置文件来配置 Spring 容器。

2)使用“Java为中心”的方式实例化容器,通过 AnnotationConfigApplicationContext 和 @ImportResource 注解按需导入 XML。这种方式允许你在基于 Java 的配置中导入 XML 配置文件,结合使用两种配置方式。

@Configuration
@ImportResource("classpath:applicationContext.xml")
public class AppConfig {
    // ...
}

7.3.9 基于 Java 的配置方式注解汇总

下表总结了基于 Java 的配置方式涉及到的常用注解,方便胖友查阅:

注解作用常用属性
@Configuration将类定义为配置类
@Bean将方法返回的对象注册到容器中name指定Bean的名称;initMethod指定Bean实例化后调用的方法名称;destroyMethod指定销毁Bean实例时调用的方法名称
@ComponentScan扫描指定包下的组件类,并将其注册到容器中basePackages指定需要扫描的包路径;includeFilters和excludeFilters指定需要或不需要扫描的组件类型(可用通配符)
@Primary当出现多个实现时,优先使用被标记的实现
@Import导入其他配置类value指定需要导入的配置类;ImportSelector和ImportBeanDefinitionRegistrar支持动态生成Bean配置
@ImportResource导入XML配置文件locations指定需要导入的XML配置文件路径
@PropertySource加载外部属性文件value指定属性文件路径;encoding指定属性文件编码
@Lazy延迟加载对象
@Scope定义对象作用域例如singleton、prototype等
@DependsOn指定Bean依赖顺序value指定Bean的依赖顺序
@EnableAspectJAutoProxy开启AOP功能proxyTargetClass指定是否使用CGLIB代理;exposeProxy指定是否需要将当前代理对象绑定到ThreadLocal中

7.4 三种配置方式的加载顺序

上面我们了解了在 Spring 中,可以通过注解、XML 和 JavaConfig 三种方式来进行依赖注入。当这三种方式同时存在时,它们的加载顺序如下:

  1. 首先,XML 配置文件中的 Bean 定义和依赖注入信息将首先被处理。
  2. 其次,Spring 会加载使用 @Configuration 注解的 Java 配置类类。这些类中的 Bean 定义和依赖注入信息会在 XML 配置文件之后被处理。
  3. 最后,Spring 会扫描使用 @Component@Service@Repository 等注解的类,并处理其中的依赖注入信息。

需要注意的是,在实际开发中,这三种方式可以相互覆盖。例如,如果一个 Bean 在 XML 配置文件中定义了,同时又在 JavaConfig 类中定义了,那么后者会覆盖前者的定义。同样,如果一个 Bean 使用了注解定义,同时又在 JavaConfig 类中定义了,那么 JavaConfig 的定义会被注解定义的 Bean 覆盖。

即 Spring 容器中同一个 Bean 只能有一种定义方式生效,如果有多种定义方式,那么优先级高的定义方式会生效,并覆盖优先级低的定义方式。其中,从高到低的优先级为:注解定义 > JavaConfig类定义 > XML配置文件定义

8.自定义 Bean 的性质

Spring 框架中,Bean 的生命周期可以分为多个阶段,每个阶段都有相应的回调方法可以进行定制。在 Bean 的创建和销毁的不同阶段,Spring 容器可以通过这些回调方法,帮助我们在特定的时机,对 Bean 进行一些自定义操作。

8.1 生命周期回调

Spring 容器接管了 Bean 的整个生命周期管理,具体如图所示:

一个 Bean 先要经过 Java 对象的创建(也就是通过 new 关键字创建一个对象),随后根据容器里的配置元数据注入所需的依赖,最后调用初始化的回调方法,经过这三个步骤才算完成了 Bean 的初始化,可以从容器中获取 Bean 进行使用。若不再需要这个 Bean,则要进行销毁操作,在正式销毁对象前,会先调用容器的销毁回调方法。

由于一切都是由 Spring 容器管理的,所以我们无法像自已控制这些动作时那样任意地在 Bean 创建后或 Bean 销毁前增加某些操作。为此,Spring Framework 为我们提供了几种途径,在这两个时间点(Bean 创建后和 Bean 销毁前)调用我们提供给容器的回调方法来对 Bean 进行一些行为控制。

可以根据不同情况选择以下三种方式之一:

  • 通过实现 InitializingBeanDisposableBean 接口;
  • 使用 JSR-250 提供的 @PostConstruct@PreDestroy 注解;
  • <bean/>@Bean 里通过 init-methoddestroy-method 配置初始化和销毁方法。

下面我们分别介绍这三种方式。

8.1.1 使用 InitializingBean 和 DisposableBean 接口

Spring 提供了两个接口,InitializingBeanDisposableBean,来实现 Bean 的初始化和销毁操作。我们可以让 Bean 实现这两个接口,并在对应的方法中添加自定义的初始化和销毁逻辑。

InitializingBean 接口只包含一个方法 afterPropertiesSet(),该方法在 Bean 的属性设置完毕后被调用,我们可以在该方法中添加自定义的初始化逻辑。例如:

public class ExampleBean implements InitializingBean {

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // 在属性设置完成后进行初始化逻辑
        System.out.println("ExampleBean initialized with name: " + name);
    }
}

DisposableBean 接口也只包含一个方法 destroy(),该方法在 Bean 被销毁之前被调用,我们可以在该方法中添加自定义的销毁逻辑。例如:

public class ExampleBean implements DisposableBean {

    @Override
    public void destroy() throws Exception {
        // 在Bean被销毁之前进行销毁逻辑
        System.out.println("ExampleBean destroyed.");
    }
}

Spring 官方并不建议使用 InitializingBean 和 DisposableBean 接口,因为它会将代码不必要地耦合到 Spring。官方建议使用注解或 XML 的方式来对 Bean 做个性化的操作。

8.1.2 使用 @PostConstruct 和 @PreDestroy 注解

除了使用 InitializingBeanDisposableBean 接口外,我们还可以使用 JSR-250 提供的 @PostConstruct@PreDestroy 两个注解来完成 Bean 的初始化和销毁操作。

InitializingBeanDisposableBean 接口相比,使用注解的方式更加简单易用,可以避免实现额外的接口带来的 Java 与 Spring 代码耦合的问题。

  • @PostConstruct 注解用于在 Bean 的属性设置完成后执行自定义的初始化逻辑,该注解所标注的方法会在 InitializingBean 接口的 afterPropertiesSet() 方法之后被调用。
  • @PreDestroy 注解用于在 Bean 销毁之前执行自定义的销毁逻辑,该注解所标注的方法会在 DisposableBean 接口的 destroy() 方法之前被调用。

下面是一个示例代码:

public class ExampleBean {

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    @PostConstruct
    public void init() {
        // 在属性设置完成后进行初始化逻辑
        System.out.println("ExampleBean initialized with name: " + name);
    }

    @PreDestroy
    public void destroy() {
        // 在Bean被销毁之前进行销毁逻辑
        System.out.println("ExampleBean destroyed.");
    }
}

需要注意的是,如果你是在 Spring Framework 项目中使用 @PostConstruct@PreDestroy 注解,需要在 Spring 配置文件中开启对注解的支持,可以通过在配置文件中添加以下内容来开启:(Spring Boot 自动开启了)

<!-- 开启注解驱动 -->
<context:annotation-config/>

spring-context 依赖中并不包含上面的两个注解,它只包含 Spring 框架本身的注解和类。jakarta.annotation.PostConstructjakarta.annotation.PreDestroy 是属于 jakarta EE 规范中的注解,需要单独导入 jakarta EE 依赖才能使用。

如果你使用的是 Spring Boot,它会默认包含 jakarta.annotation-api 依赖,如果你使用的是非 Spring Boot 的纯 Spring 项目,可以添加以下依赖来引入 jakarta.annotation-api

<dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
    <version>x.x.x</version>
</dependency>

8.1.3 使用 <bean/> 或 @Bean 注解

除了前面提到的两种方式,我们还可以在 <bean/>@Bean 声明中配置初始化和销毁方法,这样可以更加灵活地定制 Bean 的生命周期回调。

<bean/> 声明中,我们可以使用 init-method 属性来指定初始化方法,使用 destroy-method 属性来指定销毁方法,例如:

<bean id="exampleBean" class="com.example.ExampleBean" init-method="init" destroy-method="destroy">
    <property name="name" value="example"/>
</bean>

其中,init-methoddestroy-method 属性的值分别为初始化和销毁方法的名称。

@Bean 声明中,我们可以使用 initMethod 属性来指定初始化方法,使用 destroyMethod 属性来指定销毁方法,例如:

@Configuration
public class AppConfig {

    @Bean(initMethod = "init", destroyMethod = "destroy")
    public ExampleBean exampleBean() {
        ExampleBean exampleBean = new ExampleBean();
        exampleBean.setName("example");
        return exampleBean;
    }
}

需要注意的是,在使用 <bean/>@Bean 注解方式配置 Bean 的生命周期回调时,初始化和销毁方法必须是公共的非静态方法,且不能带有任何参数。 这是因为 Spring 在调用初始化和销毁方法时,是通过反射调用的,如果方法不符合上述规范,会导致反射调用失败。

总之,在 Spring 框架中,通过上述三种方式,我们可以方便地对 Bean 的生命周期进行定制化操作,这为我们在应用中使用 Spring 框架提供了很大的灵活性。

8.1.4 生命周期动作的组合时的优先级

在实际开发中,我们可能需要同时使用多种方式来完成 Bean 的生命周期回调,例如既使用 InitializingBeanDisposableBean 接口,又使用 @PostConstruct@PreDestroy 注解,或者既在 <bean/>@Bean 声明中配置初始化和销毁方法,又使用注解方式等。在这种情况下,我们需要了解这些不同方式之间的调用顺序和优先级。

Spring 框架中,不同方式的调用顺序如下:

  • 创建 Bean 后的回调:@PostConstruct 注解 > InitializingBean 接口 > <bean/>@Bean
  • 销毁 Bean 前的回调:@PreDestroy 注解 > DisposableBean 接口 > <bean/>@Bean

下面是一个示例代码,演示了如何同时使用不同方式来完成 Bean 的生命周期回调,以及它们的调用顺序。

测试 Bean:

/*
 * @author: JavGo
 * @Time: 2023/3/17  19:00
 */
public class ExampleBean implements InitializingBean, DisposableBean {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    @PostConstruct
    public void init(){
        System.out.println("Initializing ExampleBean with name: " + name + " by @PostConstruct");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("Initializing ExampleBean with name: " + name + " By InitializingBean.");
    }

    public void customInit() {
        System.out.println("Initializing ExampleBean with name: " + name + "by <bean/>");
    }

    @PreDestroy
    public void preDestroy() {
        System.out.println("Destroying ExampleBean using @PreDestroy.");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("Destroying ExampleBean using DisposableBean.");
    }

    public void customDestroy() {
        System.out.println("Destroying ExampleBean using <bean/>");
    }
}

XML 配置:

<?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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--  开启注解扫描  -->
    <context:annotation-config/>
    
    <bean id="exampleBean" class="cn.javgo.pojo.ExampleBean" init-method="customInit" destroy-method="customDestroy"/>
</beans>

模拟创建和销毁Bean:

public static void main(String[] args) throws Exception {
    // 加载Spring配置文件,创建ApplicationContext容器
    ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext-JavaConfig.xml");
    // 关闭容器
    applicationContext.close();
}

手动销毁 Bean 时,应该确保这个 Bean 的作用域是 singletonprototype,因为只有这两种作用域的 Bean 才会受到容器的管理,才能被正确地销毁。如果一个 Bean 的作用域是 requestsessionapplication 等,那么它们的销毁应该交由 Servlet 容器或 Web 容器等环境来管理。

输出结果如下:

Initializing ExampleBean with name: null by @PostConstruct
Initializing ExampleBean with name: null By InitializingBean.
Initializing ExampleBean with name: nullby <bean/>
    
Destroying ExampleBean using @PreDestroy.
Destroying ExampleBean using DisposableBean.
Destroying ExampleBean using <bean/>

8.2 ApplicationContextAware 和 BeanNameAware

在大部分情况下被容器管理的 Bean 感知不到 容器的存在,也无需感知。但总有那么一些场景中我们想要用到容器的一些特殊功能,这时候我们便可以利用 Spring Framework 提供的很多 Aware 接口,比如下面本节要介绍的 ApplicationContextAware 和 BeanNameAware 接口,让 Bean 能够感知到容器的诸多信息,从而可以用来实现依赖注入、数据访问以及 AOP 等功能。此外,有些容器相关的 Bean 不能由我们自己创建,必须由容器创建后注入我们的 Bean 中。

8.2.1 什么是 ApplicationContextAware

ApplicationContextAware 接口是 Spring 提供的一个专门用于与应用程序上下文交互的接口。当一个类实现了该接口时,Spring 在初始化该类的 Bean 时会调用它的 setApplicationContext() 方法,从而将应用程序上下文作为参数传入该方法。

接口源码如下:

public interface ApplicationContextAware extends Aware {

    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;

}

由于 ApplicationContext 是 BeanFactory 的扩展,因此 ApplicationContextAware 和 BeanFactoryAware 的区别就在于,ApplicationContextAware 接口中的参数类型是 ApplicationContext,而不是 BeanFactory。另外,ApplicationContext 具有更强大的功能,可以支持更多的应用场景。

8.2.2 什么是 BeanNameAware

BeanNameAware 接口是 Spring 的另一个扩展接口,用于获取当前 Bean 在 Spring 容器中的名称。当一个类实现了该接口时,Spring 在初始化该类的 Bean 时会调用它的 setBeanName() 方法,从而将该 Bean 的名称作为参数传入该方法。

public interface BeanNameAware extends Aware {

    void setBeanName(String name);

}

通常情况下,Bean 的名称就是在 XML 配置中指定的 id 或 name 属性的值。但是在使用注解或者 Java Config 的情况下,该名称可能会由 Spring 自动分配生成。

8.2.3 如何实现 ApplicationContextAware 和 BeanNameAware 接口

要实现 ApplicationContextAware 接口,只需要在类中定义一个成员变量 private ApplicationContext applicationContext,然后提供一个 setter 方法 setApplicationContext()。Spring 在初始化 Bean 时就会自动将 ApplicationContext 的引用传递给这个 Bean。

@Service
public class MyService implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

}

要实现 BeanNameAware 接口,需要在类中定义一个变量 private String beanName,然后提供一个 setter 方法 setBeanName()。Spring 在初始化 Bean 时就会自动将 Bean 的名称传递给这个 Bean。

@Service
public class MyService implements BeanNameAware {

    private String beanName;

    @Override
    public void setBeanName(String beanName) {
        this.beanName = beanName;
    }

}

8.2.4 ApplicationContextAware 和 BeanNameAware 接口的应用场景

在实际开发中,ApplicationContextAware 和 BeanNameAware 接口非常常用,下面我们会通过一个例子来说明它们的应用场景。

假设我们有一个类 MyService,它需要获取到另一个类 MyDao 的实例,即它要依赖注入 MyDao。而 MyDao 的实例又需要从 Spring 容器中被创建出来,我们不能或者不合适直接创建它。如果我们想要解决这个依赖注入的问题,可以使用 @Autowired 或者 @Resource 注解,但是前提是这两个注解只适用于类和属性的注入,并不适用于方法的注入。

这时,我们可以利用 ApplicationContextAware 接口来解决这个问题。首先,在 MyDao 类中,实现 ApplicationContextAware 接口,在 MyService 类中,使用 Setter 方法注入的方式,即在 MyService 类中定义一个 Setter 方法来注入 MyDao 容器实例。这个 Setter 方法需要加上 @Autowired 或者 @Resource 注解来告诉 Spring 容器自动注入。

@Service
public class MyService {

    private MyDao myDao;

    @Autowired
    public void setMyDao(MyDao myDao) {
        this.myDao = myDao;
    }

}

@Repository
public class MyDao implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

}

当容器初始化 MyDao 对象时,setApplicationContext 方法被调用,MyDao 类会获取到 Spring 容器的引用,而在 MyService 类中,我们使用了自动注入的方式,这时 MyService 类就可以从 Spring 容器中获取 MyDao 的实例并注入。

类似地,当 MyService 在容器中被创建出来时,setApplicationContext 方法并不会被调用,但是它会通过 setMyDao 方法将容器中的 MyDao 实例传递给 MyService 类。

如果我们只是简单地实现了 ApplicationContextAware 接口,那么 MyDao 对象将不会被注册到 Spring 容器中,因此,我们需要在 MyDao 类上加上 @Repository 注解,这样 Spring 容器就可以识别这个类并注入依赖。另外,如果还想为这个 Bean 指定一个名称,可以在 @Repository 注解中添加一个 value 属性,值为 Bean 的名称。

@Repository("myDao")
public class MyDao implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

}

在实现 setApplicationContext 方法中,我们可以通过 applicationContext.getBean(String name) 方法获取其他 Bean 实例,这时获取的 Bean 实例已经被 Spring 容器管理,可以直接使用并进行依赖注入。

9.容器的扩展点

9.1 使用 BeanPostProcessor 自定义 Bean

在 Spring 中,BeanPostProcessor 接口是一个非常重要的接口。它允许我们在一个 bean 初始化之前和之后作一些自己的处理。通过实现 BeanPostProcessor 接口,我们可以实现很多自定义的逻辑,这包括封装某些 bean,Spring AOP 就是一个很好的例子。

9.1.1 BeanPostProcessor 接口和它的生命周期

在介绍 BeanPostProcessor 接口之前,我们需要先详细介绍一下 Spring 的 bean 生命周期。Spring 容器在初始化一个 bean 的时候,会按照如下流程执行:

  1. 调用容器的 BeanFactoryPostProcessor 接口方法 postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory),这是一个能够修改 bean 的一些属性的机会;
  2. 调用 BeanPostProcessor 实现类 InstantiationAwareBeanPostProcessor 中的 postProcessBeforeInstantiation(Class<?> beanClass, String beanName) 方法,这个方法允许我们在 bean 实例化之前对它进行一些处理;
  3. 执行 bean 的构造函数,并填充所有的属性;
  4. 调用 BeanNameAware 中的 setBeanName 方法,这里我们可以自己设置 bean 的名字,或者检查 bean 的名字是否符合我们的规范;
  5. 调用 BeanFactoryAware 中的 setBeanFactory 方法,这里我们可以保存 bean 工厂的引用,或者做一些其它的处理;
  6. 调用 BeanPostProcessor 中的 postProcessBeforeInitialization(Object bean, String beanName) 方法,这个方法允许我们在 bean 初始化之前对它进行一些处理;(第一次调用)
  7. 调用对象的定义的 init-method 方法;
  8. 调用 BeanPostProcessor 中的 postProcessAfterInitialization(Object bean, String beanName) 方法,这个方法允许我们在 bean 初始化之后对它进行一些处理;(第二次调用)
  9. bean 正式初始化。

可以看出,在 bean 的初始化过程中,BeanPostProcessor 接口可以扮演非常重要的角色,这会在接下来的内容中有详细的说明。

9.1.2 BeanPostProcessor 使用案例

我们用一个实际案例来说明 BeanPostProcessor 的使用场景。假设我们开发了一个在线学习系统,其中有一个课程库模块,需要将多个渠道来源的课程数据集成到系统中。我们开发了一个课程数据集成工具,该工具会读取多个渠道来源的课程数据,将其格式化后存储到系统中。

在集成过程中,我们需要对课程数据进行一些自定义的处理,例如:

  • 对于价格为 0 的课程,设置为免费课程。
  • 对于某些特殊标签的课程,需要将其转为系统内部的标签 ID。
  • 将某些渠道来源的课程数据进行二次加工。

通过分析,我们发现在课程数据集成之前,我们可以通过 BeanPostProcessor 接口来对课程数据进行预处理。因此,我们可以开发一个实现了 BeanPostProcessor 接口的处理类 CourseDataProcessor,其中定义了 preProcess 方法,在该方法中进行课程数据的预处理。

CourseDataProcessor.java:

/**
 * @Author: JavGo
 * @DateTime: 2023/4/20 23:04
 **/
public class CourseDataProcessor implements BeanPostProcessor {
    private static final String FREE_COURSE_COMMENT = "价格为0,设置为免费课程";
    private static final List<String> SPECIAL_TAGS = Arrays.asList("特殊标签1", "特殊标签2", "特殊标签3");
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        System.out.println("BeanPostProcessor.postProcessBeforeInitialization() " + beanName);
        // 在Bean初始化之前,不进行任何处理,直接返回
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        System.out.println("BeanPostProcessor.postProcessAfterInitialization() " + beanName);
        // 当前Bean是渠道数据源接口的实现类,进行处理
        if (bean instanceof Course) {
            Course course = (Course) bean;
            return preProcess(course);
        }
        return bean;
    }

    private Course preProcess(Course course) {
        Course currentCourse = course;
        if (course.getPrice() == 0) {
            course.setIsFreeCourse(true);
            course.setComment(FREE_COURSE_COMMENT);
        }
        if (SPECIAL_TAGS.contains(course.getTag())) {
            course.setTag("特殊标签ID");
        }
        if (course.getSource() == Source.CHANNEL1) {
            course.setTitle(course.getTitle() + "(渠道1)");
        }
        return currentCourse;
    }
}

Course.java:

/**
 * @Author: JavGo
 * @DateTime: 2023/4/20 23:07
 **/
public class Course {
    private String name;
    private Long price;
    private String tag;
    private boolean isFreeCourse;
    private String comment;
    private Source source;
    private String title;
    
    // 构造、Setter、Getter、toString
}

Source.java:

/**
 * @Author: JavGo
 * @DateTime: 2023/4/20 23:16
 **/
public enum Source {
    CHANNEL1, CHANNEL2, CHANNEL3;
}

Application.java:

/**
 * @Author: JavGo
 * @DateTime: 2023/4/20 23:26
 **/
@Configuration
public class Application {
    @Bean(name = "course")
    public Course course() {
        return new Course("Spring Boot", 0L, "Spring", true, "Spring Boot is a great framework", Source.CHANNEL1, "特殊标签1");
    }

    @Bean(name = "courseDataProcessor")
    public BeanPostProcessor courseDataProcessor() {
        return new CourseDataProcessor();
    }

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
        Course course = context.getBean("course", Course.class);
        System.out.println(course);
    }
}

在这个案例中,我们自定义了课程预处理的逻辑,将其封装为了 BeanPostProcessor 接口的实现类,并在系统启动时对渠道数据源接口实现类进行自动处理。这种方式可以大大提高数据接入效率,避免了人工修改、处理数据的漏洞。(当然,上面的代码过于简单,在实际的生产中因该考虑更多因素,进行合理调整)

由于 Spring AOP 也是通过 BeanPostProcessor 接口实现的,因此实现该接口的类,以及其中直接引用的 Bean 都会被特殊对待,不会被 AOP 增强。此外,BeanPostProcessor 和 即将讲到的 BeanFactoryPostProcessor 都仅对当前容器上下文的 Bean 有效,不会去处理其他上下文。

9.2 使用 BeanFactoryPostProcessor 自定义配置元数据

如果说 BeanPostProcessor 是 Bean 的后置处理器,那么 BeanFactoryPostProcessor 就是 BeanFactory 的后置处理器,我们可以通过它来定制 Bean 的配置元数据。

9.2.1 BeanFactoryPostProcessor 接口介绍

BeanFactoryPostProcessor 接口是在 Spring 应用程序的启动过程中执行的一种特殊接口。它允许外部代码在 Spring 容器创建所有 BeanDefinition 实例之后修改 Spring 应用程序上下文的配置元数据。

具体来说,BeanFactoryPostProcessor 接口允许开发人员从 BeanFactory 中获取 BeanDefinition 实例,并自由地修改它们的属性。这些更改可以包括添加、删除或替换 BeanDefinition 实例中的属性。

正如我们可以从 Spring 官方文档中了解到的那样,在 Spring 中有多个内置的 BeanFactoryPostProcessor 实现。其中一些包括:

  • PropertyPlaceholderConfigurer:允许使用 ${} 语法在 Spring Application Context 中替换属性占位符。
  • ConfigurationClassPostProcessor:允许使用 @Configuration 注释加载配置类,并将它们注册为 bean。
  • AutowiredAnnotationBeanPostProcessor:解析和处理 @Autowired 注释方法和字段,注册在 Spring 应用程序上下文中的相关 bean。

我们可以使用这些内置的 BeanFactoryPostProcessor 实现或自己的实现类来改变 Spring 应用程序上下文中的配置元数据。

BeanFactoryPostProcessor 接口只提供了一个 void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) 方法,方法参数就是一个 BeanFactory 的实现,我们可以通过它来自定义配置元数据。

9.2.2 BeanFactoryPostProcessor 接口的使用

要使用 BeanFactoryPostProcessor 接口,我们需要创建一个实现了该接口的类。建议将此类命名为对当前应用程序名有意义的类名。

下面是一个简单的例子,展示了如何使用 BeanFactoryPostProcessor 接口来自定义 Spring 应用程序上下文的配置元数据。

public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 从容器中拿到 BeanDefinition
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition("myBean");
        // 添加属性
        MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
        propertyValues.addPropertyValue("myProperty", "myValue");
    }
}

在这个例子中,我们创建了一个名为 CustomBeanFactoryPostProcessor 的类,它实现了 BeanFactoryPostProcessor 接口并重写了 postProcessBeanFactory 方法。在该方法中,我们获取了名为“myBean”的 BeanDefinition,然后在其中添加了一个名为“myProperty”和值为“myValue”的属性。

在上面的例子中,我们只是向一个现有 bean 添加了一个属性。我们也可以使用 BeanFactoryPostProcessor 接口来修改任何其他 BeanDefinition 实例的属性,删除某些 BeanDefinition 实例,甚至向 Spring 应用程序上下文中添加新实例。

为了确保我们创建的 BeanFactoryPostProcessor 被 Spring 容器正确加载和执行,请将它们注册到 Spring 的 Configuration 配置类中。

@Configuration
public class AppConfig {

    @Bean
    public static BeanFactoryPostProcessor customBeanFactoryPostProcessor() {
        return new CustomBeanFactoryPostProcessor();
    }

}

在上面的例子中,我们将 CustomBeanFactoryPostProcessor 的实例作为 bean 注册到 Spring 的应用程序上下文中,这样它就会被执行。

注意:需要使用 static 修饰方法,以避免与 AppConfig 本身的 bean 初始化顺序产生问题。

9.2.3 注解驱动的 BeanFactoryPostProcessor

BeanFactoryPostProcessor 还可以使用 @Component 注释或其他 Spring Boot 的 Java 配置类来驱动。

@Component
public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition("myBean");
        MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
        propertyValues.addPropertyValue("myProperty", "myValue");
    }
}

在上面的例子中,我们创建了一个使用 @Component 注解的类,以驱动 BeanFactoryPostProcessor 接口,Spring 会自动发现并注册该类。这是与 AppConfig 类的区别,它也不需要 static 方法。

9.3 使用 FactoryBean 自定义实例化逻辑

在 Spring 框架中,有很多概念和方式可以帮助我们管理和组织应用程序中的对象。其中一个非常有用的概念是 FactoryBean。FactoryBean 是一个接口,实现该接口的类可以作为 Spring 容器中的一个 bean。与普通 bean 不同的是,FactoryBean 不是直接返回一个对象的实例,而是返回另一个对象。

FactoryBean 在 Spring Framework 中的许多地方都使用了这个概念和接口。对于 FactoryBean 接口,Spring 本身也附带了 60 多个接口实现。

9.3.1 工厂模式回顾

在深入了解 FactoryBean 之前,我们先回顾一下软件设计中的常用模式——工厂模式。在大多数软件应用程序中,通常需要创建和管理各种各样的对象。为了减少代码重复和简化代码架构,一般采用工厂模式来进行对象的创建和管理。

工厂模式分为三种类型:简单工厂模式、工厂方法模式和抽象工厂模式。其思想都是使用一个工厂类来创建和管理相关的对象。

当需要使用某个对象时,客户端代码会调用工厂中的方法,传递所需参数。工厂类使用这些参数来创建和返回相关对象的实例。这种方式可以隐藏实现细节,减少客户端代码的复杂度。

9.3.2 FactoryBean定义

在 Spring 中,通过实现 FactoryBean 接口,我们可以实现上述工厂模式的思想,方便地创建和管理对象。

下面是 FactoryBean 接口的定义:

public interface FactoryBean<T> {
   // 获取对象(该实例可能会被共享,这取决于该工厂返回的是单例(singleton)还是原型(prototype))
   @Nullable
   T getObject() throws Exception;

   // 获取对象类型 
   @Nullable
   Class<?> getObjectType();
	
   // 是否为单例 (默认实现为单例)
   default boolean isSingleton() {
      return true;
   }

}

从上面的定义可以看出,FactoryBean 接口是一个泛型接口,指定了工厂创建的对象类型 T。其中,getObject() 方法用于创建和返回对象的实例,getObjectType() 方法返回对象的类型,使用 isSingleton() 方法指定对象是否为单例模式。如果不指定,默认为单例模式。

9.3.3 自定义 FactoryBean

接下来,我们一起来看一下如何实现一个自定义的 FactoryBean。

声明一个名为 MyFactoryBean 的类并继承 FactoryBean<T> 接口:

/**
 * @Author: JavGo
 * @DateTime: 2023/4/18 18:33
 **/
public class MyFactoryBean implements FactoryBean<MyBean> {
    private String nameValue;
    private int ageValue;

    public MyFactoryBean(String nameValue, int ageValue) {
        this.nameValue = nameValue;
        this.ageValue = ageValue;
    }

    @Override
    public MyBean getObject() throws Exception {
        return new MyBean(nameValue, ageValue);
    }

    @Override
    public Class<?> getObjectType() {
        return MyBean.class;
    }
    
    @Override
    public boolean isSingleton() {
        return true;
    }
}

上面的代码比较简单,但可以帮助我们理解 FactoryBean 接口的实现方式。当调用 getObject() 方法时,该方法会返回实际的对象,并将其作为 bean 注册到 Spring 容器中。

涉及到的 MyBean 代码如下:

/**
 * @Author: JavGo
 * @DateTime: 2023/4/18 18:35
 **/
public class MyBean {
    private String name;
    private int age;

    public MyBean() {
    }

    public MyBean(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

9.3.4 使用 FactoryBean

使用 FactoryBean 构建 Spring bean 的过程与构建普通 bean 非常相似。只需要将 FactoryBean 实现类注册为 bean,然后在其他 bean 中通过名称引用即可。

以下是一个例子(基于 Spring XML 配置文件):

<bean id="myBean" class="cn.javgo.ch1springboothelloworld.helloworld.factory.MyFactoryBean">
    <constructor-arg name="nameValue" value="javgo"/>
    <constructor-arg name="ageValue" value="18"/>
</bean>

由于我们实现了 FactoryBean 接口,并重写了 getObjectisSingletongetObjectType 方法。当 IoC 容器实例化 myBean 对象时,会先根据 <bean> 元素中指定的类名(即 MyFactoryBean)创建一个 MyFactoryBean 实例,然后通过调用 getObject 方法创建并返回 MyBean 对象实例,这样我们就通过 MyFactoryBean 创建了一个 MyBean 实例。

10.容器中的几种抽象

Spring Framework 针对研发和运维过程中的很多常见场景做了抽象处理,比如本节中会讲到的针对运行环境的抽象,后续章节中会聊到的事务抽象等。正是因为存在这些抽象层,Spring Framework 才能为我们屏蔽底层的很多细节。

10.1 环境抽象

代表程序运行环境的 Environment 接口包含两个关键信息——Profile 和 Properties,下面我们将详细展开这两项内容。

10.1.1 Profile 抽象

在现代软件开发中,应用程序通常需要在多个环境中运行,例如开发、测试和生产环境等。不同的环境通常需要使用不同的配置和资源,这就需要我们能够方便地切换和管理这些配置选项。这就是 Profile 抽象的作用所在。

Profile 抽象允许我们为应用程序定义多个配置文件(无论是基于 XML 还是 JavaConfig),每个配置文件都是针对不同的环境而设置的。当应用程序在特定环境中启动时,它将自动加载对应的配置文件,并使用其中定义的配置选项。这样,我们就可以方便地管理不同环境下的配置选项,而无需手动更改应用程序代码。

在 Spring 中,我们可以使用 @Profile 注解来为应用程序定义不同的 Profile。例如:

/**
 * @Author: JavGo
 * @Description: TODO
 * @DateTime: 2023/4/21 15:10
 */
@Profile("dev")
@Configuration
public class DevConfig {
    // 配置项
}
/**
 * @Author: JavGo
 * @Description: TODO
 * @DateTime: 2023/4/21 15:10
 */
@Profile("test")
@Configuration
public class TestConfig {
    // 配置项
}

在上面的代码中,我们为应用程序定义了两个 Profile:devtest。对于不同的 Profile,我们分别定义了不同的配置类。这些配置类中可以定义各种不同的配置选项,例如数据库连接、日志级别、缓存设置等。

如果是基于 XML 配置的方式,则在 <beans> 根标签中使用 profile 属性设置即可。

除了使用 @Profile 注解外,我们还可以通过编程的方式来激活 Profile。例如:

/**
 * @Author: JavGo
 * @Description: TODO
 * @DateTime: 2023/4/21 15:15
 */
public class Application {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        // 从容其中获取环境变量,设置 profile
        context.getEnvironment().setActiveProfiles("dev");
        // 注册配置类到容器中
        context.register(DevConfig.class, TestConfig.class);
        // 刷新容器,加载配置类
        context.refresh();
    }
}

在上面的代码中,我们通过编程的方式将 dev Profile 激活,然后注册了 DevConfigTestConfig 两个配置类,并启动了应用程序上下文。

在 Spring Boot 中,我们也可以通过在 applicationContext.properties 配置文件中设置 spring.profiles.active=dev 属性来激活指定的 Profile。(关于 Spring Boot 的配置文件与 Spring 的 XML 配置文件有不少差异,我们后面会详细讨论)

当然,我们还可以在启动程序时,在命令行中增加 spring.profiles.active 来达到同样的效果:

java -Dspring.profiles.active="dev" -jar xxx.jar

Spring Framework 还提供了默认的 Profile,一般名为 default,但也可以通过 setDefaultProfiles() 方法或 spring.profiles.default 属性来修改这个名称。

原理简述:

在 Spring 中,Profile 的实现主要是通过 Environment 接口来实现的。Environment 接口提供了对应用程序环境的访问,包括系统环境变量、应用程序配置文件、Profile 等等。Spring 将应用程序的配置信息抽象成一个 Environment 对象,可以通过这个对象来访问应用程序的配置信息。

在应用程序启动时,Spring 会创建一个 Environment 对象,它会加载默认的配置信息,并根据配置文件中定义的 Profile,加载对应的配置信息,然后将这些配置信息存储在 Environment 对象中。在应用程序运行过程中,我们可以通过 Environment 对象来获取对应的配置信息。

10.1.2 PropertySource 抽象

Spring Framework 中会频繁用到属性值,而这些属性又来自于很多地方,PropertySource 抽象就屏蔽了这层差异,例如,可以从 JNDI、JVM 系统属性 (-D 命令行参数,System.getProperties() 方法能取得系统属性) 和操作系统环境变量中加载属性。

简单来说,PropertySource 抽象类就是 Spring 中用于定义属性源的基础抽象类,用于将外部属性值与 Spring 容器中的属性值进行绑定。

在 Spring 中,一般属性用小写单词表示并用点分隔,比如 foo.bar,如果是从环境变量中获取属性,会按照 foo.bar、foo_bar、F00.BAR 和 FOO_BAR 的顺序来查找。

占位符的优先级:

当存在多个配置源时,Spring 框架的占位符解析规则将按照以下优先级顺序进行解析:

  1. Java 系统属性,通过 System.getProperties() 方法获取。
  2. 操作系统环境变量。
  3. application.propertiesapplication.yml 配置文件中的属性。(这里的配置文件是用 Spring Boot 中的进行举例的)
  4. SpringApplication.setDefaultProperties 方法中设置的属性。
  5. 配置文件中的属性,如 application-{profile}.properties

我们可以像下面这样来获得属性 foo.bar :

/**
 * @Author: JavGo
 * @Description: TODO
 * @DateTime: 2023/4/21 15:43
 */
@Component
public class Hello {
    @Autowired
    private Environment environment;

    public void sayHello() {
        // 从环境变量中获取配置项
        System.out.println(environment.getProperty("jfoo.bar"));
    }
}

也可以通过前面说过的 @Value 注解,它也能获取属性,获取不到时则返回默认值:

/**
 * @Author: JavGo
 * @Description: TODO
 * @DateTime: 2023/4/21 15:45
 */
@Component
public class Hello02 {
    @Value("${foo.bar:NONE}")
    private String value;
    
    public void sayHello() {
        System.out.println("foo.bar:" + value);
    }
}

${} 占位符可以出现在 Java 类配置或 XML 文件中,Spring 容器会试图从各种已经配置了的来源中解析属性。要添加属性来源,可以在 @Configuration 类上增加 @PropertySource 注解,概述 该配置类中使用了 ${} 占位符的值从哪里获取,例如:

/**
 * @Author: JavGo
 * @Description: TODO
 * @DateTime: 2023/4/21 15:49
 */
@Configuration
@PropertySource(value = "classpath:db.properties", encoding = "UTF-8")
public class Config {
    
}

如果使用 XML 进行配置,可以像下面这样 :

<context:property-placeholder location="classpath:db.properties"/>

通常我们的预期是一定能找到需要的属性,但也有这个属性可有可无的情况,这时将注解的 ignoreResourceNotFound 或者 XML 文件的 ignore-resource-not-found 设置为 true 即可。如果存在多个配置,则可以通过 @Order 注解或 XML 文件的 order 属性来指定顺序。

也许大家会好奇,Spring Framework 是如何实现占位符解析的,这一切要归功于 PropertySourcesPlaceholderConfigurer 这个 BeanFactoryPostProcessor。它可以在实例化和初始化其他 Bean 之前修改配置文件中的占位符。

如果使用基于 XML 的 <context:property-placeholder/>,Spring Framework 会自动注册一个 PropertySourcesPlaceholderConfigurer Bean 用于解析占位符。如果是 Java 配置,则需要我们自己用 @Bean 来注册一个,例如:

@Bean
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
    return new PropertySourcesPlaceholderConfigurer();
}

在它的 postProcessBeanFactory() 方法中,Spring 会尝试用找到的属性值来替换上下文中的对应占位符,这样在 Bean 正式初始化时我们就不会再看到占位符,而是实际替换后的值。

PropertySourcesPlaceholderConfigurer 是 Spring Framework 3.1 引入的,之前大家一直使用 PropertyPlaceholderConfigurer,相比之下,后者有诸多不便。

10.2 任务抽象

看过了与环境相关的抽象后,我们再来看看与任务执行相关的内容。Spring Framework 通过TaskExecutor 和 TaskScheduler 这两个接口分别对任务的异步执行与定时执行进行了抽象,接下来就让我们一起来了解一下。

10.2.1 异步执行

Spring Framework 的 TaskExecutor 抽象是在 2.0 版本时引入的,Executor 是 Java 5 对线程池概念的抽象,如果了解 JUC (java.utl.concurrent) 的话,一定会知道 java.util.concurrent.Executor 这个接口,而 TaskExecutor 就是在它的基础上又做了一层封装,让我们可以方便地在 Spring 容器中配置多线程相关的细节。

TaskExecutor 有很多实现,例如,同步的 SyncTaskExecutor;每次创建一个新线程的 SimpleAsyncTaskExecutor;内部封装了 Executor,非常灵活的 ConcurrentTaskExecutor ,还有我们用得最多的 ThreadPoolTaskExecutor。

如果是基于 XML 方式,我们可以像下面这样直接配置一个 ThreadPoolTaskExecutor :

<!-- 注册线程池 -->
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
    <!-- 核心线程数 -->
    <property name="corePoolSize" value="5"/>
    <!-- 最大线程数 -->
    <property name="maxPoolSize" value="10"/>
    <!-- 队列容量 -->
    <property name="queueCapacity" value="25"/>
</bean>

也可使用 <task:executor/>,下面是一个等价的配置:

<task:executor id="taskExecutor02" pool-size="5" queue-capacity="25"/>

在配置好了 TaskExecutor 后,可以直接调用它的 execute() 方法,传入一个 Runnable 对象,也可以在方法上使用 @Async 注解,这个方法可以是空返回值,也可以返回一个 Future:

@Async
public void runAsynchronous(){
}

为了让该注解生效,需要在配置类上增加 @EnableAsync 注解,或者在 XML 文件中增加 <task:annotation-driven/> 配置,开启对它的支持。

默认情况下,Spring 会为 @Async 寻找合适的线程池定义: 例如上下文里唯一的 TaskExecutor;如果存在多个,则用 ID 为 taskExecutor 的那个,前面两个都找不到的话会降级使用 SimpleAsyncTaskExecutor。当然,也可以在 @Async 注解中指定一个。

对于异步执行的方法,由于在触发时主线程就返回了,我们的代码在遇到异常时可能根本无法感知,而且抛出的异常也不会被捕获,因此最好我们能自己实现一个 AsyncUncaughtExceptionHandler 对象来处理这些异常,最起码打印一个异常日志,方便问题排查。

10.2.2 定时任务

定时任务,顾名思义,就是在特定的时间执行的任务,既可以是在某个特定的时间点执行一次的任务,也可以是多次重复执行的任务。

TaskScheduler 对两者都有很好的支持,其中的几个 schedule() 方法是处理单次任务的,而 scheduleAtFixedRate() 和 scheduleWithFixedDelay() 则是处理多次任务的。scheduleAtFixedRate() 按固定频率触发任务执行,scheduleWithFixedDelay() 在第一次任务执行完毕后等待指定的时间后再触发第二次任务。

TaskScheduler.schedule() 可以通过 Trigger 来指定触发的时间,其中最常用的就是接收 Cron 表达式的 CronTrigger 了,可以像下面这样在周一到周五的下午3点15分触发任务:

scheduler.schedule(task, new CronTrigger("0 15 15 * * 1-5"));

Cron 是 Linux 系统下的一个服务,可以根据配置在特定时间执行特定任务,执行时间就是用 Cron 表达式来配置的。感兴趣的可以自行搜索相关资料进行了解。

与 TaskExecutor 类似,Spring Framework 也提供了不少 TaskScheduler 的实现,其中最常用的也是 ThreadPoolTaskScheduler。上述例子中的 scheduler 就可以是一个注入的 ThreadPoolTaskScheduler Bean。

我们可以选择用 <task:scheduler/> 来配置 TaskScheduler :

<task:scheduler id="taskScheduler" pool-size="10"/>

也可以使用注解,默认情况下,Spring 会在同一上下文中寻找唯一的 TaskScheduler Bean,有多个的话用 ID 是 taskscheduler 的,再不行就用一个单线程的 TaskScheduler。在配置任务前,需要先在配置类上添加 @EnableScheduling 注解或在 XML 文件中添加 <task:annotation-driven/> 开启注解支持。

@Configuration
@EnableScheduling
public class Config{
    
}

随后,在方法上添加 @Scheduled 注解就能让方法定时执行,例如:

@Scheduled(fixedRate=1000) // 每隔1000ms执行
public void task1(){...}

@Scheduled(fixedDelay=1000) // 每次执行完后等待1000ms再执行下一次
public void task2(){...}

@Scheduled(initialDelay=5000,fixedRate=1000) // 先等待5000ms 开始执行第一次,后续每隔1000ms执行一次
public void task2(){...}

通过本章的学习,我们对 Spring Framework 的核心容器及 Bean 的概念已经有了一个大概的了解不仅知道了如何去使用它们,更是深人了解了如何对一些特性进行定制,如何通过类似事件通知这样的机制来应对某些问题。同时,我们也了解了 Spring Framework 为了让大家专心于业务逻辑,为我们提供了很多抽象来屏蔽底层的实现,如环境抽象和任务抽象。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。