20220507 Core - 1. The IoC Container

文档地址

IoC 容器

Inversion of Control (IoC) container

控制反转

Spring IoC Container 和 Bean 介绍

org.springframework.beansorg.springframework.context 包是 Spring Framework 的 IoC 容器的基础

BeanFactory 接口提供了一种能够管理任何类型对象的高级配置机制。 ApplicationContextBeanFactory 的子接口,它补充了:

  • 更容易与 Spring 的 AOP 特性集成
  • 消息资源处理(用于国际化)
  • 事件发布
  • 应用层特定的上下文,例如在 Web 应用程序中使用的 WebApplicationContext

简而言之,BeanFactory 提供了配置框架和基本功能,ApplicationContext 添加了更多企业级别特定的功能。ApplicationContext 是对 BeanFactory 的一个完整的超集。

在 Spring 中,构成应用程序主干并由 Spring IoC 容器管理的对象称为 bean 。bean 是由 Spring IoC 容器实例化、组装和管理的对象。Bean 以及它们之间的依赖关系反映在容器使用的配置元数据中。

参考源码

  • org.springframework.beans.factory.BeanFactory
  • org.springframework.context.ApplicationContext
  • org.springframework.core.ResolvableType
  • org.springframework.beans.factory.ObjectProvider
  • org.springframework.beans.factory.ObjectFactory

容器概述

org.springframework.context.ApplicationContext 接口代表 Spring IoC 容器,负责实例化、配置和组装 bean。容器通过读取配置元数据来获取有关要实例化、配置和组装哪些对象的指令。配置元数据以 XML、Java 注解或 Java 代码表示。它可以让您表达组成应用程序的对象以及这些对象之间丰富的相互依赖关系。

Spring 提供了 ApplicationContext 接口的几个实现。在独立应用程序中,通常创建 ClassPathXmlApplicationContextFileSystemXmlApplicationContext 的实例。虽然 XML 一直是定义配置元数据的传统格式,但是您可以通过提供少量 XML 配置来声明性地支持这些额外的元数据格式,从而指示容器使用 Java 注解或代码作为元数据格式。

在大多数应用程序方案中,不需要显式用户代码来实例化 Spring IoC 容器的一个或多个实例。例如,在 Web 应用程序场景中,应用程序文件 web.xml 中的简单八行样板 Web 描述符 XML :

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

下图显示了 Spring 如何工作的高级视图。您的应用程序类与配置元数据相结合,以便在 ApplicationContext 创建和初始化之后,您就有了一个完全配置且可执行的系统或应用程序。

容器魔法

配置元数据

如上图所示,Spring IoC 容器使用一种形式的配置元数据。此配置元数据表示您作为应用程序开发人员如何告诉 Spring 容器实例化、配置和组装应用程序中的对象。Spring IoC 容器本身与实际写入此配置元数据的格式完全分离,配置元数据可以基于 XML 或 Java 配置。

Spring 配置包含至少一个并且通常不止一个容器必须管理的 bean 定义。基于 XML 的配置元数据将这些 bean 配置为顶级元素 <beans/> 中的 <bean/> 元素。而 Java 配置通常在 @Configuration 类中使用 @Bean 注解方法。

实例化一个容器

提供给 ApplicationContext 构造函数的位置路径或路径是资源字符串,它允许容器从各种外部资源(如本地文件系统、 Java CLASSPATH 等)加载配置元数据。

ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.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
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- services -->

    <bean id="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
        <property name="accountDao" ref="accountDao"/>
        <property name="itemDao" ref="itemDao"/>
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions for services go here -->

</beans>
构建基于 XML 的配置元数据

让 bean 定义跨越多个 XML 文件会很有用。通常,每个单独的 XML 配置文件都代表您架构中的一个逻辑层或模块。

可以使用应用程序上下文构造函数从所有这些 XML 片段加载 bean 定义。该构造函数采用多个 Resource 位置

ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

或者,使用一个或多个 <import/> 元素从另一个文件或多个文件加载 bean 定义

<beans>
    <import resource="services.xml"/>
    <import resource="resources/messageSource.xml"/>
    <import resource="/resources/themeSource.xml"/>

    <bean id="bean1" class="..."/>
    <bean id="bean2" class="..."/>
</beans>

所有位置路径都相对于执行导入的定义文件,因此 services.xml 必须与执行导入的文件位于同一目录或类路径位置, messageSource.xmlthemeSource.xml 必须位于 resources 目录下。前导斜杠被忽略。但是,鉴于这些路径是相对的,最好根本不使用斜杠。

使用 ../ 相对路径引用父目录中的文件是可能的,但不建议这样做。这样做会在当前应用程序之外的文件上创建一个依赖项。特别不推荐配合 classpath: 使用,运行时解析处理时会选择“最近的”类路径根,然后查看其父目录。类路径配置的更改可能导致选择不同的、不正确的目录。

可以使用完全限定的资源位置而不是相对路径: 例如,file:C:/config/services.xmlclasspath:/config/services.xml 。但是,请注意,您正在将应用程序的配置耦合到特定的绝对位置。对于这种绝对位置,通常更可取的做法是保持间接性ーー例如,通过在运行时根据 JVM 系统属性解析的 ${ ... } 占位符

使用示例
目录路径:
\resources
|-\ioc
|--my1.xml
|--my2.xml
|--\sub
|---my3.xml
|---my4.xml
ApplicationContext ac = new ClassPathXmlApplicationContext("ioc/my1.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">
    <import resource="my2.xml"/>
    <import resource="sub/my3.xml"/>
    <import resource="classpath:ioc/sub/my4.xml"/>

</beans>

使用容器

通过使用 T getBean(String name, Class<T> requiredType) 可以获取到容器中的 bean 实例

读取 bean 定义并访问它们:

// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);

// use configured instance
List<String> userList = service.getUsernameList();

最灵活的变体是 GenericApplicationContext 与 reader 委托相结合,例如,使用 XmlBeanDefinitionReader 来处理 XML 文件

GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();

可以在相同的 ApplicationContext 上混合和匹配这些读取器委托,从不同的配置源中读取 bean 定义

ApplicationContext 接口还有其他一些检索 bean 的方法,但是理想情况下,您的应用程序代码不应该使用它们。实际上,您的应用程序代码根本不应该调用 getBean() 方法,因此根本不依赖于 Spring api。

Bean 概述

Spring IoC 容器管理一个或多个 bean 。这些 bean 是使用您提供给容器的配置元数据创建的(例如,以 XML <bean/> 定义的形式)

在容器本身内,bean 定义表示为 BeanDefinition 对象,其中包含以下元数据:

  • 包限定的类名:通常是定义的 bean 的实际实现类
  • Bean 行为配置元素,它说明 Bean 在容器中的行为方式(scope, lifecycle callbacks 等)
  • 对 bean 完成工作所需的其他 bean 的引用。这些引用也称为协作者或 依赖关系
  • 在新创建的对象中设置的其他配置设置——例如,池的大小限制或在管理连接池的 bean 中使用的连接数

此元数据转换为组成每个 bean 定义的一组属性。下表描述了这些属性:

Property 含义 Explained in…
Class 类名 Instantiating Beans
Name bean 名称 Naming Beans
Scope 作用域 Bean Scopes
Constructor arguments 构造函数参数 Dependency Injection
Properties 属性 Dependency Injection
Autowiring mode 自动装配模式 Autowiring Collaborators
Lazy initialization mode 懒加载模式 Lazy-initialized Beans
Initialization method 初始化方法 Initialization Callbacks
Destruction method 销毁方法 Destruction Callbacks

除了包含有关如何创建特定 bean 的信息的 bean 定义之外,ApplicationContext 实现还允许注册容器外(由用户创建)的现有对象。这是通过 getBeanFactory() 方法访问 ApplicationContextBeanFactory 来完成的,该方法返回的 BeanFactoryDefaultListableBeanFactory 实现。DefaultListableBeanFactory 通过 registerSingleton(..)registerBeanDefinition(..) 方法支持此注册。但是,典型的应用程序仅使用通过常规 bean 定义元数据定义的 bean。

Bean 元数据和手动提供的单例实例需要尽早注册,以便容器在自动装配和其他内省步骤中正确推理它们。虽然在一定程度上支持覆盖现有元数据和现有单例实例,但官方不支持在运行时(与对工厂的实时访问同时进行)注册新 bean,并可能导致并发访问异常或者 bean 容器中的状态不一致。

参考源码

  • org.springframework.beans.factory.config.BeanDefinition
  • org.springframework.beans.factory.support.DefaultListableBeanFactory

命名 Bean

每个 bean 都有一个或多个标识符。这些标识符在承载 bean 的容器中必须是唯一的。一个 bean 通常只有一个标识符。但是,如果它需要多个,则可以将多余的视为别名。

在基于 XML 的配置元数据中,您可以使用 idname 属性来指定 bean 标识符。这些名称可以是字母数字,也可以包含特殊字符。id 属性允许您指定一个 ID。如果要为 bean 引入其他别名,也可以在 name 属性中指定它们,用逗号 , 、分号 ; 或空格分隔。

如果没有明确提供 idname 属性,则容器会为该 bean 生成一个唯一的名称。但是,如果您想通过名称引用该 bean,通过使用 ref 元素或 Service Locator 样式查找,您必须提供名称。

<bean class="study.hwj.spring.bean.MyBean2"/>
<!-- 自动生成的 bean 名称是 study.hwj.spring.bean.MyBean2#0 ,别名是 study.hwj.spring.bean.MyBean2 -->

Bean 命名约定:约定是在命名 bean 时对实例字段名称使用标准 Java 约定。也就是说,bean 名称以小写字母开头,并以驼峰格式显示。例如 accountManageraccountServiceuserDaologinController

通过类路径中的组件扫描,Spring 为未命名的组件生成 bean 名称,遵循前面描述的规则:本质上,采用简单的类名并将其初始字符转换为小写。但是,在有多个字符且第一个和第二个字符都是大写的特殊情况下,原始大小写被保留。这些规则与 java.beans.Introspector.decapitalize 定义的规则相同,也就是说, FooBah 变为 fooBahX 变为 x ,但 URL 保持为 URL

参考源码

java.beans.Introspector.decapitalize

public static String decapitalize(String name) {
    if (name == null || name.length() == 0) {
        return name;
    }
    
    // name 长度大于 1 并且 第一个和第二个字符都是大写的,返回原值
    if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&         Character.isUpperCase(name.charAt(0))){
        return name;
    }
    
    // 否则,将字符串首字母转为小写
    char chars[] = name.toCharArray();
    chars[0] = Character.toLowerCase(chars[0]);
    return new String(chars);
}
在 Bean 定义之外给 Bean 取别名

在实际定义 bean 的地方指定所有别名并不总是足够的。有时需要为在别处定义的 bean 引入别名。这在大型系统中很常见,其中配置在每个子系统之间拆分,每个子系统都有自己的一组对象定义。在基于 XML 的配置元数据中,可以使用 <alias/> 元素来完成此操作。

<alias name="fromName" alias="toName"/>

如果使用 Java 配置,可以使用 @Bean 注解提供别名。

实例化 Bean

bean 定义本质上是创建一个或多个对象的配方。

如果使用基于 XML 的配置元数据,则要在 <bean/> 元素的 class 属性中指定实例化的对象的类型。这个类属性(在内部是 BeanDefinition 实例上的 Class 属性)通常是强制的。可以通过以下两种方式之一使用 Class 属性:

  • 通常,在容器本身通过反射地调用其构造函数直接创建 bean 的情况下,指定要构造的 bean 类,这在一定程度上等同于 Java 代码的 new 操作符
  • 指定包含被调用来创建对象的静态工厂方法的实际类,在不太常见的情况下,容器调用类上的静态工厂方法来创建 bean。从静态工厂方法调用返回的对象类型可能是同一个类,也可能完全是另一个类

嵌套类名:如果您在 com.example 包中调用了一个 SomeThing 类,并且 SomeThing 类有一个名为 OtherThingstatic 嵌套类,则它们之间可以用美元符号 $ 或点 . 分隔。因此 bean 定义中的 class 属性值将是 com.example.SomeThing$OtherThingcom.example.SomeThing.OtherThing

使用构造函数实例化

根据对该特定 bean 使用的 IoC 类型,可能需要一个默认(空)构造函数。

Spring IoC 容器实际上可以管理您希望它管理的任何类。它不仅限于管理真正的 JavaBeans 。大多数 Spring 用户更喜欢只有默认(无参数)构造函数和适当的 setters 和 getter 的实际 JavaBeans ,它们是根据容器中的属性建模的。您的容器中还可以有更多奇特的非 bean 风格类。

用基于 xml 的配置元数据,指定 bean 类:

<bean id="exampleBean" class="examples.ExampleBean"/>

<bean name="anotherExample" class="examples.ExampleBeanTwo"/>
使用静态工厂方法实例化

在定义用静态工厂方法创建的 bean 时,使用 class 属性指定包含静态工厂方法的类和名为 factory-method 的属性指定工厂方法的名称。您应该能够调用这个方法,并返回一个对象,随后将其视为通过构造函数创建的对象。这种 bean 定义的一个用途是在遗留代码中调用静态工厂。

<bean id="clientService" class="examples.ClientService" factory-method="createInstance"/>
public class ClientService {
    private static ClientService clientService = new ClientService();
    private ClientService() {}

    public static ClientService createInstance() {
        return clientService;
    }
}
使用实例工厂方法实例化

使用实例工厂方法进行的实例化从容器中调用现有 bean 的非静态方法来创建新 bean。要使用这种机制,保留 class 属性为空,并在 factory-bean 属性中,在当前(或父或祖先)容器中指定 bean 的名称,该容器包含要调用来创建对象的实例方法。使用 factory-method 属性指定工厂方法本身的名称

一个工厂类也可以包含多个工厂方法

<bean id="serviceLocator" class="examples.DefaultServiceLocator">
    <!-- inject any dependencies required by this locator bean -->
</bean>

<bean id="clientService"
    factory-bean="serviceLocator"
    factory-method="createClientServiceInstance"/>

<bean id="accountService"
    factory-bean="serviceLocator"
    factory-method="createAccountServiceInstance"/>
public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();

    private static AccountService accountService = new AccountServiceImpl();

    public ClientService createClientServiceInstance() {
        return clientService;
    }

    public AccountService createAccountServiceInstance() {
        return accountService;
    }
}

这种方法表明,工厂 bean 本身可以通过依赖注入(DI)来管理和配置

在 Spring 文档中,“工厂 bean” 指的是在 Spring 容器中配置并通过实例或静态工厂方法创建对象的 bean 。相比之下,FactoryBean 引用了一个 Spring 特定的 FactoryBean 实现类。

参考源码
  • org.springframework.beans.factory.FactoryBean
确定 Bean 的运行时类型

要确定特定 bean 的运行时类型并非易事。Bean 元数据定义中的指定类仅仅是一个初始类引用,它可能与已声明的工厂方法或 FactoryBean 类结合在一起,后者可能导致 bean 的不同运行时类型,或者在实例级工厂方法(可以通过指定的工厂 bean 名称解析)的情况下根本不设置类。此外,AOP 代理可以使用基于接口的代理来包装 bean 实例,并限制目标 bean 的实际类型(仅限于实现的接口)

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

依赖关系

依赖注入(Dependency Injection , DI)

DI 有两个主要的变体: 基于构造函数的依赖注入基于 setter 的依赖注入。

基于构造函数的依赖注入

基于构造函数的 DI 是通过容器调用具有许多参数的构造函数来实现的,每个参数表示一个依赖项。与调用具有特定参数的静态工厂方法来构造 bean 几乎是等效的。

一个只能通过依赖注入的构造函数注入的类:

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on a MovieFinder
    private final MovieFinder movieFinder;

    // a constructor so that the Spring container can inject a MovieFinder
    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // business logic that actually uses the injected MovieFinder is omitted...
}
构造函数参数解析

使用参数的类型进行构造函数参数解析匹配。如果 bean 定义的构造函数参数中不存在潜在的歧义,那么在 bean 定义中定义构造函数参数的顺序就是在实例化 bean 时将这些参数提供给适当的构造函数的顺序。

以下示例,假设 ThingTwoThingThree 类不通过继承关联,则不存在潜在的歧义。

package x.y;

public class ThingOne {

    public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
        // ...
    }
}
<beans>
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg ref="beanTwo"/>
        <constructor-arg ref="beanThree"/>
    </bean>

    <bean id="beanTwo" class="x.y.ThingTwo"/>

    <bean id="beanThree" class="x.y.ThingThree"/>
</beans>

当另一个 bean 被引用时,类型是已知的,并且可以发生匹配。当使用简单类型时,例如 <value>true</value> ,Spring 无法确定值的类型,因此无法在没有帮助的情况下按类型进行匹配。考虑下面的类:

package examples;

public class ExampleBean {

    // Number of years to calculate the Ultimate Answer
    private final int years;

    // The Answer to Life, the Universe, and Everything
    private final String ultimateAnswer;

    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}
构造函数参数类型匹配

通过 type 属性显式指定构造函数参数的类型,则容器可以使用简单类型的类型匹配

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg type="int" value="7500000"/>
    <constructor-arg type="java.lang.String" value="42"/>
</bean>
构造函数参数索引

使用 index 属性显式指定构造函数参数的索引,索引是从 0 开始的

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg index="0" value="7500000"/>
    <constructor-arg index="1" value="42"/>
</bean>

除了解决多个简单值的歧义之外,指定索引还可以解决构造函数具有两个相同类型参数的歧义。

构造函数参数名称

使用构造函数参数名称进行值消歧

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg name="years" value="7500000"/>
    <constructor-arg name="ultimateAnswer" value="42"/>
</bean>

请记住,您的代码必须在启用 debug 标志的情况下进行编译,以便 Spring 可以从构造函数中查找参数名称。如果您不能或不想使用调试标志编译代码,则可以使用 @ConstructorProperties 注解显式命名构造函数参数。示例类如下所示:

package examples;

public class ExampleBean {

    @ConstructorProperties({"years", "ultimateAnswer"})
    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}
  • java.beans.ConstructorProperties
基于 Setter 的依赖注入

在调用无参数构造函数或无参数静态工厂方法实例化 bean 之后,bean 上的容器调用 setter 方法可以实现基于 setter 的 DI。

一个只能通过使用纯 setter 注入进行依赖项注入的类

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on the MovieFinder
    private MovieFinder movieFinder;

    // a setter method so that the Spring container can inject a MovieFinder
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // business logic that actually uses the injected MovieFinder is omitted...
}

ApplicationContext 为它管理的 bean 支持基于构造函数和基于 setter 的 DI。在通过构造函数方法注入一些依赖项之后,它还支持基于 setter 的 DI。您可以以 BeanDefinition 的形式配置依赖关系,将其与 PropertyEditor 实例结合使用,以将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户并不直接使用这些类(通过编程) ,而是使用 XML bean 定义、带注解的组件(用@Component@Controller 等注解的类)或基于 java 的 @Configuration 类中的 @Bean 方法。然后,这些源在内部转换为 BeanDefinition 实例,并用于加载整个 Spring IoC 容器实例。

基于构造函数还是基于 setter 的 DI?

由于您可以混合使用基于构造函数和基于 setter 的 DI,因此 最好将构造函数用于强制依赖项,将 setter 方法或配置方法用于可选依赖项。请注意, 在 setter 方法上使用 @Required 注解可用于使属性成为必需的依赖项;但是,最好使用带有参数编程验证的构造函数注入。另外,大量的构造函数参数意味着类可能有太多的责任,应该重构以更好地解决适当的关注点分离/代码。

Spring 团队通常提倡构造函数注入,因为它可以让您将应用程序组件实现为不可变对象,并确保所需的依赖项不是 null 。构造函数注入的组件总是以完全初始化的状态返回给客户端(调用)代码。

Setter 注入应该主要仅用于可以在类中分配合理默认值的可选依赖项。否则,必须在代码使用依赖项的任何地方执行非空检查。setter 注入的一个好处是 setter 方法使该类的对象可以在以后重新配置或重新注入。因此,通过 JMX mbean 进行管理是 setter 注入的一个引人注目的用例。

使用对特定类最有意义的 DI 样式。例如,如果第三方类没有公开任何 setter 方法,那么构造函数注入可能是 DI 唯一可用的形式。

依赖解析过程

容器执行 bean 依赖解析如下:

  • 创建 ApplicationContext 并使用描述所有 bean 的配置元数据进行初始化。 。配置元数据可以由 XML、Java 代码或注解指定。
  • 对于每个 bean,它的依赖关系以属性、构造函数参数或静态工厂方法的参数的形式表示。在实际创建 bean 时,将这些依赖关系提供给 bean。
  • 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个 bean 的引用。
  • 作为值的每个属性或构造函数参数都从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的值转换为所有内置类型,例如 intlongStringboolean 等。

Spring 容器在创建容器时验证每个 bean 的配置。但是,在 bean 实际创建之前,不会设置 bean 的属性。在创建容器时,将创建单例作用域并设置为预实例化(默认情况)的 bean。作用域在 Bean 作用域中定义。此外,只有在请求 bean 时才会创建它。创建 bean 可能会导致创建 bean 图,因为创建和分配 bean 的依赖项及其依赖项的依赖项等等。请注意,这些依赖项之间的解析不匹配可能会延迟出现,即在首次创建受影响的 bean 时出现。

通常可以相信 Spring 会做正确的事情。它在容器加载时检测配置问题,例如对不存在的 bean 的引用和循环依赖项。Spring 在实际创建 bean 时设置属性并尽可能晚地解析依赖项。这意味着,如果创建对象或其某个依赖项时出现问题,那么正确加载的 Spring 容器随后可以在请求对象时生成异常ーー例如,由于缺少或无效的属性,bean 抛出异常。一些配置问题的可见性可能会延迟,这就是为什么默认情况下 ApplicationContext 实现会预先实例化单例 bean。在实际需要这些 bean 之前,需要花费一些前期时间和内存来创建它们,因此在创建 ApplicationContext 时(而不是以后)会发现配置问题。您仍然可以覆盖这个默认行为,以便单例 bean 以惰性方式初始化,而不是急切地预先实例化。

如果不存在循环依赖关系,当一个或多个合作 bean 被注入到依赖 bean 中时,每个合作 bean 在被注入到依赖 bean 之前都会被完全配置。这意味着,如果 bean A 对 bean B 有依赖关系,那么在调用 bean A 上的 setter 方法之前,Spring IoC 容器将完全配置 bean B 。换句话说,bean 被实例化(如果它不是预实例化的单例) ,它的依赖关系被设置,相关的生命周期方法(如配置的 init 方法或 InitializingBean 回调方法)被调用。

循环依赖

如果您主要使用构造函数注入,则可能会创建无法解决的循环依赖场景。例如:A 类通过构造函数注入需要 B 类的实例,B 类通过构造函数注入需要 A 类的实例。如果您将类 A 和 B 的 bean 配置为相互注入,则 Spring IoC 容器在运行时检测到此循环引用,并抛出一个 BeanCurrentlyInCreationException

一种可能的解决方案是编辑一些类的源代码,以便由 setter 而不是构造函数来配置。或者,避免构造函数注入并仅使用 setter 注入。也就是说,虽然不推荐,但是可以通过 setter 注入来配置循环依赖项与典型情况(没有循环依赖)不同,bean A 和 bean B 之间的循环依赖迫使其中一个 bean 在完全初始化之前被注入另一个 bean(经典的鸡和蛋场景)。

依赖注入的例子

调用一个静态工厂方法来返回一个对象的实例,而不是使用一个构造函数:

<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
    <constructor-arg ref="anotherExampleBean"/>
    <constructor-arg ref="yetAnotherBean"/>
    <constructor-arg value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

相应的 ExampleBean 类:

public class ExampleBean {

    // a private constructor
    private ExampleBean(...) {
        ...
    }

    // a static factory method; the arguments to this method can be
    // considered the dependencies of the bean that is returned,
    // regardless of how those arguments are actually used.
    public static ExampleBean createInstance (
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {

        ExampleBean eb = new ExampleBean (...);
        // some other operations...
        return eb;
    }
}

静态工厂方法的参数由 <constructor-arg/> 元素提供,与实际使用的构造函数完全相同。工厂方法返回的类的类型不必与包含静态工厂方法的类的类型相同。实例(非静态)工厂方法可以以本质上相同的方式使用(除了使用 factory-bean 属性而不是 class 属性之外)

依赖和配置详情

可以将 bean 属性和构造函数参数定义为对其他托管 bean(协作者)的引用或作为内联定义的值。为此,Spring 的基于 XML 的配置元数据支持 <property/><constructor-arg/> 标签中的子标签。

直接值(原语、字符串等)

<property/> 元素的 value 指定属性或构造器参数的字符串表示。Spring 的 转换服务 用于将这些值从 String 转换为属性或参数的实际类型。

<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <!-- results in a setDriverClassName(String) call -->
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
    <property name="username" value="root"/>
    <property name="password" value="misterkaoli"/>
</bean>

使用 p 名称空间 进行更简洁的 XML 配置

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close"
        p:driverClassName="com.mysql.jdbc.Driver"
        p:url="jdbc:mysql://localhost:3306/mydb"
        p:username="root"
        p:password="misterkaoli"/>

</beans>

还可以配置 java.util.Properties 实例

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

    <!-- typed as a java.util.Properties -->
    <property name="properties">
        <value>
            jdbc.driver.className=com.mysql.jdbc.Driver
            jdbc.url=jdbc:mysql://localhost:3306/mydb
        </value>
    </property>
</bean>

Spring 容器使用 JavaBeans PropertyEditor 机制将 <value/> 元素内的文本转换为 java.util.Properties

idref 标签

idref 标签用来将容器内其它bean的 id 传给 <constructor-arg/><property/> 标签,同时提供错误验证功能。

<bean id="theTargetBean" class="..."/>

<bean id="theClientBean" class="...">
    <property name="targetName">
        <idref bean="theTargetBean"/>
    </property>
</bean>

等价于

<bean id="theTargetBean" class="..." />

<bean id="client" class="...">
    <property name="targetName" value="theTargetBean"/>
</bean>

使用 idref 标记可以让容器在部署时验证引用的命名 bean 是否实际存在。在第二个变体中,不对传递给客户端 bean 的 targetName 属性的值执行验证。只有在实际实例化客户端 bean 时才会发现输入错误(很可能导致致命的结果)。如果客户端 bean 是一个原型 bean,那么只有在部署容器之后很长时间才能发现这个错误。

<idref/> 标签一个常见位置是 ProxyFactoryBean 定义中 AOP 拦截器的配置。在指定拦截器名称时使用 <idref/> 元素可以防止错误拼写拦截器 ID。

对其他 Bean 的引用(协作者)( ref

ref 可用在 <constructor-arg/><property/> 标签内,包含 beanparent 属性。bean 属性引用同一容器或父容器中的任何 bean ,parent 属性指定目标 bean 会创建对当前容器的父容器中的 bean 的引用。bean 属性的值可能与目标 bean 的 id 属性相同,或者与目标 bean 的 name 属性中的一个值相同。

<ref bean="someBean"/>

<!-- 引用父容器中的 bean -->
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target">
        <ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
    </property>
    <!-- insert other configuration and dependencies as required here -->
</bean>
内部 Bean

<constructor-arg/><property/> 标签内使用 <bean/>

<bean id="outer" class="...">
    <!-- instead of using a reference to a target bean, simply define the target bean inline -->
    <property name="target">
        <bean class="com.example.Person"> <!-- this is the inner bean -->
            <property name="name" value="Fiona Apple"/>
            <property name="age" value="25"/>
        </bean>
    </property>
</bean>

内部 bean 定义不需要定义的 ID 或名称。如果指定,容器不会使用这样的值作为标识符。容器在创建时也会忽略 scope 标志,因为内部 bean 始终是匿名的,并且始终与外部 bean 一起创建。不可能独立访问内部 bean 或将它们注入除外部 bean 之外的其他协作 bean 中。

作为特殊情况,可以从自定义作用域接收 destruction 回调,例如,对于包含在单例 bean 中的请求作用域的内部 bean。内部 bean 实例的创建与其包含的 bean 绑定在一起,但是销毁回调允许它参与请求范围的生命周期。这种情况并不常见。内部 bean 通常只是简单地共享外部 bean 的作用域。

集合( <list/><set/><map/><props/>

<list/><set/><map/><props/> 标签分别对应 Java 集合类型 ListSetMap 、和 Properties

<bean id="moreComplexObject" class="example.ComplexObject">
    
    <!-- results in a setAdminEmails(java.util.Properties) call -->
    <property name="adminEmails">
        <props>
            <prop key="administrator">administrator@example.org</prop>
            <prop key="support">support@example.org</prop>
            <prop key="development">development@example.org</prop>
        </props>
    </property>
    
    <!-- results in a setSomeList(java.util.List) call -->
    <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
    </property>
    
    <!-- results in a setSomeMap(java.util.Map) call -->
    <property name="someMap">
        <map>
            <entry key="an entry" value="just some string"/>
            <entry key ="a ref" value-ref="myDataSource"/>
        </map>
    </property>
    
    <!-- results in a setSomeSet(java.util.Set) call -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
    
</bean>

map 键或值或 set 的值也可以是以下任何一个元素:

bean | ref | idref | list | set | map | props | value | null
集合合并merge

可以定义父 <list/><set/><map/><props/> 元素,并让子 <list/><set/><map/><props/> 从父集合继承和覆盖值。也就是说,子集合的值是合并父集合和子集合的元素的结果,其中子集合元素重写父集合中指定的值。

集合合并适用于 <list/><set/><map/><props/> ,但是不能合并不同的集合类型

<beans>
    <bean id="parent" abstract="true" class="example.ComplexObject">
        <property name="adminEmails">
            <props>
                <prop key="administrator">administrator@example.com</prop>
                <prop key="support">support@example.com</prop>
            </props>
        </property>
    </bean>
    <bean id="child" parent="parent">
        <property name="adminEmails">
            <!-- the merge is specified on the child collection definition -->
            <props merge="true">
                <prop key="sales">sales@example.com</prop>
                <prop key="support">support@example.co.uk</prop>
            </props>
        </property>
    </bean>
<beans>

合并后 child 的值是:

administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk

<list/><set/><map/> 集合类型的合并行为类似。在 <list/> 标签的特定情况下,维护与 List 集合类型相关的语义。父列表的值位于所有子列表的值之前。对于 MapSetProperties 集合类型,不存在排序。因此,对于构成容器内部使用的关联映射、集合和属性实现类型基础的集合类型,没有有效的排序语义。

不能合并不同的集合类型(如 MapList)。merge 属性必须在较低的继承子定义上指定。在父集合定义上指定 merge 属性是多余的,不会导致所需的合并。

强类型集合

Spring 的类型转换提供支持

public class SomeClass {

    private Map<String, Float> accounts;

    public void setAccounts(Map<String, Float> accounts) {
        this.accounts = accounts;
    }
}
<beans>
    <bean id="something" class="x.y.SomeClass">
        <property name="accounts">
            <map>
                <entry key="one" value="9.99"/>
                <entry key="two" value="2.75"/>
                <entry key="six" value="3.99"/>
            </map>
        </property>
    </bean>
</beans>
Null 和空字符串值( <null/>

Spring 将属性和类似属性的空参数视为空字符串。

<bean class="ExampleBean">
    <property name="email" value=""/>
</bean>

等价于:

exampleBean.setEmail("");
<bean class="ExampleBean">
    <property name="email">
        <null/>
    </property>
</bean>

等价于:

exampleBean.setEmail(null);
使用 p 命名空间的 XML 快捷配置

p 命名空间允许您使用 bean 元素的属性(而不是嵌套的 <property/> 元素)来描述协作 bean 的属性值,或者两者都使用。

Spring 支持基于 XML Schema 定义的名称空间的可扩展配置格式。p 命名空间并未在 XSD 文件中定义,它只存在于 Spring 的 core 中。

以下示例显示了两个解析为相同结果的 XML 片段(第一个使用标准 XML 格式,第二个使用 p 命名空间):

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 使用 property 元素 -->
    <bean name="john-classic" class="com.example.Person">
        <property name="name" value="John Doe"/>
        <property name="spouse" ref="jane"/>
    </bean>

    <!-- 使用 p 命名空间 -->
    <bean name="john-modern"
        class="com.example.Person"
        p:name="John Doe"
        p:spouse-ref="jane"/>

    <bean name="jane" class="com.example.Person">
        <property name="name" value="Jane Doe"/>
    </bean>
</beans>
使用 c 命名空间的 XML 快捷配置

c 命名空间并未在 XSD 文件中定义

对应 constructor-arg 标签

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:c="http://www.springframework.org/schema/c"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="beanTwo" class="x.y.ThingTwo"/>
    <bean id="beanThree" class="x.y.ThingThree"/>

    <!-- 指定参数名称,使用 constructor-arg -->
    <!-- traditional declaration with optional argument names -->
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg name="thingTwo" ref="beanTwo"/>
        <constructor-arg name="thingThree" ref="beanThree"/>
        <constructor-arg name="email" value="something@somewhere.com"/>
    </bean>

    <!-- 指定参数名称,使用 c 名称空间 -->
    <!-- c-namespace declaration with argument names -->
    <bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
        c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>

    <!-- 指定参数索引,使用 c 名称空间 -->
    <!-- c-namespace index declaration -->
    <bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
          c:_2="something@somewhere.com"/>
</beans>
复合属性名称

使用复合或嵌套属性名

<bean id="something" class="things.ThingOne">
    <property name="fred.bob.sammy" value="123" />
</bean>

something bean 有 fred 属性,fredbob 属性,bobsammy 属性,并且最终 sammy 属性被设置为值 123 。中间的属性不能为 null ,否则会抛出 NullPointerException

使用 depends-on

如果一个 bean 是另一个 bean 的依赖项,这通常意味着一个 bean 被设置为另一个 bean 的属性。通常,您使用基于 XML 的配置元数据中的 ref 元素来完成此操作。但是,有时 bean 之间的依赖关系不那么直接。例如,当需要触发类中的静态初始化程序时,例如数据库驱动程序注册。在初始化使用此元素的 bean 之前,depends-on 属性可以显式地强制初始化一个或多个 bean。

depends-on 属性可以指定初始化时的依赖项,并且在单例 bean 的情况下,还可以指定相应的销毁时的依赖项。在给定的 bean 本身被销毁之前,首先销毁与给定 bean 定义的 depends-on 指定的依赖 bean 。这样,depends-on也可以控制销毁顺序。

逗号、空格和分号是有效的分隔符

<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
    <property name="manager" ref="manager" />
</bean>

<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />

延迟初始化的 Bean ( lazy-init

默认情况下,ApplicationContext 实现会在初始化过程中急切地创建和配置所有单例 bean 。

可以通过将 bean 定义标记为延迟初始化来防止单例 bean 的预实例化。一个延迟初始化的 bean 告诉 IoC 容器在它第一次被请求时创建一个 bean 实例,而不是在 Spring 容器启动时。

<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>

当延迟初始化 bean 是非延迟初始化的单例 bean 的依赖项时,ApplicationContext 会在启动时创建延迟初始化 bean,因为它必须满足单例 bean 的依赖项。

可以通过使用 <beans/> 标签上的 default-lazy-init 属性在 容器级别 控制延迟初始化

<beans default-lazy-init="true">
    <!-- no beans will be pre-instantiated... -->
</beans>

自动装配协作者

Spring 容器可以自动装配协作 bean 之间的关系。您可以通过检查 ApplicationContext 的内容,让 Spring 自动为您的 bean 解析协作者。自动装配有以下优点:

  • 自动装配可以显著减少指定属性或构造函数参数的需要
  • 自动装配可以根据对象的发展更新配置。例如,如果需要向类添加依赖项,则可以自动满足该依赖项,而无需修改配置。

使用基于 XML 的配置元数据时,您可以使用 <bean/> 元素的 autowire 属性为 bean 定义指定自动装配模式。自动装配功能有四种模式。您可以为每个 bean 指定自动装配模式。下表描述了四种自动装配模式:

模式 解释
no 默认)不使用自动装配。Bean 引用必须由 ref 元素定义。对于较大的部署,不建议更改默认设置,因为明确指定依赖可以提供更好的控制和清晰度。在某种程度上,它记录了系统的结构。
byName 按属性名称自动装配。Spring 查找与需要自动装配的属性同名的 bean 。例如,如果一个 bean 定义被设置为按名称自动装配并且它包含一个 master 属性(即它有一个 setMaster 方法),Spring 会查找一个名为 master 的 bean 定义并使用它来设置属性。
byType 如果容器中恰好存在一个与属性相同类型的 bean,则让属性自动装配。如果存在多个,则会引发异常,这表明您不能为该 bean 使用 byType 自动装配。如果没有匹配的 bean,则不会发生任何事情(不会设置该属性)。
constructor 类似于 byType ,但适用于构造函数参数。如果容器中没有一个构造函数参数相同类型的 bean,则会引发致命错误。

使用 byTypeconstructor 自动装配模式,您可以装配数组和集合。在这种情况下,提供容器内与预期类型匹配的所有自动装配候选者以满足依赖关系。如果预期的键类型是 String,可以自动装配强类型 Map 实例。自动装配 Map 实例的值由与预期类型匹配的所有 bean 实例组成,并且 Map 实例的键包含相应的 bean 名称。

自动装配的局限性和缺点

自动装配在整个项目中一致使用时效果最佳。

自动装配的局限性和缺点:

  • propertyconstructor-arg 设置中的显式依赖项始终覆盖自动装配。不能自动装配简单属性,例如 基本数据类型、StringsClasses
  • 自动装配不如显式装配精确
  • 对于可能从 Spring 容器生成文档的工具,装配信息可能不可用
  • 可能存在多个相同类型的 bean 定义,解决方法:
    • 放弃自动装配,转而使用显式指定
    • 通过将 autowire-candidate 属性设置为 false 来避免对 bean 定义进行自动装配
    • 通过将 primary 属性设置为 true ,将单个 bean 定义指定为主要候选者
    • 使用基于注解的配置实现更细粒度的控制
从自动装配中排除 Bean

在 XML 配置中,将 <bean/> 标签的 autowire-candidate 属性设置为 false ,容器使该特定 bean 定义对自动装配不可用。

autowire-candidate 属性旨在仅影响基于类型的自动装配。它不会影响按名称的显式引用,即使指定的 bean 未标记为自动装配候选者,也会解析。因此,如果名称匹配,按名称自动装配仍然会注入一个 bean。

还可以根据对 bean 名称的模式匹配来限制自动装配候选者。顶级 <beans/> 标签在 default-autowire-candidates 属性中接受一个或多个模式 。例如,要将自动装配候选状态限制为名称以 Repository 结尾的任何 bean ,设置值为 *Repository 。要提供多个模式,请在 逗号分隔 的列表中定义它们。bean 定义的 autowire-candidate 属性的显式值 truefalse 始终优先。对于此类 bean,模式匹配规则不适用。

这些技术对于您永远不想通过自动装配注入其他 bean 的 bean 很有用。这并不意味着不能使用自动装配来配置被排除的 bean 本身。相反,bean 本身不是自动装配其他 bean 的候选者。

使用说明

byType 依赖注入的过程:

  1. 根据属性类型查找容器内所有相同类型 autowire-candidate="true" 的候选 bean ,autowire-candidate 属性默认为 true
  2. 如果候选 bean 超过一个,选择 primary="ture" 的 bean ,primary 属性默认为 false

方法注入

在大多数应用场景中,容器中的大部分 bean 都是 单例的。当单例 bean 需要与另一个单例 bean 协作,或者,非单例 bean 需要与另一个非单例 bean 协作时,您通常通过将一个 bean 定义为另一个 bean 的属性来处理依赖关系。但是当 bean 生命周期不同时就会出现问题。

假设在每次 A 的方法调用上,单例 bean A 需要使用非单例(原型)bean B。容器只创建单例 bean A 一次,因此只有一次设置属性的机会。容器无法在每次需要时为 bean A 提供 bean B 的新实例。

一个解决方案是放弃一些控制反转,通过实现 ApplicationContextAware 接口,并在每次 bean A 需要时对容器进行 getBean("b") 调用,请求 bean B 实例

// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;

// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class CommandManager implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    public Object process(Map commandState) {
        // grab a new instance of the appropriate Command
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }

    protected Command createCommand() {
        // notice the Spring API dependency!
        return this.applicationContext.getBean("command", Command.class);
    }

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

但这是不可取的,因为业务代码会耦合到 Spring Framework。方法注入是 Spring IoC 容器的一个有点高级的特性,可以让你干净地处理这个用例。

查找方法注入( lookup-method

查找方法注入是容器重写容器管理 bean 上的方法并返回容器中另一个命名 bean 的查找结果的能力。查找通常涉及一个原型 bean,如前面部分所描述的场景。Spring 框架通过使用从 CGLIB 库生成字节码来动态生成覆盖该方法的子类来实现此方法注入。

  • 为了使这个动态子类化生效,Spring bean 容器子类的类不能是 final 类,而要重写的方法也不能是 final
  • 单元测试具有抽象方法的类需要您自己对该类进行子类化,并提供抽象方法的存根实现
  • 具体的方法对于组件扫描也是必要的,这需要具体的类来获取
  • 另一个关键限制是查找方法不能与工厂方法一起工作,特别是不能与配置类中的 @Bean 方法一起工作,因为在这种情况下,容器不负责创建实例,因此不能动态创建运行时生成的子类
package fiona.apple;

// no more Spring imports!

public abstract class CommandManager {

    public Object process(Object commandState) {
        // grab a new instance of the appropriate Command interface
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }

    // okay... but where is the implementation of this method?
    protected abstract Command createCommand();
}

要注入的方法需要以下形式的签名:

<public|protected> [abstract] <return-type> theMethodName(no-arguments);

如果是 abstract 方法,则动态生成的子类实现该方法。否则,动态生成的子类会覆盖原始类中定义的具体方法。考虑以下示例:

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
    <!-- inject dependencies here as required -->
</bean>

<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
    <lookup-method name="createCommand" bean="myCommand"/>
</bean>

commandManager bean 在需要 myCommand bean 的新实例时调用它自己的 createCommand() 方法。如果确实需要每次都是新的 myCommand 实例,您必须小心地将 bean 的作用域设置为原型。如果是单例,则每次都返回相同的 myCommand bean 实例。

@Lookup 注解形式:

public abstract class CommandManager {

    public Object process(Object commandState) {
        Command command = createCommand();
        command.setState(commandState);
        return command.execute();
    }

    @Lookup("myCommand")
    protected abstract Command createCommand();
}

可以依靠目标 bean 根据查找方法的声明返回类型进行解析:

public abstract class CommandManager {

    public Object process(Object commandState) {
        MyCommand command = createCommand();
        command.setState(commandState);
        return command.execute();
    }

    @Lookup
    protected abstract MyCommand createCommand();
}

请注意,通常应该使用具体的存根实现来声明这种带注解的查找方法,以便它们与 Spring 的组件扫描规则兼容。

访问不同作用域的目标 bean 的其他方法:

  • ObjectFactory / Provider 注入点
  • org.springframework.beans.factory.config.ServiceLocatorFactoryBean
任意方法替换( replaced-method

与查找方法注入相比,一种不太有用的方法注入形式是能够用另一种方法实现替换托管 bean 中的任意方法。

举例:需要被覆盖的方法 computeValue

public class MyValueCalculator {

    public String computeValue(String input) {
        // some real code...
    }

    // some other methods...
}

实现 org.springframework.beans.factory.support.MethodReplacer 接口的类提供了新的方法定义

/**
 * meant to be used to override the existing computeValue(String)
 * implementation in MyValueCalculator
 */
public class ReplacementComputeValue implements MethodReplacer {

    public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
        // get the input value, work with it, and return a computed result
        String input = (String) args[0];
        ...
        return ...;
    }
}

用于部署原始类和指定方法覆盖的 bean 定义

<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
    <!-- arbitrary method replacement -->
    <replaced-method name="computeValue" replacer="replacementComputeValue">
        <arg-type>String</arg-type>
    </replaced-method>
</bean>

<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>

您可以在 <replaced-method/> 标签内使用一个或多个 <arg-type/> 元素来指示被覆盖的方法的方法签名。仅当方法重载并且类中存在多个变体时,才需要参数的签名。为方便起见,参数的类型字符串可以是完全限定类型名称的子字符串。例如,以下所有匹配 java.lang.String

java.lang.String
String
Str

Bean 作用域( scope

在创建 bean 定义时,您创建了一个配方,用于创建由该 bean 定义定义的类的实际实例。Bean 定义是一个配方的想法很重要,因为它意味着,与类一样,您可以从单个配方创建许多对象实例。

您不仅可以控制各种依赖项和配置值,这些依赖项和配置值将被插入从特定 bean 定义创建的对象中,还可以控制从特定 bean 定义创建的对象的作用域。这种方法是强大而灵活的,因为您可以选择通过配置创建的对象的作用域,而不必在 Java 类级别的对象作用域内进行操作。可以将 bean 定义为部署在许多作用域中的一个。Spring 框架支持六个作用域,其中四个作用域只有在使用感知 web ( web-aware) 的 ApplicationContext 时才可用。您还可以创建自定义作用域。

Bean 作用域 描述
singleton 默认)将单个 bean 定义作用域限定为每个 Spring IoC 容器的单个对象实例
prototype 将单个 bean 定义作用域限定为任意数量的对象实例
request 将单个 bean 定义作用域限定为单个 HTTP 请求的生命周期。也就是说,每个 HTTP 请求都有自己的 bean 实例。仅在感知 web 的 Spring ApplicationContext 上下文中有效
session 将单个 bean 定义作用域限定为 HTTP Session 的生命周期。仅在感知 web 的 Spring ApplicationContext 上下文中有效
application 将单个 bean 定义作用域限定为 ServletContext 的生命周期。仅在感知 web 的 Spring ApplicationContext 上下文中有效
websocket 将单个 bean 定义作用域限定为 WebSocket 的生命周期。在感知 web 的 Spring ApplicationContext 上下文中有效

从 Spring 3.0 开始,线程作用域可用,但默认情况下未注册。有关更多信息,请参阅 SimpleThreadScope 的文档 。有关如何注册此或任何其他自定义范围的说明,请参阅 使用自定义范围

单例作用域( singleton

只管理单一 bean 的一个共享实例,对 ID 或 ID 与 bean 定义相匹配的 bean 的所有请求都将导致 Spring 容器返回一个特定 bean 实例。

singleton

Spring 的单例 bean 概念不同于四人组 (GoF) 模式书中定义的单例模式。GoF 单例对对象的范围进行了硬编码,以便每个 ClassLoader 只创建一个特定类的一个实例。Spring 单例作用域最好描述为每个容器和每个 bean。这意味着,如果您在单个 Spring 容器中为特定类定义一个 bean,则 Spring 容器会为该 bean 定义创建类的仅一个实例。单例作用域是 Spring 中的默认作用域。

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

<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

原型作用域( prototype

bean 部署的非单例原型作用域导致每次对特定 bean 发出请求时都会创建一个新 bean 实例。

通常,您应该对所有有状态 bean 使用原型作用域,对无状态 bean 使用单例作用域。

prototype

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

Spring 不管理原型 bean 的整个生命周期。 Spring 会调用原型 bean 的初始化生命周期回调方法,但不会调用销毁生命周期回调。客户端代码必须清理原型作用域的对象并释放原型 bean 持有的昂贵资源。要让 Spring 容器释放原型作用域 bean 持有的资源,请尝试使用自定义 bean 后置处理器 post-processor,它保存对需要清理的 bean 的引用。

具有 Prototype bean 依赖关系的 Singleton Bean

当您使用单实例作用域的 bean 与原型 bean 的依赖关系时,请注意依赖关系在实例化时解析。

单例 bean 属性中注入原型 bean ,每次获取得到的是相同的 bean,不会每次都生成新的 bean 实例,因为单例 bean 的生命周期只有一次。

假设您希望单实例范围的 bean 在运行时重复获取原型范围的 bean 的新实例。不能依赖性地将原型范围的 bean 注入到单例 bean 中,因为注入只发生一次,即当 Spring 容器实例化单例 bean 并解析和注入它的依赖项时。如果在运行时多次需要原型 bean 的新实例,请参见 方法注入( lookup

Request, Session, Application, WebSocket 作用域

requestsessionapplicationwebsocket 作用域只有当你使用感知 web 的 Spring ApplicationContext 实现(例如 XmlWebApplicationContext )时可用。如果将这些作用域与常规 Spring IoC 容器(例如 ClassPathXmlApplicationContext )一起使用,会抛出异常

参考源码
  • @RequestScope@SessionScope@ApplicationScope
初始 web 配置

为了支持 requestsessionapplicationwebsocket 作用域对 bean 进行作用域限定,在定义 bean 之前需要进行一些小的初始配置。(标准作用域:singletonprototype 不需要这种初始设置)

如果您在 Spring Web MVC 中访问作用域 bean,在 Spring DispatcherServlet 处理的请求中,不需要特殊设置。

如果您使用 Servlet 2.5 web 容器,并在 Spring 的 DispatcherServlet 之外处理请求(例如,在使用 JSF 或 Struts 时),则需要注册 org.springframework.web.context.request.RequestContextListener 实现了 javax.servlet.ServletRequestListener 。对于 Servlet 3.0 + ,可以通过使用 WebApplicationInitializer 接口以编程方式完成。或者,对于较旧的容器,将以下声明添加到 web 应用程序的 web.xml 文件中:

<web-app>
    ...
    <listener>
        <listener-class>
            org.springframework.web.context.request.RequestContextListener
        </listener-class>
    </listener>
    ...
</web-app>

或者,如果监听器设置有问题,可以考虑使用 Spring 的 RequestContextFilter 。过滤器映射取决于周围的 web 应用程序配置,因此您必须根据需要更改它。下面的清单显示了 web 应用程序的过滤器部分:

<web-app>
    ...
    <filter>
        <filter-name>requestContextFilter</filter-name>
        <filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>requestContextFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    ...
</web-app>

DispatcherServletRequestContextListenerRequestContextFilter 都做完全相同的事情,即将 HTTP 请求对象绑定到服务该请求的 Thread 。这使得请求范围和会话范围的 bean 在调用链的下一级可用。

request 作用域( request
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>

Spring 容器通过为每个 HTTP 请求使用 LoginAction bean 定义来创建新实例。当请求完成处理时,作用域为 request 的 bean 被丢弃。

可以使用 @RequestScope 注解

session 作用域( session
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>

Spring 容器通过在单个 HTTP Session 的生存期内使用 UserPreferences bean 定义创建 UserPreferences bean 的新实例。当 HTTP 会话最终被丢弃时,作用域为特定 HTTP 会话的 bean 也被丢弃。

可以使用 @SessionScope 注解

application 作用域( application
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>

Spring 容器为整个 web 应用程序使用一次 AppPreferences bean 定义,从而创建 AppPreferences bean 的一个新实例。也就是说,appPreferences bean 的作用域在 ServletContext 级别,并作为常规 ServletContext 属性存储。这有点类似于 Spring 单例 bean,但是在两个重要方面有所不同:它是对应 ServletContext 的一个单例,而不是对应 Spring 的 ApplicationContext (在任何给定的 web 应用程序中都可能有多个),它实际上是公开的,因此可以作为一个 ServletContext 属性看到。

可以使用 @ApplicationScope 注解

作用域 Bean 作为依赖项

Spring IoC 容器不仅管理对象(bean)的实例化,还管理协作者(或依赖项)的连接。如果您想将一个 HTTP request 作用域的 bean 注入到另一个生命周期更长的 bean 中,您可以选择注入一个 AOP 代理来代替该作用域的 bean。也就是说,您需要注入一个代理对象,该对象公开与作用域对象相同的公共接口,但也可以从相关作用域(例如 HTTP 请求)中检索真实目标对象,并将方法调用委托给真实对象。

可以在作用域为单例的 bean 之间使用 <aop:scoped-proxy/> ,然后通过一个可序列化的中间代理,从而能够在反序列化中重新获得目标单例 bean 。

当针对一个原型作用域 bean 声明 <aop:scoped-proxy/> 时,共享代理上的每个方法调用都会创建一个新的目标实例,然后将调用转发到该实例。

而且,作用域代理并不是以生命周期安全的方式从较短的作用域访问 bean 的唯一方法。您还可以将注入点(即构造函数或 setter 参数或自动装配字段)声明为 ObjectFactory<MyTargetBean> ,从而允许 getObject() 调用在每次需要时根据需要检索当前实例,而不必保留实例或分别存储它。

作为一个扩展的变体,您可以声明 ObjectProvider<MyTargetBean> ,它提供了几个附加的访问变体,包括 getIfAvailablegetIfUnique

这种方法的 JSR-330 变体被称为 Provider ,与 Provider<MyTargetBean> 声明以及每次检索尝试的相应 get() 调用一起使用。

将短命的作用域 bean 注入长命的作用域 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"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- an HTTP Session-scoped bean exposed as a proxy -->
    <bean id="userPreferences" class="com.something.UserPreferences" scope="session">
        <!-- instructs the container to proxy the surrounding bean -->
        <aop:scoped-proxy/> 
    </bean>

    <!-- a singleton-scoped bean injected with a proxy to the above bean -->
    <bean id="userService" class="com.something.SimpleUserService">
        <!-- a reference to the proxied userPreferences bean -->
        <property name="userPreferences" ref="userPreferences"/>
    </bean>
</beans>
参考源码
  • org.springframework.beans.factory.ObjectFactory
  • org.springframework.beans.factory.ObjectProvider
选择要创建的代理类型

默认情况下,当 Spring 容器为标记有 <aop:scoped-proxy/> 元素的 bean 创建代理时,会创建一个基于 CGLIB 的类代理。

CGLIB 代理只拦截公共方法调用!不要在这样的代理上调用非公共方法。它们不会委托给实际作用域的目标对象。

或者,您可以配置 Spring 容器,通过指定 <aop:scoped-proxy/> 元素的 proxy-target-class 属性值 false,为此类作用域 bean 创建标准的基于 JDK 接口的代理。使用基于 JDK 接口的代理意味着您不需要应用程序类路径中的其他库来影响此类代理。但是,这也意味着作用域 bean 的类必须至少实现一个接口,并且注入作用域 bean 的所有协作者都必须通过其接口之一引用 bean。以下示例显示了基于接口的代理:

<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
    <aop:scoped-proxy proxy-target-class="false"/>
</bean>

<bean id="userManager" class="com.stuff.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</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"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
 http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
">

    <bean id="singletonBean" class="study.hwj.spring.scope.SingletonBean">
        <property name="prototypeBean" ref="prototypeBean"></property>
    </bean>

    <bean id="prototypeBean" class="study.hwj.spring.scope.PrototypeBean" scope="thread">
        <aop:scoped-proxy/>
    </bean>
</beans>

如上配置,每个线程里 singletonBean 能够获取到一个新的 PrototypeBean 实例

自定义作用域

bean 作用域机制是可扩展的。您可以定义自己的范围,甚至可以重新定义现有的范围,尽管后者被认为是不好的实践,而且您不能覆盖内置的单例和原型范围。

要将自定义作用域集成到 Spring 容器中,需要实现 org.springframework.beans.factory.config.Scope 接口

Scope 方法名称 方法声明 描述
get public Object get(String name, ObjectFactory<?> objectFactory) 从作用域返回对象
remove public Object remove(String name) 将对象从作用域中移除
registerDestructionCallback public void registerDestructionCallback(String name, Runnable callback) 注册了一个回调,当作用域被销毁或作用域中指定的对象被销毁时,调用这个回调
getConversationId public String getConversationId() 获取作用域的会话标识符,此标识符对于每个作用域是不同的

使用自定义作用域:org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope

参考实现:org.springframework.context.support.SimpleThreadScope

Java 方式注册自定义作用域:

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);

<bean id="..." class="..." scope="thread">

XML 配置方式注册自定义作用域:

<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
    <property name="scopes">
        <map>
            <entry key="thread">
                <bean class="org.springframework.context.support.SimpleThreadScope"/>
            </entry>
        </map>
    </property>
</bean>

当您在 FactoryBean 实现的 <bean> 声明中时设置 <aop:scoped-proxy/> ,作用域作用的是工厂 bean 本身,而不是 getObject() 返回的对象。

自定义 Bean 的生命周期

Spring 框架提供了许多接口,您可以使用它们来自定义 bean 的生命周期:

  • 生命周期回调
  • ApplicationContextAwareBeanNameAware
  • 其他 Aware 接口

生命周期回调

在内部,Spring 框架使用 BeanPostProcessor 实现来处理它能够找到并调用适当方法的任何回调接口。如果您需要自定义特性或其他 Spring 默认不提供的生命周期行为,您可以自己实现 BeanPostProcessor

除了初始化和销毁回调之外,Spring 管理的对象还可以实现 Lifecycle 接口,以便这些对象能够参与启动和关闭过程,这是由容器自身的生命周期驱动的。

初始化、销毁回调
  • JSR-250 的 @PostConstruct@PreDestroy 注解(不与 Spring 耦合)

  • Spring 的 InitializingBeanDisposableBean 接口

    • afterPropertiesSet destroy 方法
  • Bean 定义时配置:

    • <bean> 标签的 init-methoddestroy-method 属性
    • @Bean 注解的 initMethoddestroyMethod

您可以为 <bean> 元素的 destroy-method 属性设置一个特殊值 (inferred),它指示 Spring 自动检测 bean 类上的 public 修饰的 closeshutdown 方法。(可以匹配任何实现 java.lang.AutoCloseablejava.io.Closeable 的类)您还可以在 <beans> 元素的 default-destroy-method 属性上设置此特殊值 (inferred) ,以将此行为应用于整个 bean 集。

默认初始化和销毁方法

在编写不使用 Spring 特定的 InitializingBeanDisposableBean 回调接口的初始化和销毁方法回调时,通常使用 initinitializedispose 等名称编写方法。理想情况下,这种生命周期回调方法的名称在项目中是标准化的,这样所有开发人员都使用相同的方法名称,并确保一致性。

顶级 <beans/> 元素的 default-init-methoddefault-destroy-method 属性配置默认的初始化和销毁方法

如果现有的 bean 类已经有按照约定命名的回调方法,那么可以通过使用 <bean/> 本身的 init-methoddestroy-method 属性指定方法名称来覆盖缺省值。

Spring 容器保证在提供所有依赖项的 bean 之后立即调用已配置的初始化回调。因此,在原始 bean 引用上调用初始化回调,这意味着还没有将 AOP 拦截器等应用到 bean 上。首先完全创建一个目标 bean,然后应用一个带有拦截器链的 AOP 代理。如果分别定义了目标 bean 和代理,您的代码甚至可以绕过代理与原始目标 bean 交互。因此,将拦截器应用到 init 方法是不一致的,因为这样做会将目标 bean 的生命周期耦合到它的代理或拦截器上,并在代码直接与原始目标 bean 进行交互时留下奇怪的语义。

结合生命周期机制

为同一个 bean 配置的多个生命周期机制,具有不同的初始化方法,调用顺序(注解,接口,自定义)如下:

  1. 带有 @PostConstruct 注解的方法
  2. InitializingBean 回调接口定义 afterPropertiesSet()
  3. 自定义配置方法 init()

销毁方法以相同的顺序调用:

  1. 带有 @PreDestroy 注解的方法
  2. DisposableBean 回调接口定义 destroy()
  3. 自定义配置方法 destroy()

如果为一个以上的生命周期机制配置了相同的 init 方法,那么该方法将运行一次

启动和关闭回调

org.springframework.context.Lifecycle 接口为任何具有自己生命周期要求的对象定义了基本方法

任何 Spring 管理的对象都可以实现 Lifecycle 接口。然后,当 ApplicationContext 接收到启动和停止信号时(例如,对于运行时的停止/重启场景),它将这些调用传递给在该上下文中定义的所有 Lifecycle 实现。它通过委托给 LifecycleProcessor 来做到这一点,LifecycleProcessor 本身是 Lifecycle 接口的扩展。它还添加了另外两种方法来对正在刷新和关闭的上下文做出反应。

注意,常规 org.springframework.context.Lifecycle 接口是用于显式启动和停止通知的简单约定,并不意味着在上下文刷新时自动启动。要对特定 bean 的自动启动(包括启动阶段)进行细粒度控制,请考虑实现 org.springframework.context.SmartLifecycle

另外,请注意,不能保证在销毁之前发出 stop 通知。在常规关闭时,所有 Lifecycle bean 在传播常规销毁回调之前首先收到 stop 通知。但是,在上下文生命周期内的热刷新或停止刷新尝试时,只会调用销毁方法。

启动和关闭调用的顺序可能很重要。如果任意两个对象之间存在“依赖”关系,依赖端在其依赖关系之后开始,在其依赖关系之前停止。但是,有时直接依赖关系是未知的。您可能只知道某种类型的对象应该先于另一种类型的对象开始。SmartLifecycle 接口定义了另一个方法,即在其超级接口 Phased 上定义的方法 getPhase()

启动时,phase 最低的对象首先启动。停止时,遵循相反的顺序SmartLifecyclephase 默认为 Integer.MAX_VALUE 。没有实现 SmartLifecycle 的任何 Lifecycle 对象的默认 phase0 也很重要。因此,任何负相位值都表明一个对象应该在这些标准组件之前开始(并在它们之后停止)。对于任何正的相位值,反之亦然。

SmartLifecycle 定义的 stop 方法接受回调。任何实现都必须在该实现的关闭过程完成后调用该回调的 run 方法。在必要的情况下,启用异步关机,因为 LifecycleProcessor 接口的默认实现 DefaultLifecycleProcessor 等待每个阶段中的对象组的超时值来调用该回调。每个阶段的默认超时时间为 30 秒。您可以通过在上下文中定义一个名为 lifecycleProcessor 的 bean 来覆盖默认的生命周期处理器实例。如果你只想修改超时,定义下面的代码就足够了:

<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
    <!-- timeout value in milliseconds -->
    <property name="timeoutPerShutdownPhase" value="10000"/>
</bean>

如前所述,LifecycleProcessor 接口定义了用于刷新和关闭上下文的回调方法。后者驱动关闭进程,就好像已经显式调用了 stop 一样,但是它发生在上下文关闭时。另一方面,“刷新”回调启用了 SmartLifecycle bean 的另一个特性。当上下文刷新(在所有对象实例化和初始化之后)时,将调用该回调。此时,默认的生命周期处理器检查每个 SmartLifecycle 对象的 isAutoStartup 方法返回的布尔值。如果为 true ,那么该对象将在该点启动,而不是等待对上下文的显式调用或其自己的 start 方法(与上下文刷新不同,对于标准上下文实现,上下文启动不会自动发生)。阶段值( phase )和任何“依赖”关系决定前面描述的启动顺序。

使用说明
  • 容器启动过程不会自动触发 Lifecycle#start ,会自动触发 SmartLifecycle#start
  • 调用 ConfigurableApplicationContext#start ,会触发 Lifecycle#start
参考源码
  • org.springframework.context.Lifecycle
  • org.springframework.context.SmartLifecycle
  • org.springframework.context.LifecycleProcessor
  • org.springframework.context.support.DefaultLifecycleProcessor
在非 Web 应用程序中优雅地关闭 Spring IoC 容器

本节仅适用于非 web 应用程序。Spring 的基于 web 的 ApplicationContext 实现已经准备好了代码,可以在相关 web 应用程序关闭时优雅地关闭 Spring IoC 容器。

如果在非 web 应用程序环境中使用 Spring 的 IoC 容器,请向 JVM 注册一个 shutdown hook 。这样做可以确保优雅地关闭并调用单例 bean 上的相关 destroy 方法,从而释放所有资源。

向 JVM 注册关闭挂钩:

org.springframework.context.ConfigurableApplicationContext#registerShutdownHook

AnnotationConfigApplicationContext ctx =
    new AnnotationConfigApplicationContext(Config1.class);
// ctx.registerShutdownHook();  // 加上这句后,会触发销毁回调,否则不触发
System.exit(0);

ApplicationContextAwareBeanNameAware

实现 ApplicationContextAware ,容器会提供 ApplicationContext 实例给 bean

自动装配是获取 ApplicationContext 引用的另一种选择。传统的 constructorbyType 自动装配模式可以分别为构造函数参数或 setter 方法参数提供 ApplicationContext 类型的依赖项。为了获得更大的灵活性,包括自动装配字段和多参数方法的能力,请使用基于注解的自动装配特性。如果这个字段、构造函数或者方法带有 @Autowired 注解,ApplicationContext 就会被自动装配到一个字段、构造函数参数或者方法参数中

实现 BeanNameAware 接口,容器会提供 bean 实例的名称给 bean

触发的时机在属性赋值后,初始化方法执行前

参考源码

  • org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean
    • org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#invokeAwareMethods
  • org.springframework.context.support.ApplicationContextAwareProcessor#invokeAwareInterfaces

ApplicationContextAware 是通过 ApplicationContextAwareProcessorBeanPostProcessor )后置处理注入的,BeanNameAware 不是

其他 Aware 接口

除了 ApplicationContextAwareBeanNameAware 之外,Spring 还提供了大量的 Aware 回调接口,允许 bean 向容器指示它们需要某种基础结构依赖项。作为一般规则,名称表示依赖类型。

名称 注入依赖项 链接
ApplicationContextAware 声明 ApplicationContext ApplicationContextAwareBeanNameAware
ApplicationEventPublisherAware 封闭 ApplicationContext 的事件发布者 ApplicationContext 的附加功能
BeanClassLoaderAware 用于加载 bean 类的类加载器 实例化 Bean
BeanFactoryAware 声明 BeanFactory ApplicationContextAwareBeanNameAware
BeanNameAware 声明 bean 的名称 ApplicationContextAwareBeanNameAware
LoadTimeWeaverAware 用于在加载时处理类定义的编织器 在 Spring 框架中使用 AspectJ 进行加载时编织
MessageSourceAware 用于解析消息的配置策略(支持参数化和国际化) ApplicationContext 的附加功能
NotificationPublisherAware Spring JMX 通知发布者 通知
ResourceLoaderAware 配置加载程序,用于低级别访问资源 资源
ServletConfigAware 当前运行容器的 ServletConfig。仅在基于 web 的 Spring ApplicationContext 中有效 Spring MVC
ServletContextAware 当前运行容器的 ServletContext。仅在基于 web 的Spring ApplicationContext 中有效 Spring MVC

Bean 定义继承

bean 定义可以包含很多配置信息,包括构造函数参数、属性值和特定于容器的信息,例如初始化方法、静态工厂方法名称等。子 bean 定义从父定义继承配置数据。子定义可以根据需要覆盖某些值或添加其他值。使用父和子 bean 定义可以节省大量输入。实际上,这是一种模板形式。

如果您以编程方式使用 ApplicationContext 接口,则子 bean 定义由 ChildBeanDefinition 类表示。

使用基于 xml 的配置元数据时,可以通过使用父属性指定父 bean 作为该属性的值来指定子 bean 定义:

<bean id="inheritedTestBean" abstract="true"
        class="org.springframework.beans.TestBean">
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>

<bean id="inheritsWithDifferentClass"
        class="org.springframework.beans.DerivedTestBean"
        parent="inheritedTestBean" init-method="initialize">  
    <property name="name" value="override"/>
    <!-- the age property value of 1 will be inherited from parent -->
</bean>

<bean> 定义为 abstract 时,它只能用作纯模板 bean 定义,用作子定义的父定义。

父 bean 不能自己实例化,因为它是不完整的,并且显式地标记为 abstract 。当定义是 abstract 的时候,它只能作为纯模板 bean 定义使用,纯模板 bean 定义作为子定义的父定义。尝试单独使用这样的 abstract 父 bean,通过将其引用为另一个 bean 的 ref 属性或使用父 bean ID 执行显式的 getBean 调用会返回一个错误。类似地,容器的内部 preInstantiateSingletons 方法忽略定义为 abstract 的 bean 定义。

容器扩展点

通常,应用程序开发人员不需要对 ApplicationContext 实现类进行子类化。相反,可以通过插入 特殊集成接口 的实现来扩展 Spring IoC 容器

使用 BeanPostProcessor 自定义 Bean

BeanPostProcessor 接口定义了回调方法,您可以实现这些方法来提供您自己的(或者覆盖容器的默认)实例化逻辑、依赖解析逻辑等等。如果希望在 Spring 容器完成实例化、配置和初始化 bean 之后实现一些自定义逻辑,可以插入一个或多个自定义 BeanPostProcessor 实现。

BeanPostProcessor 被调用的时机是在 bean 初始化方法前后

可以配置多个 BeanPostProcessor 实例,并且可以通过设置 order 属性来控制这些 BeanPostProcessor 实例的运行顺序。仅当 BeanPostProcessor 实现Ordered 接口时才能设置 order 属性。如果您自己编写BeanPostProcessor,也应该考虑实现 Ordered 接口。

BeanPostProcessor 实例对 bean 实例进行操作。也就是说,Spring IoC 容器实例化一个 bean 实例,然后 BeanPostProcessor 实例执行工作。

每个容器都有 BeanPostProcessor 实例的作用域。只有在使用容器层次结构时,这才是相关的。如果您在一个容器中定义一个 BeanPostProcessor,那么它只后置处理该容器中的 bean。换句话说,在一个容器中定义的 bean 不会由在另一个容器中定义的 BeanPostProcessor 进行后置处理,即使两个容器都属于相同的层次结构。

要更改实际的 bean 定义( BeanDefinition ),需要使用 BeanFactoryPostProcessor

org.springframework.beans.factory.config.BeanPostProcessor 接口恰好由两个回调方法( postProcessBeforeInitializationpostProcessAfterInitialization )组成。当这样的类注册为容器的后置处理器时,对于容器创建的每个 bean 实例,后置处理器在调用容器初始化方法(如 InitializingBean.afterPropertiesSet 或任何声明的 init 方法)之前和任何 bean 初始化回调之后都会从容器获得一个回调。后置处理器可以对 bean 实例采取任何操作,包括完全忽略回调。Bean 后置处理器通常检查回调接口,或者用代理包装 bean。为了提供代理包装逻辑,一些 Spring AOP 基础结构类被实现为 bean 后处理器。

ApplicationContext 在配置元数据中自动检测任何实现 BeanPostProcessor 接口的 bean 定义。ApplicationContext 将这些 bean 注册为后置处理器,以便稍后在创建 bean 时调用它们。Bean 后置处理器可以像其他 Bean 一样部署在容器中。

注意,在配置类上使用 @Bean 工厂方法声明 BeanPostProcessor 时,工厂方法的返回类型应该是实现类本身,或者至少是 org.springframework.beans.factory.config.BeanPostProcessor 接口,清楚地表明了 bean 的后置处理特性。否则,ApplicationContext 无法在完全创建它之前通过类型自动检测它。由于 BeanPostProcessor 需要在早期实例化,以便应用于上下文中其他 bean 的初始化,因此早期类型检测非常关键。

以编程方式注册 BeanPostProcessor 实例:org.springframework.beans.factory.config.ConfigurableBeanFactory#addBeanPostProcessor

注意,以 BeanPostProcessor 编程方式注册的实例总是在通过自动检测注册的实例之前处理,而不管任何显式排序( Ordered 接口 )。

实现 BeanPostProcessor 接口的类是特殊的,容器对它们的处理不同。所有 BeanPostProcessor 实例和被直接引用的 bean 都在启动时实例化,作为 ApplicationContext 特殊启动阶段的一部分。接下来,以排序的方式注册所有 BeanPostProcessor 实例,并将其应用于容器中的所有其他 bean。

因为 AOP 自动代理是通过 BeanPostProcessor 实现的,所以 BeanPostProcessor 实例和它们直接引用的Bean 都不符合自动代理的条件。对于任何这样的 Bean,您应该看到一条信息性的日志消息:Bean someBean is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying)

使用说明
  • BeanPostProcessor 和它依赖的 bean 会更早的实例化,并且无法使用 AOP 自动代理
  • BeanPostProcessor bean 需要比普通 bean 更早的实例化,在 registerBeanPostProcessors(beanFactory); 这一步中实例化,普通 bean 在 finishBeanFactoryInitialization(beanFactory); 方法里实例化
  • 容器启动时,默认添加两个 BeanPostProcessor
    • org.springframework.context.support.ApplicationContextAwareProcessor
      • 检测实现 xxxAware 接口的 bean 并注入 xxx
    • org.springframework.context.support.ApplicationListenerDetector
      • 检测 org.springframework.context.ApplicationListener 接口的 bean ,并将其中的单例 bean 加入 ApplicationContext
参考源码
  • org.springframework.beans.factory.config.BeanPostProcessor
  • org.springframework.beans.factory.config.BeanFactoryPostProcessor
  • org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
  • org.springframework.beans.factory.config.PropertyOverrideConfigurer

使用 BeanFactoryPostProcessor 自定义配置元数据

org.springframework.beans.factory.config.BeanFactoryPostProcessor 接口的语义类似于 BeanPostProcessor ,但有一个主要区别: BeanFactoryPostProcessor 对 bean 配置元数据进行操作。也就是说,Spring IoC 容器允许 BeanFactoryPostProcessor 在容器实例化除 BeanFactoryPostProcessor 实例以外的任何 bean 之前读取配置元数据并更改它。

BeanFactoryPostProcessor 可以在任何非 BeanFactoryPostProcessor 类型的 bean 实例化前更改配置元信息

可以配置多个 BeanFactoryPostProcessor 实例,并通过设置 order 属性来控制 BeanFactoryPostProcessor 实例的运行顺序。但是,只有在 BeanFactoryPostProcessor 实现 Ordered 接口时,才能设置此属性。

如果您想要更改实际的 bean 实例,那么您需要使用 BeanPostProcessor 。虽然在技术上可以在 BeanFactoryPostProcessor 中使用 bean 实例(例如,使用 BeanFactory.getBean ) ,但这样做会导致 bean 提前实例化,从而违反标准容器生命周期。这可能会导致负面的副作用,例如绕过 bean 的后期处理。

此外,BeanFactoryPostProcessor 实例的作用域为每个容器。只有在使用容器层次结构时,这才是相关的。如果在一个容器中定义 BeanFactoryPostProcessor,则它只应用于该容器中的 bean 定义。一个容器中的 Bean 定义不会由另一个容器中的 BeanFactoryPostProcessor 实例进行后处理,即使两个容器都属于同一层次结构。

当在 ApplicationContext 中声明 BeanFactory 后置处理器时会自动运行 ,以便将更改应用于定义容器的配置元数据。Spring 包含许多预定义的 BeanFactory 后置处理器,例如 PropertyOverrideConfigurerPropertySourcesPlaceholderConfigurer 。您还可以使用自定义 BeanFactoryPostProcessor ,例如,注册自定义属性编辑器。

BeanPostProcessors 一样,您通常不希望配置 BeanFactoryPostProcessors 为延迟加载。如果没有其他 Bean 引用 Bean(Factory) 后处理器,那么该后置处理器根本不会被实例化。因此,将其标记为惰性初始模式将被忽略,即使您在 <beans/> 元素的声明中将 default-lazy-init 属性设置为 true ,Bean(Factory) PostProcessor 也会被急切地实例化。

示例:类名替换 PropertySourcesPlaceholderConfigurer

可以使用 PropertySourcesPlaceholderConfigurer 以标准 Java Properties 格式将 bean 的属性值定义在一个单独的外部化文件中,例如数据库 URL 和密码。

<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
    <property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>

<bean id="dataSource" destroy-method="close"
        class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

在运行时,PropertySourcesPlaceholderConfigurer 应用于替换数据源的某些属性的元数据。要替换的值被指定为表单的占位符 ${property-name} ,它遵循 Ant 和 log4j 以及 JSP EL 样式,可以自定义占位符前缀和后缀

jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root

使用 Spring 2.5 中引入的 context 命名空间,您可以使用专用配置元素配置属性占位符。您可以在 location 属性中以逗号分隔列表的形式提供一个或多个位置

<context:property-placeholder location="classpath:com/something/jdbc.properties"/>

PropertySourcesPlaceholderConfigurer 不仅将查找指定的 Properties 文件。默认情况下,如果在指定的属性文件中找不到属性,它会检查 Spring Environment 属性和常规 Java System 属性

可以使用 PropertySourcesPlaceholderConfigurer 来替换类名,如果该类在运行时无法解析为有效类,则在创建 bean 时解析 bean 失败,这是在 ApplicationContext 非延迟初始化 bean 的 preInstantiateSingletons() 阶段。

<bean class="org.springframework.beans.factory.config.PropertySourcesPlaceholderConfigurer">
    <property name="locations">
        <value>classpath:com/something/strategy.properties</value>
    </property>
    <property name="properties">
        <value>custom.strategy.class=com.something.DefaultStrategy</value>
    </property>
</bean>

<bean id="serviceStrategy" class="${custom.strategy.class}"/>
使用说明
<?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 class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
        <property name="locations" value="classpath:postprocess/beanfactory/bean.properties"/>
        <property name="properties" >
            <value>id=11</value>
        </property>
    </bean>

    <bean id="propertiesBean" class="${cls.name}">
        <property name="id" value="${id}"/>
        <property name="name" value="${name}"/>
    </bean>
</beans>
#id=9
name=我是中国人
cls.name=study.hwj.spring.postprocess.beanfactory.PropertiesBean
package study.hwj.spring.postprocess.beanfactory;

import lombok.Data;

@Data
public class PropertiesBean {
    private Integer id;
    private String name;

}

PropertySourcesPlaceholderConfigurer#properties 既可以从文件中读取,也可以定义在 bean 属性上,会进行合并,文件中的属性优先级高于定义在 bean 上的属性

参考源码
  • org.springframework.beans.factory.config.BeanFactoryPostProcessor
  • org.springframework.context.support.PropertySourcesPlaceholderConfigurer
示例:PropertyOverrideConfigurer

另一个 BeanFactory 后处理器 PropertyOverrideConfigurer 类似于 PropertySourcesPlaceholderConfigurer ,但与后者不同,原始定义可以有 bean 属性的默认值或根本没有值。如果重写的 Properties 文件没有某个 bean 属性的条目,则使用默认上下文定义。

注意,bean 定义不知道正在被重写,因此从 XML 定义文件中不能立即看出正在使用重写配置。如果多个 PropertyOverrideConfigurer 实例为同一个 bean 属性定义不同的值,由于覆盖机制,最后一个实例获胜。

指定的重写值总是文字值。它们不会被翻译成 bean 引用。当 XML bean 定义中的原始值指定 bean 引用时,该约定也适用。

作用是属性值覆盖

beanName.property=value

context 命名空间,可以使用专用的配置元素配置重写属性

<context:property-override location="classpath:override.properties"/>
使用说明
<?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">

    <bean class="org.springframework.beans.factory.config.PropertyOverrideConfigurer">
        <property name="locations" value="classpath:postprocess/beanfactory/override.properties"/>
        <property name="properties" >
            <value>propertiesBean.id=111</value>
        </property>
    </bean>

    <bean id="propertiesBean" class="study.hwj.spring.postprocess.beanfactory.PropertiesBean">
        <property name="id" value="1"/>
        <property name="name" value="n1"/>
    </bean>
</beans>
propertiesBean.id=110

使用 FactoryBean 自定义实例化逻辑

可以为本身就是工厂的对象实现 org.springframework.beans.factory.FactoryBean 接口

FactoryBean 接口是 Spring IoC 容器实例化逻辑的可插入点。如果您有复杂的初始化代码,可以用 Java 更好地表达,而不是冗长的 XML,您可以创建自己的 FactoryBean ,在该类中编写复杂的初始化,然后将您的自定义 FactoryBean 插入到容器中

FactoryBean 接口提供了三个方法:

  • T getObject() :返回此工厂创建的对象的实例。实例可能会被共享,这取决于这个工厂是返回单例还是原型
  • boolean isSingleton() :如果 FactoryBean 返回单例则返回 true 其他情况返回 false。默认返回 true
  • Class getObjectType() :返回 getObject() 方法返回的对象类型,如果类型事先未知返回 null

FactoryBean 概念和接口在 Spring 框架中的很多地方都有使用。Spring 提供了 FactoryBean 接口的 50 多个实现

当你需要获取 FactoryBean 实例本身,而不是它创建的 bean,调用 ApplicationContextgetBean() 方法时,bean的 id 加上前缀 & 。因此,对于具有 idmyBean 的给定 FactoryBean ,调用 getBean("myBean") 时,容器返回 FactoryBean 工厂产生的对象,而调用 getBean("&myBean") 时,返回 FactoryBean 实例本身

参考源码
  • org.springframework.beans.factory.FactoryBean
  • org.springframework.beans.factory.SmartFactoryBean

基于注解的容器配置

注解配置是否优于 XML 配置?答案是 视情况而定

每种方法都有其优点和缺点,通常由开发人员决定哪种策略更适合他们。由于它们的定义方式,注解在其声明中提供了大量上下文,从而使配置更简洁。然而,XML 擅长将组件连接起来而不触及它们的源代码或重新编译它们。一些开发人员倾向于让装配接近源代码,而其他人则认为带注解的类不再是 POJO ,而且配置变得分散且难以控制。

无论选择什么,Spring 都可以容纳这两种风格,甚至可以将它们混合在一起。值得指出的是,通过 JavaConfig 选项,Spring 允许以非侵入性的方式使用注解,而不涉及目标组件的源代码

结合使用 BeanPostProcessor 和注解是扩展 Spring IoC 容器的常用方法,例如 AutowiredAnnotationBeanPostProcessor 。Spring 引入了 @Required 注解强制必需属性的可能性。支持 @Autowired ,JSR-250 注解 ( @PostConstruct@PreDestroy ), JSR-330 注解 ( @Inject@Named

注解注入在 XML 注入之前执行。因此,XML 配置会覆盖注解的配置。

可以将后置处理器注册为单独的 bean 定义,也可以通过在基于 XML 的 Spring 配置中包含以下标记来隐式注册它们(注意包含 context 命名空间):

<?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
        https://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/>

</beans>

<context:annotation-config/> 元素隐式注册以下后置处理器:

<context:annotation-config/> 只在定义 bean 的应用程序上下文中查找 bean 上的注解。这意味着,如果将 <context:annotation-config/> 放在对于 DispatcherServletWebApplicationContext 中,它只检查控制器中的 @Autowired bean 。

参考源码

  • org.springframework.context.annotation.AnnotationConfigUtils#registerAnnotationConfigProcessors

@Required

@Required 注解适用于 bean 属性 setter 方法

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

必须注册 RequiredAnnotationBeanPostProcessor bean 来启用支持 @Required 注解

从 Spring Framework 5.1 开始,@Required 注解和 RequiredAnnotationBeanPostProcessor 正式弃用,推荐使用构造函数注入进行所需的设置(或自定义实现 InitializingBean.afterPropertiesSet() 或自定义 @PostConstruct 方法以及 bean 属性设置方法)。

@Autowired

可以使用 JSR 330 的 @Inject 注解来代替 Spring 的 @Autowired 注解

可以将 @Autowired 注解应用于构造函数、setter 方法

从 Spring Framework 4.3 开始,如果目标 bean 只定义了一个构造函数,则不再需要对此类构造函数进行 @Autowired 注解。但是,如果有多个构造函数可用并且没有默认构造函数,则必须至少对其中一个构造函数进行 @Autowired 注解,以指示容器使用哪一个。

可以将 @Autowired 注解应用于具有任意名称和多个参数的方法

也可以应用于字段,甚至可以将其与构造函数混合使用

确保您的目标组件一致地由 @Autowired 注解声明。否则,注入可能会失败,原因是在运行时出现“未找到类型匹配”错误。

对于通过类路径扫描找到的 XML 定义的 bean 或组件类,容器通常预先知道具体类型。但是,对于 @Bean 工厂方法,您需要确保声明的返回类型足够具体。对于实现多个接口的组件或由其实现类型可能引用的组件,请考虑在您的工厂方法中声明最具体的返回类型(至少与引用您的 bean 的注入点要求的一样具体)。

@Autowired 注解添加到需要该类型数组或集合的字段或方法来指示 Spring 提供特定类型的所有 bean ,如果您希望数组或列表中的项目按特定顺序排序,您的目标 bean 可以实现 org.springframework.core.Ordered 接口或使用 @Order 或 标准 @Priority 注解。否则,它们的顺序遵循容器中相应目标 bean 定义的注册顺序。

@Autowired
private MovieCatalog[] movieCatalogs;

// -----------------------------------------

private Set<MovieCatalog> movieCatalogs;

@Autowired
public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
    this.movieCatalogs = movieCatalogs;
}

// -----------------------------------------

private Map<String, MovieCatalog> movieCatalogs;

@Autowired
public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
    this.movieCatalogs = movieCatalogs;
}

@Order 值可能会影响注入点的优先级,但请注意,它们不会影响单例启动顺序,这是由依赖关系和 @DependsOn 声明决定的。

标准 javax.annotation.Priority 注解在 @Bean 级别不可用 ,因为它不能在方法上声明。它的语义可以通过 @Order 值结合 @Primary 每个类型的单个 bean 进行建模。

如果自动注入的类型是 Map ,只要 key 是 String 类型,也可以注入进去。key 对应的是 bean 的名称。

默认情况下,当给定注入点没有匹配的候选 bean 时,自动装配失败。对于声明的数组、集合或映射,至少需要一个匹配元素。

默认行为是将带注解的方法和字段视为指示所需的依赖项。通过将 @Autowiredrequired 属性设置为 false ,可以跳过不能满足的注入点。

@Autowiredrequired 可以替代过期的 @Required

可以通过 Java 8 的 java.util.Optional 来表达特定依赖项的非必需性质

从 Spring Framework 5.0 开始,您还可以使用 JSR-305 的 @Nullable@NonNull 注解

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

还可以使用 @Autowired 对于那些众所周知的解析依赖接口:BeanFactoryApplicationContextEnvironmentResourceLoaderApplicationEventPublisher,和MessageSource。这些接口及其扩展接口(例如 ConfigurableApplicationContextResourcePatternResolver )会自动解析,无需特殊设置。

@Autowired@Inject@Value@Resource 注解由 Spring 的 BeanPostProcessor 实现处理。这意味着您不能在自己的 BeanPostProcessorBeanFactoryPostProcessor 类型中应用这些注解。这些类型必须使用 XML 或 Spring @Bean 方法显式注入

使用 @Primary 微调基于注解的自动装配

由于按类型自动装配可能会导致多个候选对象,因此通常需要对选择过程进行更多控制。实现此目的的一种方法是使用 Spring 的 @Primary 注解。@Primary 表示当多个 bean 是自动装配到单值依赖项的候选者时,应优先考虑特定 bean。如果候选中恰好存在一个 primary bean,则它成为自动装配的值。

@Configuration
public class MovieConfiguration {

    @Bean
    @Primary
    public MovieCatalog firstMovieCatalog() { ... }

    @Bean
    public MovieCatalog secondMovieCatalog() { ... }

    // ...
}

@Primary 对应 <bean>primary="true"

使用限定符微调基于注解的自动装配

当可以确定一个主要候选对象时,@Primary 是在多个实例中按类型使用自动装配的有效方法。当您需要对选择过程进行更多控制时,可以使用 Spring 的 @Qualifier 注解。您可以将限定符值与特定参数相关联,缩小类型匹配的范围,以便为每个参数选择一个特定的 bean。

public class MovieRecommender {

    @Autowired
    @Qualifier("main")
    private MovieCatalog movieCatalog;

    // ...
}

@Qualifier("main") 对应 <qualifier value="main"/>

如果没有指定 @Qualifiervalue ,默认为 bean 名称。限定符值在类型匹配集合中始终具有收缩语义,@Qualifier 在语义表达上不是指 bean 的唯一 id。推荐的限定值是 mainpersistent 等,表达的是独立于 id 的特定部件的特性。

限定符也适用于类型化集合,例如,Set<MovieCatalog> 。在这种情况下,根据声明的限定符,所有匹配的 bean 作为集合注入。这意味着限定符不必是唯一的。相反,它们构成过滤标准。例如,您可以定义多个具有相同限定符值 actionMovieCatalog bean,所有这些 @Qualifier("action") bean 都被注入到 Set<MovieCatalog>

让限定符值在类型匹配候选中针对目标 bean 名称进行选择,不需要在注入点进行 @Qualifier 注解。如果没有其他解析指示符(例如 @Qualifier@Primary ),对于非唯一依赖的情况,Spring 将注入点名称(即字段名称或参数名称)与目标 bean 名称进行匹配并选择相同名称的 bean。

如果您打算按名称表达注解驱动的注入,请不要使用 @Autowired ,即使它能够在类型匹配候选者中按 bean 名称进行选择。相反,应该使用 JSR-250 @Resource 注解,该注解在语义上定义为通过其唯一名称标识特定目标组件,声明的类型与匹配过程无关。

@Autowired 具有相当不同的语义:按类型选择候选 bean 后,指定的 String 限定符值仅在那些类型选择的候选中考虑(例如,将 account 限定符与标记有相同限定符标签的 bean 进行匹配)。

对于被定义为集合、 Map 或数组类型的 bean ,@Resource 是一个很好的解决方案,通过唯一名称引用特定的集合或数组 bean。也就是说,在4.3集合中,你可以通过 Spring 的 @Autowired 类型匹配算法匹配 Map 和数组类型,只要元素类型信息保留在 @Bean 返回类型签名或集合继承层次结构中。在这种情况下,可以使用限定符值在相同类型的集合中进行选择。

@Autowired 还考虑注入的自引用。注意,自我注入是一种回退。对其他组件的常规依赖关系始终具有优先权。从这个意义上说,自我推荐并不参与正常的候选选择,因此自引用从来不是 primary 的。相反,它们总是以最低优先级结束。实际上,您应该将自引用作为最后的手段(例如,通过 bean 的事务代理调用同一实例上的其他方法)。在这种情况下,可以考虑将受影响的方法分解为一个单独的委托 bean。或者,您可以使用 @Resource ,它可以通过当前 bean 的唯一名称获得返回当前 bean 的代理。

试图将来自@Bean 方法的结果注入到同一个配置类中也是一种有效的自引用场景。要么在方法签名中实际需要的地方惰性地解析这些引用(相对于配置类中的 autowired 字段) ,要么将受影响的@Bean 方法声明为静态,将它们与包含的配置类实例及其生命周期解耦。否则,这样的 bean 只能在回退阶段考虑,在其他配置类上选择 bean 作为主要候选类。

@Autowired 适用于字段、构造函数和多参数方法,允许在参数级通过 qualifier 注解进行收缩。相比之下,@Resource 仅支持字段和 bean 属性 setter 方法,并且只有一个参数。因此,如果注入目标是构造函数或多参数方法,则应该坚持使用限定符。

可以创建自己的自定义限定符注解:

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {

    String value();
}
public class MovieRecommender {

    @Autowired
    @Genre("Action")
    private MovieCatalog actionCatalog;

    private MovieCatalog comedyCatalog;

    @Autowired
    public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) {
        this.comedyCatalog = comedyCatalog;
    }

    // ...
}

可以添加 <qualifier/> 标签作为 <bean/> 标签的子元素,然后指定 typevalue 以匹配您的自定义限定符注解。该类型与注解的完全限定类名匹配。或者,如果不存在名称冲突的风险,为了方便起见,您可以使用短类名称。以下示例演示了这两种方法:

<?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
        https://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 class="example.SimpleMovieCatalog">
        <qualifier type="Genre" value="Action"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="example.Genre" value="Comedy"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>

在某些情况下,使用没有值的注解可能就足够了:

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {

}
public class MovieRecommender {

    @Autowired
    @Offline 
    private MovieCatalog offlineCatalog;

    // ...
}
<bean class="example.SimpleMovieCatalog">
    <qualifier type="Offline"/> 
    <!-- inject any dependencies required by this bean -->
</bean>

除了简单 value 属性之外,您还可以定义接受命名属性的自定义限定符注解。如果随后在要自动装配的字段或参数上指定了多个属性值,则 bean 定义必须匹配所有此类属性值才能被视为自动装配候选者。

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {

    String genre();

    Format format();
}
public enum Format {
    VHS, DVD, BLURAY
}
public class MovieRecommender {

    @Autowired
    @MovieQualifier(format=Format.VHS, genre="Action")
    private MovieCatalog actionVhsCatalog;

    @Autowired
    @MovieQualifier(format=Format.VHS, genre="Comedy")
    private MovieCatalog comedyVhsCatalog;

    @Autowired
    @MovieQualifier(format=Format.DVD, genre="Action")
    private MovieCatalog actionDvdCatalog;

    @Autowired
    @MovieQualifier(format=Format.BLURAY, genre="Comedy")
    private MovieCatalog comedyBluRayCatalog;

    // ...
}
<?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
        https://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 class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS"/>
            <attribute key="genre" value="Action"/>
        </qualifier>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS"/>
            <attribute key="genre" value="Comedy"/>
        </qualifier>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="DVD"/>
        <meta key="genre" value="Action"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="BLURAY"/>
        <meta key="genre" value="Comedy"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

</beans>

使用泛型作为自动装配限定符

除了 @Qualifier 注解之外,您还可以使用 Java 泛型类型作为限定的隐式形式

@Configuration
public class MyConfiguration {

    @Bean
    public StringStore stringStore() {
        return new StringStore();
    }

    @Bean
    public IntegerStore integerStore() {
        return new IntegerStore();
    }
}
@Autowired
private Store<String> s1; // <String> qualifier, injects the stringStore bean

@Autowired
private Store<Integer> s2; // <Integer> qualifier, injects the integerStore bean

通用限定符也适用于自动装配列表、Map 实例和数组。

// Inject all Store beans as long as they have an <Integer> generic
// Store<String> beans will not appear in this list
@Autowired
private List<Store<Integer>> s;

使用 CustomAutowireConfigurer

CustomAutowireConfigurer 是一个 BeanFactoryPostProcessor ,允许您注册自己的自定义限定符注解类型,即使它们没有使用 Spring 的 @Qualifier 注解。以下示例显示了如何使用 CustomAutowireConfigurer

<bean id="customAutowireConfigurer"
        class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
    <property name="customQualifierTypes">
        <set>
            <value>example.CustomQualifier</value>
        </set>
    </property>
</bean>

AutowireCandidateResolver 通过以下方式确定自动装配候选者:

  • 每个 bean 定义的 autowire-candidate
  • <beans/> 元素上 default-autowire-candidates
  • @Qualifier 注解的和任何 CustomAutowireConfigurer 注册的自定义注解

当多个 bean 有资格作为自动装配候选时,primary 的确定如下:如果候选中的一个 bean 定义的 primary 属性设置为 true ,则选择它。

参考源码
  • org.springframework.beans.factory.annotation.CustomAutowireConfigurer
  • org.springframework.beans.factory.annotation.CustomAutowireConfigurer#postProcessBeanFactory

@Resource 注入

JSR-250 注解,支持字段注入或 setter 方法注入

@Resource 采用 name 属性。默认情况下,Spring 将该值解释为要注入的 bean 名称。

如果未明确指定名称,则默认名称源自字段名称或 setter 方法。如果是字段,则采用字段名称。在 setter 方法的情况下,它采用 bean 属性名称。

CommonAnnotationBeanPostProcessor@Resource 提供支持。如果您显式配置 Spring 的 SimpleJndiBeanFactory,则可以通过 JNDI 解析这些名称。但是,我们建议您依赖默认行为,并使用 Spring 的 JNDI 查找功能来保持间接级别。

@Resource 没有指定显式名称的情况下,与 @Autowired 类似,@Resource 会找到一个 primary 类型匹配,而不是一个特定的命名 bean,并解析依赖项:BeanFactoryApplicationContextResourceLoaderApplicationEventPublisherMessageSource 接口。

在下面的示例中,customerPreferenceDao 字段注入时会首先查找名为 customerPreferenceDao 的 bean,然后返回到 customerPreferenceDao 类型的 primary 类型匹配

public class MovieRecommender {

    @Resource
    private CustomerPreferenceDao customerPreferenceDao;

    @Resource
    private ApplicationContext context; 

    public MovieRecommender() {
    }

    // ...
}

使用 @Value

@Value 通常用于注入外部属性

@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("${catalog.name}") String catalog) {
        this.catalog = catalog;
    }
}

使用以下配置:

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

以及 application.properties 文件:

catalog.name=MovieCatalog

可以将值 MovieCatalog 注入 catalog 字段

Spring 提供了一个默认的宽松嵌入值解析器。它将尝试解析属性值,如果无法解析,则属性名称(例如 ${catalog.name} )将作为值注入。如果你想对不存在的值保持严格的控制,应该声明一个 PropertySourcesPlaceholderConfigurer bean。

@Configuration
public class AppConfig {

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

当使用 JavaConfig 配置 PropertySourcesPlaceholderConfigurer 时, @Bean 方法必须是 static

如果无法解析 ${} 占位符,则使用上述配置可确保 Spring 初始化失败。也可以使用方法setPlaceholderPrefixsetPlaceholderSuffixsetValueSeparator 自定义占位符。

Spring Boot 默认情况下,配置了 PropertySourcesPlaceholderConfigurer ,从 application.propertiesapplication.yml 文件中获取属性

Spring 提供的内置转换器支持允许自动处理简单的类型转换(例如,将字符串转为 Integerint )。多个逗号分隔的值可以自动转换为 String 数组。

@Value("${catalog.name:defaultCatalog}") 提供默认值 defaultCatalog

Spring BeanPostProcessor 底层使用 ConversionService 来处理将 String 值转换为 @Value 目标类型的过程。如果您想为您自己的自定义类型提供转换支持,您可以提供您自己的 ConversionServicebean 实例:

@Configuration
public class AppConfig {

    @Bean
    public ConversionService conversionService() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
        conversionService.addConverter(new MyCustomConverter());
        return conversionService;
    }
}

@Value 包含 SpEL表达式时 ,该值将在运行时动态计算,例如 @Value("#{systemProperties['user.catalog'] + 'Catalog' }")@Value("#{{'Thriller': 100, 'Comedy': 300}}") Map<String, Integer> countOfMoviesPerCatalog

参考源码
  • org.springframework.context.support.PropertySourcesPlaceholderConfigurer
  • org.springframework.core.convert.ConversionService
  • org.springframework.core.convert.support.DefaultConversionService
  • org.springframework.format.support.DefaultFormattingConversionService

使用 @PostConstruct@PreDestroy

CommonAnnotationBeanPostProcessor 不仅支持 @Resource 注解,也支持 JSR-250 的生命周期注解: javax.annotation.PostConstructjavax.annotation.PreDestroy

如果 CommonAnnotationBeanPostProcessor 在 Spring ApplicationContext 中注册,那么将调用带有这些注解的方法,作为对应的 Spring 生命周期接口方法或显式声明的回调方法。

@Resource@PostConstruct@PreDestroy 注解是标准 Java 库的一部分。然而,整个 javax.annotation 包在 JDK 9 里从 Java 核心模块分离,并最终在 JDK 11 里被删除。如果需要的话,可以通过 Maven Central 获得 javax.annotation-api 组件。

类路径扫描和托管组件

从 Spring 3.0开始,Spring JavaConfig 项目提供的许多特性都是核心 Spring 框架的一部分。这允许您使用 Java 而不是传统的 XML 文件来定义 bean。例如 @Configuration@Bean@Import ,和 @DependsOn 注解

@Component 和更多的模板注解

@Repository 注解是针对满足的存储库角色(也被称为数据访问对象或 DAO )的任何类的标记。此标记的用途之一是异常的自动转换

Spring 提供模板注解:@Component@Service@Controller@Component 是任何 Spring 管理的组件的通用模板。 @Repository@Service@Controller@Component 针对更具体用例(分别在持久层、服务层和表示层)的特化。因此,通过 @Repository@Service , @Controller 注解,你的类能更好地适合于通过工具处理,或与切面进行关联。

@Repository , @Service , 和 @Controller 还可以在 Spring Framework 的未来版本中携带额外的语义。@Repository 已经支持作为持久层中自动异常转换的标记。

使用元注解和组合注解

Spring 提供的许多注解都可以在您自己的代码中用作 元注解 。元注解是可以应用于另一个注解的注解。例如 @Component@Service 等。

还可以组合元注解来创建 组合注解 ,例如 Spring MVC 的 @RestController 注解是由 @Controller@ResponseBody 组成的

组合注解可以选择性地从元注解重新声明属性以允许自定义。当您只想公开元注解属性的子集时,这尤其有用。例如,Spring 的 @SessionScope 注解将 scope 名称硬编码为 session,但仍允许自定义 proxyMode

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {

    /**
     * Alias for {@link Scope#proxyMode}.
     * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
     */
    @AliasFor(annotation = Scope.class)
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

自动检测类并注册 Bean 定义( @ComponentScan

Spring 可以自动检测模板注解类(例如 @Service@Repository )并使用 ApplicationContext 注册 BeanDefinition

要自动检测这些类并注册相应的bean,您需要添加 @ComponentScan@Configuration 类中,其中 basePackages 属性指定要扫描的包。(可以指定包含 逗号或分号或空格 分隔的列表)

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    // ...
}

@ComponentScan(basePackages = "org.example") 等同于 @ComponentScan("org.example")

使用 XML 配置:

<context:component-scan base-package="org.example"/>

使用 <context:component-scan> 隐式启用 <context:annotation-config> 。使用 <context:annotation-config> 时通常不需要包含 <context:component-scan> 元素 。

当您使用 component-scan 标签时,AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 都隐式包含在内。

可以通过指定 <context:component-scan>annotation-config 属性值为 false ,可以禁用注册AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor

使用过滤器自定义扫描( includeFiltersexcludeFilters

默认情况下,用 @Component@Repository@Service@Controller@Configuration 注解的类或本身被 @Component 注解的自定义注解是唯一被检测到的候选组件。但是,您可以通过应用自定义过滤器来修改和扩展此行为。添加它们作为 @ComponentScan 注解的 includeFiltersexcludeFilters 属性(或作为 XML 配置中 <context:component-scan> 标签的 <context:include-filter /><context:exclude-filter /> 子标签)。每个过滤器元素都需要 typeexpression 属性。

下表描述了过滤选项( org.springframework.context.annotation.FilterType ):

过滤器类型 示例表达式 描述
annotation默认 org.example.SomeAnnotation 在目标组件中的类定义上存在模板注解或自定义注解上有模板元注解
assignable org.example.SomeClass 指定(扩展或实现)的类(或接口)的目标组件
aspectj org.example..*Service+ 与目标组件匹配的 AspectJ 类型表达式
regex org\.example\.Default.* 与目标组件的类名匹配的正则表达式
custom org.example.MyTypeFilter org.springframework.core.type.TypeFilter 接口的自定义实现

以下示例表示忽略所有 @Repository 注解而使用 Stub 存储库的配置:

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

等效的 XML 配置:

<beans>
    <context:component-scan base-package="org.example">
        <context:include-filter type="regex" expression=".*Stub.*Repository"/>
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>
</beans>

还可以通过在注解上设置 useDefaultFilters=false 或设置 <component-scan/> 标签的 use-default-filters="false" 属性来禁用默认过滤器。这样有效地禁止自动检测 @Component@Repository@Service@Controller@RestController@Configuration 注解或元注解的类。

在组件中定义 Bean 元数据

Spring 组件还可以将 bean 定义元数据提供给容器。您可以使用与在 @Configuration 注解类中定义 bean 元数据相同的 @Bean 注解来实现这一点。

@Component
public class FactoryMethodComponent {

    @Bean
    @Qualifier("public")
    public TestBean publicInstance() {
        return new TestBean("publicInstance");
    }

    public void doWork() {
        // Component method implementation omitted
    }
}

注意:bean 定义工厂方法 publicInstance()@Bean 通过注解标识工厂方法和其他 bean 定义属性,例如限定符值 @Qualifier 。可以指定其他方法级别的注解是 @Scope@Lazy 和自定义限定符注解。

除了用于组件初始化的作用外,您还可以将 @Lazy 注解放置在标有 @Autowired@Inject 的注入点上。在这种情况下,它导致注入一个延迟解析代理。

从 Spring Framework 4.3 开始,您还可以声明类型 InjectionPoint(或其更具体的子类:DependencyDescriptor )的工厂方法参数来访问触发当前 bean 创建的请求注入点。请注意,这仅适用于 bean 实例的实际创建,不适用于现有实例的注入。因此,此功能对于原型作用域的 bean 最有意义。对于其他作用域,工厂方法只会看到在给定作用域内触发创建新 bean 实例的注入点(例如,触发创建惰性单例 bean 的依赖项)。在这种情况下,可以使用提供的注入点元数据进行语义关注。

@Component
public class FactoryMethodComponent {

    @Bean @Scope("prototype")
    public TestBean prototypeInstance(InjectionPoint injectionPoint) {
        return new TestBean("prototypeInstance for " + injectionPoint.getMember());
    }
}

常规 Spring 组件中的 @Bean 方法的处理方式与 Spring @Configuration 类中的对应方法不同。不同之处在于,@Component 类没有使用 CGLIB 来增强以拦截方法和字段的调用。CGLIB 代理是调用 @Configuration 类中 @Bean 方法中的方法或字段来创建对协作对象的 Bean 元数据引用的方法。这些方法不是用普通的 Java 语义调用的,而是通过容器来提供通常的生命周期管理和 Spring bean 的代理,甚至在通过编程调用 @Bean 方法引用其他 bean 时也是如此。相比之下,在普通的 @Component 类中调用 @Bean 方法中的方法或字段具有标准的 Java 语义,不应用特殊的 CGLIB 处理或其他约束。

@Configuration
//     @Component
public class Config2 {
    @Bean
    public MyBean1 myBean1() {
        MyBean1 myBean1 = new MyBean1();
        myBean1.setMyBean2(myBean2());
        return myBean1;
    }

    @Bean
    public MyBean2 myBean2() {
        MyBean2 myBean2 = new MyBean2();
        return myBean2;
    }
}
AnnotationConfigApplicationContext ctx =
    new AnnotationConfigApplicationContext(Config2.class);
MyBean1 myBean1 = ctx.getBean(MyBean1.class);
MyBean2 myBean2 = ctx.getBean(MyBean2.class);
System.out.println("if same :: " + (myBean1.getMyBean2() == myBean2));	// 如果Config2上的注解是@Configuration, 返回 true ;如果是 @Component ,返回 false ;两个注解都存在,true ;都不存在,false

可以将 @Bean 方法声明为 static ,从而允许在不将其包含的配置类创建为实例的情况下调用它们。这在定义后置处理器 bean(例如,BeanFactoryPostProcessorBeanPostProcessor )时特别有意义,因为此类 bean 在容器生命周期的早期被初始化,并且应避免在此时触发配置的其他部分。

由于技术限制,对静态 @Bean 方法的调用永远不会被容器拦截,即使在 @Configuration 类中也不会 :CGLIB 子类化只能重写非静态方法。因此,对另一个 @Bean 方法的直接调用具有标准的 Java 语义,导致直接从工厂方法本身返回一个独立的实例,而不是返回容器内的单例对象。

@Bean 方法的 Java 语言可见性不会对 Spring 容器中生成的 bean 定义产生直接影响。您可以自由地在非 @Configuration 类中以及任何地方的静态方法中自由地声明您认为合适的工厂方法。然而, @Configuration 类中的常规 @Bean 方法需要是可覆盖的——也就是说,它们不能被声明为 privatefinal

@Bean 方法也可以在给定组件或配置类的基类上发现,以及在组件或配置类实现的接口中声明的 Java 8 默认方法( default )上。这为组合复杂的配置安排提供了很大的灵活性,甚至可以通过 Java 8 默认方法(从 Spring 4.2 开始)进行多重继承。

最后,单个类可能包含同一个 bean 的多个 @Bean 方法。这与在其他配置场景中选择“最贪婪”的构造函数或工厂方法的算法相同:在构造时选择具有最多可满足依赖项的变体,类似于容器如何在多个 @Autowired 构造函数之间进行选择。

命名自动检测的组件( nameGenerator

当一个组件作为扫描过程的一部分被自动检测时,它的 bean 名称由该扫描器已知的 BeanNameGenerator 策略生成。默认情况下,任何包含 value 的 Spring 构造型注解( @Component@Repository@Service@Controller )通过 value 将该名称提供给相应的 bean 定义。

如果不包含 value 值,则默认的 bean 名称生成器返回未大写的非限定类名。

@Service("myMovieLister")	// bean 名称是 myMovieLister
public class SimpleMovieLister {
    // ...
}


@Repository	// bean 名称是 movieFinderImpl
public class MovieFinderImpl implements MovieFinder {
    // ...
}

如果不想依赖默认的 bean 命名策略,可以提供自定义 bean 命名策略。首先,实现 BeanNameGenerator 接口,并确保包含一个默认的无参数构造函数。

如果由于多个自动检测到的组件具有相同的非限定类名(即具有相同名称但驻留在不同包中的类)而遇到命名冲突,您可能需要配置一个 BeanNameGenerator 默认为生成的完全限定类名。从 Spring Framework 5.2.3 开始, 位于包 org.springframework.context.annotation 中的 FullyQualifiedAnnotationBeanNameGenerator 可用于此类目的。

在配置自动扫描时指定名称生成器:

@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
    // ...
}
<beans>
    <context:component-scan base-package="org.example" name-generator="org.example.MyNameGenerator" />
</beans>

作为一般规则,每当其他组件可能对注解进行显式引用时,请考虑使用注解指定名称。另一方面,只要容器负责连接,自动生成的名称就足够了。

参考源码
  • org.springframework.beans.factory.support.BeanNameGenerator

为自动检测组件提供作用域( scopeResolver

@Scope 注解指定的不同作用域。可以在注释中提供作用域的名称,例如 @Scope("prototype")@Scope(org.springframework.beans.factory.config.ConfigurableBeanFactory#SCOPE_PROTOTYPE)

@Scope 注解仅在具体 bean 类(用于带注解的组件)或工厂方法(用于 @Bean 方法)上进行自省。与 XML bean 定义相反,没有 bean 定义继承的概念,类级别的继承层次结构与元数据目的无关。

也可以使用 Spring 的元注解方法来构建自己的作用域注解:例如,使用 @Scope("prototype") 进行自定义注解的元注解,还可能声明一个自定义作用域代理模式。

要为作用域解析提供自定义策略而不是依赖基于注解的方法,您可以实现 ScopeMetadataResolver 接口。确保包含默认的无参数构造函数。然后您可以在配置扫描器时提供完全限定的类名:

@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
<context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>

当使用某些非单例作用域时,可能需要为作用域对象生成代理。为此,可以在 component-scan 元素上使用 scoped-proxy 属性。值域是:nointerfacestargetClass

@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
<context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
参考源码
  • org.springframework.context.annotation.ScopeMetadataResolver

提供带有注解的限定符元数据( @Qualifier

当依靠类路径扫描来自动检测组件时,可以在候选类上为 qualifier 元数据提供类型级别的注释。

@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}


@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}


@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
    // ...
}

与大多数基于注解的备选方案一样,请记住,注解元数据绑定到类定义本身,而 XML 的使用允许同一类型的多个 bean 提供其限定符元数据的变体,因为元数据是按实例而不是按类提供的。

生成候选组件的索引

虽然类路径扫描非常快,但可以通过在 编译时 创建一个静态候选列表来提高大型应用程序的启动性能。在此模式下,所有作为组件扫描目标的模块都必须使用此机制。

现有的 @ComponentScan<context:component-scan/> 指令必须保持不变才能请求上下文扫描某些包中的候选。当 ApplicationContext 检测到这样的索引时,它会自动使用它而不是扫描类路径。

要生成索引,请向包含作为组件扫描指令目标的组件的每个模块添加额外的依赖项。

对于 Maven:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.3.8</version>
        <optional>true</optional>
    </dependency>
</dependencies>

对于 Gradle :

dependencies {
    annotationProcessor "org.springframework:spring-context-indexer:{spring-version}"
}

spring-context-indexer 工件生成一个包含在 jar 文件中的 META-INF/spring.components 文件

在 IDE 中使用此模式时,必须将 spring-context-indexer 注册为注解处理器,以确保在更新候选组件时索引是最新的。

当在类路径上找到 META-INF/spring.components 文件时,索引会自动启用。如果某个索引对某些库部分可用,但无法为整个应用程序构建,则可以通过设置 spring.index.ignoretrue ,将其设置为 JVM 系统属性或通过 SpringProperties 机制,回退到常规类路径排列(就好像根本不存在索引)。

使用 JSR 330 标准注解

JSR-330 标准注解(依赖注入)

需要增加依赖:

<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>

使用 @Inject@Named 进行依赖注入

@Inject 类比 @Autowired

@Autowired 一样,您可以在字段级别、方法级别和构造函数-参数级别使用 @Inject

可以将注入点声明为 javax.inject.Provider ,从而允许按需访问范围较短的 bean 或通过 Provider.get() 调用延迟访问其他 bean 。

import javax.inject.Inject;
import javax.inject.Provider;

public class SimpleMovieLister {

    private Provider<MovieFinder> movieFinder;

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

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

想对应该注入的依赖项使用限定名称,则应使用 @Named 注解

import javax.inject.Inject;
import javax.inject.Named;

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

@Autowired 一样,@Inject 也可以与 java.util.Optional@Nullable 一起使用。这在这里更适用,因为 @Inject 没有 required 属性

public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(Optional<MovieFinder> movieFinder) {
        // ...
    }
}
public class SimpleMovieLister {

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

@Named@ManagedBean :与 @Component 注解相同

可以使用 @javax.inject.Namedjavax.annotation.ManagedBean代替 @Component

import javax.inject.Inject;
import javax.inject.Named;

@Named("movieListener")  // @ManagedBean("movieListener") could be used as well
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

使用 @Component 时不为组件指定名称是很常见的。@Named 可以以类似的方式使用

使用 @Named@ManagedBean 时,您可以以与使用 Spring 注解时完全相同的方式使用组件扫描

@Component 不同,JSR-330 的 @Named 和 JSR-250 的 ManagedBean 注解不可组合。您应该使用 Spring 的模板来构建自定义组件注解。

JSR-330 标准注解的限制

Spring 注解与 JSR-330 标准注解的比较

Spring javax.inject.* javax.inject 限制/描述
@Autowired @Inject @Inject 没有 required 属性。但是可以与 Optional 一起使用
@Component @Named / @ManagedBean JSR-330 不提供可组合模型,仅提供一种识别命名组件的方法
@Scope("singleton") @Singleton JSR-330 默认作用域类似于 Spring 的 prototype 。但是,为了与 Spring 的一般默认值保持一致,Spring 容器中声明的 JSR-330 bean 默认是 singleton为了使用除 singleton 之外的作用域,应该使用 Spring 的 @Scope 注解。javax.inject 还提供了一个 @Scope 注解。然而,这仅用于创建自定义注解
@Qualifier @Qualifier / @Named javax.inject.Qualifier 只是用于构建自定义限定符的元注解。具体的 String 限定符(如带有值的 Spring @Qualifier)可以通过使用 javax.inject.Named
@Value - 没有等价物
@Required - 没有等价物
@Lazy - 没有等价物
ObjectFactory Provider javax.inject.Provider 是 Spring 的 ObjectFactory 的直接替代品,get() 方法名称更短。它也可以与 Spring 的 @Autowired 或没有注解的构造函数和 setter 方法结合使用

基于 Java 的容器配置

介绍如何在 Java 代码中使用注解来配置 Spring 容器

基本概念:@Bean@Configuration

@Bean@Configuration 是 Spring 基于 Java 配置的核心构件

@Bean 注解用于指示方法实例化、配置和初始化将由 Spring IoC 容器管理的新对象

@Bean 注解扮演着与<bean/> 标签相同的角色

@Configuration 注解一个类表明它的主要目的是作为 bean 定义的来源。@Configuration 类允许通过调用同一类中的其他 @Bean 方法来定义 bean 间的依赖关系。

最简单的 @Configuration 类如下:

@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}
完全( full )的 @Configuration 配置 VS 精简( lite )的 @Bean 模式?

当在没有 @Configuration 注解的类中声明 @Bean 方法时 ,它们被称为在“精简”模式下处理。在精简模式下,@Bean 方法是一种通用的工厂方法机制,不应调用其他 @Bean 方法,精简模式不能声明 bean 间的依赖关系。

在常见的场景中,@Bean 方法在 @Configuration 类中声明,确保始终使用 “full” 模式,因此跨方法引用被重定向到容器的生命周期管理。这可以防止同一个 @Bean 方法意外地通过一个普通的 Java 调用被调用,这有助于减少在 “lite” 模式下运行时难以跟踪的细微错误。

使用说明

@Bean 方法内调用另一个 @Bean 方法时,配置类必须是代理类,否则拿到的不是容器内的 bean 。可以使用方法参数注入的方式避免这个问题。

// @Configuration(proxyBeanMethods = true)
public class ConfigurationConfig {
    @Bean
    public MyBean2 myBean22() {
        return new MyBean2(2, "a2");
    }


    @Bean
    public MyBean3 myBean3() {
        MyBean3 myBean3 = new MyBean3();
        myBean3.setMyBean2(myBean22());
        return myBean3;
    }
}

使用 AnnotationConfigApplicationContext 实例化 Spring 容器

AnnotationConfigApplicationContext 不仅能够接受 @Configuration 类作为输入,还能够接受普通 @Component 类和用 JSR-330 注解的类。

简单构造
// @Configuration 类
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);

// 普通 @Component 类和用 JSR-330 注解的类
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
以编程方式构建容器 register(Class…)
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class, OtherConfig.class);
ctx.register(AdditionalConfig.class);
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
启用组件扫描 scan(String…)
@Configuration
@ComponentScan(basePackages = "com.acme") 
public class AppConfig  {
    ...
}

等价于

<beans>
    <context:component-scan base-package="com.acme"/>
</beans>

等价于

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();

@Component@Configuration 的元注解

支持 Web 应用程序 AnnotationConfigWebApplicationContext

可以通过 AnnotationConfigWebApplicationContext 获得 AnnotationConfigApplicationContextWebApplicationContext 变量

web.xml 里的模板代码:

<web-app>
    <!-- Configure ContextLoaderListener to use AnnotationConfigWebApplicationContext
        instead of the default XmlWebApplicationContext -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>

    <!-- Configuration locations must consist of one or more comma- or space-delimited
        fully-qualified @Configuration classes. Fully-qualified packages may also be
        specified for component-scanning -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.acme.AppConfig</param-value>
    </context-param>

    <!-- Bootstrap the root application context as usual using ContextLoaderListener -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- Declare a Spring MVC DispatcherServlet as usual -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- Configure DispatcherServlet to use AnnotationConfigWebApplicationContext
            instead of the default XmlWebApplicationContext -->
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <!-- Again, config locations must consist of one or more comma- or space-delimited
            and fully-qualified @Configuration classes -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.acme.web.MvcConfig</param-value>
        </init-param>
    </servlet>

    <!-- map all requests for /app/* to the dispatcher servlet -->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
</web-app>

参考源码:

  • org.springframework.web.context.WebApplicationContext

使用 @Bean 注解

@Bean 是方法级别的注解,可以对比 XML 配置里的 <bean/> 标签

@Configuration
public class AppConfig {

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

注意 @Bean 方法的返回类型 ,可以使用接口(或基类)返回类型声明 @Bean 方法,但是推荐返回最具体的类型

Bean 依赖

@Bean 注解的方法可以具有任意数量的参数,这些参数描述构建 bean 所需的依赖项。

@Configuration
public class AppConfig {

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

解析机制与基于构造函数的依赖注入几乎相同。

支持生命周期回调

使用 @Bean 注解定义的任何类都支持常规的生命周期回调,并且可以使用 JSR-250 中的 @PostConstruct@PreDestroy 注解

完全支持常规的 Spring 生命周期回调。如果 bean 实现 InitializingBeanDisposableBeanLifecycle ,则容器将调用它们各自的方法

完全支持 *Aware 接口

默认情况下,使用 Java 配置定义的具有公共 closeshutdown 方法的 bean 会自动配置为销毁回调。如果您不希望在容器关闭时调用它,您可以添加 @Bean(destroyMethod="") 到您的 bean 定义中以禁用默认推断 (inferred) 模式。

下面的示例演示如何防止数据源的自动销毁回调:

@Bean(destroyMethod="")
public DataSource dataSource() throws NamingException {
    return (DataSource) jndiTemplate.lookup("MyDS");
}

@Bean 注解支持指定任意初始化和销毁回调方法

public class BeanOne {

    public void init() {
        // initialization logic
    }
}

public class BeanTwo {

    public void cleanup() {
        // destruction logic
    }
}

@Configuration
public class AppConfig {

    @Bean(initMethod = "init")
    public BeanOne beanOne() {
        return new BeanOne();
    }

    @Bean(destroyMethod = "cleanup")
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

也可以不依赖容器的生命周期回调,直接调用初始化方法:

@Bean
public BeanOne beanOne() {
    BeanOne beanOne = new BeanOne();
    beanOne.init();
    return beanOne;
}
指定 Bean 作用域
  • @Scope
  • ConfigurableBeanFactory.SCOPE_PROTOTYPE
  • ConfigurableBeanFactory.SCOPE_SINGLETON

默认作用域是 singleton,可以使用 @Scope 注解覆盖它

@Bean
@Scope("prototype")
public Encryptor encryptor() {
    // ...
}
@Scopescoped-proxy

Spring 提供了一种通过限定作用域的代理处理限定作用域的依赖项的方便方法。使用 XML 配置时创建这样一个代理的最简单方法是 <aop: scoped-proxy/> 元素。在 Java 中使用 @Scope 注解配置 bean 可以提供与 proxyMode 属性相当的支持。缺省值是 ScopedProxyMode.DEFAULT ,它通常表示除非在组件扫描指令级别配置了不同的缺省值,否则不应创建作用域代理。你可以指定 ScopedProxyMode.TARGET_CLASSScopedProxyMode.INTERFACES 或者 ScopedProxyMode.NO

自定义 Bean 命名

默认情况下,配置类使用 @Bean 方法的名称作为结果 bean 的名称。可以使用 name 属性指定名称

@Configuration
public class AppConfig {

    @Bean(name = "myThing")
    public Thing thing() {
        return new Thing();
    }
}

有时需要给单个 bean 多个名称:

@Configuration
public class AppConfig {

    @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
    public DataSource dataSource() {
        // instantiate, configure and return DataSource bean...
    }
}
Bean 描述

使用注解 @Description 提供 bean 的更详细的文本描述。当 bean 被公开(可能通过 JMX )用于监视目的时,这可能特别有用。

@Configuration
public class AppConfig {

    @Bean
    @Description("Provides a basic example of a bean")
    public Thing thing() {
        return new Thing();
    }
}

使用 @Configuration 注解

@Configuration 是一个类级别的注释,指示对象是 bean 定义的源。 @Configuration 类通过@Bean 注释的方法声明 bean。对 @Configuration 类上的 @Bean 方法的调用也可用于定义 bean 之间的依赖关系。

注入 Bean 间依赖
@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        return new BeanOne(beanTwo());
    }

    @Bean
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

这种声明 bean 间依赖关系的 @Bean 方法仅在该方法在 @Configuration 类中声明时才有效。不能使用普通 @Component 类来声明 bean 间的依赖关系。

查找方法注入( Lookup )

在单例作用域的 bean 依赖于原型作用域的 bean 的情况下,查找方法注入很有用

可以实现单例每次获取原型作用域的 bean 时,都是新的原型 bean

@Configuration
public class Config2Bean {
    @Bean
    @Scope("prototype")
    public AsyncCommand asyncCommand() {
        AsyncCommand command = new AsyncCommand();
        // inject dependencies here as required
        return command;
    }

    @Bean
    public CommandManager commandManager() {
        // return new anonymous implementation of CommandManager with createCommand()
        // overridden to return a new prototype Command object
        return new CommandManager() {
            protected Command createCommand() {
                return asyncCommand();
            }
        };
    }
}
public abstract class CommandManager {

    public Object process(Object commandState) {
        // grab a new instance of the appropriate Command interface
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }

    // okay... but where is the implementation of this method?
    protected abstract Command createCommand();
}
public class AsyncCommand extends Command {
    public AsyncCommand() {
        System.out.println("AsyncCommand Constructor..." + hashCode());
    }

    @Override
    public Object execute() {
        System.out.println("AsyncCommand execute ..");
        return null;
    }
}
@Test
public void test4() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config2Bean.class);

    for (int i = 0; i < 5; i++) {
        System.out.println("================");

        CommandManager commandManager = context.getBean(CommandManager.class);
        System.out.println(commandManager);
        System.out.println(commandManager.process("abc"));
    }
}
关于基于 Java 的配置如何在内部工作的更多信息

所有 @Configuration 类在启动时都使用 CGLIB 代理类,子方法在调用父方法和创建新实例之前,首先检查容器中是否有任何缓存的(作用域)bean

从 Spring 3.2 开始,不再需要将 CGLIB 添加到类路径中,因为 CGLIB 类已经被重新打包到 org.springframework.cglib 下,并直接包含在 spring-core JAR 中。

由于 CGLIB 在启动时动态添加功能,因此存在一些限制。特别是,配置类不能是 final 的。但是,从 4.3 开始,配置类上允许使用任何构造函数,包括使用 @Autowired 或单个非默认构造函数声明进行默认注入。

如果您希望避免任何 CGLIB 强加的限制,请考虑在非 @Configuration 类上声明您的 @Bean 方法(例如,改为在普通 @Component 类上)。这样的话,@Bean 方法之间的跨方法调用不会被拦截,因此您必须完全依赖构造函数或方法级别的依赖注入。

对基于 Java 的配置进行组合

使用 @Import 注解

对标 XML 里的 <import/>

@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();
    }
}

这样在容器启动时,只需要指定一个配置类

ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

从 Spring Framework 4.2 开始,@Import 也支持对常规组件类的引用,类似于 AnnotationConfigApplicationContext.register 方法。通过使用一些配置类作为入口点来显式定义所有组件。

注入被导入依赖项的 @Bean 定义

在大多数实际场景中,bean 跨配置类彼此依赖。在使用 XML 时,这不是问题,因为不涉及编译器,您可以声明 ref="someBean" ,并相信 Spring 会在容器初始化期间解决这个问题。当使用 @Configuration 类时,Java 编译器在配置模型上存在约束,对其他 bean 的引用必须是有效的 Java 语法。

注意@Configuration 类最终只是容器中的另一个 bean :这意味着它们可以利用 @Autowired@Value 注入以及与任何其他 bean 相同的特性。

bean 跨配置类相互依赖的场景

  • 通过 @Autowired 注入
  • 通过 @Bean 方法参数注入

@Configuration 类在上下文的初始化过程中很早就被处理,使用 @Autowired 方法注入会导致过早的初始化,因此尽可能使用基于 @Bean 方法参数注入

通过 @BeanBeanPostProcessorBeanPactoryPostProcessor 定义要特别小心。这些方法通常应该声明为静态 static @Bean 方法,这样不会触发其包含的配置类的实例化。否则,@Autowired@Value 可能无法在配置类本身上工作,因为可能将其创建为早于 AutowiredAnnotationBeanPostProcessor 的 bean 实例。

从 Spring Framework 4.3 开始,支持 @Configuration 类中的构造函数注入。另请注意,如果目标 bean 仅定义一个构造函数,则无需指定 @Autowired

在不同 @Configuration 类中导航 bean

@Autowired 效果很好并提供了所需的模块化,但是对于确定自动装配 bean 定义的确切位置仍然有些模糊。

如果这种歧义是不可接受的,并且您希望在 IDE 中从一个 @Configuration 类直接导航到另一个 @Configuration 类,请考虑注入配置类本身。

@Configuration
public class ServiceConfig {

    @Autowired
    private RepositoryConfig repositoryConfig;

    @Bean
    public TransferService transferService() {
        // navigate 'through' the config class to the @Bean method!
        return new TransferServiceImpl(repositoryConfig.accountRepository());
    }
}

在前面的情况中,AccountRepository 的定义是完全明确的。 ServiceConfigRepositoryConfig 是强关联的,可以通过接口缓解耦合:

@Configuration
public class ServiceConfig {

    @Autowired
    private RepositoryConfig repositoryConfig;

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

@Configuration
public interface RepositoryConfig {

    @Bean
    AccountRepository accountRepository();
}

@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {

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

@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class})  // 导入具体类
public class SystemTestConfig {

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

}

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

如果您想影响某些 bean 的启动创建顺序,请考虑将其中一些声明为 @Lazy (用于在首次访问时创建而不是在启动时创建)或 @DependsOn (确保在当前 bean 之前创建特定的其他 bean ,而不是后者的直接依赖项)。

@DependsOn 注解主要用于指定当前 bean 所依赖的 beans 。任何被指定依赖的 bean 都由 Spring 保证在当前 bean 之前创建。在少数情况下,bean 不是通过属性或构造函数参数显式依赖于另一个 bean ,但却需要要求另一个 bean 优先完成初始化,则可以使用@DependsOn这个注解。

@DependsOn 既可以指定初始化依赖顺序,也可以指定bean相应的销毁执行顺序(仅在单例bean的情况下)。

条件判断( Condition )包含 @Configuration 类或 @Bean 方法

@Profile :只有在 Spring Environment 中启用了 profile 时才使用 @Profile 注解激活 bean

@Profile 注解是通过使用更灵活的 @Conditional 注解实际执行。@Conditional 注解指示注册 @Bean 前应咨询特定 org.springframework.context.annotation.Condition 的实现。

Condition 接口的实现提供了一个返回 truefalsematches(…) 方法。参考 org.springframework.context.annotation.ProfileCondition

组合 Java 配置和 XML 配置

Spring 的 @Configuration 类支持并不是要 100% 完全替代 Spring XML。某些工具,例如 Spring XML 命名空间,仍然是配置容器的理想方式。在 XML 更方便或必要的情况下,你可以选择:通过 ClassPathXmlApplicationContext 实例化容器中的 “XML 配置为中心” 的方式,或者通过使用 AnnotationConfigApplicationContext 实例化一个 “Java 配置为中心” 的方式,然后使用 @ImportResource 注解根据需要导入 XML。

以 XML 为中心使用 @Configuration

从 XML 引导 Spring 容器并以特定的方式包含 @Configuration 类可能更可取。例如,在使用 Spring XML 的大型现有代码库中,根据需要创建 @Configuration 类并从现有 XML 文件中包含它们会更容易。

@Configuration 类声明为普通的 Spring <bean/> 元素

@Configuration 类最终是容器中的 bean 定义。在本系列示例中,我们创建了一个名为 AppConfig@Configuration 类,并将其作为 <bean/> 定义包含在 system-test-config.xml 中。因为 <context:annotation-config/> ,所以容器识别 @Configuration 注解并正确处理 AppConfig 中声明的 @Bean 方法。

@Configuration
public class AppConfig {

    @Autowired
    private DataSource dataSource;

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

    @Bean
    public TransferService transferService() {
        return new TransferService(accountRepository());
    }
}
<beans>
    <!-- enable processing of annotations such as @Autowired and @Configuration -->
    <context:annotation-config/>
    
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <bean class="com.acme.AppConfig"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>
使用 <context:component-scan/> 获取 @Configuration

@Component@Configuration 的元注解,@Configuration 注解的类是组件扫描的候选对象

不需要再显式声明 <context:annotation-config/> ,因为 <context:component-scan/> 支持相同的功能。

<beans>
    <!-- picks up and registers AppConfig as a bean definition -->
    <context:component-scan base-package="com.acme"/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>
@Configuration 类为中心使用 @ImportResource 引入 XML
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    @Bean
    public DataSource dataSource() {
        return new DriverManagerDataSource(url, username, password);
    }
}

properties-config.xml

<beans>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>

Environment 抽象

Environment 接口是集成在容器中的抽象,它为应用程序环境的两个关键方面建模:profilesproperties

profile 是一组命名的、逻辑 bean 定义,仅当给定的 profile 处于活动状态时才向容器注册。Bean 可以分配给 profile ,无论是在 XML 中定义还是使用注解。Environment 对象与 profile 的关系是确定哪些 profile 当前是活动状态的,以及默认情况下哪些 profile 应该是活动的。

属性( properties )在几乎所有应用程序中都扮演着重要的角色,并且可能来自各种来源:属性文件、JVM 系统属性、系统环境变量、JNDI、servlet 上下文参数、ad-hoc Properties 对象、Map 对象等。Environment 对象与属性相关的作用是为用户提供方便的服务接口,用于配置属性源并从中解析属性。

Bean 定义 Profiles

Bean 定义 profile 在核心容器中提供了一种机制,允许在不同环境中注册不同的 Bean。此功能的作用场景,包括:

  • 在开发中使用内存数据源,而不是在 QA 或生产中从 JNDI 查找相同的数据源。
  • 仅在将应用程序部署到生产环境中时才注册监控基础设施。
  • 为客户 A 和客户 B 部署注册 bean 的自定义实现。
使用 @Profile

@Profile 可以用在类或者方法上。

@Profile 注解指示组件在一个或多个指定的 profile 中是活动的。

@Profile("production")

profile 字符串可以包含一个简单的名称(例如 production)或表达式。profile 表达式允许表达更复杂的 profile 逻辑(例如 production & us-east )。profile 表达式支持以下运算符:

  • ! :逻辑“非”
  • & :逻辑“与”
  • | :逻辑“或”

不能在不使用括号的情况下混合使用 &| 运算符。例如, production & us-east | eu-central 不是有效的表达式。它必须表示为 production & (us-east | eu-central)

@Profile 可以用作元注解来创建自定义组合注解。以下示例定义了一个自定义 @Production 注解,您可以将其用作 @Profile("production") 的替代品:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

如果一个 @Configuration 类被标记为 @Profile ,除非 profile 处于活动状态,否则与该类关联的所有 @Bean 方法和 @Import 注解都将被绕过。如果 @Component@Configuration 类标有 @Profile({"p1", "p2"}) ,则除非已激活 p1p2 ,否则不会注册该类。如果给定的 profile 以 NOT 运算符 ! 为前缀,则仅当 profile 未处于活动状态时才注册带注解的元素。例如,@Profile({"p1", "!p2"}) 如果 p1 处于活动状态或 p2 未处于活动状态,则会发生注册。

对于 @Profile 注解的 @Bean 方法,对于重载的相同 Java 方法名称的 @Bean 方法(类似于构造函数重载) ,需要在所有重载方法上一致地声明 @Profile 条件。如果条件不一致,则重载方法中只有第一个声明的条件有关系。因此,不能使用 @Profile 选择具有特定参数签名的重载方法。同一个 bean 的所有工厂方法之间的解析在创建时遵循 Spring 的构造函数解析算法。

如果希望定义具有不同配置文件条件的替代 bean,可以使用不同的 Java 方法名称,通过使用 @Beanname 属性指向相同的 bean 名称

XML Bean 定义 Profile

@Profile 注解对标 <beans> 元素的 profile 属性

<beans profile="development"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="...">

    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
</beans>

使用嵌套 <beans/>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <!-- other bean definitions -->

    <beans profile="development">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>

    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>
</beans>

XML 不支持前面描述的配置文件表达式。但是,可以使用 ! 运算符来否定配置文件。也可以通过嵌套配置文件来应用逻辑“与”

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <!-- other bean definitions -->

    <beans profile="production">
        <beans profile="us-east">
            <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
        </beans>
    </beans>
</beans>
激活 profile

可以通过多种方式激活 profile ,最直接的是针对 Environment API 以编程方式进行,通过 ApplicationContext 调用

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

还可以通过 spring.profiles.active 属性激活 profile ,这可以通过系统环境变量、JVM 系统属性、web.xml 中的 servlet 上下文参数指定,甚至作为 JNDI 中的条目(请参阅 PropertySource抽象 )来指定。在集成测试中,可以使用 spring-test 模块中的@ActiveProfiles 注解来声明活动 profile

profile 不是非此即彼,可以一次激活多个 profile

可以混合使用 Java API 和属性配置激活 profile

默认 profile
@Profile("default")

如果没有激活任何 profile ,那么默认激活的 profile 是 default ,如果存在激活的 profile ,不会激活默认的 default

可以通过 EnvironmentsetDefaultProfiles() 更改默认的 profile ,或者通过配置 spring.profiles.default 属性

PropertySource 抽象

Spring 的 Environment 抽象提供了对属性源的 可配置 层次结构 的搜索操作

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);

Environment 对象对一组 PropertySource 对象执行搜索。PropertySource 是对任何键值对源的简单抽象,Spring 的 StandardEnvironment 配置了两个 PropertySource 对象, 一个表示 JVM 系统属性集 ( System.getProperties() ) 和一个表示系统环境变量集 ( System.getenv() )

这些默认属性源用于 StandardEnvironment ,用于独立应用程序。StandardServletEnvironment 还使用额外的默认属性源进行填充,包括 servlet 配置和 servlet 上下文参数。它可以选择性的启用一个 JndiPropertySource

执行搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用期间碰巧在两个地方都设置了 my-property 属性,则系统属性值“获胜”并返回。请注意,属性值不会合并,而是被前面的条目完全覆盖。

对于通用的 StandardServletEnvironment ,完整的层次结构如下,最高优先级的条目位于顶部:

  1. ServletConfig 参数(在 DispatcherServlet 上下文的情况下)
  2. ServletContext 参数( web.xml 里的 context-param
  3. JNDI 环境变量( java:comp/env/
  4. JVM 系统属性( -D 命令行参数)
  5. JVM 系统环境(操作系统环境变量)

整个机制是可配置的。也许您有一个自定义的属性源,希望将其集成到此搜索中。为此,实现并实例化您自己的 PropertySource,并将其添加到当前 EnvironmentPropertySources 集合中

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

在前面的代码中,MyPropertySource 以最高优先级添加到了搜索中。MutablePropertySources API 公开了许多允许精确操作属性源集的方法。

参考源码
  • org.springframework.core.env.MutablePropertySources

使用 @PropertySource

@PropertySource 注解提供了一种方便的机制来将 PropertySource 添加到 Spring 的 Environment

@PropertySource("classpath:/com/myco/app.properties")

@PropertySource 中存在的任何 ${…} 占位符都针对已在环境中注册的属性源进行解析

@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")

假设 my.placeholder 存在于已注册的属性源(例如,系统属性或环境变量)中,占位符将解析为相应的值。如果没有,则将 default/path 用作默认值。如果未指定默认值且无法解析属性,则抛出 IllegalArgumentException

@PropertySource 注解是可重复的。但是,所有此类 @PropertySource 注解都需要在同一级别声明,要么直接在配置类上声明,要么作为同一自定义注解中的元注解。不推荐混合直接注解和元注解,因为直接注解有效地覆盖了元注解。

也可以直接使用 @PropertySources

语句中的占位符解析

Environment 抽象是集成在整个容器中的,很容易通过它来对占位符进行解析。这意味着可以按照自己喜欢的任何方式配置解析过程。可以更改搜索系统属性和环境变量的优先级或完全删除它们,还可以根据需要将自己的属性源添加到组合中。

具体来说,以下语句无论在何处定义属性都有效,只要 customerEnvironment 中可用:

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>

注册 LoadTimeWeaver

Spring 使用 LoadTimeWeaver 动态转换加载到 Java 虚拟机( JVM )中的类。

要启用加载时编织( load-time weaving ),可以将 @EnableLoadTimeWeaving 添加到 @Configuration 类上

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

等价于

<beans>
    <context:load-time-weaver/>
</beans>

配置后,ApplicationContext 中的任何 bean 都可以实现 LoadTimeWeaverAware ,从而接收对加载时编织器实例的引用。这在结合 Spring 的 JPA 支持时 特别有用, 因为 JPA 类转换可能需要加载时编织。

ApplicationContext 的附加功能

为了以更面向框架的风格增强 BeanFactory 功能,ApplicationContext 还提供了以下功能:

  • 通过 MessageSource 接口访问 i18n 风格的消息。
  • 通过 ResourceLoader 接口访问资源,例如 URL 和文件。
  • 事件发布,即通过使用 ApplicationEventPublisher 接口发布事件到实现 ApplicationListener 接口的 bean 。
  • 通过 HierarchicalBeanFactory 接口加载多个(分层)上下文,让每个上下文都专注于一个特定的层,例如应用程序的 Web 层 。

使用 MessageSource 国际化

MessageSource 提供了国际化功能,HierarchicalMessageSource 提供了分层解析消息的功能。

当加载 ApplicationContext 时,它自动搜索在上下文中定义的 MessageSource bean。bean 名称必须是 messageSource 。如果找到这样的 bean,则对 getMessage 方法的所有调用都将委托给消息源。如果未找到 MessageSource ,则 ApplicationContext 尝试查找包含同名 bean 的父级。如果找到,它将使用该 bean 作为 MessageSource 。如果 ApplicationContext 找不到任何消息来源,则会实例化一个空 DelegatingMessageSource 对象,以便能够接受对 getMessage 方法的调用。

Spring 提供了三种 MessageSource 实现。ResourceBundleMessageSourceReloadableResourceBundleMessageSourceStaticMessageSource 。为了进行嵌套消息传递,它们都实现了 HierarchicalMessageSourceStaticMessageSource 很少使用,但它提供了向消息源添加消息的编程方式。下面的例子展示了 ResourceBundleMessageSource :

<beans>
    <bean id="messageSource"
            class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>format</value>
                <value>exceptions</value>
                <value>windows</value>
            </list>
        </property>
    </bean>
</beans>

该示例假定在类路径中定义了三个资源包,分别称为 formatexceptionswindows任何解析消息的请求都以 JDK 标准的方式处理,即通过 ResourceBundle 对象解析消息。

注意:所有 ApplicationContext 实现也是 MessageSource 实现,因此可以强制转换为 MessageSource 接口。

public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("message", null, "Default", Locale.ENGLISH);
    System.out.println(message);
}

messageSource bean 定义通过其基名属性引用了许多资源包。在列表中传递给 basenames 属性的三个文件分别称为 format.propertiesexceptions.propertieswindows.properties ,它们作为文件存在于类路径的根目录中。

关于国际化(“i18n”),Spring 的各种 MessageSource 实现遵循与标准 JDK ResourceBundle 相同的区域设置解析和回退规则 。如果你想对英国(en-GB)的语言环境,你需要分别创建的文件名为format_en_GB.propertiesexceptions_en_GB.propertieswindows_en_GB.properties

可以实现 MessageSourceAware 接口以获取 MessageSource

作为 ResourceBundleMessageSource 的替代,Spring 提供了 ReloadableResourceBundleMessageSource 类。此变体支持相同的包文件格式,但比基于标准 JDK 的 ResourceBundleMessageSource 实现更灵活 。特别是,它允许从任何 Spring 资源位置(不仅从类路径)读取文件,并支持属性文件的热重载。

ResourceBundleMessageSource 简单使用
<?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="messageSource"
          class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>message.format</value>
                <value>message.exceptions</value>
                <value>message.windows</value>
            </list>
        </property>
    </bean>
</beans>

该示例假设您有三个名为 formatexceptionswindows 的资源包,在您的类路径中定义,任何解析消息的请求都以 JDK 标准的通过 ResourceBundle 对象解析消息的方式处理。

# in format.properties
message=Alligators rock!

# in exceptions.properties
argument.required=The {0} argument is required.
public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("message", null, "Default", Locale.ENGLISH);
    System.out.println(message);
}
传递给消息查找的参数
<beans>

    <!-- this MessageSource is being used in a web application -->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="exceptions"/>
    </bean>

    <!-- lets inject the above MessageSource into this POJO -->
    <bean id="example" class="com.something.Example">
        <property name="messages" ref="messageSource"/>
    </bean>

</beans>
public class Example {

    private MessageSource messages;

    public void setMessages(MessageSource messages) {
        this.messages = messages;
    }

    public void execute() {
        String message = this.messages.getMessage("argument.required",
            new Object [] {"userDao"}, "Required", Locale.ENGLISH);
        System.out.println(message);
    }
}
参考源码
  • org.springframework.context.MessageSource
  • org.springframework.context.HierarchicalMessageSource
  • org.springframework.context.support.ReloadableResourceBundleMessageSource

标准事件和自定义事件

ApplicationContext 中的事件处理是通过 ApplicationEvent 类和 ApplicationListener 接口提供的。如果将实现 ApplicationListener 接口的 bean 部署到上下文中,则每次将 ApplicationEvent 发布到 ApplicationContext 时,都会通知该 bean。本质上,这是标准的观察者设计模式。

从 Spring 4.2 开始,提供了 基于注解的模型 以及发布任意事件(也就是不一定从 ApplicationEvent 扩展的对象)的能力。当此类对象发布时,我们会为您将其包装在一个事件中。

Spring 提供的标准事件:

事件 描述
ContextRefreshedEvent ApplicationContext 初始化( initialized )或刷新时发布(例如,通过使用 ConfigurableApplicationContext 接口上的 refresh() 方法)。在这里,initialized 意味着所有 bean 都被加载,后置处理器 bean 被检测并激活,预先实例化单例对象,并且准备好使用 ApplicationContext 对象。只要上下文尚未关闭,就可以多次触发刷新,前提是所选 ApplicationContext 支持这种“热”刷新。例如,XmlWebApplicationContext 支持热刷新,但 GenericApplicationContext 不支持 。
ContextStartedEvent 使用 ConfigurableApplicationContext 接口上的 start() 方法启动 ApplicationContext 时发布。在这里,started 意味着所有Lifecycle bean 都收到一个明确的启动信号。通常,此信号用于在显式停止后重新启动 bean,但它也可用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。
ContextStoppedEvent 使用 ConfigurableApplicationContext 接口上的 stop() 方法停止 ApplicationContext 时发布。这里,stopped 意味着所有 Lifecycle bean 都收到一个明确的停止信号。停止的上下文可以通过调用 start() 重新启动 。
ContextClosedEvent 在使用 ConfigurableApplicationContext 接口上的 close() 方法或关闭 ApplicationContext 时通过 JVM 关闭挂钩发布。在这里,closed 意味着所有的单例 bean 都将被销毁。一旦上下文关闭,它就会到达生命的尽头,无法刷新或重新启动。
RequestHandledEvent 特定于 Web 的事件,告诉所有 bean HTTP 请求已经得到服务。此事件在请求完成后发布。此事件仅适用于使用 Spring 的 DispatcherServlet 的 web 应用程序
ServletRequestHandledEvent RequestHandledEvent 的一个子类,添加了特定于 Servlet 的上下文信息

还可以创建和发布自己的自定义事件。下面的示例显示了一个简单的类,它扩展了 Spring 的 ApplicationEvent

public class BlockedListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlockedListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }

    // accessor and other methods...
}

要发布自定义 ApplicationEvent ,调用 ApplicationEventPublisher 上的 publishEvent() 方法 。通常,这是通过创建一个实现 ApplicationEventPublisherAware 的类并将其注册为 Spring bean 来完成的

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blockedList;
    private ApplicationEventPublisher publisher;

    public void setBlockedList(List<String> blockedList) {
        this.blockedList = blockedList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blockedList.contains(address)) {
            publisher.publishEvent(new BlockedListEvent(this, address, content));
            return;
        }
        // send email...
    }
}

在配置 EmailService 时,Spring 容器会检测到实现 ApplicationEventPublisherAware 并自动调用 setApplicationEventPublisher()实际上,传入的参数是 Spring 容器本身。您正在通过 ApplicationEventPublisher 接口与应用程序上下文进行交互。

要接收自定义 ApplicationEvent ,您可以创建一个实现 ApplicationListener 的类并将其注册为 Spring bean。

public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

请注意,ApplicationListener 通常使用自定义事件的类型进行参数化(例如示例中的 BlockedListEvent )。这意味着 onApplicationEvent() 方法可以保持类型安全,避免任何向下转型的需要。您可以根据需要注册任意数量的事件监听器,但请注意,默认情况下,事件监听器会同步接收事件。这意味着 publishEvent() 方法会阻塞,直到所有监听器都完成对事件的处理。这种同步和单线程方法的一个优点是,当监听器接收到事件时,如果事务上下文可用,它就会在发布者的事务上下文中运行。如果需要另一种事件发布策略,请参阅 Spring ApplicationEventMulticaster 接口和 SimpleApplicationEventMulticaster 实现。

以下示例显示了用于注册和配置上述每个类的 bean 定义:

<bean id="emailService" class="example.EmailService">
    <property name="blockedList">
        <list>
            <value>known.spammer@example.org</value>
            <value>known.hacker@example.org</value>
            <value>john.doe@example.org</value>
        </list>
    </property>
</bean>

<bean id="blockedListNotifier" class="example.BlockedListNotifier">
    <property name="notificationAddress" value="blockedlist@example.org"/>
</bean>

当调用 emailService bean 的 sendEmail() 方法时,如果有任何应该被阻止的电子邮件消息,会发布一个自定义事件 BlockedListEventblockedListNotifier bean 被注册为接收 BlockedListEventApplicationListener

Spring 的事件机制是为同一应用程序上下文中 Spring bean 之间的简单通信而设计的。然而,对于更复杂的企业集成需求,单独维护的 Spring Integration 项目为构建基于 Spring 编程模型的轻量级、面向模式、事件驱动架构提供了完整的支持 。

基于注解的事件监听器

可以使用 @EventListener 注解在托管 bean 的任何方法上注册事件监听器

BlockedListNotifier 可改写如下:

public class BlockedListNotifier {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    @EventListener
    public void processBlockedListEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

方法签名再次声明它监听的事件类型,但这次使用了灵活的名称,并且没有实现特定的监听器接口。只要实际事件类型在其实现层次结构中解析您的泛型参数,也可以通过泛型缩小事件类型。

如果您的方法应该监听多个事件,或者您想定义它时根本不带参数,则还可以在注解本身上指定事件类型。以下示例显示了如何执行此操作:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}

还可以通过使用注解的 condition 属性添加额外的运行时过滤,该属性定义一个 SpEL表达式 ,表达式应该匹配以实际调用特定事件的方法。

以下示例显示了如何重写我们的通知程序,使其仅在事件的 content 属性等于 my-event 时才被调用 :

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
    // notify appropriate parties via notificationAddress...
}

每个 SpEL 表达式都针对专用上下文进行评估。下表列出了可用于上下文的项目,可以将它们用于条件事件处理:

Name Location 描述 例子
事件 根对象 实际的 ApplicationEvent #root.eventevent
参数数组 根对象 调用方法的参数(作为对象数组) #root.argsargs ;
args[0] 访问第一个参数
参数名称 评估上下文 任何方法参数的名称。如果由于某种原因,名称不可用(例如,因为已编译的字节码中没有调试信息),也可以使用 #a<#arg> 语法使用单个参数,其中 <#arg> 表示参数索引(从 0 开始) #blEvent#a0(也可以使用 #p0#p<#arg> 参数表示法作为别名)

请注意 ,即使您的方法签名实际上是指已发布的任意对象,您也可以通过 #root.event 访问基础事件。

如果您需要发布一个事件作为处理另一个事件的结果,您可以更改方法签名以返回应该发布的事件,如以下示例所示:

@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}

异步监听器 不支持此功能

handleBlockedListEvent() 方法为它处理的每一个 BlockedListEvent 发布一个新的 ListUpdateEvent 。如果您需要发布多个事件,则可以返回一个 Collection 或一个事件数组。

异步监听器

如果您希望特定监听器异步处理事件,则可以重用 常规 @Async 支持 。以下示例显示了如何执行此操作:

@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
}

使用异步事件时请注意以下限制:

  • 如果异步事件监听器抛出 Exception ,则不会将其传播给调用者。参阅 AsyncUncaughtExceptionHandler
  • 异步事件监听器方法无法通过返回值来发布后续事件。如果您需要发布另一个事件作为处理结果,请注入 ApplicationEventPublisher 以手动发布事件
监听器排序

可以在方法声明上添加 @Order 注解

@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress...
}
泛型事件

您还可以使用泛型进一步定义事件的结构。考虑使用 EntityCreatedEvent<T> ,这里 T 是创建的实际实体的类型。例如,您可以创建以下监听器定义只接收泛型类型是 PersonEntityCreatedEvent

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    // ...
}

由于类型擦除,这仅在触发的事件解析事件监听器过滤的泛型参数(即类似 class PersonCreatedEvent extends EntityCreatedEvent<Person> { … } )时才有效 。

可以实现 ResolvableTypeProvider 来指导框架超越运行时环境提供的功能。以下事件显示了如何执行此操作:

public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
    }
}

这不仅适用于 ApplicationEvent ,也适用于作为事件发送的任何任意对象。

便捷的访问底层资源

应该熟悉 Spring 的 Resource 抽象

应用程序上下文是 ResourceLoader ,可用于加载 Resource 对象。Resource 本质上是 JDK java.net.URL 类的功能更丰富的版本。事实上,在适当的情况下,Resource 实现包装 java.net.URL 的实例。Resource 可以以透明的方式从几乎任何位置获取低级资源,包括从类路径、文件系统位置、用标准 URL 描述的任何位置以及其他一些变体。如果资源位置字符串是一个没有任何特殊前缀的简单路径,那么这些资源的来源是特定的,并且适合实际的应用程序上下文类型。

您可以将应用程序上下文中的 bean 实现特殊的回调接口 ResourceLoaderAware ,以在初始化时自动回调应用程序上下文本身作为 ResourceLoader 。您还可以公开 Resource 类型的属性,用于访问静态资源。它们像任何其他属性一样被注入其中。您可以将这些 Resource 属性指定为简单 String 路径,在部署 bean 时这些文本字符串自动转换到实际 Resource 对象。

提供给 ApplicationContext 构造函数的位置路径实际上是资源字符串,根据特定的上下文实现进行适当处理。例如,ClassPathXmlApplicationContext 将简单的位置路径视为类路径位置。还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或 URL 加载定义,而不管实际的上下文类型。

应用程序启动跟踪

ApplicationContext 管理 Spring 应用程序生命周期,并围绕组件提供丰富的编程模型。因此,复杂的应用程序可能具有同样复杂的组件图和启动阶段。

使用特定指标跟踪应用程序启动步骤有助于了解启动阶段的时间花在何处,也可以用作更好地了解整个上下文生命周期的方法。

AbstractApplicationContext(及其子类)使用 ApplicationStartup 进行检测 ,它收集各个启动阶段的 StartupStep 数据:

  • 应用程序上下文生命周期(基本包扫描、配置类管理)
  • bean 生命周期(实例化、智能初始化、后置处理处理)
  • 应用事件处理

下面是 AnnotationConfigApplicationContext 中的一个插装示例:

// create a startup step and start recording
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages);
// end the current step
scanPackages.end();

应用程序上下文已经通过多个步骤进行了检测。记录后,可以使用特定工具收集、显示和分析这些启动步骤。有关现有启动步骤的完整列表,您可以查看 专用附录部分

默认 ApplicationStartup 实现是无操作变体,以实现最小开销。这意味着默认情况下不会在应用程序启动期间收集任何指标。Spring Framework 附带了一个使用 Java Flight Recorder 跟踪启动步骤的实现: FlightRecorderApplicationStartup 。要使用此变体,您必须在创建 ApplicationContext 后立即将其配置。

如果开发人员提供他们自己的 AbstractApplicationContext 子类,或者如果他们希望收集更精确的数据,他们也可以使用 ApplicationStartup 基础设施 。

ApplicationStartup 仅在应用程序启动期间和核心容器中使用;这绝不是 Java 分析器或 Micrometer 等度量库的替代品。

要开始收集自定义 StartupStep,组件可以直接从应用程序上下文中获取 ApplicationStartup 实例,使它们的组件实现 ApplicationStartupAware ,或者在任何注入点上注入 ApplicationStartup 类型。

开发人员在创建自定义启动步骤时不应使用 spring.* 命名空间。这个命名空间是为内部 Spring 使用保留的,可能会发生变化。

Web 应用的 ApplicationContext 方便实例化

可以通过使用 ContextLoader 以声明方式创建 ApplicationContext 实例 。当然,您也可以通过使用 ApplicationContext 实现以编程方式创建 ApplicationContext 实例。

可以使用 ContextLoaderListener 注册 ApplicationContext

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

监听器检查 contextConfigLocation 参数。如果该参数不存在,则监听器将 /WEB-INF/applicationContext.xml 用作默认值。当参数确实存在时,监听器通过使用预定义的分隔符(逗号、分号和空格)来分隔 String 参数,并将这些值用作搜索应用程序上下文的位置。支持 Ant 风格的路径模式。示例是 /WEB-INF/*Context.xml(对于名称以 Context.xml 结尾并驻留在 WEB-INF 目录中的所有文件)和 /WEB-INF/**/*Context.xml(对于 WEB-INF 的任何子目录中的所有此类文件)。

将 Spring ApplicationContext 部署为 Java EE RAR 文件

可以将 Spring ApplicationContext 部署为 RAR 文件,将上下文及其所有必需的 bean 类和库 JAR 封装在 Java EE RAR 部署单元中。这相当于引导独立的 ApplicationContext (仅托管在 Java EE 环境中)能够访问 Java EE 服务器设施。RAR 部署是部署无头(headless ) WAR 文件场景的更自然的替代方案——实际上,这是一个没有任何 HTTP 入口点的 WAR 文件,仅用于在 Java EE 环境中引导 Spring ApplicationContext

BeanFactory

BeanFactory API 为 Spring 的 IoC 功能提供了基础。它多用于与 Spring 的其他部分以及相关的第三方框架的集成

DefaultListableBeanFactory 实现是高级 GenericApplicationContext 容器内的关键委托

BeanFactory 和相关接口(如 BeanFactoryAwareInitializingBeanDisposableBean )是其他框架组件的重要集成点。通过不需要任何注解甚至反射,它们允许容器及其组件之间进行非常有效的交互。应用程序级 bean 可以使用相同的回调接口,但通常更喜欢声明性的依赖注入,或者通过注解或者通过编程配置。

核心 BeanFactory API 以及 DefaultListableBeanFactory 实现不对要使用的配置格式或任何组件注解做出假设。所有这些都通过扩展(例如 XmlBeanDefinitionReaderAutowiredAnnotationBeanPostProcessor )引入,并作为核心元数据表示对共享 BeanDefinition 对象进行操作。这就是使 Spring 的容器如此灵活和可扩展的本质。

BeanFactory 或者 ApplicationContext

除非有充分的理由,否则应该使用 ApplicationContext ,而 GenericApplicationContext 及其子类 AnnotationConfigApplicationContext 是自定义引导的通用实现。这些是 Spring 核心容器的主要入口点,用于所有常见目的: 加载配置文件、触发类路径扫描、以编程方式注册 bean 定义和带注解的类,以及(从5.0开始)注册函数 bean 定义。

由于 ApplicationContext 包含 BeanFactory 的所有功能,因此通常建议不要使用普通 BeanFactory ,除非需要完全控制 bean 处理。在 ApplicationContext 中(例如 GenericaApplicationContext 实现),通过约定(即通过 bean 名称或 bean 类型)检测几种 bean —— 特别是后置处理器),而普通的 DefaultListableBeanFactory 对任何特殊 bean 都是不可知的。

对于很多扩展的容器特性,比如注解处理和 AOP 代理,BeanPostProcessor 扩展点 是必不可少的。如果您只使用一个普通的 DefaultListableBeanFactory ,默认情况下不会检测到并激活此类后处理器。

功能 BeanFactory ApplicationContext
Bean 实例化/装配 Y Y
集成的生命周期管理 N Y
自动 BeanPostProcessor 注册 N Y
自动 BeanFactoryPostProcessor 注册 N Y
方便的 MessageSource 访问(用于国际化) N Y
内置 ApplicationEvent 发布机制 N Y

要使用 DefaultListableBeanFactory 显式注册 bean 后处理器,您需要以编程方式调用 addBeanPostProcessor

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions

// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// now start using the factory

要将 BeanFactoryPostProcessor 应用于普通 DefaultListableBeanFactory ,您需要调用 postProcessBeanFactory 方法

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// bring in some property values from a Properties file
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// now actually do the replacement
cfg.postProcessBeanFactory(factory);

在这两种情况下,显式注册都不方便,这就是为什么在 Spring 支持的应用程序中,各种 ApplicationContext 比普通的 DefaultListableBeanFactory 更受欢迎的原因,特别是在典型的企业设置中,需要依赖 BeanFactoryPostProcessorBeanPostProcessor 实例实现扩展容器功能时。

AnnotationConfigApplicationContext 注册了所有常见的注解后置处理器,并且可以通过配置注解(如@EnableTransactionManagement)引入额外的处理器。在 Spring 基于注解的配置模型的抽象层上,bean 后置处理器的概念仅仅成为一个内部容器细节。

posted @ 2022-06-09 21:13  流星<。)#)))≦  阅读(20)  评论(0编辑  收藏  举报