Spring Framework

文章目录

一 Spring Framework简介

1.1 Spring Framework系统架构

1.2 对spring的理解

1.3  设计理念

二 核心

1. IoC 容器

1.1. Spring IoC容器和Bean简介

1.2. 容器概述

1.2.1. 配置元数据

1.2.2. 实例化一个容器

构建基于XML的配置元数据

Groovy Bean Definition DSL

1.2.3. 使用容器

1.3. Bean 概览

1.3.1. Bean 命名

在 Bean Definition 之外对Bean进行别名

1.3.2. 实例化 Bean

用构造函数进行实例化

用静态工厂方法进行实例化

用实例工厂方法进行实例化

确定Bean的运行时类型

1.4. 依赖

1.4.1. 依赖注入

基于构造器的依赖注入

构造函数参数解析

基于Setter的依赖注入

依赖的解析过程

依赖注入的例子

1.4.2. 依赖和配置的细节

字面值 (基本类型、 String 等)

idref 元素

对其他Bean的引用(合作者)

内部 Bean

集合(Collection)

集合合并

集合合并的限制

强类型的集合

Null and Empty String Values

使用p命名空间的XML快捷方式

使用c命名空间的XML快捷方式

复合属性名

1.4.3. 使用 depends-on

1.4.4. 懒加载的Bean

1.4.5. 注入协作者(Autowiring Collaborators)

自动注入的限制和缺点

从自动注入中排除一个Bean

1.4.6. 方法注入

查找方法依赖注入

任意方法替换

1.5. Bean Scope

1.5.1. Singleton Scope

1.5.2. Prototype Scope

1.5.3. singleton Bean 和 prototype bean 依赖

1.5.4. Request、 Session、 Application 和 WebSocket Scope

初始 Web 配置

Request scope

Session Scope

Application Scope

WebSocket Scope

作为依赖的 Scope Bean

选择要创建的代理类型

1.5.5. 自定义 Scope

创建自定义 Scope

使用自定义 Scope

1.6. 自定义Bean的性质(Nature)

1.6.1. 生命周期回调

初始化回调

销毁回调

默认的初始化和销毁方法

结合生命周期机制

启动和关闭的回调

在非Web应用中优雅地关闭Spring IoC容器

1.6.2. ApplicationContextAware 和 BeanNameAware

1.6.3. 其他 Aware 接口

1.7. Bean 定义(Definition)的继承

1.8. 容器扩展点

1.8.1. 使用 BeanPostProcessor 自定义 Bean

示例: Hello World, BeanPostProcessor

示例: AutowiredAnnotationBeanPostProcessor

1.8.2. 用 BeanFactoryPostProcessor 定制配置元数据

示例: 类名替换 PropertySourcesPlaceholderConfigurer

示例:PropertyOverrideConfigurer

1.8.3. 用 FactoryBean 自定义实例化逻辑

1.9. 基于注解的容器配置

1.9.1. 使用 @Autowired

1.9.2. 用 @Primary 对基于注解的自动注入进行微调

1.9.3. 用 Qualifiers 微调基于注解的自动注入

1.9.4. 使用泛型作为自动注入 Qualifier

1.9.5. 使用 CustomAutowireConfigurer

1.9.6. 用 @Resource 注入

1.9.7. 使用 @Value

1.9.8. 使用 @PostConstruct 和 @PreDestroy

1.10. Classpath扫描和管理的组件

1.10.1. @Component 和进一步的 Stereotype 注解

1.10.2. 使用元注解和组合注解

1.10.3. 自动检测类和注册Bean定义

1.10.4. 使用Filter来自定义扫描

1.10.5. 在组件中定义Bean元数据

1.10.6. 命名自动检测的组件

1.10.7. 为自动检测的组件提供一个Scope

1.10.8. 用注解提供 Qualifier 元数据

1.10.9. 生成一个候选组件的索引

1.11. 使用JSR 330标准注解

1.11.1. 用 @Inject 和 @Named 进行依赖注入

1.11.2. @Named 和 @ManagedBean:与 @Component 注解的标准对等物

1.11.3. JSR-330 标准注解的局限性

1.12. 基于Java的容器配置

1.12.1. 基本概念:@Bean 和 @Configuration

1.12.2. 通过使用 AnnotationConfigApplicationContext 实例化Spring容器

简单构造

通过使用 register(Class…​) 以编程方式构建容器。

用 scan(String…​) 启用组件扫描。

用 AnnotationConfigWebApplicationContext 支持Web应用程序

1.12.3. 使用 @Bean 注解

声明一个 Bean

Bean 依赖

接收生命周期的回调

指定 Bean 的 Scope

使用 @Scope 注解

@Scope 和 scoped-proxy

自定义Bean的命名

Bean 别名

Bean 描述(Description)

1.12.4. 使用 @Configuration 注解

注入bean间的依赖

查询方法注入

关于基于Java的配置如何在内部工作的进一步信息

1.12.5. 构建基于Java的配置

使用 @Import 注解

在导入的 @Bean 定义上注入依赖

有条件地包括 @Configuration 类或 @Bean 方法

将Java和XML配置相结合

以XML为中心使用 @Configuration 类

@Configuration 以类为中心使用XML与 @ImportResource

1.13. Environment 抽象

1.13.1. Bean定义配置

使用 @Profile

XML Bean 定义配置

激活一个 Profile

默认 Profile

1.13.2. PropertySource 抽象

1.13.3. 使用 @PropertySource

1.13.4. 声明中的占位符解析

1.14. 注册 LoadTimeWeaver

1.15. ApplicationContext 的附加功能

1.15.1. 使用 MessageSource 进行国际化

1.15.2. 标准和自定义事件

基于注解的事件监听器

异步监听器

监听顺序

一般性事件

1.15.3. 方便地获取低级别的资源

1.15.4. 应用程序启动跟踪

总结


前言

本文主要讲解spring Framework的基础知识,案例经供参考。

一 Spring Framework简介

Spring Framework 是一个功能强大的 Java 应用程序框架,旨在提供高效且可扩展的开发环境。它结合了轻量级的容器和依赖注入功能,提供了一种使用 POJO 进行容器配置和面向切面的编程的简单方法,以及一组用于AOP的模块。Spring 框架还支持各种移动应用开发技术,如 Android 和 iOS。此外,它还提供了对事务管理、对象/关系映射、JavaBeans、JDBC、JMS 和其他技术的支持,从而确保高效开发。

1.1 Spring Framework系统架构

Spring Framework的每个模块(或组件)都可以单独存在,或者与其他一个或多个模块联合实现。每个模块的功能如下:

  • Spring Core(核心容器):核心容器提供 Spring 框架的基本功能。核心容器的主要组件是 BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转 (IOC) 模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。
  • Spring Context(上下文):Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如:JNDI、EJB、电子邮件、国际化、校验和调度功能。
  • Spring AOP:通过配置管理特性,Spring AOP 模块直接将面向方面的编程功能集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理的任何对象支持 AOP。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖 EJB 组件,就可以将声明性事务管理集成到应用程序中。
  • Spring DAO:JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构。
  • Spring ORM:Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map。所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构。
  • Spring Web 模块:Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。所以,Spring 框架支持与 Jakarta Struts 的集成。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。
  • Spring MVC 框架:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI。

从图中可以看出,IOC 的实现包 spring-beans 和 AOP 的实现包 spring-aop 也是整个框架的基础,而 spring-core 是整个框架的核心,基础的功能都在这里。

在此基础之上,spring-context 提供上下文环境,为各个模块提供粘合作用。

在 spring-context 基础之上提供了 spring-tx 和 spring-orm包,而web部分的功能,都是要依赖spring-web来实现的。

1.2 对spring的理解

术语 "Spring" 在不同的语境中意味着不同的东西。它可以用来指代Spring框架项目本身,它是一切的开始。随着时间的推移,其他Spring项目也被建立在Spring框架之上。大多数时候,当人们说 "Spring" 时,他们指的是整个项目家族(全家桶)。这个参考文档的重点是基础:Spring框架本身。

Spring 框架被划分为多个模块。应用程序可以选择他们需要的模块。core 是核心容器的模块,包括一个配置模型和一个依赖注入机制。除此之外,Spring框架还为不同的应用架构提供了基础支持,包括消息传递、事务性数据和持久性以及Web。它还包括基于Servlet的 Spring MVC Web 框架,以及并行的Spring WebFlux 响应式 web 框架。

关于模块的说明。Spring框架的jar允许部署到JDK 9的模块路径("Jigsaw")。为了在支持Jigsaw的应用程序中使用,Spring Framework 5的jar带有 "Automatic-Module-Name" 清单项,它定义了独立于jar artifact 名称的稳定的语言级模块名称("spring.core"、"spring.context" 等)(jar遵循相同的命名模式,以"-"代替".",例如 "spring-core" 和 "spring-context")。当然,Spring框架的jar在JDK 8和9+的classpath上都保持正常工作。


Spring框架支持依赖注入( JSR 330)和通用注解( JSR 250)规范,应用程序开发人员可以选择使用这些规范来代替Spring框架提供的Spring专用机制。最初,这些都是基于常见的 javax 包。

从Spring框架6.0开始,Spring已经升级到Jakarta EE 9级别(例如Servlet 5.0+,JPA 3.0+),基于 jakarta 命名空间而不是传统的 javax 包。由于EE 9是最低标准,并且已经支持EE 10,Spring准备为Jakarta EE API的进一步发展提供开箱即用的支持。Spring Framework 6.0与Tomcat 10.1、Jetty 11和Undertow 2.3作为Web服务器完全兼容,同时也与Hibernate ORM 6.1兼容。

随着时间的推移,Java/Jakarta EE在应用程序开发中的作用已经发生了变化。在J2EE和Spring的早期,应用程序是为了部署到应用服务器上而创建的。今天,在Spring Boot的帮助下,应用程序是以一种对开发者和云计算友好的方式创建的,Servlet容器是嵌入式的,并且易于改变。从Spring框架5开始,WebFlux应用程序甚至不直接使用Servlet API,可以在非Servlet容器的服务器(如Netty)上运行。

Spring不断创新,不断发展。除了Spring框架,还有其他项目,如Spring Boot、Spring Security、Spring Data、Spring Cloud、Spring Batch等。重要的是要记住,每个项目都有自己的源代码库、issue tracker 和发布节奏。参见 spring.io/projects,了解Spring项目的完整列表。

1.3  设计理念

当你了解一个框架时,重要的是不仅要知道它做什么,还要知道它遵循什么原则。下面是Spring框架的指导原则。

  • 在每个层面上提供选择。Spring让你尽可能晚地推迟设计决策。例如,你可以通过配置来切换持久化供应商,而不需要改变你的代码。对于许多其他基础设施问题和与第三方API的集成也是如此。

  • 适应不同的观点。Spring拥抱灵活性,对事情应该如何做不持意见。它支持具有不同视角的广泛的应用需求。

  • 保持强大的后向兼容性。Spring的演进是经过精心管理的,在不同的版本之间几乎不存在破坏性的变化。Spring支持一系列精心选择的JDK版本和第三方库,以方便维护依赖Spring的应用程序和库。

  • 关心API的设计。Spring团队花了很多心思和时间来制作直观的API,并且在很多版本和很多年中都能保持良好的效果。

  • 为代码质量设定高标准。Spring框架非常强调有意义的、最新的和准确的javadoc。它是为数不多的可以宣称代码结构干净、包与包之间没有循环依赖关系的项目之一。

二 核心

最重要的是Spring框架的反转控制(IoC)容器。在对Spring框架的IoC容器进行彻底处理后,紧接着是对Spring面向切面编程(AOP)技术的全面介绍。Spring框架有自己的AOP框架,在概念上很容易理解,它成功地解决了Java企业编程中 AOP 要求的 80% 的最佳需求点。

AOT处理可以用来提前(ahead-of-time)优化你的应用程序。它通常用于使用GraalVM的原生镜像部署。

1. IoC 容器

本章介绍了Spring的反转控制(IoC)容器。

1.1. Spring IoC容器和Bean简介

本章介绍了Spring框架对反转控制(IoC)原则的实现。IoC也被称为依赖注入(DI)。它是一个过程,对象仅通过构造参数、工厂方法的参数或在对象实例被构造或从工厂方法返回后在其上设置的属性来定义其依赖关系(即它们与之合作的其他对象)。然后容器在创建 bean 时注入这些依赖关系。这个过程从根本上说是Bean本身通过使用直接构建类或诸如服务定位模式的机制来控制其依赖关系的实例化或位置的逆过程(因此被称为控制反转)。

org.springframework.beans 和 org.springframework.context 包是Spring Framework的IoC容器的基础。 BeanFactory 接口提供了一种高级配置机制,能够管理任何类型的对象。 ApplicationContext 是 BeanFactory 的一个子接口。它增加了:

  • 更容易与Spring的AOP功能集成

  • Message resource 处理(用于国际化)

  • 事件发布

  • 应用层的特定上下文,如 WebApplicationContext,用于 web 应用

简而言之,BeanFactory 提供了配置框架和基本功能,而 ApplicationContext 则增加了更多的企业特定功能。ApplicationContext 是 BeanFactory 的一个完整的超集,在本章对Spring的IoC容器的描述中专门使用。关于使用 BeanFactory 而不是 ApplicationContext 的更多信息,请参见涵盖 BeanFactory API 的章节。

在Spring中,构成你的应用程序的骨干并由Spring IoC容器管理的对象被称为Bean。Bean是一个由Spring IoC容器实例化、组装和管理的对象。否则,Bean只是你的应用程序中众多对象中的一个。Bean以及它们之间的依赖关系都反映在容器使用的配置元数据中。

1.2. 容器概述

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

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

在大多数应用场景中,不需要明确的用户代码来实例化Spring IoC容器的一个或多个实例。例如,在Web应用场景中,通常只需在应用程序的 web.xml 文件中编写8行(或更多)模板式的Web描述符就足够了(参见 为web应用程序提供方便的 ApplicationContext 实例化)。如果你使用 Spring Tools for Eclipse(一个由Eclipse驱动的开发环境),你只需点击几下鼠标或按键就可以轻松创建这种模板配置。

下图显示了Spring工作方式的高层视图。你的应用程序类与配置元数据相结合,这样,在 ApplicationContext 被创建和初始化后,你就有了一个完全配置好的可执行系统或应用程序。

container magic

Figure 1. Spring IoC容器

1.2.1. 配置元数据

如上图所示,Spring IoC容器消费一种配置元数据。这种配置元数据代表了你,作为一个应用开发者,如何告诉Spring容器在你的应用中实例化、配置和组装对象。

配置元数据传统上是以简单直观的XML格式提供的,这也是本章大部分内容用来传达Spring IoC容器的关键概念和特性。

基于XML的元数据并不是配置元数据的唯一允许形式。Spring IoC容器本身与这种配置元数据的实际编写格式是完全解耦的。如今,许多开发者为他们的Spring应用程序选择 基于Java的配置

关于在Spring容器中使用其他形式的元数据的信息,请参见。

Spring的配置包括至少一个,通常是一个以上的Bean定义,容器必须管理这些定义。基于XML的配置元数据将这些Bean配置为顶层 <beans/> 元素内的 <bean/> 元素。Java配置通常使用 @Configuration 类中的 @Bean 注解的方法。

这些Bean的定义对应于构成你的应用程序的实际对象。通常,你会定义服务层对象、持久层对象(如存储库或数据访问对象(DAO))、表现对象(如Web控制器)、基础设施对象(如JPA EntityManagerFactory)、JMS队列等等。通常,人们不会在容器中配置细粒度的domain对象,因为创建和加载domain对象通常是 repository 和业务逻辑的责任。

下面的例子显示了基于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">

    <bean id="..." class="...">  
        <!-- 这个bean的合作者和配置在这里 -->
    </bean>

    <bean id="..." class="...">
        <!-- c这个bean的合作者和配置在这里 -->
    </bean>

    <!-- 更多bean 定义在这里 -->

</beans>

id 属性是一个字符串,用于识别单个Bean定义。
class 属性定义了 Bean 的类型,并使用类的全路径名。

id 属性的值可以用来指代协作对象。本例中没有显示用于引用协作对象的XML。更多信息请参见 依赖

1.2.2. 实例化一个容器

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

Java

Kotlin

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

在了解了Spring的IoC容器后,你可能想了解更多关于Spring的 Resource 抽象(如 资源(Resources) 中所述),它为从URI语法中定义的位置读取 InputStream 提供了方便的机制。特别是,Resource 路径被用来构建应用上下文,如 Application Context 和资源路径 中所述。

下面的例子显示了 service 对象(services.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>

下面的例子显示了数据访问对象(data access object) 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">

    <bean id="accountDao"
        class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions for data access objects go here -->

</beans>

在前面的例子中,服务层由 PetStoreServiceImpl 类和两个类型为 JpaAccountDao 和 JpaItemDao 的数据访问对象组成(基于JPA对象-关系映射标准)。property name 元素指的是 JavaBean 属性的名称,而 ref 元素指的是另一个Bean定义的名称。id 和 ref 元素之间的这种联系表达了协作对象之间的依赖关系。关于配置一个对象的依赖关系的细节,请看 依赖

构建基于XML的配置元数据

让Bean的定义跨越多个XML文件可能很有用。通常情况下,每个单独的XML配置文件代表了你架构中的一个逻辑层或模块。

你可以使用 application context 构造函数从所有这些XML片段中加载Bean定义。这个构造函数需要多个 Resource 位置,如 上一节 所示。或者,使用一个或多个 <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>

在前面的例子中,外部Bean定义从三个文件中加载:services.xmlmessageSource.xml 和 themeSource.xml。所有的位置路径都是相对于进行导入的定义文件而言的,所以 services.xml 必须与进行导入的文件在同一目录或 classpath 位置,而 messageSource.xml 和 themeSource.xml 必须在导入文件的位置以下的 resources 位置。正如你所看到的,前导斜线会被忽略。然而,鉴于这些路径是相对的,最好不要使用斜线。被导入文件的内容,包括顶层的 <beans/> 元素,必须是有效的XML Bean定义,根据Spring Schema。

使用相对的 "../" 路径来引用父目录中的文件是可能的,但不推荐这样做。这样做会造成对当前应用程序之外的文件的依赖。特别是,这种引用不推荐用于 classpath: URL(例如, classpath:../services.xml),其中运行时解析过程选择 "最近的" classpath root,然后查找其父目录。Classpath配置的变化可能导致选择不同的、不正确的目录。

你总是可以使用完全限定的资源位置而不是相对路径:例如,file:C:/config/services.xml 或 classpath:/config/services.xml。然而,请注意,你正在将你的应用程序的配置与特定的绝对位置相耦合。一般来说,最好是为这种绝对位置保留一个指示 - 例如,通过 "${…​}" 占位符,在运行时针对JVM系统属性(system properties)进行解析。

命名空间本身提供了导入指令的功能。除了普通的Bean定义之外,更多的配置功能可以在Spring提供的一些XML命名空间中获得,例如,context 和 util 命名空间。

Groovy Bean Definition DSL

作为外部化配置元数据的另一个例子,Bean定义也可以用Spring的Groovy Bean Definition DSL来表达,正如Grails框架所知道的。通常情况下,这种配置存在于 ".groovy" 文件中,其结构如下例所示。

beans {
    dataSource(BasicDataSource) {
        driverClassName = "org.hsqldb.jdbcDriver"
        url = "jdbc:hsqldb:mem:grailsDB"
        username = "sa"
        password = ""
        settings = [mynew:"setting"]
    }
    sessionFactory(SessionFactory) {
        dataSource = dataSource
    }
    myService(MyService) {
        nestedBean = { AnotherBean bean ->
            dataSource = dataSource
        }
    }
}

这种配置风格基本上等同于XML Bean定义,甚至支持Spring的XML配置命名空间。它还允许通过 importBeans 指令导入XML Bean定义文件。

1.2.3. 使用容器

ApplicationContext 是一个高级工厂的接口,能够维护不同Bean及其依赖关系的注册表。通过使用方法 T getBean(String name, Class<T> requiredType),你可以检索到Bean的实例。

ApplicationContext 可以让你读取Bean定义(definition)并访问它们,如下例所示。

Java

Kotlin

// 创建和配置bean
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

// 检索配置的实例
PetStoreService service = context.getBean("petStore", PetStoreService.class);

// 使用配置的实例
List<String> userList = service.getUsernameList();

通过Groovy配置,引导看起来非常相似。它有一个不同的 context 实现类,它能识别Groovy(但也能理解XML bean定义)。下面的例子显示了 Groovy 配置。

Java

Kotlin

ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");

最灵活的变体是 GenericApplicationContext 与 reader delegate 的结合—​例如,与 XmlBeanDefinitionReader 一起用于XML文件,如下例所示。

Java

Kotlin

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

你也可以将 GroovyBeanDefinitionReader 用于Groovy文件,如下例所示。

Java

Kotlin

GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();

你可以在同一个 ApplicationContext 上混合和匹配这样的 reader delegate,从不同的配置源读取bean定义。

然后你可以使用 getBean 来检索Bean的实例。ApplicationContext 接口还有其他一些检索Bean的方法,但理想情况下,你的应用代码不应该使用这些方法。事实上,你的应用程序代码根本就不应该调用 getBean() 方法,因此对Spring的API根本就没有依赖性。例如,Spring与Web框架的集成为各种Web框架组件(如 controller 和JSF管理的Bean)提供了依赖注入,让你通过元数据(如autowiring注解)声明对特定Bean的依赖。

1.3. Bean 概览

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

在容器本身中,这些Bean定义被表示为 BeanDefinition 对象,它包含(除其他信息外)以下元数据。

  • 一个全路径类名:通常,被定义的Bean的实际实现类。

  • Bean的行为配置元素,它说明了Bean在容器中的行为方式(scope、生命周期回调,等等)。

  • 对其他Bean的引用,这些Bean需要做它的工作。这些引用也被称为合作者或依赖。

  • 要在新创建的对象中设置的其他配置设置—​例如,pool的大小限制或在管理连接池的Bean中使用的连接数。

这个元数据转化为构成每个Bean定义的一组属性。下表描述了这些属性。

Table 1. bean definition
属性解释…​

Class

实例化 Bean

Name

Bean 命名

Scope

Bean Scope

Constructor arguments

依赖注入

Properties

依赖注入

Autowiring mode

注入协作者(Autowiring Collaborators)

Lazy initialization mode

懒加载的Bean

Initialization method

初始化回调

Destruction method

销毁回调

除了包含如何创建特定 Bean 的信息的 Bean 定义外,ApplicationContext 实现还允许注册在容器外(由用户)创建的现有对象。这是通过 getBeanFactory() 方法访问 ApplicationContext 的 BeanFactory 来实现的,该方法返回 DefaultListableBeanFactory 实现。DefaultListableBeanFactory 通过 registerSingleton(..) 和 registerBeanDefinition(..) 方法支持这种注册。然而,典型的应用程序只与通过常规Bean定义元数据定义的Bean一起工作。

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

1.3.1. Bean 命名

每个Bean都有一个或多个标识符(identifier)。这些标识符在承载Bean的容器中必须是唯一的。一个Bean通常只有一个标识符。然而,如果它需要一个以上的标识符,多余的标识符可以被视为别名。

在基于XML的配置元数据中,你可以使用 id 属性、name 属性或两者来指定Bean标识符。id 属性允许你精确地指定一个 id。传统上,这些名字是字母数字('myBean'、'someService’等),但它们也可以包含特殊字符。如果你想为Bean引入其他别名,你也可以在 name 属性中指定它们,用逗号(,)、分号(;)或空格分隔。尽管 id 属性被定义为 xsd:string 类型,但 bean id 的唯一性是由容器强制执行的,尽管不是由 XML 解析器执行。

你不需要为Bean提供一个 name 或 id。如果你不明确地提供 name 或 id,容器将为该 Bean 生成一个唯一的名称。然而,如果你想通过使用 ref 元素或服务定位器风格的查找来引用该 bean 的名称,你必须提供一个名称。不提供名字的动机与使用 内部Bean 和 注入协作者(Autowiring Collaborators) 有关。

Bean的命名规则

惯例是在命名Bean时使用标准的Java惯例来命名实例字段名。也就是说,Bean的名字以小写字母开始,然后以驼峰字母开头。这种名称的例子包括 accountManageraccountServiceuserDaologinController 等等。

统一命名Bean使你的配置更容易阅读和理解。另外,如果你使用Spring AOP,在对一组按名称相关的Bean应用 advice 时,也有很大的帮助。

在classpath中的组件扫描(component scanning),Spring为未命名的组件生成Bean名称,遵循前面描述的规则:基本上,取简单的类名并将其初始字符变成小写。然而,在(不寻常的)特殊情况下,当有一个以上的字符,并且第一个和第二个字符都是大写时,原来的大小写会被保留下来。这些规则与 java.beans.Introspector.decapitalize(Spring在此使用)所定义的规则相同。
在 Bean Definition 之外对Bean进行别名

在 Bean 定义中,你可以为Bean提供一个以上的名字,通过使用由 id 属性指定的最多一个名字和 name 属性中任意数量的其他名字的组合。这些名字可以是同一个Bean的等效别名,在某些情况下很有用,比如让应用程序中的每个组件通过使用一个特定于该组件本身的Bean名字来引用一个共同的依赖关系。

然而,在实际定义Bean的地方指定所有别名并不总是足够的。有时,为一个在其他地方定义的Bean引入别名是可取的。这种情况通常发生在大型系统中,配置被分割到每个子系统中,每个子系统都有自己的对象定义集。在基于XML的配置元数据中,你可以使用 <alias/> 元素来实现这一点。下面的例子展示了如何做到这一点。

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

在这种情况下,一个名为 fromName 的bean(在同一个容器中)在使用这个别名定义后,也可以被称为 toName

例如,子系统A的配置元数据可以引用一个名为 subsystemA-dataSource 的数据源。子系统B的配置元数据可以引用一个名为 subsystemB-dataSource 的数据源。当组成使用这两个子系统的主应用程序时,主应用程序以 myApp-dataSource 的名字来引用数据源。为了让这三个名字都指代同一个对象,你可以在配置元数据中添加以下别名定义。

<alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
<alias name="myApp-dataSource" alias="subsystemB-dataSource"/>

现在,每个组件和主应用程序都可以通过一个独特的名称来引用dataSource,并保证不与任何其他定义冲突(有效地创建了一个命名空间),但它们引用的是同一个bean。

Java 配置

如果你使用Java配置,@Bean 注解可以被用来提供别名。详情请参见 使用 @Bean 注解

1.3.2. 实例化 Bean

bean 定义(definition)本质上是创建一个或多个对象的“配方”。容器在被要求时查看命名的Bean的“配方”,并使用该Bean定义所封装的配置元数据来创建(或获取)一个实际的对象。

如果你使用基于XML的配置元数据,你要在 <bean/> 元素的 class 属性中指定要实例化的对象的类型(或class)。这个 class 属性(在内部是 BeanDefinition 实例的 Class 属性)通常是强制性的。(关于例外情况,请看 用实例工厂方法进行实例化 和 Bean 定义(Definition)的继承)。你可以以两种方式之一使用 Class 属性。

  • 通常,在容器本身通过反射式地调用构造函数直接创建Bean的情况下,指定要构造的Bean类,有点相当于Java代码中的 new 操作符。

  • 在不太常见的情况下,即容器在一个类上调用 static 工厂方法来创建 bean 时,要指定包含被调用的 static 工厂方法的实际类。从 static 工厂方法的调用中返回的对象类型可能是同一个类或完全是另一个类。

嵌套类名

如果你想为一个嵌套类配置一个Bean定义(definition),你可以使用嵌套类的二进制名称或源(source)名称。

例如,如果你在 com.example 包中有一个叫做 SomeThing 的类,而这个 SomeThing 类有一个叫做 OtherThing 的静态嵌套类,它们可以用美元符号($)或点(.)分开。所以在Bean定义中的 class 属性的值将是 com.example.SomeThing$OtherThing 或 com.example.SomeThing.OtherThing

用构造函数进行实例化

当你用构造函数的方法创建一个Bean时,所有普通的类都可以被Spring使用并与之兼容。也就是说,被开发的类不需要实现任何特定的接口,也不需要以特定的方式进行编码。只需指定Bean类就足够了。然而,根据你对该特定Bean使用的IoC类型,你可能需要一个默认(空)构造函数。

Spring IoC容器几乎可以管理任何你希望它管理的类。它并不局限于管理真正的JavaBean。大多数Spring用户更喜欢真正的JavaBean,它只有一个默认的(无参数)构造函数,以及按照容器中的属性建模的适当的setter和getter。你也可以在你的容器中拥有更多奇特的非bean风格的类。例如,如果你需要使用一个绝对不遵守JavaBean规范的传统连接池,Spring也可以管理它。

通过基于XML的配置元数据,你可以按以下方式指定你的bean类。

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

<bean name="anotherExample" class="examples.ExampleBeanTwo"/>

关于向构造函数提供参数(如果需要)和在对象被构造后设置对象实例属性的机制的详细信息,请参见 依赖注入

用静态工厂方法进行实例化

在定义一个用静态工厂方法创建的Bean时,使用 class 属性来指定包含 static 工厂方法的类,并使用名为 factory-method 的属性来指定工厂方法本身的名称。你应该能够调用这个方法(有可选的参数,如后文所述)并返回一个活的对象,随后该对象被视为通过构造函数创建的。这种Bean定义的一个用途是在遗留代码中调用 static 工厂。

下面的Bean定义规定,Bean将通过调用工厂方法来创建。该定义并没有指定返回对象的类型(class),而是指定了包含工厂方法的类。在这个例子中,createInstance() 方法必须是一个 static 方法。下面的例子显示了如何指定一个工厂方法。

<bean id="clientService"
    class="examples.ClientService"
    factory-method="createInstance"/>

下面的例子显示了一个可以与前面的Bean定义(definition)一起工作的类。

Java

Kotlin

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。

<!-- the factory bean, which contains a method called createInstance() -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
    <!-- inject any dependencies required by this locator bean -->
</bean>

<!-- the bean to be created via the factory bean -->
<bean id="clientService"
    factory-bean="serviceLocator"
    factory-method="createClientServiceInstance"/>

下面的例子显示了相应的类。

Java

Kotlin

public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();

    public ClientService createClientServiceInstance() {
        return clientService;
    }
}

一个工厂类也可以容纳一个以上的工厂方法,如下例所示。

<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"/>

下面的例子显示了相应的类。

Java

Kotlin

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文档中,“factory bean” 是指在Spring容器中配置的Bean,它通过 实例 或 静态工厂方法创建对象。相比之下,FactoryBean(注意大写字母)是指Spring特定的FactoryBean 实现类。
确定Bean的运行时类型

要确定一个特定Bean的运行时类型是不容易的。在Bean元数据定义中指定的类只是一个初始的类引用,可能与已声明的工厂方法相结合,或者是一个 FactoryBean 类,这可能导致Bean的运行时类型不同,或者在实例级工厂方法的情况下根本没有被设置(而是通过指定的 factory-bean 名称来解决)。此外,AOP代理可能会用基于接口的代理来包装Bean实例,对目标Bean的实际类型(只是其实现的接口)的暴露有限。

要了解某个特定Bean的实际运行时类型,推荐的方法是对指定的Bean名称进行 BeanFactory.getType 调用。这将考虑到上述所有情况,并返回 BeanFactory.getBean 调用将为同一Bean名称返回的对象类型。

1.4. 依赖

一个典型的企业应用程序并不是由单一的对象(或Spring术语中的bean)组成的。即使是最简单的应用也有一些对象,它们一起工作,呈现出最终用户所看到的连贯的应用。下一节将解释你如何从定义一些单独的Bean定义到一个完全实现的应用,在这个应用中,各对象相互协作以实现一个目标。

1.4.1. 依赖注入

依赖注入(DI)是一个过程,对象仅通过构造参数、工厂方法的参数或在对象实例被构造或从工厂方法返回后在其上设置的属性来定义它们的依赖(即与它们一起工作的其它对象)。然后,容器在创建 bean 时注入这些依赖。这个过程从根本上说是Bean本身通过使用类的直接构造或服务定位模式来控制其依赖的实例化或位置的逆过程(因此被称为控制反转)。

采用DI原则,代码会更干净,当对象被提供其依赖时,解耦会更有效。对象不会查找其依赖,也不知道依赖的位置或类别。因此,你的类变得更容易测试,特别是当依赖是在接口或抽象基类上时,这允许在单元测试中使用stub或mock实现。

DI有两个主要的变体。 基于构造器的依赖注入 和 基于setter的依赖注入

基于构造器的依赖注入

基于构造函数的 DI 是通过容器调用带有许多参数的构造函数来完成的,每个参数代表一个依赖。调用带有特定参数的 static 工厂方法来构造 bean 几乎是等价的,本讨论对构造函数的参数和 static 工厂方法的参数进行类似处理。下面的例子显示了一个只能用构造函数注入的依赖注入的类。

Java

Kotlin

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...
}

请注意,这个类并没有什么特别之处。它是一个POJO,对容器的特定接口、基类或注解没有依赖。

构造函数参数解析

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

Java

Kotlin

package x.y;

public class ThingOne {

    public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
        // ...
    }
}

假设 ThingTwo 和 ThingThree 类没有继承关系,就不存在潜在的歧义。因此,下面的配置可以正常工作,你不需要在 <constructor-arg/> 元素中明确指定构造函数参数的索引或类型。

<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不能确定值的类型,所以在没有帮助的情况下不能通过类型进行匹配。考虑一下下面这个类。

Java

Kotlin

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 属性来明确指定构造函数参数的索引,如下例所示。

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

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

索引(下标)从0开始。

构造函数参数名

你也可以使用构造函数的参数名称来进行消歧,如下面的例子所示。

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

请记住,要使这一方法开箱即用,你的代码在编译时必须启用debug标志,以便Spring能够从构造函数中查找参数名称。如果你不能或不想用debug标志编译你的代码,你可以使用 @ConstructorProperties JDK注解来明确命名你的构造函数参数。这样一来,示例类就得如下。

Java

Kotlin

package examples;

public class ExampleBean {

    // Fields omitted

    @ConstructorProperties({"years", "ultimateAnswer"})
    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}

基于Setter的依赖注入

基于 Setter 的 DI 是通过容器在调用无参数的构造函数或无参数的 static 工厂方法来实例化你的 bean 之后调用 Setter 方法来实现的。

下面的例子显示了一个只能通过使用纯 setter 注入的类的依赖注入。这个类是传统的Java。它是一个POJO,对容器的特定接口、基类(base class)或注解没有依赖。

Java

Kotlin

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容器实例。

基于构造器的DI还是基于setter的DI?

由于你可以混合使用基于构造函数的DI和基于setter的DI,一个好的经验法则是对强制依赖使用构造函数,对可选依赖使用setter方法或配置方法。请注意,在setter方法上使用 @Autowired 注解可以使属性成为必须的依赖;然而,带有参数程序化验证的构造器注入是更好的。

Spring团队通常提倡构造函数注入,因为它可以让你将应用组件实现为不可变的对象,并确保所需的依赖不为 null。此外,构造函数注入的组件总是以完全初始化的状态返回给客户端(调用)代码。顺便提一下,大量的构造函数参数是一种不好的代码气味,意味着该类可能有太多的责任,应该重构以更好地解决适当的分离问题。

Setter注入主要应该只用于在类中可以分配合理默认值的可选依赖。否则,必须在代码使用依赖的所有地方进行非null值检查。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 Scope 中定义。否则,Bean只有在被请求时才会被创建。创建 bean 有可能导致创建 bean 图(graph),因为 bean 的依赖关系和它的依赖关系(等等)被创建和分配。请注意,这些依赖关系之间的解析不匹配可能会出现得很晚—​也就是说,在第一次创建受影响的Bean时。

循环依赖

如果你使用主要的构造函数注入,就有可能产生一个无法解决的循环依赖情况。

比如说。类A通过构造函数注入需要类B的一个实例,而类B通过构造函数注入需要类A的一个实例。如果你将A类和B类的Bean配置为相互注入,Spring IoC容器会在运行时检测到这种循环引用,并抛出一个 BeanCurrentlyInCreationException

一个可能的解决方案是编辑一些类的源代码,使其通过setter而不是构造器进行配置。或者,避免构造器注入,只使用setter注入。换句话说,虽然不推荐这样做,但你可以用setter注入来配置循环依赖关系。

与典型的情况(没有循环依赖关系)不同,Bean A和Bean B之间的循环依赖关系迫使其中一个Bean在被完全初始化之前被注入到另一个Bean中(一个典型的鸡生蛋蛋生鸡的场景)。

一般来说,你可以相信Spring会做正确的事情。它在容器加载时检测配置问题,例如对不存在的bean的引用和循环依赖。在实际创建Bean时,Spring尽可能晚地设置属性和解析依赖关系。这意味着,当你请求一个对象时,如果在创建该对象或其某个依赖关系时出现问题,已经正确加载的Spring容器就会产生一个异常—​例如,Bean由于缺少或无效的属性而抛出一个异常。这种对某些配置问题的潜在延迟可见性是 ApplicationContext 实现默认预置单例Bean的原因。在实际需要之前创建这些Bean需要付出一些前期时间和内存的代价,当 ApplicationContext 被创建时,你会发现配置问题,而不是后来。你仍然可以覆盖这个默认行为,这样单例Bean就会懒加载地初始化,而不是急切地预实例化。

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

依赖注入的例子

下面的例子将基于XML的配置元数据用于基于setter的DI。一个Spring XML配置文件的一小部分指定了一些Bean的定义,如下所示。

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- setter injection using the nested ref element -->
    <property name="beanOne">
        <ref bean="anotherExampleBean"/>
    </property>

    <!-- setter injection using the neater ref attribute -->
    <property name="beanTwo" ref="yetAnotherBean"/>
    <property name="integerProperty" value="1"/>
</bean>

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

下面的例子显示了相应的 ExampleBean 类。

Java

Kotlin

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

    public void setBeanOne(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }

    public void setBeanTwo(YetAnotherBean beanTwo) {
        this.beanTwo = beanTwo;
    }

    public void setIntegerProperty(int i) {
        this.i = i;
    }
}

在前面的例子中,setter被声明为与XML文件中指定的属性相匹配。下面的例子使用基于构造函数的DI。

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- constructor injection using the nested ref element -->
    <constructor-arg>
        <ref bean="anotherExampleBean"/>
    </constructor-arg>

    <!-- constructor injection using the neater ref attribute -->
    <constructor-arg ref="yetAnotherBean"/>

    <constructor-arg type="int" value="1"/>
</bean>

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

下面的例子显示了相应的 ExampleBean 类。

Java

Kotlin

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

    public ExampleBean(
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
        this.beanOne = anotherBean;
        this.beanTwo = yetAnotherBean;
        this.i = i;
    }
}

在 bean 定义中指定的构造器参数被用作 ExampleBean 的构造器参数。

现在考虑这个例子的一个变体,即不使用构造函数,而是让Spring调用一个 static 工厂方法来返回对象的实例。

<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 类。

Java

Kotlin

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;
    }
}

static 工厂方法的参数由 <constructor-arg/> 元素提供,与实际使用的构造函数完全相同。被工厂方法返回的类的类型不一定与包含 static 工厂方法的类的类型相同(尽管在这个例子中,它是相同的)。实例(非静态)工厂方法可以以基本相同的方式使用(除了使用 factory-bean 属性而不是 class 属性),所以我们在此不讨论这些细节。

1.4.2. 依赖和配置的细节

正如 上一节 所述,你可以将Bean属性和构造函数参数定义为对其他托管Bean(协作者)的引用,或者定义为内联的值。Spring的基于XML的配置元数据支持 <property/> 和 <constructor-arg/> 元素中的子元素类型,以达到这个目的。

字面值 (基本类型、 String 等)

<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-namespace 来实现更简洁的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>

前面的XML更简洁。然而,除非你使用的IDE(如 IntelliJ IDEA 或 Spring Tools for Eclipse)支持在你创建Bean定义时自动补全属性,否则错别字会在运行时而非设计时发现。强烈建议使用这样的IDE帮助。

你也可以配置一个 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容器通过使用 JavaBean 的 PropertyEditor 机制将 <value/> 元素中的文本转换为 java.util.Properties 实例。这是一个很好的捷径,也是Spring团队倾向于使用嵌套的 <value/> 元素而不是 value 属性风格的几个地方之一。

idref 元素

idref 元素仅仅是将容器中另一个 bean 的 id(一个字符串值—​不是引用)传递给 <constructor-arg/> 或 <property/> 元素的一种防错方式。下面的例子展示了如何使用它。

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

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

前面的Bean定义片段完全等同于(在运行时)下面的片段。

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

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

第一种形式比第二种形式好,因为使用 idref 标签可以让容器在部署时验证被引用的、命名的 bean 是否真的存在。在第二种变体中,没有对传递给 client Bean 的 targetName 属性的值进行验证。只有在 client Bean实际被实例化时,才会发现错误(很可能是致命的结果)。如果 client Bean是一个 prototype Bean,那么这个错别字和由此产生的异常可能只有在容器被部署后很久才能被发现。

4.0 版Bean XSD中不再支持 idref 元素上的 local 属性,因为它不再提供比普通 bean 引用更多的价值。在升级到4.0 schema时,将你现有的 idref local 引用改为 idref bean

<idref/> 元素带来价值的一个常见地方(至少在早于Spring 2.0的版本中)是在 ProxyFactoryBean Bean定义中配置 AOP interceptor(拦截器)。当你指定拦截器名称时,使用 <idref/> 元素可以防止你把拦截器的ID拼错。

对其他Bean的引用(合作者)

ref 元素是 <constructor-arg/> 或 <property/> 定义元素中的最后一个元素。在这里,你把一个 bean 的指定属性的值设置为对容器所管理的另一个 bean(协作者)的引用。被引用的 bean 是其属性要被设置的 bean 的依赖关系,它在属性被设置之前根据需要被初始化。(如果协作者是一个单例bean,它可能已经被容器初始化了)。所有的引用最终都是对另一个对象的引用。scope和验证取决于你是否通过 bean 或 parent 属性来指定其他对象的ID或名称。

通过 <ref/> 标签的 bean 属性指定目标 bean 是最一般的形式,它允许创建对同一容器或父容器中的任何 bean 的引用,不管它是否在同一个 XML 文件中。bean 属性的值可以与目标bean的 id 属性相同,或者与目标bean的 name 属性中的一个值相同。下面的例子显示了如何使用一个 ref 元素。

<ref bean="someBean"/>

通过 parent 属性指定目标Bean,可以创建对当前容器的父容器中的Bean的引用。 parent 属性的值可以与目标Bean的 id 属性或目标Bean的 name 属性中的一个值相同。目标Bean必须在当前容器的一个父容器中。当你有一个分层的容器,你想用一个与父级Bean同名的代理来包装父级容器中的现有Bean时,你应该使用这种Bean引用变体。下面的一对列表展示了如何使用 parent 属性。

<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
    <!-- insert dependencies as required here -->
</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>

在4.0 beans XSD中不再支持 ref 元素上的 local 属性,因为它不再提供比普通 bean 引用更多的价值。在升级到4.0 schema时,将你现有的 ref local 引用改为 ref bean
内部 Bean

在 <property/> 或 <constructor-arg/> 元素内的 <bean/> 元素定义了一个内部Bean,如下例所示。

<bean id="outer" class="...">
    <!-- 而不是使用对目标Bean的引用,只需在行内定义目标Bean即可 -->
    <property name="target">
        <bean class="com.example.Person"> <!-- 这是内部Bean -->
            <property name="name" value="Fiona Apple"/>
            <property name="age" value="25"/>
        </bean>
    </property>
</bean>

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

作为一个转折点,可以从自定义scope中接收销毁回调—​例如,对于包含在单例 bean 中的请求scope的内层 bean。内层 bean 实例的创建与它所包含的 bean 相联系,但是销毁回调让它参与到请求作用域的生命周期中。这并不是一种常见的情况。内层Bean通常只是共享其包含Bean的scope。

集合(Collection)

<list/><set/><map/> 和 <props/> 元素分别设置Java Collection 类型 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 的 key 值或 value 值,或 set 值,也可以是以下任何元素。

bean | ref | idref | list | set | map | props | value | null

集合合并

Spring容器也支持合并集合。开发者可以定义一个父 <list/><map/><set/> 或 <props/> 元素,让子 <list/><map/><set/> 或 <props/> 元素继承和覆盖父集合的值。也就是说,子集合的值是合并父集合和子集合的元素的结果,子集合的元素覆盖父集合中指定的值。

关于合并的这一节讨论了父子bean机制。不熟悉父子Bean定义的读者可能希望在继续阅读 相关章节

下面的例子演示了集合的合并。

<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>

注意在子Bean定义的 adminEmails 属性的 <props/> 元素上使用了 merge=true 属性。当 child Bean被容器解析并实例化时,产生的实例有一个 adminEmails Properties 集合,它包含了将 child Bean的 adminEmails 集合与父Bean的 adminEmails 集合合并的结果。下面的列表显示了这个结果。

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

子代 Properties 集合的值继承了父代 <props/> 中的所有属性元素,子代的 support 值会覆盖父代集合中的值。

这种合并行为类似于适用于 <list/><map/> 和 <set/> 集合类型。在 <list/> 元素的特殊情况下,与 List 集合类型相关的语义(也就是值的有序集合的概念)被保持。父列表的值在所有子列表的值之前。在 MapSet 和 Properties 集合类型的情况下,不存在排序。因此,对于容器在内部使用的相关的 MapSet 和 Properties 实现类型的基础上的集合类型,没有排序语义。

集合合并的限制

你不能合并不同的集合类型(例如 Map 和 List)。如果你试图这样做,会抛出一个适当的 Exceptionmerge 属性必须被指定在较低的、继承的、子定义上。在父级集合定义上指定 merge 属性是多余的,并且不会导致期望的合并。

强类型的集合

由于Java对泛型的支持,你可以使用强类型的 Collection。也就是说,我们可以声明一个 Collection 类型,使其只能包含(例如)String 元素。如果你使用Spring将一个强类型的 Collection 依赖性注入到Bean中,你可以利用Spring的类型转换支持,这样你的强类型 Collection 实例的元素在被添加到集合中之前就被转换为适当的类型。下面的Java类和Bean定义展示了如何做到这一点。

Java

Kotlin

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>

当 something bean 的 account 属性准备注入时,关于强类型的 Map<String, Float> 的元素类型的泛型信息可以通过反射获得。因此,Spring的类型转换基础设施将各种值元素识别为 Float 类型,而字符串值(9.992.75 和 3.99)被转换为实际的 Float 类型。

Null and Empty String Values

Spring将属性等的空参数视为空字符串。下面这个基于XML的配置元数据片段将 email 属性设置为空字符串值("")。

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

前面的例子相当于下面的Java代码。

Java

Kotlin

exampleBean.setEmail("");

<null/> 元素处理 null 值。下面的列表显示了一个例子。

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

前面的配置等同于以下Java代码。

Java

Kotlin

exampleBean.setEmail(null);

使用p命名空间的XML快捷方式

p-namespace(命名空间) 让你使用 bean 元素的属性(而不是嵌套的 <property/> 元素)来描述你的属性值合作Bean,或者两者都是。

Spring支持具有 命名空间 的可扩展配置格式,这些命名空间是基于XML Schema定义的。本章讨论的 beans 配置格式是在 XML Schema 文件中定义的。然而,p-namespace 没有在XSD文件中定义,只存在于Spring的核心(core)中。

下面的例子显示了两个XML片段(第一个使用标准的XML格式,第二个使用p-namespace),它们的解析结果相同。

<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 name="classic" class="com.example.ExampleBean">
        <property name="email" value="someone@somewhere.com"/>
    </bean>

    <bean name="p-namespace" class="com.example.ExampleBean"
        p:email="someone@somewhere.com"/>
</beans>

这个例子显示了在bean定义中,p-namespace中有一个名为 email 的属性。这告诉Spring包括一个属性声明。如前所述,p-namespace没有schema定义,所以你可以将attribute的名称设置为property名称。

接下来的例子包括了另外两个Bean定义,它们都有对另一个Bean的引用。

<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 name="john-classic" class="com.example.Person">
        <property name="name" value="John Doe"/>
        <property name="spouse" ref="jane"/>
    </bean>

    <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>

这个例子不仅包括使用p命名空间的属性值,而且还使用了一种特殊的格式来声明属性引用。第一个Bean定义使用 <property name="spouse" ref="jane"/> 来创建一个从Bean john 到Bean jane 的引用,而第二个Bean定义使用 p:spouse-ref="jane" 作为属性来做完全相同的事情。在这种情况下,spouse 是属性名称,而 -ref 部分表明这不是一个直接的值,而是对另一个bean的引用。

p命名空间不像标准的XML格式那样灵活。例如,声明属性引用的格式与以 Ref 结尾的属性发生冲突,而标准的XML格式则不会。我们建议你仔细选择你的方法,并将其传达给你的团队成员,以避免产生同时使用三种方法的XML文档。
使用c命名空间的XML快捷方式

与 使用p命名空间的XML快捷方式 类似,Spring 3.1中引入的c命名空间允许配置构造器参数的内联属性,而不是嵌套的 constructor-arg 元素。

下面的例子使用 c: 命名空间来做与 基于构造器的依赖注入 相同的事情。

<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"/>

    <!-- 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-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"/>

</beans>

c: 命名空间使用了与 p: 命名空间相同的约定(Bean引用的尾部 -ref),用于按名称设置构造函数参数。同样,它也需要在XML文件中声明,尽管它没有在XSD schema中定义(它存在于Spring 核心(core)中)。

对于构造函数参数名称不可用的罕见情况(通常是字节码编译时没有debug信息),你可以使用回退到参数索引(下标),如下所示。

<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
    c:_2="something@somewhere.com"/>

由于XML语法的原因,索引符号需要有前面的 _,因为XML属性名不能以数字开头(尽管有些IDE允许这样做)。相应的索引符号也可用于 <constructor-arg> 元素,但并不常用,因为通常情况下,普通的声明顺序已经足够了。

在实践中,构造函数解析 机制 在匹配参数方面相当有效,所以除非你真的需要,否则我们建议在整个配置中使用名称符号。

复合属性名

当你设置Bean属性时,你可以使用复合或嵌套的属性名,只要路径中除最终属性名外的所有组件不为 null。考虑一下下面的Bean定义。

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

something Bean有一个 fred 属性,它有一个 bob 属性,它有一个 sammy 属性,最后的 sammy 属性被设置为 123 的值。为了使这个方法奏效,something 的 fred 属性和 fred 的 bob 属性在构建 bean 后不能为 null。否则就会抛出一个 NullPointerException

1.4.3. 使用 depends-on

如果一个Bean是另一个Bean的依赖,这通常意味着一个Bean被设置为另一个Bean的一个属性。通常,你可以通过基于XML的配置元数据中的 <ref/> 元素 来实现这一点。然而,有时Bean之间的依赖关系并不那么直接。一个例子是当一个类中的静态初始化器需要被触发时,比如数据库驱动程序的注册。depends-on 属性可以明确地强制一个或多个Bean在使用此元素的Bean被初始化之前被初始化。下面的例子使用 depends-on 属性来表达对单个 bean 的依赖性。 s

<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />

要表达对多个Bean的依赖,请提供一个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" />

depends-on 属性可以指定初始化时间的依赖关系,而在 单例 Bean的情况下,也可以指定相应的销毁时间的依赖关系。与给定Bean定义了 depends-on 的依赖Bean会在给定Bean本身被销毁之前被首先销毁。因此,depends-on 也可以控制关闭的顺序。
1.4.4. 懒加载的Bean

默认情况下,ApplicationContext 的实现会急切地创建和配置所有的 单例 Bean,作为初始化过程的一部分。一般来说,这种预实例化是可取的,因为配置或周围环境中的错误会立即被发现,而不是几小时甚至几天之后。当这种行为不可取时,你可以通过将Bean定义标记为懒加载来阻止单例Bean的预实例化。懒加载的 bean 告诉IoC容器在第一次被请求时创建一个bean实例,而不是在启动时。

在XML中,这种行为是由 <bean/> 元素上的 lazy-init 属性控制的,如下例所示。

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

当前面的配置被 ApplicationContext 消耗时,当 ApplicationContext 启动时,lazy Bean不会被急切地预实化,而 not.lazy Bean则被急切地预实化了。

然而,当懒加载Bean是未被懒加载的单例Bean的依赖关系时,ApplicationContext 会在启动时创建懒加载 Bean,因为它必须满足单例的依赖关系。懒加载的 Bean 被注入到其他没有被懒加载的单例 Bean中。

你也可以通过使用 <beans/> 元素上的 default-lazy-init 属性来控制容器级的懒加载,如下例所示。

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

1.4.5. 注入协作者(Autowiring Collaborators)

Spring容器可以自动连接协作Bean之间的关系。你可以让Spring通过检查 ApplicationContext 的内容为你的Bean自动解决协作者(其他Bean)。自动注入有以下优点。 * 自动注入可以大大减少对指定属性或构造函数参数的需要。(其他机制,如 本章其他地方 讨论的 bean template,在这方面也很有价值)。 * 自动注入可以随着你的对象的发展而更新配置。例如,如果你需要给一个类添加一个依赖,这个依赖可以自动满足,而不需要你修改配置。因此,自动在开发过程中可能特别有用,而不会否定在代码库变得更加稳定时切换到显式注入的选择。

当使用基于XML的配置元数据时(见 依赖注入),你可以用 <bean/> 元素的 autowire 属性来指定bean定义的自动注入模式。自动注入功能有四种模式。你可以为每个Bean指定自动注入,从而选择哪些要自动注入。下表描述了四种自动注入模式。

Table 2. Autowiring modes
模式解释

no

(默认)没有自动注入。Bean引用必须由 ref 元素来定义。对于大型部署来说,不建议改变默认设置,因为明确指定协作者会带来更大的控制力和清晰度。在某种程度上,它记录了一个系统的结构。

byName

通过属性名称进行自动注入。Spring寻找一个与需要自动注入的属性同名的Bean。例如,如果一个Bean定义被设置为按名称自动注入,并且它包含一个 master 属性(也就是说,它有一个 setMaster(..) 方法),Spring会寻找一个名为 master 的Bean定义并使用它来设置该属性。

byType

如果容器中正好有一个 property 类型的 bean 存在,就可以自动注入该属性。如果存在一个以上的bean,就会抛出一个致命的 exception,这表明你不能对该bean使用 byType 自动注入。如果没有匹配的 bean,就不会发生任何事情(该属性没有被设置)。

constructor

类似于 byType,但适用于构造函数参数。如果容器中没有一个构造函数参数类型的bean,就会产生一个致命的错误。

通过 byType 或 constructor 自动注入模式,你可以给数组(array)和泛型集合(collection)注入。在这种情况下,容器中所有符合预期类型的自动注入候选者都被提供来满足依赖。如果预期的key类型是 String,你可以自动注入强类型的 Map 实例。自动注入的 Map 实例的值由符合预期类型的所有 bean 实例组成,而 Map 实例的key包含相应的 bean 名称。

自动注入的限制和缺点

当自动注入在整个项目中被一致使用时,它的效果最好。如果自动注入没有被普遍使用,那么只用它来注入一个或两个Bean定义可能会让开发者感到困惑。

考虑自动注入的限制和弊端。

  • property 和 constructor-arg 设置中的明确依赖关系总是覆盖自动注入。你不能自动注入简单的属性,如基本数据、String 和 Class(以及此类简单属性的数组)。这个限制是设计上的。

  • 自动注入不如显式注入精确。尽管正如前面的表格中所指出的,Spring很小心地避免在模糊不清的情况下进行猜测,这可能会产生意想不到的结果。你的Spring管理的对象之间的关系不再被明确地记录下来。

  • 对于可能从Spring容器中生成文档的工具来说,注入信息可能无法使用。

  • 容器中的多个Bean定义可以与setter方法或构造参数指定的类型相匹配,以实现自动注入。对于数组、集合或 Map 实例,这不一定是个问题。然而,对于期待单一值的依赖关系,这种模糊性不会被任意地解决。如果没有唯一的Bean定义,就会抛出一个异常。

在后一种情况下,你有几种选择。

  • 放弃自动注入,改用明确注入。

  • 通过将bean定义的 autowire-candidate 属性设置为 false 来避免bean定义的自动注入,如 下一节 所述。

  • 通过将 <bean/> 元素的 primary 属性设置为 true,将单个Bean定义指定为主要候选者。

  • 实现基于注解的配置所提供的更精细的控制,如 基于注解的容器配置 中所述。

从自动注入中排除一个Bean

在每个bean的基础上,你可以将一个bean排除在自动注入之外。在Spring的XML格式中,将 <bean/> 元素的 autowire-candidate 属性设置为 false。容器使特定的Bean定义对自动注入基础设施不可用(包括注解式配置,如 @Autowired)。

autowire-candidate 属性被设计为只影响基于类型的自动注入。它不影响通过名称的显式引用,即使指定的 bean 没有被标记为 autowire 候选者,它也会被解析。因此,如果名称匹配,通过名称进行的自动注入还是会注入一个Bean。

你也可以根据对Bean名称的模式匹配来限制autowire候选人。顶层的 <beans/> 元素在其 default-autowire-candidates 属性中接受一个或多个模式。例如,要将自动注入候选状态限制在名称以 Repository 结尾的任何 bean,请提供 *Repository 的值。要提供多个模式,请用逗号分隔的列表定义它们。Bean定义的 autowire-candidate 属性的明确值为 true 或 false,总是优先考虑。对于这样的Bean,模式匹配规则并不适用。

这些技术对于那些你永远不想通过自动注入注入到其他 Bean 中的 Bean 是很有用的。这并不意味着排除在外的 Bean 本身不能通过使用 autowiring 进行配置。相反,Bean本身不是自动注入其他 Bean 的候选人。

1.4.6. 方法注入

在大多数应用场景中,容器中的大多数Bean是 单例。当一个单例Bean需要与另一个单例Bean协作或一个非单例Bean需要与另一个非单例Bean协作时,你通常通过将一个Bean定义为另一个Bean的一个属性来处理这种依赖关系。当Bean的生命周期不同时,问题就出现了。假设单例Bean A需要使用非单例(prototype)Bean B,也许是在A的每个方法调用上。容器只创建一次单例Bean A,因此只有一次机会来设置属性。容器不能在每次需要Bean B的时候为Bean A提供一个新的实例。

一个解决方案是放弃一些控制的反转。你可以通过实现 ApplicationContextAware 接口 给 bean A 注入容器,并在 bean A 需要时让 容器的 getBean("B") 调用 询问(一个典型的 new)bean B 实例。下面的例子展示了这种方法。

Java

Kotlin

package fiona.apple;

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

/**
 * A class that uses a stateful Command-style class to perform
 * some processing.
 */
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框架。方法注入(Method Injection)是Spring IoC容器的一个高级功能,可以让你干净地处理这种用例。

你可以在 这篇博客文章 中阅读更多关于方法注入的动机。

查找方法依赖注入

查询方法注入是指容器能够覆盖容器管理的 bean 上的方法并返回容器中另一个命名的 bean 的查询结果。这种查找通常涉及到一个原型(prototype)Bean,就像 上一节中描述的情景。Spring框架通过使用CGLIB库的字节码生成来实现这种方法注入,动态地生成一个覆盖该方法的子类。

  • 为了实现此动态子类化功能,被 Spring bean 容器子类化的类不能为 final,要被复写的方法也不能为 final

  • 对一个包含 abstract 方法的类进行单元测试,需要你自己对这个类进行子类化,并提供一个 abstract 方法的 stub 实现。

  • 体的方法对于组件扫描也是必要的,这需要具体的类来接续。

  • 另一个关键的限制是,查找方法对工厂方法不起作用,特别是对配置类中的 @Bean 方法不起作用,因为在这种情况下,容器不负责创建实例,因此不能即时创建运行时生成的子类。

在前面代码片段中的 CommandManager 类的情况下,Spring容器动态地覆写了 createCommand() 方法的实现。CommandManager 类没有任何Spring的依赖,正如重写的例子所示。

Java

Kotlin

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

在包含要注入的方法的客户端类中(本例中是 CommandManager),要注入的方法需要一个如下形式的签名。

<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>

每当需要一个新的 myCommand Bean的实例时,被识别为 commandManager 的bean就会调用它自己的 createCommand() 方法。你必须注意将 myCommand Bean部署为一个原型(prototype),如果这确实是需要的。如果它是一个 单例,每次都会返回同一个 myCommand Bean实例。

另外,在基于注解的组件模型中,你可以通过 @Lookup 注解来声明一个查找方法,如下例所示。

Java

Kotlin

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对查找方法的声明返回类型进行解析。

Java

Kotlin

public abstract class CommandManager {

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

    @Lookup
    protected abstract Command createCommand();
}

请注意,你通常应该用具体的 stub 实现来声明这种注解的查找方法,以使它们与Spring的组件扫描规则兼容,因为抽象类会被默认忽略。这一限制并不适用于明确注册或明确导入的Bean类。

访问不同 scope 的目标Bean的另一种方式是 ObjectFactory/Provider 注入点。请看 作为依赖的 Scope Bean

你可能还会发现 ServiceLocatorFactoryBean(在 org.springframework.beans.factory.config 包中)很有用。

任意方法替换

与查找方法注入相比,方法注入的一个不太有用的形式是用另一个方法实现替换托管Bean中的任意方法的能力。你可以安全地跳过本节的其余部分,直到你真正需要这个功能。

通过基于XML的配置元数据,你可以使用 replaced-method 元素来替换现有的方法实现,为已部署的Bean提供另一种方法。考虑一下下面这个类,它有一个我们想要覆盖的名为 computeValue 的方法。

Java

Kotlin

public class MyValueCalculator {

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

    // some other methods...
}

实现 org.springframework.beans.factory.support.MethodReplacer 接口的类提供了新的方法定义,如下例所示。

Java

Kotlin

/**
 * 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

因为参数的数量往往足以区分每个可能的选择,这个快捷方式可以节省大量的输入,让你只输入符合参数类型的最短字符串。

1.5. Bean Scope

当你创建一个Bean定义时,你创建了一个“配方”,用于创建该Bean定义(definition)是所定义的类的实际实例。Bean定义(definition)是一个“配方”的想法很重要,因为它意味着,就像一个类一样,你可以从一个“配方”中创建许多对象实例。

你不仅可以控制各种依赖和配置值,将其插入到从特定Bean定义创建的对象中,还可以控制从特定Bean定义创建的对象的scope。这种方法是强大而灵活的,因为你可以通过配置来选择你所创建的对象的scope,而不是在Java类级别上烘托出一个对象的scope。Bean可以被定义为部署在若干scope中的一个。Spring框架支持六个scope,其中四个只有在你使用Web感知(aware)的 ApplicationContext 时才可用。你也可以创建 一个自定义 scope

下表描述了支持的 scope。

Table 3. Bean scope
Scope说明

singleton

(默认情况下)为每个Spring IoC容器将单个Bean定义的Scope扩大到单个对象实例。

prototype

将单个Bean定义的Scope扩大到任何数量的对象实例。

request

将单个Bean定义的Scope扩大到单个HTTP请求的生命周期。也就是说,每个HTTP请求都有自己的Bean实例,该实例是在单个Bean定义的基础上创建的。只在Web感知的Spring ApplicationContext 的上下文中有效。

session

将单个Bean定义的Scope扩大到一个HTTP Session 的生命周期。只在Web感知的Spring ApplicationContext 的上下文中有效。

application

将单个Bean定义的 Scope 扩大到 ServletContext 的生命周期中。只在Web感知的Spring ApplicationContext 的上下文中有效。

websocket

将单个Bean定义的 Scope 扩大到 WebSocket 的生命周期。仅在具有Web感知的 Spring ApplicationContext 的上下文中有效。

一个 thread scope 是可用的,但默认情况下没有注册。欲了解更多信息,请参阅 SimpleThreadScope 的文档。关于如何注册这个或任何其他自定义 scope 的说明,请参见 使用自定义 Scope
1.5.1. Singleton Scope

只有一个单例 Bean 的共享实例被管理,所有对具有符合该Bean定义的ID的Bean的请求都会被Spring容器返回该特定的Bean实例。

换句话说,当你定义了一个Bean定义(define),并且它被定义为 singleton,Spring IoC容器就会为该Bean定义的对象创建一个确切的实例。这个单一的实例被存储在这种单体Bean的缓存中,所有后续的请求和对该命名Bean的引用都会返回缓存的对象。下面的图片显示了 singleton scope 是如何工作的。

singleton

Spring 的 singleton Bean概念与Gang of Four(GoF)模式书中定义的singleton模式不同。GoF singleton模式对对象的范围进行了硬编码,即每个ClassLoader创建一个且仅有一个特定类的实例。Spring单例的范围最好被描述为每个容器和每个bean。这意味着,如果你在一个Spring容器中为一个特定的类定义了一个Bean,Spring容器就会为该Bean定义的类创建一个且只有一个实例。Singleton scope 是Spring的默认 scope。要在XML中把一个Bean定义为singleton,你可以像下面的例子那样定义一个Bean。

<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"/>

1.5.2. Prototype Scope

Bean 部署的非 singleton prototype scope 导致每次对该特定Bean的请求都会创建一个新的Bean实例。也就是说,该 bean 被注入到另一个 bean 中,或者你通过容器上的 getBean() 方法调用来请求它。作为一项规则,你应该对所有有状态的 bean 使用 prototype scope,对无状态的 bean 使用 singleton scope。

下图说明了Spring prototype scope。

prototype

(数据访问对象(DAO)通常不被配置为 prototype,因为典型的DAO并不持有任何对话状态。对我们来说,重用 singleton 图的核心是比较容易的)。

下面的例子在XML中定义了一个 prototype bean。

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

与其他scope相比,Spring并不管理 prototype Bean的完整生命周期。容器对prototype对象进行实例化、配置和其他方面的组装,并将其交给客户端,而对该prototype实例没有进一步的记录。因此,尽管初始化生命周期回调方法在所有对象上被调用,而不考虑scope,但在prototype的情况下,配置的销毁生命周期回调不会被调用。客户端代码必须清理prototype scope 内的对象,并释放原prototype Bean持有的昂贵资源。为了让Spring容器释放由 prototype scopeBean 持有的资源,可以尝试使用自定义 Bean后处理器,它持有对需要清理的Bean的引用。

在某些方面,Spring容器在 prototype scope Bean 方面的作用是替代Java的 new 操作。所有超过该点的生命周期管理必须由客户端处理。(关于Spring容器中Bean的生命周期的详细信息,请参见 生命周期回调)。

1.5.3. singleton Bean 和 prototype bean 依赖

当你使用对 prototype Bean 有依赖的 singleton scope Bean时,请注意依赖关系是在实例化时解析的。因此,如果你将一个 prototype scope 的Bean依赖性注入到一个 singleton scope 的Bean中,一个新的 prototype Bean 被实例化,然后被依赖注入到 singleton Bean中。prototype 实例是唯一提供给 singleton scope Bean的实例。

然而,假设你想让 singleton scope 的Bean在运行时反复获得 prototype scope 的Bean的新实例。你不能将 prototype scope 的Bean 依赖注入到你的 singleton Bean中,因为这种注入只发生一次,当Spring容器实例化 singleton Bean 并解析和注入其依赖关系时。如果你在运行时需要一个新的 prototype Bean 实例不止一次,请参阅 方法注入

1.5.4. Request、 Session、 Application 和 WebSocket Scope

requestsessionapplication 和 websocket scope只有在你使用Web感知的Spring ApplicationContext 实现(如 XmlWebApplicationContext)时才可用。如果你将这些scope与常规的Spring IoC容器(如 ClassPathXmlApplicationContext)一起使用,就会抛出一个 IllegalStateException,抱怨有未知的Bean scope。

初始 Web 配置

为了支持Bean在 request、 sessionapplication 和 Websocket 级别的scope(Web scope 的Bean),在你定义Bean之前,需要一些小的初始配置。(对于标准作用域(singleton 和 prototype)来说,这种初始设置是不需要的)。

你如何完成这个初始设置取决于你的特定Servlet环境。

如果你在Spring Web MVC中访问 scope 内的Bean,实际上是在一个由Spring DispatcherServlet 处理的请求(request)中,就不需要进行特别的设置。 DispatcherServlet 已经暴露了所有相关的状态。

如果你使用Servlet Web容器,在Spring的 DispatcherServlet 之外处理请求(例如,在使用JSF时),你需要注册 org.springframework.web.context.request.RequestContextListener ServletRequestListener。这可以通过使用 WebApplicationInitializer 接口以编程方式完成。或者,在你的Web应用程序的 web.xml 文件中添加以下声明。

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

另外,如果你的监听器(listener)设置有问题,可以考虑使用Spring的 RequestContextFilter。过滤器(filter)的映射取决于周围的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>

DispatcherServletRequestContextListener 和 RequestContextFilter 都做了完全相同的事情,即把HTTP请求对象绑定到为该请求服务的 Thread。这使得 request scope 和 session scope 的Bean可以在调用链的更远处使用。列表显示了Web应用程序的过滤器部分。

Request scope

考虑以下用于Bean定义的XML配置。

<bean id="loginAction" class="com.something.LoginAction" scope="request"/>

Spring容器通过为每一个HTTP请求使用 loginAction Bean定义来创建 LoginAction Bean的新实例。也就是说,loginAction Bean在HTTP请求层面上是有 scope 的。你可以随心所欲地改变被创建的实例的内部状态,因为从同一个 loginAction Bean定义中创建的其他实例不会看到这些状态的变化。它们是针对单个请求的。当请求完成处理时,该请求所涉及的Bean会被丢弃。

当使用注解驱动(annotation-driven)的组件或Java配置时,@RequestScope 注解可以用来将一个组件分配到 request scope。下面的例子展示了如何做到这一点。

Java

Kotlin

@RequestScope
@Component
public class LoginAction {
    // ...
}

Session Scope

考虑以下用于Bean定义的XML配置。

<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>

Spring容器通过使用 userPreferences Bean定义,在单个HTTP Session 的生命周期内创建一个新的 UserPreferences Bean实例。换句话说,userPreferences Bean在HTTP Session 级别上是有效的scope。与 request scope 的Bean一样,你可以随心所欲地改变被创建的实例的内部状态,要知道其他HTTP Session 实例也在使用从同一个 userPreferences Bean定义中创建的实例,它们不会看到这些状态的变化,因为它们是特定于单个HTTP Session。当HTTP Session 最终被丢弃时,作用于该特定HTTP Session 的bean也被丢弃。

当使用注解驱动(annotation-driven)的组件或Java配置时,你可以使用 @SessionScope 注解来将一个组件分配到 session scope。

Java

Kotlin

@SessionScope
@Component
public class UserPreferences {
    // ...
}

Application Scope

考虑以下用于Bean定义的XML配置。

<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>

Spring容器通过为整个Web应用程序使用一次 appPreferences Bean定义来创建 AppPreferences Bean的新实例。也就是说,appPreferences Bean是在 ServletContext 级别上的scope,并作为常规的 ServletContext 属性存储。这有点类似于Spring的 singleton Bean,但在两个重要方面有所不同。它是每个 ServletContext 的单例,而不是每个Spring ApplicationContext(在任何给定的Web应用程序中可能有几个),而且它实际上是暴露的,因此作为 ServletContext 属性可见。

当使用注解驱动(annotation-driven)的组件或Java配置时,你可以使用 @ApplicationScope 注解来将一个组件分配到 application scope。下面的例子显示了如何做到这一点。

Java

Kotlin

@ApplicationScope
@Component
public class AppPreferences {
    // ...
}

WebSocket Scope

WebSocket scope 与WebSocket会话的生命周期相关,适用于通过 WebSocket 实现的 STOMP 应用程序,详情请参见 WebSocket scope

作为依赖的 Scope Bean

Spring IoC容器不仅管理对象(Bean)的实例化,而且还管理协作者(或依赖)的连接。如果你想把(例如)一个HTTP request scope 的Bean注入到另一个时间较长的scope的Bean中,你可以选择注入一个AOP代理来代替这个 scope 的Bean。也就是说,你需要注入一个代理对象,它暴露了与 scope 对象相同的公共接口,但它也可以从相关的scope(如HTTP request)中检索到真正的目标对象,并将方法调用委托给真正的对象。

你也可以在 scope 为 singleton 的Bean之间使用 <aop:scoped-proxy/>,引用会经过一个可序列化的中间代理,因此能够在反序列化时重新获得目标 singleton Bean。

当针对scope 为 prototype 的Bean声明 <aop:scoped-proxy/> 时,对共享代理的每个方法调用都会导致创建一个新的目标实例,然后调用被转发到该实例。

另外,scope代理并不是以生命周期安全的方式从较短的scope访问Bean的唯一方法。你也可以将你的注入点(也就是构造器或设置器参数或自动注入的字段)声明为 ObjectFactory<MyTargetBean>,允许在每次需要时通过 getObject() 调用来检索当前的实例—​而不需要保留实例或单独存储它。

作为一个扩展变量,你可以声明 ObjectProvider<MyTargetBean>,它提供了几个额外的访问变体,包括 getIfAvailable 和 getIfUnique

JSR-330 的变体被称为 Provider,并在每次检索时使用 Provider<MyTargetBean> 声明和相应的 get() 调用。关于JSR-330整体的更多细节,请看 这里

下面的例子中的配置只有一行,但理解其背后的 "为什么" 以及 "如何" 是很重要的。

<?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>

定义代理的那一行。

要创建这样的代理,你需要在一个 scope Bean定义中插入一个子 <aop:scoped-proxy/> 元素(参见 选择要创建的代理类型 和 基于 XML Schema 的配置)。为什么在 requestsession 和自定义 scope 层次上的Bean定义需要 <aop:scoped-proxy/> 元素?请考虑下面的 singleton Bean 定义,并与你需要为上述 scope 定义的内容进行对比(注意,下面的 userPreferences Bean定义是不完整的)。

<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>

<bean id="userManager" class="com.something.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

在前面的例子中,singleton Bean(userManager)被注入了对HTTP Session scope Bean(userPreferences)的引用。这里突出的一点是 userManager Bean是一个 singleton:它在每个容器中只被实例化一次,它的依赖关系(在这种情况下只有一个,即 userPreferences Bean)也只被注入一次。这意味着 userManager Bean只对完全相同的 userPreferences 对象(也就是它最初被注入的对象)进行操作。

当把一个生命周期较短的 scope Bean 注入一个生命周期较长的 scope Bean时,这不是你想要的行为(例如,把一个HTTP Session scope的协作Bean作为依赖关系注入 singleton Bean)。相反,你需要一个单例的 userManager 对象,而且,在 HTTP Session 的生命周期内,你需要一个特定于 HTTP Session 的 userPreferences 对象。因此,容器创建一个与 UserPreferences 类完全相同的公共接口的对象(最好是一个 UserPreferences 实例的对象),它可以从 scope 机制(HTTP request、Session 等)中获取真正的 UserPreferences 对象。容器将这个代理对象注入到 userManager Bean中,而 userManager Bean并不知道这个 UserPreferences 引用是一个代理。在这个例子中,当 UserManager 实例调用依赖注入的 UserPreferences 对象上的方法时,它实际上是调用了代理上的方法。然后,代理从(在这种情况下)HTTP Session 中获取真正的 UserPreferences 对象,并将方法调用委托给检索到的真正 UserPreferences 对象。

因此,在将 request scope 和 session scope 的Bean注入协作对象时,你需要以下(正确和完整的)配置,正如下面的例子所示。

<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
    <aop:scoped-proxy/>
</bean>

<bean id="userManager" class="com.something.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

选择要创建的代理类型

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

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

另外,你也可以通过为 <aop:scoped-proxy/> 元素的 proxy-target-class 属性的值指定 false 来配置Spring容器,使其为这种 scope 内的Bean创建基于JDK接口的标准代理。使用基于JDK接口的代理意味着你不需要在你的应用程序 classpath 中使用额外的库来影响这种代理。然而,这也意味着scope Bean的类必须至少实现一个接口,并且scope 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>

关于选择基于类或基于接口的代理的更多详细信息,请参阅 代理机制

1.5.5. 自定义 Scope

Bean的Scope机制是可扩展的。你可以定义你自己的Scope,甚至重新定义现有的Scope,尽管后者被认为是不好的做法,你不能覆盖内置的 singleton 和 prototype scope。

创建自定义 Scope

为了将你的自定义scope集成到 Spring 容器中,你需要实现 org.springframework.beans.factory.config.Scope 接口,本节将介绍该接口。要了解如何实现你自己的scope,请参阅 Spring 框架本身提供的 Scope 实现,以及 Scope javadoc,其中更详细地解释了你需要实现的方法。

Scope 接口有四个方法来从scope中获取对象,从scope中移除对象,以及让对象被销毁。

例如,session scope的实现会返回 session scope 的 bean(如果它不存在,该方法会返回一个新的 bean 实例,在把它绑定到 session 上供将来引用)。下面的方法从底层scope返回对象。

Java

Kotlin

Object get(String name, ObjectFactory<?> objectFactory)

例如,session scope的实现是将session scope的Bean从底层session中移除。该对象应该被返回,但是如果没有找到指定名称的对象,你可以返回 null。下面的方法将对象从底层scope中删除。

Java

Kotlin

Object remove(String name)

下面的方法注册了一个callback,当scope被销毁或scope中的指定对象被销毁时,该callback应该被调用。

Java

Kotlin

void registerDestructionCallback(String name, Runnable destructionCallback)

请参阅 javadoc 或Spring scope 的实现,以了解更多关于销毁callback的信息。

下面的方法获得底层scope的conversation id。

Java

Kotlin

String getConversationId()

这个 id 对每个 scope 都是不同的。对于一个 session scope 的实现,这个 id 可以是 session id。

使用自定义 Scope

在你编写并测试了一个或多个自定义 Scope 实现之后,你需要让 Spring 容器知道你的新 Scope。下面的方法是向Spring容器注册新 Scope 的核心方法。

Java

Kotlin

void registerScope(String scopeName, Scope scope);

这个方法是在 ConfigurableBeanFactory 接口上声明的,它可以通过Spring的大多数具体 ApplicationContext 实现上的 BeanFactory 属性获得。

registerScope(..) 方法的第一个参数是与一个 scope 相关的唯一的名称。在 Spring 容器本身中这种名称的例子是 singleton 和 prototyperegisterScope(..) 方法的第二个参数是你希望注册和使用的自定义 Scope 实现的实际实例。

假设你写了你的自定义 Scope 的实现,然后按下一个例子所示注册它。

下一个例子使用了 SimpleThreadScope,它包含在Spring中,但默认没有注册。对于你自己的自定义 Scope 实现,其说明是一样的。

Java

Kotlin

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

然后你可以创建符合你的自定义 Scope 的 scope 规则的bean定义,如下所示。

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

有了自定义的 Scope 实现,你就不局限于以编程方式注册该scope了。你也可以通过使用 CustomScopeConfigurer 类,以声明的方式进行 Scope 注册,如下例所示。

<?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">

    <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>

    <bean id="thing2" class="x.y.Thing2" scope="thread">
        <property name="name" value="Rick"/>
        <aop:scoped-proxy/>
    </bean>

    <bean id="thing1" class="x.y.Thing1">
        <property name="thing2" ref="thing2"/>
    </bean>

</beans>

当你把 <aop:scoped-proxy/> 放在 FactoryBean 实现的 <bean> 声明中时,是 factory bean 本身被限定了scope,而不是从 getObject() 返回的对象。

1.6. 自定义Bean的性质(Nature)

Spring框架提供了许多接口,你可以用它们来定制Bean的性质。本节将它们分组如下。

1.6.1. 生命周期回调

为了与容器对Bean生命周期的管理进行交互,你可以实现Spring InitializingBean 和 DisposableBean 接口。容器为前者调用 afterPropertiesSet(),为后者调用 destroy(),让Bean在初始化和销毁你的Bean时执行某些动作。

JSR-250的 @PostConstruct 和 @PreDestroy 注解通常被认为是在现代Spring应用程序中接收生命周期回调的最佳实践。使用这些注解意味着你的Bean不会被耦合到Spring特定的接口。详情请参见 使用 使用 @PostConstruct 和 @PreDestroy

如果你不想使用JSR-250注解,但你仍然想消除耦合,可以考虑用 init-method 和 destroy-method bean 定义元数据。

在内部,Spring框架使用 BeanPostProcessor 实现来处理它能找到的任何回调接口并调用相应的方法。如果你需要自定义功能或其他Spring默认不提供的生命周期行为,你可以自己实现一个 BeanPostProcessor。欲了解更多信息,请参见 容器扩展点

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

生命周期回调接口在本节中描述。

初始化回调

org.springframework.beans.factory.InitializingBean 接口让Bean在容器对Bean设置了所有必要的属性后执行初始化工作。InitializingBean 接口指定了一个方法。

void afterPropertiesSet() throws Exception;

我们建议你不要使用 InitializingBean 接口,因为它不必要地将代码与Spring耦合。另外,我们建议使用 @PostConstruct 注解或指定一个POJO初始化方法。在基于XML的配置元数据中,你可以使用 init-method 属性来指定具有 void 无参数签名的方法的名称。对于Java配置,你可以使用 @Bean 的 initMethod 属性。参见 接收生命周期的回调。考虑一下下面的例子。

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>

Java

Kotlin

public class ExampleBean {

    public void init() {
        // do some initialization work
    }
}

前面的例子与下面的例子(由两个列表组成)的效果几乎完全相同。

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>

Java

Kotlin

public class AnotherExampleBean implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
        // do some initialization work
    }
}

然而,前面两个例子中的第一个并没有将代码与Spring耦合。

销毁回调

实现 org.springframework.beans.factory.DisposableBean 接口可以让Bean在包含它的容器被销毁时获得一个回调。DisposableBean 接口指定了一个方法。

void destroy() throws Exception;

我们建议你不要使用 DisposableBean 回调接口,因为它不必要地将代码耦合到Spring。另外,我们建议使用 @PreDestroy 注解或指定一个bean定义所支持的通用方法。对于基于XML的配置元数据,你可以使用 <bean/> 上的 destroy-method 属性。使用Java配置,你可以使用 @Bean 的 destroyMethod 属性。参见接收生命周期的回调。考虑一下下面的定义。

<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>

Java

Kotlin

public class ExampleBean {

    public void cleanup() {
        // do some destruction work (like releasing pooled connections)
    }
}

前面的定义与下面的定义几乎有完全相同的效果。

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>

Java

Kotlin

public class AnotherExampleBean implements DisposableBean {

    @Override
    public void destroy() {
        // do some destruction work (like releasing pooled connections)
    }
}

然而,前面两个定义中的第一个并没有将代码与Spring耦合。

你可以给 <bean> 元素的 destroy-method 属性分配一个特殊的 (inferred) 值,它指示Spring自动检测特定bean类上的public close 或 shutdown 方法。(任何实现了 java.lang.AutoCloseable 或 java.io.Closeable 的类都可以匹配)。你也可以在 <beans> 元素的 default-destroy-method 属性上设置这个特殊的 (inferred) 值,将这个行为应用于整个Bean集合(参见 默认的初始化和销毁方法)。请注意,这是用Java配置的默认行为。
默认的初始化和销毁方法

当你写初始化和销毁方法回调时,如果不使用Spring特定的 InitializingBean 和 DisposableBean 回调接口,你通常会写一些名称为 init()initialize()dispose() 等的方法。理想情况下,这种生命周期回调方法的名称在整个项目中是标准化的,这样所有的开发者都会使用相同的方法名称,确保一致性。

你可以将Spring容器配置为在每个Bean上 "寻找" 命名的初始化和销毁回调方法名称。这意味着你,作为应用开发者,可以编写你的应用类并使用名为 init() 的初始化回调,而不必为每个Bean定义配置 init-method="init" 属性。当Bean被创建时,Spring IoC容器会调用该方法(并且符合 之前描述 的标准生命周期回调约定)。这一特性也为初始化和销毁方法的回调执行了一致的命名规则。

假设你的初始化回调方法被命名为 init(),你的销毁回调方法被命名为 destroy()。那么你的类就类似于下面这个例子中的类。

Java

Kotlin

public class DefaultBlogService implements BlogService {

    private BlogDao blogDao;

    public void setBlogDao(BlogDao blogDao) {
        this.blogDao = blogDao;
    }

    // this is (unsurprisingly) the initialization callback method
    public void init() {
        if (this.blogDao == null) {
            throw new IllegalStateException("The [blogDao] property must be set.");
        }
    }
}

然后你可以在一个类似于以下的bean中使用该类。

<beans default-init-method="init">

    <bean id="blogService" class="com.something.DefaultBlogService">
        <property name="blogDao" ref="blogDao" />
    </bean>

</beans>

顶层 <beans/> 元素属性中 default-init-method 属性的存在会使Spring IoC容器识别出Bean类中名为 init 的方法作为初始化方法的回调。当一个Bean被创建和装配时,如果Bean类有这样的方法,它就会在适当的时候被调用。

你可以通过使用顶层 <beans/> 元素上的 default-destroy-method 属性,类似地配置 destroy 方法回调(在XML中,也就是)。

如果现有的Bean类已经有了与惯例不同的回调方法,你可以通过使用 <bean/> 本身的 init-method 和 destroy-method 属性来指定(在XML中)方法的名称,从而覆盖默认值。

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

结合生命周期机制

从Spring 2.5开始,你有三个选项来控制Bean的生命周期行为。

如果为一个bean配置了多个生命周期机制,并且每个机制都配置了不同的方法名称,那么每个配置的方法都会按照本说明后面列出的顺序运行。然而,如果同一方法名称被配置—​例如,init() 为一个初始化方法—​用于多个这些生命周期机制,则该方法将被运行一次,如 上一节 所解释的。

为同一个Bean配置的多个生命周期机制,具有不同的初始化方法,其调用方式如下。

  1. 注解了 @PostConstruct 的方法。

  2. afterPropertiesSet(),如 InitializingBean 回调接口所定义。

  3. 一个自定义配置的 init() 方法。

销毁方法的调用顺序是一样的。

  1. 注解了 @PreDestroy 的方法。

  2. destroy(),正如 DisposableBean 回调接口所定义的那样。

  3. 一个自定义配置的 destroy() 方法。

启动和关闭的回调

Lifecycle 接口定义了任何有自己的生命周期要求的对象的基本方法(如启动和停止一些后台进程)。

public interface Lifecycle {

    void start();

    void stop();

    boolean isRunning();
}

任何Spring管理的对象都可以实现 Lifecycle 接口。然后,当 ApplicationContext 本身收到启动和停止信号时(例如,在运行时的停止/重启场景),它将这些调用级联到定义在该上下文中的所有 Lifecycle 实现。它通过委托给一个 LifecycleProcessor 来实现,如下表所示。

public interface LifecycleProcessor extends Lifecycle {

    void onRefresh();

    void onClose();
}

请注意,LifecycleProcessor 本身就实现了 Lifecycle 接口。它还添加了另外两个方法来对 context 的刷新和关闭做出反应。

请注意,常规的 org.springframework.context.Lifecycle 接口是一个明确的start和stop通知的普通约定,并不意味着在上下文刷新时自动启动。如果要对特定Bean的自动启动进行细粒度控制(包括启动阶段),可以考虑实现 org.springframework.context.SmartLifecycle 来代替。

另外,请注意,stop通知并不保证在销毁之前出现。在定期关机时,所有的 Lifecycle Bean都会在一般的销毁回调被传播之前首先收到stop通知。然而,在上下文生命周期中的热刷新或stop刷新尝试时,只有销毁方法被调用。

启动和关闭调用的顺序可能很重要。如果任何两个对象之间存在 "依赖" 关系,被依赖方在其依赖方之后启动,在其依赖方之前停止。然而,有时候,直接的依赖关系是未知的。你可能只知道某种类型的对象应该在另一种类型的对象之前启动。在这些情况下, SmartLifecycle 接口定义了另一个选项,即其超接口 Phased 上定义的 getPhase() 方法。下面的列表显示了 Phased 接口的定义。

public interface Phased {

    int getPhase();
}

下面列出了 SmartLifecycle 接口的定义。

public interface SmartLifecycle extends Lifecycle, Phased {

    boolean isAutoStartup();

    void stop(Runnable callback);
}

启动时,phase最低的对象先启动。当停止时,遵循相反的顺序。因此,一个实现了 SmartLifecycle 并且其 getPhase() 方法返回 Integer.MIN_VALUE 的对象将是最先启动和最后停止的对象。在 spectrum 的另一端,一个 Integer.MAX_VALUE 的 phase 值将表明该对象应该最后启动并首先停止(可能是因为它依赖于其他进程的运行)。在考虑 phase 值时,同样重要的是要知道,任何没有实现 SmartLifecycle 的 "正常" Lifecycle 对象的默认 phase 是 0。 因此,任何负的 phase 值表示一个对象应该在那些标准组件之前开始(并在它们之后停止)。反之,任何正的 phase 值也是如此。

由 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 接口也定义了用于刷新和关闭上下文(context )的回调方法。后者驱动关闭过程,就像明确调用 stop() 一样,但它发生在上下文关闭的时候。另一方面,"refresh" 回调方法实现了 SmartLifecycle Bean的另一个特性。当上下文被刷新时(在所有对象都被实例化和初始化后),该回调被调用。这时,默认的生命周期处理器会检查每个 SmartLifecycle 对象的 isAutoStartup() 方法所返回的布尔值。如果为 true,该对象将在此时启动,而不是等待上下文或其自身 start() 方法的显式调用(与上下文刷新不同,上下文的启动不会自动发生在标准的上下文实现中)。如前所述,phase 值和任何 "依赖" 关系决定了启动的顺序。

在非Web应用中优雅地关闭Spring IoC容器

本节仅适用于非Web应用。Spring的基于Web的 ApplicationContext 实现已经有代码可以在相关Web应用关闭时优雅地关闭Spring IoC容器。

如果你在非web应用环境中使用Spring的IoC容器(例如,在客户端桌面环境中),请向JVM注册一个shutdown hook。这样做可以确保优雅地关闭,并在你的singleton Bean上调用相关的 destroy 方法,从而释放所有资源。你仍然必须正确配置和实现这些 destroy 回调。

要注册一个 shutdown hook,请调用 registerShutdownHook() 方法,该方法在 ConfigurableApplicationContext 接口上声明,如下例所示。

Java

Kotlin

public final class Boot {

    public static void main(final String[] args) throws Exception {
        ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");

        // add a shutdown hook for the above context...
        ctx.registerShutdownHook();

        // app runs here...

        // main method exits, hook is called prior to the app shutting down...
    }
}

1.6.2. ApplicationContextAware 和 BeanNameAware

当 ApplicationContext 创建一个实现 org.springframework.context.ApplicationContextAware 接口的对象实例时,该实例被提供给该 ApplicationContext 的引用。下面的列表显示了 ApplicationContextAware 接口的定义。

public interface ApplicationContextAware {

    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

因此,Bean可以通过 ApplicationContext 接口或通过将引用转换为该接口的已知子类(如 ConfigurableApplicationContext,它暴露了额外的功能),以编程方式操作创建它们的 ApplicationContext。一个用途是对其他Bean进行编程式检索。有时这种能力是很有用的。然而,一般来说,你应该避免这样做,因为它将代码与Spring耦合在一起,并且不遵循控制反转(Inversion of Control)的风格,即合作者作为属性提供给Bean。ApplicationContext 的其他方法提供了对文件资源的访问,发布应用程序事件,以及访问 MessageSource。这些额外的功能将在 ApplicationContext 的附加功能 中描述。

Autowire 是获得对 ApplicationContext 引用的另一种选择。传统的 constructor 和 byType 自动注入模式(如 注入协作者(Autowiring Collaborators) 中所述)可以分别为构造器参数或设 setter 方法参数提供 ApplicationContext 类型的依赖。为了获得更多的灵活性,包括自动注入字段和多个参数方法的能力,请使用基于注解的自动注入功能。如果你这样做,ApplicationContext 将被自动注入到字段、构造函数参数或方法参数中,如果有关字段、构造函数或方法带有 @Autowired 注解,则期望 ApplicationContext 类型。更多信息请参见 使用 @Autowired

当 ApplicationContext 创建一个实现 org.springframework.beans.factory.BeanNameAware 接口的类时,该类被提供给其相关对象定义中定义的名称的引用。下面的列表显示了 BeanNameAware 接口的定义。

public interface BeanNameAware {

    void setBeanName(String name) throws BeansException;
}

这个回调是在正常的Bean属性之后,但在 InitializingBean.afterPropertiesSet() 或自定义 init-method 等初始化回调之前调用的。

1.6.3. 其他 Aware 接口

除了 ApplicationContextAware 和 BeanNameAware (前面讨论过),Spring还提供了一系列的 Aware 回调接口,让Bean向容器表明它们需要某种基础设施的依赖性。一般来说,名称表示依赖关系的类型。下表总结了最重要的 Aware 接口。

Table 4. Aware 接口
接口名称注入的依赖性解释

ApplicationContextAware

声明 ApplicationContext

ApplicationContextAware 和 BeanNameAware

ApplicationEventPublisherAware

封装了 ApplicationContext 的 Event publisher 。

ApplicationContext 的附加功能

BeanClassLoaderAware

用来加载Bean类的类加载器(Class loader)。

实例化 Bean

BeanFactoryAware

声明 BeanFactory

BeanFactory API

BeanNameAware

声明Bean的名称。

ApplicationContextAware 和 BeanNameAware

LoadTimeWeaverAware

定义了用于在加载时处理类定义的织入点。

在Spring框架中用AspectJ进行加载时织入(Load-time Weaving)

MessageSourceAware

配置解析消息的策略(支持参数化和国际化)。

ApplicationContext 的附加功能

NotificationPublisherAware

Spring JMX notification publisher。

Notifications

ResourceLoaderAware

配置的加载器用于低级别的资源访问。

资源(Resources)

ServletConfigAware

容器所运行的当前 ServletConfig。仅在 Web 感知的 Spring ApplicationContext 中有效。

Spring MVC

请再次注意,使用这些接口会将你的代码与Spring API捆绑在一起,并且不遵循反转控制的风格。因此,我们建议那些需要对容器进行编程访问的基础设施Bean使用这些接口。

1.7. Bean 定义(Definition)的继承

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

如果你以编程方式处理 ApplicationContext 接口,子bean定义由 ChildBeanDefinition 类表示。大多数用户不会在这个层面上与他们一起工作。相反,他们在 ClassPathXmlApplicationContext 这样的类中声明性地配置Bean定义。当你使用基于XML的配置元数据时,你可以通过使用 parent 属性来指示子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>

注意 parent 属性。

如果没有指定,子Bean定义会使用父定义中的Bean类,但也可以覆盖它。在后一种情况下,子Bean类必须与父类兼容(也就是说,它必须接受父类的属性值)。

子Bean定义从父级继承scope、构造函数参数值、属性值和方法重写,并可以选择添加新的值。你指定的任何scope、初始化方法、销毁(destroy)方法或 static 工厂方法设置都会覆盖相应的父类设置。

其余的设置总是来自于子定义:依赖、自动注入模式、依赖检查、singleton和懒加载。

前面的例子通过使用 abstract 属性明确地将父类Bean定义标记为抽象的。如果父定义没有指定一个类,就需要明确地将父Bean定义标记为抽象的,如下例所示。

<bean id="inheritedTestBeanWithoutClass" abstract="true">
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>

<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
        parent="inheritedTestBeanWithoutClass" init-method="initialize">
    <property name="name" value="override"/>
    <!-- age will inherit the value of 1 from the parent bean definition-->
</bean>

父类Bean不能被单独实例化,因为它是不完整的,而且它也被明确标记为 abstract 的。当一个定义是 abstract 的,它只能作为一个纯模板Bean定义使用,作为子定义的父定义。试图单独使用这样的 abstract 父类 bean,通过将其作为另一个 bean 的 ref 属性来引用,或者用父类 bean 的 ID 进行显式 getBean() 调用,会返回一个错误。同样地,容器内部的 preInstantiateSingletons() 方法也会忽略被定义为抽象的 bean 定义。

ApplicationContext 默认预设了所有的singleton。因此,重要的是(至少对于singleton Bean来说),如果你有一个(父)Bean定义,你打算只作为模板使用,并且这个定义指定了一个类,你必须确保将 abstract 属性设置为 true,否则应用上下文将实际(试图)预实化 abstract Bean。

1.8. 容器扩展点

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

1.8.1. 使用 BeanPostProcessor 自定义 Bean

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

你可以配置多个 BeanPostProcessor 实例,你可以通过设置 order 属性控制这些 BeanPostProcessor 实例的运行顺序。只有当 BeanPostProcessor 实现了 Ordered 接口时,你才能设置这个属性。如果你编写自己的 BeanPostProcessor,你也应该考虑实现 Ordered 接口。关于进一步的细节,请参阅 BeanPostProcessor 和 Ordered 接口的 javadoc。也请参见关于 BeanPostProcessor 实例的程序化注册 的说明。

BeanPostProcessor 实例对Bean(或对象)实例进行操作。也就是说,Spring IoC容器实例化一个Bean实例,然后由 BeanPostProcessor 实例来完成其工作。

BeanPostProcessor 实例是按容器范围的。这只有在你使用容器层次结构时才有意义。如果你在一个容器中定义了一个 BeanPostProcessor,它只对该容器中的Bean进行后处理。换句话说,在一个容器中定义的 BeanPostProcessor 不会对另一个容器中定义的 BeanPostProcessor 进行后处理,即使两个容器都是同一层次结构的一部分。

要改变实际的Bean定义(即定义Bean的蓝图),你需要使用 BeanFactoryPostProcessor,如 用 BeanFactoryPostProcessor 定制配置元数据 中所述。

org.springframework.beans.factory.config.BeanPostProcessor 接口正好由两个回调方法组成。当这样的类被注册为容器的后处理器时,对于容器创建的每个 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 实例

虽然推荐的 BeanPostProcessor 注册方法是通过 ApplicationContext 自动检测(如前所述),但你可以通过使用 addBeanPostProcessor 方法,针对 ConfigurableBeanFactory 以编程方式注册它们。当你需要在注册前评估条件逻辑或甚至在一个层次结构中跨上下文复制Bean Post处理器时,这可能很有用。然而,请注意,以编程方式添加的 BeanPostProcessor 实例并不尊重 Ordered 接口。这里,是注册的顺序决定了执行的顺序。还要注意的是,以编程方式注册的 BeanPostProcessor 实例总是在通过自动检测注册的实例之前被处理,而不考虑任何明确的顺序。

BeanPostProcessor 实例和AOP自动代理

实现了 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)

如果你通过使用自动注入或 @Resource(可能会退回到自动注入)将 BeanPostProcessor 连接起来,Spring在搜索类型匹配的依赖候选者时可能会访问意想不到的Bean,因此,使它们没有资格进行自动代理或其他种类的Bean后处理。例如,如果你有一个用 @Resource 注解的依赖,其中字段或setter的名称不直接对应于Bean的声明名称,并且没有使用name属性,Spring会访问其他Bean以通过类型匹配它们。

下面的例子展示了如何在 ApplicationContext 中编写、注册和使用 BeanPostProcessor 实例。

示例: Hello World, BeanPostProcessor

这第一个例子说明了基本用法。这个例子展示了一个自定义的 BeanPostProcessor 实现,它在容器创建每个Bean时调用 toString() 方法,并将结果字符串打印到系统控制台。

下面的列表显示了自定义 BeanPostProcessor 实现类的定义。

Java

Kotlin

package scripting;

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

    // simply return the instantiated bean as-is
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        return bean; // we could potentially return any object reference here...
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) {
        System.out.println("Bean '" + beanName + "' created : " + bean.toString());
        return bean;
    }
}

下面的bean元素使用了 InstantiationTracingBeanPostProcessor

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

    <lang:groovy id="messenger"
            script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
        <lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
    </lang:groovy>

    <!--
    when the above bean (messenger) is instantiated, this custom
    BeanPostProcessor implementation will output the fact to the system console
    -->
    <bean class="scripting.InstantiationTracingBeanPostProcessor"/>

</beans>

注意 InstantiationTracingBeanPostProcessor 是如何被定义的。它甚至没有名字,而且因为它是一个Bean,它可以像其他Bean一样被依赖注入。(前面的配置还定义了一个由Groovy脚本支持的Bean。Spring的动态语言支持详见 动态语言支持 一章)。

下面的Java应用程序运行前面的代码和配置。

Java

Kotlin

public final class Boot {

    public static void main(final String[] args) throws Exception {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml");
        Messenger messenger = ctx.getBean("messenger", Messenger.class);
        System.out.println(messenger);
    }

}

前面的应用程序的输出类似于以下内容。

Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961
示例: AutowiredAnnotationBeanPostProcessor

将回调接口或注解与自定义 BeanPostProcessor 实现结合起来使用,是扩展Spring IoC容器的一种常见手段。一个例子是Spring的 AutowiredAnnotationBeanPostProcessor — 一个 BeanPostProcessor 实现,它与 Spring distribution 一起,自动注入注解字段、setter方法和任意的配置方法。

1.8.2. 用 BeanFactoryPostProcessor 定制配置元数据

我们看的下一个扩展点是 org.springframework.beans.factory.config.BeanFactoryPostProcessor。这个接口的语义与 BeanPostProcessor 的语义相似,但有一个主要区别。BeanFactoryPostProcessor 对Bean配置元数据进行操作。也就是说,Spring IoC容器让 BeanFactoryPostProcessor 读取配置元数据,并在容器实例化 BeanFactoryPostProcessor 实例以外的任何Bean之前对其进行潜在的修改。

你可以配置多个 BeanFactoryPostProcessor 实例,你可以通过设置 order 属性控制这些 BeanFactoryPostProcessor 实例的运行顺序。然而,只有当 BeanFactoryPostProcessor 实现了 Ordered 接口时,你才能设置这个属性。如果你编写自己的 BeanFactoryPostProcessor,你也应该考虑实现 Ordered 接口。请参阅 BeanFactoryPostProcessor 和 Ordered 接口的 javadoc,了解更多细节。

如果你想改变实际的Bean实例(即从配置元数据中创建的对象),那么你需要使用 BeanPostProcessor(如前面 使用 BeanPostProcessor 自定义 Bean 中的描述)。虽然在技术上可以在 BeanFactoryPostProcessor 中处理Bean实例(例如,通过使用 BeanFactory.getBean()),但这样做会导致过早的Bean实例化,违反了标准容器的生命周期。这可能会导致负面的副作用,比如绕过Bean的后期处理。

另外,BeanFactoryPostProcessor 实例是按容器范围的。这只有在你使用容器层次结构时才有意义。如果你在一个容器中定义了一个 BeanFactoryPostProcessor,它将只应用于该容器中的Bean定义。一个容器中的 Bean 定义不会被另一个容器中的 BeanFactoryPostProcessor 实例进行后处理,即使两个容器都是同一层次结构的一部分。

当Bean工厂在 ApplicationContext 内声明时,会自动运行Bean工厂后处理器,以便对定义容器的配置元数据进行修改。Spring包括一些预定义的Bean Factory后处理器,如 PropertyOverrideConfigurer 和 PropertySourcesPlaceholderConfigurer。你也可以使用一个自定义的 BeanFactoryPostProcessor--例如,注册自定义的属性编辑器(property editor)。

ApplicationContext 会自动检测被部署到其中的实现了 BeanFactoryPostProcessor 接口的任何Bean。它在适当的时候将这些Bean用作Bean Factory后处理器。你可以像部署其他Bean一样部署这些后处理器Bean。

与 BeanPostProcessor 一样,你通常不希望将 BeanFactoryPostProcessor 配置为懒加载。如果没有其他bean引用 Bean(Factory)PostProcessor,该 PostProcessor 将根本不会被实例化。因此,将其标记为懒加载将被忽略,即使你在 <beans /> 元素的声明中把 default-lazy-init 属性设置为 trueBean(Factory)PostProcessor 也会被急切地实例化。
示例: 类名替换 PropertySourcesPlaceholderConfigurer

你可以使用 PropertySourcesPlaceholderConfigurer,通过使用标准的Java Properties 格式,将Bean定义中的属性值外化到一个单独的文件中。这样做使部署应用程序的人能够定制特定环境的属性,如数据库URL和密码,而不需要修改容器的主要XML定义文件或文件的复杂性或风险。

考虑以下基于XML的配置元数据片段,其中定义了一个具有占位值的 DataSource

<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>

这个例子显示了从外部 Properties 文件配置的属性。在运行时, PropertySourcesPlaceholderConfigurer 被应用到元数据中,取代了 DataSource 的一些属性。要替换的值被指定为 ${property-name} 形式的占位符,它遵循Ant和log4j以及JSP EL的风格。

实际的数值来自另一个文件,是标准的Java Properties 格式。

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

因此,${jdbc.username} 字符串在运行时被替换为值 'sa',这同样适用于其他与properties文件中的key相匹配的占位符值。 PropertySourcesPlaceholderConfigurer 会检查Bean定义的大多数属性中的占位符。此外,你可以自定义占位符的前缀(prefix)和后缀(suffix)。

通过Spring 2.5中引入的 context 命名空间,你可以用一个专门的配置元素来配置属性占位符。你可以在 location 属性中提供一个或多个位置作为逗号分隔的列表,如下例所示。

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

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

你可以使用 PropertySourcesPlaceholderConfigurer 来替换类名,当你必须在运行时挑选一个特定的实现类时,这有时很有用。下面的例子显示了如何做到这一点。

<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}"/>

如果该类在运行时不能被解析为一个有效的类,那么在即将创建Bean时,也就是在非 lazy-init Bean 的 ApplicationContext 的 preInstantiateSingletons() 阶段,Bean的解析会失败。

示例:PropertyOverrideConfigurer

PropertyOverrideConfigurer 是另一个 Bean factory 的后处理器,与 PropertySourcesPlaceholderConfigurer 相似,但与后者不同的是,原始定义可以为Bean属性设置默认值或根本没有值。如果覆盖的 Properties 文件中没有某个Bean属性的条目,就会使用默认的上下文定义。

请注意,Bean定义并不知道被覆盖,所以从XML定义文件中并不能立即看出正在使用覆盖的配置器(override configurer)。如果有多个 PropertyOverrideConfigurer 实例为同一个Bean属性定义不同的值,由于覆盖机制的存在,最后一个实例获胜。

Properties 文件配置行的格式如下。

beanName.property=value

下面的列表显示了一个格式的例子。

dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb

这个示例文件可用于包含一个名为 dataSource 的Bean的容器定义,该Bean具有 driver 和 url 属性。

也支持复合属性名,只要路径中的每个组件,除了被重载的最终属性外,都已经是非null的(大概是被构造函数初始化了)。在下面的例子中,tom Bean的 fred 属性的 bob 属性的 sammy 属性被设置为标量值 123

tom.fred.bob.sammy=123
指定的覆盖值总是字面值。它们不被翻译成 bean 引用。当 XML Bean 定义中的原始值指定了一个 bean 引用时,这一约定也适用。

通过Spring 2.5中引入的 context 命名空间,可以用一个专门的配置元素来配置属性重写,如下例所示。

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

1.8.3. 用 FactoryBean 自定义实例化逻辑

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

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

FactoryBean<T> 接口提供三个方法。

  • T getObject(): 返回本工厂创建的对象的一个实例。该实例可能会被共享,这取决于该工厂是返回singleton还是prototype。

  • boolean isSingleton(): 如果这个 FactoryBean 返回 singleton,则返回 true,否则返回 false。这个方法的默认实现会返回 true

  • Class<?> getObjectType(): 返回由 getObject() 方法返回的对象类型,如果事先不知道类型,则返回 null

在Spring框架中,FactoryBean 的概念和接口在很多地方都有使用。Spring本身就有50多个 FactoryBean 接口的实现。

当你需要向容器索取一个实际的 FactoryBean 实例而不是它产生的Bean时,在调用 ApplicationContext 的 getBean() 方法时,在Bean的 id 前加上安培符号(&)。因此,对于一个 id 为 myBean 的 FactoryBean,在容器上调用 getBean("myBean") 会返回 FactoryBean 的产物,而调用 getBean("&myBean") 会返回 FactoryBean 实例本身。

1.9. 基于注解的容器配置

在配置Spring时,注解是否比XML更好?

基于注解的配置的引入提出了这样一个问题:这种方法是否比XML "更好"。简短的回答是 "视情况而定"。长的答案是,每种方法都有它的优点和缺点,而且,通常是由开发者来决定哪种策略更适合他们。由于它们的定义方式,注解在其声明中提供了大量的上下文,导致了更短、更简洁的配置。然而,XML擅长于在不触及源代码或重新编译的情况下对组件进行注入。一些开发者更喜欢在源码附近注入,而另一些人则认为带注解的类不再是POJO,此外,配置变得分散,更难控制。

不管是哪种选择,Spring都能适应这两种风格,甚至将它们混合在一起。值得指出的是,通过其 JavaConfig 选项,Spring允许以非侵入性的方式使用注解,而不触及目标组件的源代码,在工具方面,所有的配置风格都被 Spring Tools for Eclipse、Visual Studio Code 和 Theia 所支持。

基于注解的配置提供了XML设置的替代方案,它依靠字节码元数据来注入组件而不是XML声明。开发者通过在相关的类、方法或字段声明上使用注解,将配置移入组件类本身,而不是使用XML来描述bean的装配。正如 示例: AutowiredAnnotationBeanPostProcessor 中提到的,将 BeanPostProcessor 与注解结合使用是扩展Spring IoC容器的常见手段。例如,@Autowired 注解提供了与 注入协作者(Autowiring Collaborators) 中所描述的相同的功能,但控制范围更细,适用性更广。此外,Spring还提供了对JSR-250注解的支持,如 @PostConstruct 和 @PreDestroy,以及对JSR-330(Java的依赖注入)注解的支持,该注解包含在 jakarta.inject 包中,如 @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/> 元素隐含地注册了下列后处理器( post-processor)。

<context:annotation-config/> 只在定义它的同一应用上下文(application context)中寻找对Bean的注解。这意味着,如果你把 <context:annotation-config/> 放在 DispatcherServlet 的 WebApplicationContext 中,它只检查 controller 中的 @Autowired Bean,而不是 service。参见 DispatcherServlet 以了解更多信息。

1.9.1. 使用 @Autowired

JSR 330的 @Inject 注解可以代替Spring的 @Autowired 注解在本节包含的例子中使用。更多细节请看 这里

你可以将 @Autowired 注解应用于构造函数,如下例所示。

Java

Kotlin

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

从Spring Framework 4.3开始,如果目标Bean一开始就只定义了一个构造函数,那么在这样的构造函数上就不再需要 @Autowired 注解。然而,如果有几个构造函数,而且没有主要/默认构造函数,那么至少有一个构造函数必须用 @Autowired 注解,以便指示容器使用哪一个。详情请参见关于 构造函数解析 的讨论。

你也可以将 @Autowired 注解应用于传统的setter方法,如下例所示。

Java

Kotlin

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

你也可以将注解应用于具有任意名称和多个参数的方法,如下例所示。

Java

Kotlin

public class MovieRecommender {

    private MovieCatalog movieCatalog;

    private CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public void prepare(MovieCatalog movieCatalog,
            CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

你也可以将 @Autowired 应用于字段,甚至将其与构造函数混合,如下例所示。

Java

Kotlin

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    private MovieCatalog movieCatalog;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

确保你的目标组件(例如 MovieCatalog 或 CustomerPreferenceDao)由你用于 @Autowired 注解的注入点的类型统一声明。否则,注入可能会在运行时由于 "no type match found" 的错误而失败。

对于通过classpath扫描找到的XML定义的Bean或组件类,容器通常预先知道具体类型。然而,对于 @Bean 工厂方法,你需要确保声明的返回类型有足够的表现力。对于实现了多个接口的组件或可能被其实现类型引用的组件,考虑在你的工厂方法上声明最具体的返回类型(至少要与引用你的 bean 的注入点所要求的具体类型一样)。

你也可以指示Spring从 ApplicationContext 中提供所有特定类型的Bean,方法是将 @Autowired 注解添加到期望有该类型数组的字段或方法中,如下例所示。

Java

Kotlin

public class MovieRecommender {

    @Autowired
    private MovieCatalog[] movieCatalogs;

    // ...
}

这同样适用于类型化的(泛型)集合,正如下面的例子所示。

Java

Kotlin

public class MovieRecommender {

    private Set<MovieCatalog> movieCatalogs;

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

    // ...
}

你的目标Bean可以实现 org.springframework.core.Ordered 接口,如果你想让数组或列表中的项目以特定的顺序排序,可以使用 @Order 或标准的 @Priority 注解。否则,它们的顺序将遵循容器中相应目标Bean定义的注册顺序。

你可以在目标类层面和 @Bean 方法上声明 @Order 注解,可能是针对单个Bean定义(在使用相同Bean类的多个定义的情况下)。@Order 值可以影响注入点的优先级,但要注意它们不会影响singleton的启动顺序,这是一个由依赖关系和 @DependsOn 声明决定的正交问题。

请注意,标准的 jakarta.annotation.Priority 注解在 @Bean 级别上是不可用的,因为它不能被声明在方法上。它的语义可以通过 @Order 值与 @Primary 在每个类型的单例Bean上的组合来建模。

即使是类型化的 Map 实例也可以被自动注入,只要预期的key类型是 String。map的值包含所有预期类型的Bean,而key则包含相应的Bean名称,正如下面的例子所示。

Java

Kotlin

public class MovieRecommender {

    private Map<String, MovieCatalog> movieCatalogs;

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

    // ...
}

默认情况下,当一个给定的注入点没有匹配的候选Bean可用时,自动注入就会失败。在声明的数组、collection或map的情况下,预计至少有一个匹配的元素。

默认行为是将注解的方法和字段视为表示必须的依赖关系。你可以改变这种行为,就像下面的例子所展示的那样,通过将其标记为非必需(即通过将 @Autowired 中的 required 属性设置为 false),使框架能够跳过一个不可满足的注入点。

Java

Kotlin

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired(required = false)
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

如果一个非必须(required)的方法(或者在有多个参数的情况下,它的一个依赖关系)不可用,那么它将根本不会被调用。在这种情况下,一个非必须(required)字段将根本不会被填充,而是将其默认值留在原地。

换句话说,将 required 属性设置为 false 表示相应的属性对于自动注入来说是可选的,如果该属性不能被自动注入,它将被忽略。这使得属性可以被分配默认值,这些默认值可以通过依赖注入选择性地被重写。

注入的构造函数和工厂方法参数是一种特殊情况,因为由于Spring的构造函数解析算法有可能处理多个构造函数,所以 @Autowired 中的 required 属性有一些不同的含义。构造函数和工厂方法参数实际上是默认需要的,但在单构造函数的情况下有一些特殊的规则,比如多元素注入点(数组、collection、map)如果没有匹配的Bean,则解析为空实例。这允许一种常见的实现模式,即所有的依赖关系都可以在一个独特的多参数构造函数中声明—​例如,声明为一个没有 @Autowired 注解的单一公共构造函数。

任何给定的Bean类中只有一个构造函数可以声明 @Autowired,并将 required 属性设置为 true,表示该构造函数在用作Spring Bean时要自动注入。因此,如果 required 属性的默认值为 true,则只有一个构造函数可以使用 @Autowired 注解。如果有多个构造函数声明该注解,它们都必须声明 required=false,才能被视为自动注入的候选者(类似于XML中的 autowire=constructor)。具有最大数量的依赖关系的构造函数将被选中,这些依赖关系可以通过Spring容器中的匹配Bean来满足。如果没有一个候选者可以被满足,那么将使用一个主要的/默认的构造函数(如果存在的话)。同样地,如果一个类声明了多个构造函数,但没有一个是用 @Autowired 注解的,那么将使用一个主要/默认构造函数(如果存在的话)。如果一个类一开始只声明了一个构造函数,那么即使没有注解,它也会被使用。请注意,被注解的构造函数不一定是公共的(public)。

另外,你可以通过Java 8的 java.util.Optional 来表达特定依赖的非必须性质,正如下面的例子所示。

public class SimpleMovieLister {

    @Autowired
    public void setMovieFinder(Optional<MovieFinder> movieFinder) {
        ...
    }
}

从Spring Framework 5.0开始,你也可以使用 @Nullable 注解(任何包中的任何类型—​例如JSR-305中的 javax.annotation.Nullable),或者直接利用Kotlin内置的 null-safety 支持。

Java

Kotlin

public class SimpleMovieLister {

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

你也可以对那些众所周知的可解析依赖的接口使用 @AutowiredBeanFactoryApplicationContextEnvironmentResourceLoaderApplicationEventPublisher 和 MessageSource。这些接口和它们的扩展接口,如 ConfigurableApplicationContext 或 ResourcePatternResolver,将被自动解析,不需要特别的设置。下面的例子是自动注入一个 ApplicationContext 对象。

Java

Kotlin

public class MovieRecommender {

    @Autowired
    private ApplicationContext context;

    public MovieRecommender() {
    }

    // ...
}

@Autowired@Inject@Value 和 @Resource 注解是由Spring BeanPostProcessor 实现处理的。这意味着你不能在你自己的 BeanPostProcessor 或 BeanFactoryPostProcessor 类型(如果有的话)中应用这些注解。这些类型必须通过使用XML或Spring @Bean 方法明确地 "注入"。

1.9.2. 用 @Primary 对基于注解的自动注入进行微调

因为按类型自动注入可能会导致多个候选者,所以经常需要对选择过程进行更多的控制。实现这一目标的方法之一是使用Spring的 @Primary 注解。@Primary 表示,当多个Bean是自动注入到一个单值(single value)依赖的候选者时,应该优先考虑一个特定的Bean。如果在候选者中正好有一个主要(primary)Bean存在,它就会成为自动注入的值。

考虑以下配置,它将 firstMovieCatalog 定义为主 MovieCatalog

Java

Kotlin

@Configuration
public class MovieConfiguration {

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

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

    // ...
}

通过前面的配置,下面的 MovieRecommender 被自动注入到 firstMovieCatalog

Java

Kotlin

public class MovieRecommender {

    @Autowired
    private MovieCatalog movieCatalog;

    // ...
}

相应的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: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" primary="true">
        <!-- inject any dependencies required by this bean -->
    </bean>

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

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

</beans>

1.9.3. 用 Qualifiers 微调基于注解的自动注入

当可以确定一个主要的候选者时,@Primary 是按类型使用自动装配的一种有效方式,有几个实例。当你需要对选择过程进行更多控制时,你可以使用Spring的 @Qualifier 注解。你可以将限定符的值与特定的参数联系起来,缩小类型匹配的范围,从而为每个参数选择一个特定的bean。在最简单的情况下,这可以是一个普通的描述性值,如下面的例子所示。

Java

Kotlin

public class MovieRecommender {

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

    // ...
}

你也可以在单个构造函数参数或方法参数上指定 @Qualifier 注解,如以下例子所示。

Java

Kotlin

public class MovieRecommender {

    private final MovieCatalog movieCatalog;

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public void prepare(@Qualifier("main") MovieCatalog movieCatalog,
            CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

下面的例子显示了相应的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: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 value="main"/> 

        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier value="action"/> 

        <!-- inject any dependencies required by this bean -->
    </bean>

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

</beans>

具有 main qualifier 值的bean与具有相同 qualifier 值的构造函数参数相注入
具有 action qualifier 值的bean与具有相同 qualifier 值的构造器参数相注入。

对于回退匹配(fallback match),Bean的名字被认为是默认的限定符值。因此,你可以用 main 的 id 来定义Bean,而不是嵌套的限定符元素,导致同样的匹配结果。然而,尽管你可以使用这个约定来引用特定的Bean的名字,但 @Autowired 从根本上说是关于类型驱动的注入,并带有可选的语义限定词。这意味着限定符的值,即使有Bean名称的回退,也总是在类型匹配的集合中具有缩小的语义。它们在语义上并不表达对唯一Bean id 的引用。好的限定符值是 main 或 EMEA 或 persistent,表达了独立于Bean id 的特定组件的特征,在匿名Bean定义的情况下,如前面的例子中,它可能是自动生成的。

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

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

也就是说,如果你打算通过名字来表达注解驱动的注入,请不要主要使用 @Autowired,即使它能够在类型匹配的候选者中通过bean的名字来选择。相反,使用JSR-250的 @Resource 注解,它在语义上被定义为通过其唯一的名称来识别特定的目标组件,而声明的类型与匹配过程无关。@Autowired 具有相当不同的语义。在通过类型选择候选Bean后,指定的 String qualifier 值只在这些类型选择的候选中被考虑(例如,将 account qualifier 与标有相同 qualifier 标签的Bean匹配)。

对于那些本身被定义为集合、Map 或数组类型的Bean,@Resource 是一个很好的解决方案,它通过唯一的名称来引用特定的集合或数组Bean。也就是说,从4.3版本开始,你也可以通过Spring的 @Autowired 类型匹配算法来匹配集合、Map 和数组类型,只要在 @Bean 返回类型签名或集合继承层次中保留元素类型信息。在这种情况下,你可以使用 qualifier 值在相同类型的集合中进行选择,如上一段所述。

从4.3版开始,@Autowired 也考虑到了用于注入的自我引用(也就是对当前注入的Bean的引用)。请注意,自我注入是一种回退(fallback)。对其他组件的常规依赖总是具有优先权。在这个意义上,自我引用不参与常规的候选选择,因此特别是永远不会是主要的(primary)。相反,他们总是以最低的优先级结束。在实践中,你应该把自引用作为最后的手段(例如,通过Bean的事务代理调用同一实例上的其他方法)。在这种情况下,可以考虑将受影响的方法分解到一个单独的委托Bean中。另外,你也可以使用 @Resource,它可以通过唯一的名字获得一个回到当前Bean的代理。

试图在同一个配置类上注入 @Bean 方法的结果,实际上也是一种自我引用的情况。要么在实际需要的地方(相对于配置类中的自动注入字段)的方法签名中延迟地解析这种引用,要么将受影响的 @Bean 方法声明为 static,将它们与包含的配置类实例及其生命周期解耦。否则,这些Bean只在 fallback 阶段被考虑,其他配置类上的匹配Bean将被选为主要(primary)候选者(如果有的话)。

@Autowired 适用于字段、构造函数和多参数方法,允许在参数级别上通过 qualifier 注解来缩小范围。相比之下,@Resource 只支持字段和只有一个参数的bean属性setter方法。因此,如果你的注入目标是构造函数或多参数方法,你应该坚持使用 qualifier。

你可以创建你自己的自定义 qualifier 注解。要做到这一点,请定义一个注解,并在你的定义中提供 @Qualifier 注解,如下面的例子所示。

Java

Kotlin

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

    String value();
}

然后你可以在自动注入的字段和参数上提供自定义 qualifier,如下例所示。

Java

Kotlin

public class MovieRecommender {

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

    private MovieCatalog comedyCatalog;

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

    // ...
}

接下来,你可以提供候选Bean定义的信息。你可以添加 <qualifier/> 标签作为 <bean/> 标签的子元素,然后指定 type 和 value 来匹配你的自定义qualifier注解。type是与注解的全限定类名相匹配的。另外,如果不存在名称冲突的风险,作为一种方便,你可以使用简短的类名。下面的例子演示了这两种方法。

<?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>

在 Classpath扫描和管理的组件 中,你可以看到一个基于注解的替代方案,以XML提供限定符元数据。具体来说,请看 用注解提供 Qualifier 元数据

在某些情况下,使用没有值的注解可能就足够了。当注解服务于一个更通用的目的并且可以应用于几个不同类型的依赖关系时,这可能是有用的。例如,你可以提供一个 offline 目录,在没有互联网连接的情况下可以进行搜索。首先,定义简单的注解,如下面的例子所示。

Java

Kotlin

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

然后将注解添加到要自动注入的字段或属性中,如下面的例子所示。

Java

Kotlin

public class MovieRecommender {

    @Autowired
    @Offline 
    private MovieCatalog offlineCatalog;

    // ...
}

这一行添加了 @Offline 注解。

现在,Bean定义只需要一个 qualifier type,如下面的例子所示。

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

这个元素指定了 qualifier。

你也可以定义自定义的 qualifier 注解,除了简单的 value 属性之外,还接受命名的(named)属性,或者代替简单的值属性。如果在要自动注入的字段或参数上指定了多个属性值,那么Bean定义必须与所有这些属性值相匹配才能被认为是自动注入的候选者。作为一个例子,考虑下面的注解定义。

Java

Kotlin

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

    String genre();

    Format format();
}

在这种情况下,Format 是一个枚举,定义如下。

Java

Kotlin

public enum Format {
    VHS, DVD, BLURAY
}

要自动注入的字段用自定义 qualifier 注解,并包括两个属性的值:genre 和 format,如下面的例子所示。

Java

Kotlin

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;

    // ...
}

最后,Bean的定义应该包含匹配的 qualifier 值。这个例子还展示了你可以使用Bean元属性来代替 <qualifier/> 元素。如果有的话,<qualifier/> 元素及其属性优先,但如果没有这样的qualifier,自动注入机制就会回到 <meta/> 标签中提供的值,就像下面例子中的最后两个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: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>

1.9.4. 使用泛型作为自动注入 Qualifier

除了 @Qualifier 注解外,你还可以使用Java泛型作为隐含的限定形式。例如,假设你有下面的配置。

Java

Kotlin

@Configuration
public class MyConfiguration {

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

    @Bean
    public IntegerStore integerStore() {
        return new IntegerStore();
    }
}

假设前面的Bean实现了一个泛型接口,(即 Store<String> 和 Store<Integer>),你可以 @Autowire Store 接口,泛型被用作qualifier,如下例所示。

Java

Kotlin

@Autowired
private Store<String> s1; // <String> qualifier, injects the stringStore bean

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

泛型 qualifier 也适用于自动注入list、Map 实例和数组。下面的例子是自动注入一个泛型 List

Java

Kotlin

// 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;

1.9.5. 使用 CustomAutowireConfigurer

CustomAutowireConfigurer 是一个 BeanFactoryPostProcessor,可以让你注册自己的自定义 qualifier 注解类型,即使它们没有用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/> 元素上可用的任何默认的 autowire-candidates pattern。

  • 存在 @Qualifier 注解和任何用 CustomAutowireConfigurer 注册的自定义注解。

当多个Bean有资格成为自动注入的候选者时,“primary” 的确定方法如下。如果候选Bean定义中正好有一个 Primary 属性被设置为 true,它就被选中。

1.9.6. 用 @Resource 注入

Spring还支持通过在字段或Bean属性设置方法上使用JSR-250 @Resource 注解(jakarta.annotation.Resource)进行注入。这是Jakarta EE中的一种常见模式:例如,在JSF管理的Bean和JAX-WS端点。对于Spring管理的对象,Spring也支持这种模式。

@Resource 需要一个 name 属性。默认情况下,Spring将该值解释为要注入的Bean名称。换句话说,它遵循按名称的语义,正如下面的例子所展示的。

Java

Kotlin

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource(name="myMovieFinder") 
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

这一行注入了一个 @Resource

如果没有明确指定名字,默认的名字来自于字段名或setter方法。如果是一个字段,它采用字段名。如果是setter方法,则采用Bean的属性名。下面的例子将把名为 movieFinder 的bean注入它的setter方法中。

Java

Kotlin

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

与注解一起提供的名称被 CommonAnnotationBeanPostProcessor 所知道的 ApplicationContext 解析为一个Bean名称。如果你明确地配置Spring的 SimpleJndiBeanFactory,这些名字可以通过JNDI解析。然而,我们建议你依靠默认行为,并使用Spring的JNDI查询功能来保留代理的级别。

在没有明确指定名称的 @Resource 使用的特殊情况下,与 @Autowired 类似,@Resource 找到一个主要的类型匹配,而不是一个特定的命名的 bean,并解析众所周知的可解析的依赖:BeanFactoryApplicationContext、 ResourceLoaderApplicationEventPublisher 和 MessageSource 接口。

因此,在下面的例子中,customerPreferenceDao 字段首先寻找名为 "customerPreferenceDao" 的Bean,然后回退到 CustomerPreferenceDao 类型的 primary 类型匹配。

Java

Kotlin

public class MovieRecommender {

    @Resource
    private CustomerPreferenceDao customerPreferenceDao;

    @Resource
    private ApplicationContext context; 

    public MovieRecommender() {
    }

    // ...
}

context 字段是根据已知的可解析依赖类型注入的:ApplicationContext
1.9.7. 使用 @Value

@Value 通常用于注入外部化properties。

Java

Kotlin

@Component
public class MovieRecommender {

    private final String catalog;

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

采用以下配置。

Java

Kotlin

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

以及以下 application.properties 文件。

catalog.name=MovieCatalog

在这种情况下,catalog 参数和字段将等于 MovieCatalog 值。

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

Java

Kotlin

@Configuration
public class AppConfig {

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

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

使用上述配置可以确保在任何 ${} 占位符无法解析的情况下Spring初始化失败。也可以使用 setPlaceholderPrefixsetPlaceholderSuffix 或 setValueSeparator 等方法来定制占位符。

Spring Boot默认配置了一个 PropertySourcesPlaceholderConfigurer Bean,它将从 application.properties 和 application.yml 文件中获取属性。

Spring提供的内置转换器支持允许自动处理简单的类型转换(例如转换为 Integer 或 int)。多个逗号分隔的值可以自动转换为 String 数组,无需额外的操作。

可以提供一个默认值,如下所示。

Java

Kotlin

@Component
public class MovieRecommender {

    private final String catalog;

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

Spring BeanPostProcessor 在幕后使用一个 ConversionService 来处理将 @Value 中的 String 值转换为目标类型的过程。如果你想为你自己的自定义类型提供转换支持,你可以提供你自己的 ConversionService Bean实例,如下例所示。

Java

Kotlin

@Configuration
public class AppConfig {

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

当 @Value 包含一个 SpEL 表达式 时,该值将在运行时被动态计算出来,如下例所示。

Java

Kotlin

@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) {
        this.catalog = catalog;
    }
}

SpEL还能够使用更复杂的数据结构。

Java

Kotlin

@Component
public class MovieRecommender {

    private final Map<String, Integer> countOfMoviesPerCatalog;

    public MovieRecommender(
            @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map<String, Integer> countOfMoviesPerCatalog) {
        this.countOfMoviesPerCatalog = countOfMoviesPerCatalog;
    }
}

1.9.8. 使用 @PostConstruct 和 @PreDestroy

CommonAnnotationBeanPostProcessor 不仅可以识别 @Resource 注解,还可以识别JSR-250生命周期注解:jakarta.annotation.PostConstruct 和 jakarta.annotation.PreDestroy。在Spring 2.5中引入,对这些注解的支持为 初始化回调销毁回调中描述的生命周期回调机制提供了一个替代方案。只要在Spring ApplicationContext 中注册了 CommonAnnotationBeanPostProcessor,携带这些注解之一的方法就会在生命周期中与相应的Spring生命周期接口方法或明确声明的回调方法在同一时间被调用。在下面的例子中,缓存在初始化时被预先填充,在销毁时被清除。

Java

Kotlin

public class CachingMovieLister {

    @PostConstruct
    public void populateMovieCache() {
        // populates the movie cache upon initialization...
    }

    @PreDestroy
    public void clearMovieCache() {
        // clears the movie cache upon destruction...
    }
}

关于结合各种生命周期机制的效果,详见 结合生命周期机制

与 @Resource 一样,@PostConstruct 和 @PreDestroy 注解类型在JDK 6到8中是标准Java库的一部分。然而,整个 javax.annotation 包在JDK 9中从核心Java模块中分离出来,最终在JDK 11中被删除。从Jakarta EE 9开始,该包现在住在 jakarta.annotation 中。如果需要,现在需要通过Maven中心获得 jakarta.annotation-api 工件,只需像其他库一样添加到应用程序的classpath中即可。

1.10. Classpath扫描和管理的组件

本章中的大多数例子都使用XML来指定配置元数据,在Spring容器中产生每个 BeanDefinition。上一节(基于注解的容器配置)演示了如何通过源级注解提供大量的配置元数据。然而,即使在这些例子中,"基础" Bean定义也是在XML文件中明确定义的,而注解只驱动依赖性注入。本节描述了一个通过扫描classpath来隐式检测候选组件的选项。候选组件是与过滤器标准相匹配的类,并且有一个在容器中注册的相应的bean定义。这样就不需要使用 XML 来执行 bean 注册了。相反,你可以使用注解(例如 @Component)、AspectJ类型表达式或你自己的自定义过滤标准来选择哪些类在容器中注册了Bean定义。

你可以使用Java来定义 Bean,而不是使用XML文件。看看 @Configuration@Bean@Import 和 @DependsOn 注解,看看如何使用这些功能的例子。

1.10.1. @Component 和进一步的 Stereotype 注解

@Repository 注解是任何满足 repository(也被称为数据访问对象或 DAO)角色或 stereotype 的类的标记。这个标记的用途包括异常的自动翻译,如 异常翻译 中所述。

Spring提供了更多的 stereotype 注解。@Component@Service, 和 @Controller。 @Component 是一个通用的stereotype,适用于任何Spring管理的组件。@Repository、 @Service 和 @Controller 是 @Component 的特殊化,用于更具体的使用情况(分别在持久层、服务层和表现层)。因此,你可以用 @Component 来注解你的组件类,但是,通过用 @Repository@Service 或 @Controller 来注解它们,你的类更适合于被工具处理或与切面关联。例如,这些stereotype注解是指向性的理想目标。在Spring框架的未来版本中,@Repository@Service 和 @Controller 还可以携带额外的语义。因此,如果你要在服务层使用 @Component 或 @Service 之间进行选择,@Service 显然是更好的选择。同样地,如前所述,@Repository 已经被支持作为持久层中自动异常翻译的标记。

1.10.2. 使用元注解和组合注解

Spring提供的许多注解都可以在你自己的代码中作为元注解使用。元注解是一个可以应用于另一个注解的注解。例如,前面 提到的 @Service 注解是用 @Component 进行元注解的,如下例所示。

Java

Kotlin

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component 
public @interface Service {

    // ...
}

@Component 使 @Service 与 @Component 的处理方式相同。

你也可以结合元注解来创建 “composed annotations”(组合注解)。例如,Spring MVC的 @RestController 注解是由 @Controller 和 @ResponseBody 组成。

此外,组合注解可以选择性地重新声明来自元注解的属性以允许定制。当你想只暴露元注解的一个子集的属性时,这可能特别有用。例如,Spring的 @SessionScope 注解将 scope 名称硬编码为 session,但仍然允许自定义 proxyMode。下面的列表显示了 SessionScope 注解的定义。

Java

Kotlin

@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;

}

然后,你可以使用 @SessionScope,而不用声明 proxyMode,如下所示。

Java

Kotlin

@Service
@SessionScope
public class SessionScopedService {
    // ...
}

你也可以覆盖 proxyMode 的值,如下例所示。

Java

Kotlin

@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
    // ...
}

更多细节,请参见 Spring 注解编程模型 维基页面。

1.10.3. 自动检测类和注册Bean定义

Spring可以自动检测 stereotype 的类,并在 ApplicationContext 中注册相应的 BeanDefinition 实例。例如,以下两个类符合这种自动检测的条件。

Java

Kotlin

@Service
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

Java

Kotlin

@Repository
public class JpaMovieFinder implements MovieFinder {
    // implementation elided for clarity
}

为了自动检测这些类并注册相应的Bean,你需要在你的 @Configuration 类中添加 @ComponentScan,其中 basePackages 属性是这两个类的共同父包。(或者,你可以指定一个用逗号或分号或空格分隔的列表,其中包括每个类的父包。)

Java

Kotlin

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

为了简洁起见,前面的例子可以使用注解的 value 属性(即 @ComponentScan("org.example"))。

以下是使用XML的替代方案。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        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:component-scan base-package="org.example"/>

</beans>

使用 <context:component-scan> 就隐含地实现了 <context:annotation-config> 的功能。在使用 <context:component-scan> 时,通常不需要包括 <context:annotation-config> 元素。

扫描classpath包需要classpath中存在相应的目录项。当你用Ant构建JAR时,确保你没有激活JAR任务的 files-only 开关。另外,在某些环境中,根据安全策略,classpath目录可能不会被暴露—​例如,JDK 1.7.0_45及以上版本的独立应用程序(这需要在清单中设置 'Trusted-Library',见 https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。

在JDK 9的模块路径(Jigsaw)上,Spring的classpath扫描一般都能按预期工作。但是,请确保你的组件类在你的 module-info 描述符中被导出。如果你希望Spring调用你的类中的非public成员,请确保它们是 "开放的"(也就是说,它们在你的 module-info 描述符中使用 opens 声明而不是 exports 声明)。

此外,当你使用组件扫描元素时,AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 都被隐含地包括在内。这意味着这两个组件被自动检测并连接在一起—​所有这些都不需要在XML中提供任何bean配置元数据。

你可以通过包含值为 false 的 annotation-config 属性来禁用 AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 的注册。
1.10.4. 使用Filter来自定义扫描

默认情况下,用 @Component@Repository@Service@Controller、 @Configuration 注解的类,或者本身用 @Component 注解的自定义注解是唯一被检测到的候选组件。然而,你可以通过应用自定义 filter 来修改和扩展这种行为。将它们作为 @ComponentScan 注解的 includeFilters 或 excludeFilters 属性(或作为XML配置中 <context:component-scan> 元素的 <context:include-filter /> 或 <context:exclud-filter /> 子元素)。每个 filter 元素都需要 type 和 expression 属性。下表描述了过滤选项。

Table 5. Filter Type
Filter Type示例表达式说明

注解 (默认)

org.example.SomeAnnotation

一个注解在目标组件中的类型级别是 present 或 meta-present

可指定

org.example.SomeClass

目标组件可分配给(继承或实现)的一个类(或接口)。

aspectj

org.example..*Service+

要被目标组件匹配的 AspectJ type 表达式。

regex

org\.example\.Default.*

一个与目标组件的类名相匹配的 regex expression。

自定义

org.example.MyTypeFilter

org.springframework.core.type.TypeFilter 接口的自定义实现。

下面的例子显示了配置忽略了所有 @Repository 注解,而使用 “stub” repository。

Java

Kotlin

@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 或者提供 use-default-filters="false" 作为 <component-scan/> 元素的属性来禁用默认过滤器。这将有效地禁止对用 @Component@Repository@Service@Controller、 @RestController 或 @Configuration 注解或元注解的类进行自动检测。
1.10.5. 在组件中定义Bean元数据

Spring组件也可以向容器贡献Bean定义元数据。你可以用用于在 @Configuration 注解的类中定义Bean元数据的相同的 @Bean 注解来做到这一点。下面的例子展示了如何做到这一点。

Java

Kotlin

@Component
public class FactoryMethodComponent {

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

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

前面的类是一个Spring组件,在它的 doWork() 方法里有特定的应用代码。然而,它也贡献了一个Bean定义,它有一个工厂方法,引用了 publicInstance() 方法。@Bean 注解标识了工厂方法和其他Bean定义属性,例如通过 @Qualifier 注解标识了 qualifier 值。其他可以指定的方法级注解有 @Scope@Lazy 和自定义 qualifier 注解。

除了对组件初始化的作用,你还可以将 @Lazy 注解放在标有 @Autowired 或 @Inject 的注入点上。在这种情况下,它导致了一个延迟解析的代理的注入。然而,这种代理方法是相当有限的。对于复杂的懒加载交互,特别是与可选的依赖关系相结合,我们推荐使用 ObjectProvider<MyTargetBean> 来代替。

如前所述,支持自动注入的字段和方法,并额外支持 @Bean 方法的自动注入。下面的例子展示了如何做到这一点。

Java

Kotlin

@Component
public class FactoryMethodComponent {

    private static int i;

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

    // use of a custom qualifier and autowiring of method parameters
    @Bean
    protected TestBean protectedInstance(
            @Qualifier("public") TestBean spouse,
            @Value("#{privateInstance.age}") String country) {
        TestBean tb = new TestBean("protectedInstance", 1);
        tb.setSpouse(spouse);
        tb.setCountry(country);
        return tb;
    }

    @Bean
    private TestBean privateInstance() {
        return new TestBean("privateInstance", i++);
    }

    @Bean
    @RequestScope
    public TestBean requestScopedInstance() {
        return new TestBean("requestScopedInstance", 3);
    }
}

这个例子将 String 方法的参数 country 自动注入到另一个名为 privateInstance 的bean上的 age 属性值。一个Spring表达式语言元素通过符号 #{ <expression> } 定义了该属性的值。对于 @Value 注解,表达式解析器被预设为在解析表达式文本时寻找Bean名称。

从Spring Framework 4.3开始,你也可以声明一个 InjectionPoint(或其更具体的子类: DependencyDescriptor)类型的工厂方法参数来访问触发创建当前Bean的请求注入点。请注意,这只适用于Bean实例的实际创建,而不适用于现有实例的注入。因此,这个功能对 prototype scope 的Bean最有意义。对于其它scope,工厂方法只看到在给定scope中触发创建新 bean 实例的注入点(例如,触发创建 lazy singleton bean 的依赖关系)。在这种情况下,你可以在语义上注意使用所提供的注入点元数据。下面的例子显示了如何使用 InjectionPoint

Java

Kotlin

@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处理或其他约束条件适用。

你可以将 @Bean 方法声明为 static 的,这样就可以在不创建其包含的配置类实例的情况下调用它们。在定义后处理bean(例如,BeanFactoryPostProcessor 或 BeanPostProcessor 类型)时,这一点特别有意义,因为这种bean在容器生命周期的早期被初始化,并且应该避免在这一点上触发配置的其它部分。

由于技术上的限制,对静态 @Bean 方法的调用永远不会被容器拦截,甚至在 @Configuration 类中也不会(如本节前面所述)。CGLIB子类只能覆盖非静态方法。因此,对另一个 @Bean 方法的直接调用具有标准的Java语义,结果是直接从工厂方法本身返回一个独立实例。

@Bean 方法的Java语言可见性对Spring容器中产生的Bean定义没有直接影响。你可以在非 @Configuration 类中自由地声明你的工厂方法,也可以在任何地方为静态方法声明。然而, @Configuration 类中的常规 @Bean 方法需要是可重写的—​也就是说,它们不能被声明为 private 或 final

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

最后,一个类可以为同一个Bean持有多个 @Bean 方法,作为对多个工厂方法的安排,根据运行时的可用依赖关系来使用。这与在其他配置情况下选择 "最环保" 的构造函数或工厂方法的算法相同。在构造时选择具有最大数量的可满足的依赖关系的变量,类似于容器在多个 @Autowired 构造函数之间进行选择。

1.10.6. 命名自动检测的组件

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

如果这样的注解不包含 name value,或者对于任何其他检测到的组件(比如那些由自定义过滤器发现的组件),默认的bean类名称生成器会返回未加大写的非限定类名称。例如,如果检测到以下组件类,其名称将是 myMovieLister 和 movieFinderImpl

Java

Kotlin

@Service("myMovieLister")
public class SimpleMovieLister {
    // ...
}

Java

Kotlin

@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}

如果你不想依赖默认的 Bean 命名策略,你可以提供一个自定义的 Bean 命名策略。首先,实现 BeanNameGenerator 接口,并确保包含一个默认的无参数构造函数。然后,在配置扫描器时提供全路径类名,正如下面的注解和 Bean 定义示例所示。

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

Java

Kotlin

@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>

一般来说,只要其他组件可能对它进行显式引用,就要考虑用注解来指定名称。另一方面,只要容器负责注入,自动生成的名字就足够了。

1.10.7. 为自动检测的组件提供一个Scope

与一般的Spring管理的组件一样,自动检测的组件的默认和最常见的scope是 singleton。然而,有时你需要一个不同的scope,可以通过 @Scope 注解来指定。你可以在注解中提供scope的名称,如下面的例子所示。

Java

Kotlin

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}

@Scope 注解只对具体的Bean类(对于注解的组件)或工厂方法(对于 @Bean 方法)进行内省。与XML bean定义相比,没有bean定义继承的概念,而且类级别的继承层次与元数据的目的无关。

有关Spring context 中 “request” 或 “session” 等Web特定 scope 的详细信息,请参阅 Request、 Session、 Application 和 WebSocket Scope。与这些 scope 的预制注解一样,你也可以通过使用Spring的元注解方法来组成你自己的 scope 注解:例如,用 @Scope("prototype") 元注解的自定义注解,可能还会声明一个自定义 scope 代理(scoped-proxy)模式。

为了给 scope 解析提供一个自定义的策略,而不是依赖基于注解的方法,你可以实现 ScopeMetadataResolver 接口。请确保包含一个默认的无参数构造函数。然后你可以在配置扫描器时提供全路径的类名,正如下面这个注解和 bean 定义的例子所示。

Java

Kotlin

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

<beans>
    <context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>

当使用某些非 singleton scope 时,可能需要为 scope 对象生成代理。这在 “作为依赖的 Scope Bean”中有所描述。为了这个目的,在组件扫描元素上有一个 scoped-proxy 属性。三个可能的值是:nointerfaces 和 targetClass。例如,以下配置的结果是标准的JDK动态代理。

Java

Kotlin

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

<beans>
    <context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>

1.10.8. 用注解提供 Qualifier 元数据

@Qualifier 注解在 “用 Qualifiers 微调基于注解的自动注入” 中讨论过。那一节中的例子演示了如何使用 @Qualifier 注解和自定义 qualifier 注解来提供细粒度的控制,当你解决自动注入候选对象时。因为这些例子是基于XML Bean定义的,qualifier 元数据是通过使用XML中 bean 元素的 qualifier 或 meta 子元素提供给候选Bean定义的。当依靠classpath扫描来自动检测组件时,你可以在候选类上用类型级注解来提供 qualifier 元数据。下面的三个例子演示了这种技术。

Java

Kotlin

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

Java

Kotlin

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

Java

Kotlin

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

与大多数基于注解的替代方案一样,请记住,注解元数据是与类定义本身绑定的,而XML的使用允许同一类型的多个Bean在其限定符元数据中提供变化,因为该元数据是按实例而不是按类提供的。
1.10.9. 生成一个候选组件的索引

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

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

要生成索引,请为每个包含组件的模块添加一个额外的依赖,这些组件是组件扫描指令的目标。下面的例子说明了如何用Maven做到这一点。

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

对于Gradle 4.5和更早的版本,应该在 compileOnly 配置中声明该依赖关系,如下面的例子所示。

dependencies {
    compileOnly "org.springframework:spring-context-indexer:6.0.8-SNAPSHOT"
}

在Gradle 4.6及以后的版本中,应该在 annotationProcessor 配置中声明该依赖,如以下例子所示。

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

spring-context-indexer 工件会生成一个 META-INF/spring.component 文件,并包含在jar文件中。

当在你的IDE中使用这种模式时,spring-context-indexer 必须被注册为注解处理器,以确保候选组件更新时索引是最新的。
当在classpath上发现 META-INF/spring.component 文件时,索引会自动启用。如果索引对某些库(或用例)是部分可用的,但不能为整个应用程序建立索引,你可以通过将 spring.index.ignore 设置为 true,或者通过 SpringProperties 机制,退回到常规的 classpath 安排(就像根本没有索引存在一样)。

1.11. 使用JSR 330标准注解

Spring提供对JSR-330标准注解的支持(依赖注入)。这些注解的扫描方式与Spring注解的扫描方式相同。要使用它们,你需要在你的classpath中拥有相关的jar。

如果你使用Maven,jakarta.inject 工件在标准的Maven资源库( Central Repository: jakarta/inject/jakarta.inject-api/2.0.0 )中可以找到。你可以在你的pom.xml文件中添加以下依赖。

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

1.11.1. 用 @Inject 和 @Named 进行依赖注入

你可以使用 @Jakarta.inject.Inject 来代替 @Autowired,如下所示。

Java

Kotlin

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

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

和 @Autowired 一样,你可以在字段级、方法级和构造函数-参数级使用 @Inject。此外,你可以将你的注入点声明为一个 Provider,允许按需访问较短scope的Bean,或通过 Provider.get() 调用懒加载地访问其他Bean。下面的例子提供了前述例子的一个变体。

Java

Kotlin

public class SimpleMovieLister {

    private Provider<MovieFinder> movieFinder;

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

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

如果你想为应该被注入的依赖使用一个 qualifier 的名字,你应该使用 @Named 注解,正如下面的例子所示。

Java

Kotlin

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 属性。下面的一对例子展示了如何使用 @Inject 和 @Nullable

public class SimpleMovieLister {

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

Java

Kotlin

public class SimpleMovieLister {

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

1.11.2. @Named 和 @ManagedBean:与 @Component 注解的标准对等物

你可以使用 @jakarta.inject.Named 或 jakarta.annotation.ManagedBean 来代替 @Component,如下例所示。

Java

Kotlin

@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 可以以类似的方式使用,如下面的例子所示。

Java

Kotlin

@Named
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

当你使用 @Named 或 @ManagedBean 时,你可以以与使用Spring注解完全相同的方式使用组件扫描,正如下面的例子所示。

Java

Kotlin

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

与 @Component 相比,JSR-330的 @Named 和JSR-250的 @ManagedBean 注解是不可组合的。你应该使用Spring 的 stereotype 模型来构建自定义组件注解。
1.11.3. JSR-330 标准注解的局限性

当你使用标准注解工作时,你应该知道一些重要的功能是不可用的,如下表所示。

Table 6. Spring组件模型元素与JSR-330变体的比较
Springjakarta.inject.*jakarta.inject 限制 / 说明

@Autowired

@Inject

@Inject 没有 'required' 属性。可以用 Java 8的 Optional 来代替。

@Component

@Named / @ManagedBean

JSR-330并没有提供一个可组合的模型,只是提供了一种识别命名组件的方法。

@Scope("singleton")

@Singleton

JSR-330的默认scope就像Spring的 prototype。然而,为了与Spring的一般默认值保持一致,在Spring容器中声明的JSR-330 Bean默认是一个 singleton。为了使用 singleton 以外的scope,你应该使用Spring的 @Scope 注解。jakarta.inject 也提供了一个 jakarta.inject.Scope 注解:不过,这个注解只用于创建自定义注解。

@Qualifier

@Qualifier / @Named

jakarta.inject.Qualifier 只是一个用于构建自定义 qualifier 的元注解。具体的 String qualifier(像Spring的 @Qualifier 一样有一个值)可以通过 jakarta.inject.Named 来关联。

@Value

-

没有对应的

@Lazy

-

没有对应的

ObjectFactory

Provider

jakarta.inject.Provider 是Spring的 ObjectFactory 的直接替代品,只是 get() 方法的名字比较短。它也可以与Spring的 @Autowired 结合使用,或者与非注解的构造器和setter方法结合使用。

1.12. 基于Java的容器配置

本节介绍了如何在你的Java代码中使用注解来配置Spring容器。它包括以下主题。

1.12.1. 基本概念:@Bean 和 @Configuration

Spring的Java配置支持的核心工件是 @Configuration 注解的类和 @Bean 注解的方法。

@Bean 注解用来表示一个方法实例化、配置和初始化了一个新的对象,由Spring IoC容器管理。对于那些熟悉Spring的 <beans/> XML配置的人来说,@Bean 注解的作用与 <bean/> 元素的作用相同。你可以在任何Spring @Component 中使用 @Bean 注解的方法。然而,它们最常被用于 @Configuration Bean。

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

Java

Kotlin

@Configuration
public class AppConfig {

    @Bean
    public MyServiceImpl myService() {
        return new MyServiceImpl();
    }
}

前面的 AppConfig 类等同于下面的 Spring <beans/> XML。

<beans>
    <bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>

完整的 @Configuration 与 "精简的" @Bean 模式?

当 @Bean 方法被声明在没有 @Configuration 注解的类中时,它们被称为以 "精简" 模式处理。在 @Component 或甚至在一个普通的类中声明的Bean方法被认为是 "精简" 的,包含类的主要目的不同,而 @Bean 方法是那里的一种奖励。例如,服务组件可以通过每个适用的组件类上的一个额外的 @Bean 方法向容器暴露管理视图。在这种情况下,@Bean 方法是一种通用的工厂方法机制。

与完整的 @Configuration 不同,精简的 @Bean 方法不能声明bean间的依赖关系。相反,它们对其包含的组件的内部状态进行操作,也可以选择对其可能声明的参数进行操作。因此,这样的 @Bean 方法不应该调用其他的 @Bean 方法。每个这样的方法从字面上看只是一个特定的Bean引用的工厂方法,没有任何特殊的运行时语义。这里的正面效果是,在运行时不需要应用CGLIB子类,所以在类的设计方面没有任何限制(也就是说,包含的类可以是 final 的等等)。

在常见的情况下,@Bean 方法要在 @Configuration 类中声明,确保始终使用 "完整" 模式,因此跨方法引用会被重定向到容器的生命周期管理。这可以防止同一个 @Bean 方法被意外地通过普通的Java调用来调用,这有助于减少在 "精简" 模式下操作时很难追踪的细微Bug。

下面几节将深入讨论 @Bean 和 @Configuration 注解。然而,首先,我们将介绍通过使用基于Java的配置来创建spring容器的各种方法。

1.12.2. 通过使用 AnnotationConfigApplicationContext 实例化Spring容器

下面的章节记录了Spring的 AnnotationConfigApplicationContext,它在Spring 3.0中引入。这个多功能的 ApplicationContext 实现不仅能够接受 @Configuration 类作为输入,还能够接受普通的 @Component 类和用JSR-330元数据注解的类。

当 @Configuration 类被提供为输入时,@Configuration 类本身被注册为Bean定义,该类中所有声明的 @Bean 方法也被注册为Bean定义。

当 @Component 和JSR-330类被提供时,它们被注册为bean定义,并且假定DI元数据如 @Autowired 或 @Inject 在必要时被用于这些类。

简单构造

与实例化 ClassPathXmlApplicationContext 时使用Spring XML文件作为输入一样,你可以在实例化 AnnotationConfigApplicationContext 时使用 @Configuration 类作为输入。这使得Spring容器的使用完全不需要XML,正如下面的例子所示。

Java

Kotlin

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

如前所述,AnnotationConfigApplicationContext 不限于只与 @Configuration 类一起工作。任何 @Component 或JSR-330注解的类都可以作为输入提供给构造函数,正如下面的例子所示。

Java

Kotlin

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

前面的例子假设 MyServiceImplDependency1 和 Dependency2 使用Spring的依赖注入注解,如 @Autowired

通过使用 register(Class<?>…​) 以编程方式构建容器。

你可以通过使用无参数构造函数来实例化 AnnotationConfigApplicationContext,然后通过 register() 方法来配置它。这种方法在以编程方式构建 AnnotationConfigApplicationContext 时特别有用。下面的例子展示了如何做到这一点。

Java

Kotlin

public static void main(String[] args) {
    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 类添加如下注解。

Java

Kotlin

@Configuration
@ComponentScan(basePackages = "com.acme") 
public class AppConfig  {
    // ...
}

这个注解可以实现组件扫描。

有经验的Spring用户可能熟悉来自Spring的 context: namespace的XML声明等价物,如下例所示。

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

在前面的例子中,com.acme 包被扫描以寻找任何 @Component 注解的类,这些类被注册为容器中的Spring Bean定义。AnnotationConfigApplicationContext 暴露了 scan(String…​) 方法,以实现同样的组件扫描功能,如下例所示。

Java

Kotlin

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.scan("com.acme");
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
}

请记住,@Configuration 类是用 @Component 元注解的,所以它们是组件扫描的候选者。在前面的例子中,假设 AppConfig 是在 com.acme 包(或下面的任何包)中声明的,它在调用 scan() 时被选中。在 refresh() 时,它的所有 @Bean 方法都被处理并注册为容器中的 bean 定义。
用 AnnotationConfigWebApplicationContext 支持Web应用程序

AnnotationConfigApplicationContext 的一个 WebApplicationContext 变体可以用 AnnotationConfigWebApplicationContext。你可以在配置Spring ContextLoaderListener servlet listener、Spring MVC DispatcherServlet 等时使用这个实现。下面的 web.xml 片段配置了一个典型的Spring MVC Web应用(注意使用 contextClass 的 context-param 和 init-param)。

<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>

对于编程用例,GenericWebApplicationContext 可以作为 AnnotationConfigWebApplicationContext 的替代。请参阅 GenericWebApplicationContext javadoc 了解详情。
1.12.3. 使用 @Bean 注解

@Bean 是一个方法级注解,是XML <bean/> 元素的直接类似物。该注解支持 <bean/> 所提供的一些属性,例如。

你可以在 @Configuration 或 @Component 注解的类中使用 @Bean 注解。

声明一个 Bean

为了声明一个Bean,你可以用 @Bean 注解来注解一个方法。你可以用这个方法在 ApplicationContext 中注册一个Bean定义,该类型被指定为该方法的返回值。默认情况下,Bean的名字和方法的名字是一样的。下面的例子显示了一个 @Bean 方法声明。

Java

Kotlin

@Configuration
public class AppConfig {

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

前面的配置完全等同于下面的Spring XML。

<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

这两个声明使 ApplicationContext 中一个名为 transferService 的Bean可用,并与 TransferServiceImpl 类型的对象实例绑定,正如下面的文字图片所示。

transferService -> com.acme.TransferServiceImpl

你也可以使用 default 方法来定义Bean。这允许通过在默认方法上实现带有Bean定义的接口来组成Bean配置。

Java

public interface BaseConfig {

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

@Configuration
public class AppConfig implements BaseConfig {

}

你也可以用一个接口(或基类)的返回类型来声明你的 @Bean 方法,如下例所示。

Java

Kotlin

@Configuration
public class AppConfig {

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

然而,这将提前类型预测的可见性限制在指定的接口类型(TransferService)。然后,只有在受影响的 singleton Bean被实例化后,容器才知道完整的类型(TransferServiceImpl)。非lazy单体Bean根据它们的声明顺序被实例化,所以你可能会看到不同的类型匹配结果,这取决于另一个组件何时试图通过非声明的类型进行匹配(比如 @Autowired TransferServiceImpl,它只在 transferService Bean被实例化后才会解析)。

如果你一直通过声明的服务接口来引用你的类型,你的 @Bean 返回类型可以安全地加入这个设计决定。然而,对于实现了多个接口的组件或可能被其实现类型引用的组件来说,声明最具体的返回类型是比较安全的(至少要与引用你的Bean的注入点要求的具体类型一样)。
Bean 依赖

一个 @Bean 注解的方法可以有任意数量的参数,描述构建该Bean所需的依赖关系。例如,如果我们的 TransferService 需要一个 AccountRepository,我们可以用一个方法参数将这种依赖关系具体化,如下例所示。

Java

Kotlin

@Configuration
public class AppConfig {

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

解析机制与基于构造函数的依赖注入基本相同。更多细节见 相关章节

接收生命周期的回调

任何用 @Bean 注解定义的类都支持常规的生命周期回调,并且可以使用JSR-250的 @PostConstruct 和 @PreDestroy 注解。更多细节请参见 JSR-250注解

常规的Spring 生命周期 回调也被完全支持。如果一个bean实现了 InitializingBeanDisposableBean 或 Lifecycle,它们各自的方法就会被容器调用。

标准的 *Aware 接口集(如 BeanFactoryAwareBeanNameAwareMessageSourceAwareApplicationContextAware 等)也被完全支持。

@Bean 注解支持指定任意的初始化和销毁回调方法,就像Spring XML在 bean 元素上的 init-method 和 destroy-method 属性一样,如下例所示。

Java

Kotlin

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

默认情况下,用Java配置定义的具有 public 的 close 或 shutdown 方法的Bean会自动被列入销毁回调。如果你有一个 public 的 close 或 shutdown 方法,并且你不希望它在容器关闭时被调用,你可以在你的Bean定义中添加 @Bean(destroyMethod = "") 来禁用默认 (inferred) 模式。

你可能想对你用JNDI获取的资源默认这样做,因为它的生命周期是在应用程序之外管理的。特别是,要确保总是对 DataSource 这样做,因为它在Jakarta EE应用服务器上已知是有问题的。

下面的例子显示了如何阻止一个 DataSource 的自动销毁回调。

Java

Kotlin

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

另外,对于 @Bean 方法,你通常使用程序化的JNDI查找,要么使用Spring的 JndiTemplate 或 JndiLocatorDelegate helper,要么直接使用JNDI InitialContext,但不使用 JndiObjectFactoryBean 变体(这将迫使你将返回类型声明为 FactoryBean 类型,而不是实际的目标类型,使得它难以用于其他 @Bean 方法中的交叉引用调用,这些方法打算引用这里提供的资源)。

就前文例子中的 BeanOne 而言,在构造过程中直接调用 init() 方法同样有效,正如下面的例子所示。

Java

Kotlin

@Configuration
public class AppConfig {

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

    // ...
}

当你直接在Java中工作时,你可以对你的对象做任何你喜欢的事情,而不一定需要依赖容器的生命周期。
指定 Bean 的 Scope

Spring包括 @Scope 注解,这样你就可以指定Bean的 scope。

使用 @Scope 注解

你可以指定你用 @Bean 注解定义的 Bean 应该有一个特定的 scope。你可以使用 Bean Scopes 部分中指定的任何一个标准 scope。

默认的scope是 singleton,但你可以用 @Scope 注解来覆盖它,如下例所示。

Java

Kotlin

@Configuration
public class MyConfiguration {

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

@Scope 和 scoped-proxy

Spring提供了一种通过 scope 代理 来处理scope 依赖的便捷方式。使用XML配置时,创建这种代理的最简单方法是 <aop:scoped-proxy/> 元素。在Java中用 @Scope 注解配置你的Bean,提供了与 proxyMode 属性相当的支持。默认是 ScopedProxyMode.DEFAULT,这通常表示不应该创建任何 scope 代理,除非在组件扫描指令级别配置了不同的默认值。你可以指定 ScopedProxyMode.TARGET_CLASSScopedProxyMode.INTERFACES 或 ScopedProxyMode.NO

如果你把XML参考文档中的 scope 代理例子(见 scope 代理)移植到我们使用Java的 @Bean 上,它类似于以下内容。

Java

Kotlin

// an HTTP Session-scoped bean exposed as a proxy
@Bean
@SessionScope
public UserPreferences userPreferences() {
    return new UserPreferences();
}

@Bean
public Service userService() {
    UserService service = new SimpleUserService();
    // a reference to the proxied userPreferences bean
    service.setUserPreferences(userPreferences());
    return service;
}

自定义Bean的命名

默认情况下,配置类使用 @Bean 方法的名称作为结果Bean的名称。然而,这个功能可以通过 name 属性来重写,正如下面的例子所示。

Java

Kotlin

@Configuration
public class AppConfig {

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

Bean 别名

正如在 Bean 命名 中所讨论的,有时最好给一个Bean起多个名字,也就是所谓的Bean别名。@Bean 注解的 name 属性接受一个 String 数组来实现这一目的。下面的例子展示了如何为一个Bean设置若干别名。

Java

Kotlin

@Configuration
public class AppConfig {

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

Bean 描述(Description)

有时,为 Bean 提供更详细的文本描述是有帮助的。当Bean被暴露(也许是通过JMX)用于监控目的时,这可能特别有用。

为了给 @Bean 添加描述,你可以使用 @Description 注解,如下图所示。

Java

Kotlin

@Configuration
public class AppConfig {

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

1.12.4. 使用 @Configuration 注解

@Configuration 是一个类级注解,表示一个对象是Bean定义的来源。@Configuration 类通过 @Bean 注解的方法声明bean。对 @Configuration 类上的 @Bean 方法的调用也可以用来定义bean间的依赖关系。参见 “基本概念:@Bean 和 @Configuration” 的一般介绍。

注入bean间的依赖

当Bean相互之间有依赖关系时,表达这种依赖关系就像让一个Bean方法调用另一个一样简单,正如下面的例子所示。

Java

Kotlin

@Configuration
public class AppConfig {

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

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

在前面的例子中,beanOne 通过构造函数注入收到了对 beanTwo 的引用。

这种声明bean间依赖关系的方法只有在 @Configuration 类中声明了 @Bean 方法时才有效。你不能通过使用普通的 @Component 类来声明bean间的依赖关系。
查询方法注入

如前所述,查找方法注入 是一个高级功能,你应该很少使用。在 singleton scope 的Bean对 prototype scope 的Bean有依赖性的情况下,它是很有用的。为这种类型的配置使用Java提供了实现这种模式的自然手段。下面的例子展示了如何使用查找方法注入。

Java

Kotlin

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

通过使用Java配置,你可以创建一个 CommandManager 的子类,其中抽象的 createCommand() 方法被重载,这样它就可以查找到一个新的(prototype) command 对象。下面的例子显示了如何做到这一点。

Java

Kotlin

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

关于基于Java的配置如何在内部工作的进一步信息

考虑一下下面的例子,它显示了一个 @Bean 注解的方法被调用了两次。

Java

Kotlin

@Configuration
public class AppConfig {

    @Bean
    public ClientService clientService1() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        clientService.setClientDao(clientDao());
        return clientService;
    }

    @Bean
    public ClientService clientService2() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        clientService.setClientDao(clientDao());
        return clientService;
    }

    @Bean
    public ClientDao clientDao() {
        return new ClientDaoImpl();
    }
}

clientDao() 在 clientService1() 和 clientService2() 中被调用了一次。由于该方法创建了一个新的 ClientDaoImpl 实例并将其返回,你通常会期望有两个实例(每个服务都有一个)。这肯定是有问题的:在Spring中,实例化的Bean默认有一个 singleton scope。这就是神奇之处。所有的 @Configuration 类都是在启动时用 CGLIB 子类化的。在子类中,子方法首先检查容器中是否有任何缓存(scope)的Bean,然后再调用父方法并创建一个新实例。

根据你的Bean的scope,其行为可能是不同的。我们在这里讨论的是singleton(单例)。

没有必要将CGLIB添加到你的classpath中,因为CGLIB类被重新打包到 org.springframework.cglib 包中,并直接包含在 spring-core JAR中。

由于CGLIB在启动时动态地添加功能,所以有一些限制。特别是,配置类不能是 final 的。然而,配置类的任何构造函数都是允许的,包括使用 @Autowired 或单一的非默认构造函数声明进行默认注入。

如果你想避免任何CGLIB施加的限制,可以考虑在非 @Configuration 类中声明你的 @Bean 方法(例如,在普通的 @Component 类中声明),或者用 @Configuration(proxyBeanMethods = false) 来注释你的配置类。这样,@Bean 方法之间的跨方法调用就不会被拦截,所以你必须完全依赖构造函数或方法级别的依赖注入。

1.12.5. 构建基于Java的配置

Spring基于Java的配置功能让你可以编写注解,这可以降低配置的复杂性。

使用 @Import 注解

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

Java

Kotlin

@Configuration
public class ConfigA {

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

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

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

现在,在实例化上下文时不需要同时指定 ConfigA.class 和 ConfigB.class,而只需要明确提供 ConfigB,正如下面的例子所示。

Java

Kotlin

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

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

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

从Spring框架4.2开始,@Import 也支持对常规组件类的引用,类似于 AnnotationConfigApplicationContext.register 方法。如果你想避免组件扫描,通过使用一些配置类作为入口点来明确定义你的所有组件,这就特别有用。
在导入的 @Bean 定义上注入依赖

前面的例子是可行的,但也是简单的。在大多数实际情况下,Bean在配置类之间有相互依赖的关系。当使用XML时,这不是一个问题,因为不涉及编译器,你可以声明 ref="someBean" 并相信Spring会在容器初始化过程中解决这个问题。当使用 @Configuration 类时,Java编译器会对配置模型进行约束,即对其他Bean的引用必须是有效的Java语法。

幸运的是,解决这个问题很简单。正如我们 已经讨论过 的,一个 @Bean 方法可以有任意数量的参数来描述Bean的依赖关系。考虑下面这个更真实的场景,有几个 @Configuration 类,每个类都依赖于其他类中声明的bean。

Java

Kotlin

@Configuration
public class ServiceConfig {

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

@Configuration
public class RepositoryConfig {

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

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

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

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

还有一种方法可以达到同样的效果。记住,@Configuration 类最终只是容器中的另一个Bean。这意味着它们可以像其他Bean一样利用 @Autowired 和 @Value 注入以及其他功能。

请确保你用这种方式注入的依赖关系是最简单的那种。@Configuration 类在上下文的初始化过程中会被很早地处理,强行以这种方式注入依赖关系可能会导致意外的早期初始化。只要有可能,就采用基于参数的注入方式,如前面的例子。

另外,对于通过 @Bean 定义的 BeanPostProcessor 和 BeanFactoryPostProcessor 要特别小心。那些通常应该被声明为 static @Bean 方法,而不是触发其包含的配置类的实例化。否则,@Autowired 和 @Value 可能对配置类本身不起作用,因为它有可能在 AutowiredAnnotationBeanPostProcessor 之前作为一个bean实例创建它。

下面的例子显示了一个Bean是如何被自动注入到另一个Bean的。

Java

Kotlin

@Configuration
public class ServiceConfig {

    @Autowired
    private AccountRepository accountRepository;

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

@Configuration
public class RepositoryConfig {

    private final DataSource dataSource;

    public RepositoryConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

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

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

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

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

从Spring Framework 4.3开始,@Configuration 类中的构造函数注入才被支持。还要注意的是,如果目标Bean只定义了一个构造函数,就不需要指定 @Autowired

便于浏览的全限定名称的 import bean

在前面的场景中,使用 @Autowired 效果很好,并提供了所需的模块化,但确定自动注入的Bean定义到底在哪里声明,还是有些模糊。例如,作为一个查看 ServiceConfig 的开发者,你怎么知道 @Autowired AccountRepository Bean到底是在哪里声明的?它在代码中并不明确,而这可能就很好。请记住, Spring Tools for Eclipse 提供的工具可以呈现图形,显示一切是如何注入的,这可能就是你所需要的。另外,你的Java IDE可以很容易地找到 AccountRepository 类型的所有声明和使用,并快速显示返回该类型的 @Bean 方法的位置。

在不能接受这种模糊性的情况下,你希望在你的IDE中从一个 @Configuration 类直接导航到另一个,可以考虑自动注入配置类本身。下面的例子展示了如何做到这一点。

Java

Kotlin

@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 的定义是完全明确的。然而,ServiceConfig 现在与 RepositoryConfig 紧密耦合了。这就是权衡的结果。通过使用基于接口或基于抽象类的 @Configuration 类,这种紧密耦合可以得到一定程度的缓解。考虑一下下面的例子。

Java

Kotlin

@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})  // import the concrete config!
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");
}

现在,ServiceConfig 与具体的 DefaultRepositoryConfig 是松散耦合的,内置的IDE工具仍然有用。你可以轻松获得 RepositoryConfig 实现的类型层次。这样一来,浏览 @Configuration 类和它们的依赖关系就变得与浏览基于接口的代码的通常过程没有什么不同。

如果你想影响某些Bean的启动创建顺序,可以考虑将其中一些Bean声明为 @Lazy(在第一次访问时创建,而不是在启动时创建)或者声明为 @DependsOn 某些其他Bean(确保特定的其他Bean在当前Bean之前创建,超出后者的直接依赖关系)。
有条件地包括 @Configuration 类或 @Bean 方法

根据一些任意的系统状态,有条件地启用或禁用一个完整的 @Configuration 类,甚至是单个的 @Bean 方法,往往是很有用的。一个常见的例子是使用 @Profile 注解来激活Bean,只有在Spring Environment 中启用了特定的配置文件时(详见 Bean定义配置)。

@Profile 注解实际上是通过使用一个更灵活的注解来实现的,这个注解叫做 @Conditional@Conditional 注解指出了特定的 org.springframework.context.annotation.Condition 实现,在注册 @Bean 之前应该参考这些实现。

Condition 接口的实现提供了一个 matches(…​) 方法,它返回 true 或 false。例如,下面的列表显示了用于 @Profile 的实际 Condition 实现。

Java

Kotlin

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    // Read the @Profile annotation attributes
    MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
    if (attrs != null) {
        for (Object value : attrs.get("value")) {
            if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
                return true;
            }
        }
        return false;
    }
    return true;
}

更多细节请参见 @Conditional javadoc。

将Java和XML配置相结合

Spring的 @Configuration 类支持的目的并不是要100%完全取代Spring XML。一些设施,如Spring XML命名空间,仍然是配置容器的理想方式。在XML方便或必要的情况下,你有一个选择:要么通过使用例如 ClassPathXmlApplicationContext 以 "以XML为中心" 的方式实例化容器,要么通过使用 AnnotationConfigApplicationContext 和 @ImportResource 注解来根据需要导入XML,以 "以Java为中心" 的方式实例化它。

以XML为中心使用 @Configuration 类

从XML引导Spring容器并以临时的方式包含 @Configuration 类可能是更好的做法。例如,在一个使用Spring XML的大型现有代码库中,根据需要创建 @Configuration 类并从现有的XML文件中包含它们是比较容易的。在本节后面,我们将介绍在这种 "以XML为中心" 的情况下使用 @Configuration 类的选项。

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

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

下面的例子显示了Java中的一个普通配置类。

Java

Kotlin

@Configuration
public class AppConfig {

    @Autowired
    private DataSource dataSource;

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

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

下面的例子显示了 system-test-config.xml 文件样本的一部分。

<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>

下面的例子显示了一个可能的 jdbc.properties 文件。

jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=

Java

Kotlin

public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}

在 system-test-config.xml 文件中,AppConfig <bean/> 并没有声明一个 id 元素。虽然这样做是可以接受的,但考虑到没有其他Bean会引用它,而且它也不可能被明确地从容器中获取名字,所以这样做是不必要的。同样地,DataSource Bean只按类型自动注入,所以严格来说不需要明确的bean id

使用 <context:component-scan/> 来拾取 @Configuration 类

因为 @Configuration 是用 @Component 元注解的,所以 @Configuration 注解的类自动成为组件扫描的候选对象。使用与前面例子中描述的相同场景,我们可以重新定义 system-test-config.xml 以利用组件扫描。注意,在这种情况下,我们不需要明确声明 <context:annotation-config/>,因为 <context:component-scan/> 可以实现同样的功能。

下面的例子显示了修改后的 system-test-config.xml 文件。

<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 以类为中心使用XML与 @ImportResource

在 @Configuration 类是配置容器的主要机制的应用中,仍然可能需要至少使用一些 XML。在这些情况下,你可以使用 @ImportResource 并只定义你需要的 XML。这样做实现了 "以 Java 为中心" 的配置容器的方法,并使 XML 保持在最低限度。下面的例子(包括一个配置类、一个定义 bean 的 XML 文件、一个 properties 文件和 main 类)显示了如何使用 @ImportResource 注解来实现 "以 Java 为中心" 的配置,并根据需要使用 XML。

Java

Kotlin

@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>

jdbc.properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=

Java

Kotlin

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}

1.13. Environment 抽象

Environment 接口是一个集成在容器中的抽象,它对 application environment 的两个关键方面进行建模:配置文件(profiles) 和 属性(properties)

profile是一个命名的、逻辑上的bean定义组,只有在给定的profile处于活动状态时才会在容器中注册。无论是用 XML 定义的还是用注解定义的,Bean 都可以被分配给一个profile。Environment 对象在profile方面的作用是确定哪些profile(如果有的话)是当前活动(active)的,以及哪些profile(如果有的话)应该是默认活动的。

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

1.13.1. Bean定义配置

Bean定义配置(Bean definition profiles) 在核心容器中提供了一种机制,允许在不同的环境中注册不同的bean。“环境”这个词对不同的用户来说意味着不同的东西,而这个功能可以帮助许多用例,包括。

  • 在开发中针对内存中的数据源工作,而在QA或生产中从JNDI查找相同的数据源。

  • 仅在将应用程序部署到 performance 环境中时才注册监控基础设施。

  • 为 customer A 与 customer B 的部署注册定制的bean实现。

考虑一下实际应用中的第一个用例,它需要一个 DataSource。在一个测试环境中,配置可能类似于以下。

Java

Kotlin

@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("my-schema.sql")
        .addScript("my-test-data.sql")
        .build();
}

现在考虑如何将这个应用程序部署到QA或production环境中,假设该应用程序的数据源是在生产应用服务器的JNDI目录下注册的。我们的 dataSource bean现在看起来如下。

Java

Kotlin

@Bean(destroyMethod = "")
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

问题是如何根据当前环境在使用这两种变化之间切换。随着时间的推移,Spring用户已经设计出了许多方法来完成这一工作,通常是依靠系统环境变量和包含 ${placeholder} 标记的XML <import/> 语句的组合,根据环境变量的值解析到正确的配置文件路径。Bean定义配置是一个核心的容器功能,为这个问题提供了一个解决方案。

如果我们把前面的例子中显示的环境特定的Bean定义的用例进行概括,我们最终需要在某些情况下注册某些Bean定义,但在其他情况下不需要。你可以说,你想在情况A中注册某种类型的bean定义,而在情况B中注册另一种类型。

使用 @Profile

@Profile 注解让你表明当一个或多个指定的配置文件处于活动状态时,一个组件就有资格注册。使用我们前面的例子,我们可以重写 dataSource 配置如下。

Java

Kotlin

@Configuration
@Profile("development")
public class StandaloneDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}

Java

Kotlin

@Configuration
@Profile("production")
public class JndiDataConfig {

    @Bean(destroyMethod = "") 
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

@Bean(destroyMethod = "") 禁用默认的销毁方法推理。
如前所述,对于 @Bean 方法,你通常会选择使用程序化的JNDI查找,通过使用Spring的 JndiTemplate/JndiLocatorDelegat helper 或前面显示的直接使用JNDI InitialContext,但不使用 JndiObjectFactoryBean 的变体,这将迫使你将返回类型声明为 FactoryBean 类型。

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

  • !: profile的 NOT 逻辑

  • &: profile的 AND 的逻辑

  • |: profile的 OR 的逻辑

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

你可以使用 @Profile 作为 元注解,以创建一个自定义的组成注解。下面的例子定义了一个自定义的 @Production 注解,你可以把它作为 @Profile("production") 的直接替换。

Java

Kotlin

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

如果一个 @Configuration 类被标记为 @Profile,所有与该类相关的 @Bean 方法和 @Import 注解都会被绕过,除非一个或多个指定的 profiles 处于激活状态。如果一个 @Component 或 @Configuration 类被标记为 @Profile({"p1", "p2"}),该类不会被注册或处理,除非 profiles "p1" 或 "p2" 已经被激活。如果一个给定的profiles前缀为NOT操作符(!),那么只有在该profiles没有激活的情况下,才会注册被注解的元素。例如,给定 @Profile({"p1", "!p2"}),如果profile 'p1' 被激活或 profile 'p2' 未被激活,注册将发生。

@Profile 也可以在方法层面上声明,以便只包括一个配置类的一个特定Bean(例如,对于一个特定Bean的备选变体),正如下面的例子所示。

Java

Kotlin

@Configuration
public class AppConfig {

    @Bean("dataSource")
    @Profile("development") 
    public DataSource standaloneDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }

    @Bean("dataSource")
    @Profile("production") 
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

StandaloneDataSource 方法只在 development profile 中可用。
jndiDataSource 方法只在 production profile 中可用。

对于 @Bean 方法的 @Profile,一个特殊的情况可能适用。在同一Java方法名的重载 @Bean 方法的情况下(类似于构造函数重载),@Profile 条件需要在所有重载的方法上一致声明。如果条件不一致,那么在重载的方法中,只有第一个声明的条件才是重要的。因此,@Profile 不能被用来选择具有特定参数签名的重载方法而不是另一个。同一个Bean的所有工厂方法之间的解析遵循Spring在创建时的构造函数解析算法。

如果你想定义具有不同概况条件的备选Bean,请使用不同的Java方法名,通过使用 @Bean name 属性指向同一个Bean名称,如前面的例子所示。如果参数签名都是一样的(例如,所有的变体都有无参数的工厂方法),这是首先在一个有效的Java类中表示这种安排的唯一方法(因为一个特定名称和参数签名的方法只能有一个)。

XML Bean 定义配置

XML的对应部分是 <beans> 元素的 profile 属性。我们前面的配置样本可以用两个XML文件重写,如下。

<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 profile="production"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</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>

spring-bean.xsd 已经被限制了,只允许在文件中最后出现这样的元素。这应该有助于提供灵活性,而不会在XML文件中产生混乱。

对应的XML不支持前面描述的 profile 表达式。然而,可以通过使用 ! 操作符来否定一个 profile。也可以通过嵌套 profile 来应用逻辑 “and”,如下面的例子所示。

<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>

在前面的例子中,如果 production 和 us-east profiles都处于活动状态,那么 dataSource Bean就会被暴露。

激活一个 Profile

现在我们已经更新了我们的配置,我们仍然需要指示Spring哪个profile是激活的。如果我们现在启动我们的示例应用程序,我们会看到一个 NoSuchBeanDefinitionException 被抛出,因为容器找不到名为 dataSource 的Spring Bean。

激活一个 profile 可以通过几种方式进行,但最直接的是以编程方式对环境API进行激活,该API可以通过 ApplicationContext 获得。下面的例子显示了如何做到这一点。

Java

Kotlin

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

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

请注意,profiles 不是一个 "非此即彼" 的命题。你可以同时激活多个profiles。在程序上,你可以向 setActiveProfiles() 方法提供多个profiles名称,该方法接受 String…​ 可变参数。下面的例子激活了多个profiles。

Java

Kotlin

ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

在声明上,spring.profiles.active 可以接受一个用逗号分隔的 profile 名称列表,正如下面的例子所示。

-Dspring.profiles.active="profile1,profile2"
默认 Profile

默认 profile 代表默认启用的 profile。考虑一下下面的例子。

Java

Kotlin

@Configuration
@Profile("default")
public class DefaultDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .build();
    }
}

如果没有激活profile,就会创建 dataSource。你可以把它看作是为一个或多个Bean提供默认定义的一种方式。如果任何profile被启用,默认的profile就不应用。

你可以通过在环境中使用 setDefaultProfiles() 来改变默认配置文件的名称,或者通过声明性地使用 spring.profiles.default 属性。

1.13.2. PropertySource 抽象

Spring的 Environment 抽象提供了对可配置的属性源层次结构的搜索操作。考虑一下下面的列表。

Java

Kotlin

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);

在前面的片段中,我们看到了一种询问Spring的高级方式,即询问 my-property 属性是否为当前环境所定义。为了回答这个问题,Environment 对象在一组 PropertySource 对象上执行搜索。PropertySource 是对任何key值对来源的简单抽象,Spring的 StandardEnvironment 配置了两个 PropertySource 对象—​一个代表JVM系统属性集合(System.getProperties()),一个代表系统环境变量集合(System.getenv())。

这些默认的属性源存在于 StandardEnvironment 中,用于独立的应用程序中。 StandardServletEnvironment 被填充了额外的默认属性源,包括servlet config、servlet context参数,以及 JndiPropertySource(如果JNDI可用)。

具体来说,当你使用 StandardEnvironment 时,如果运行时存在 my-property 系统属性或 my-property 环境变量,调用 env.containsProperty("my-property") 会返回 true

执行的搜索是分层次的。默认情况下,系统属性(system properties)比环境变量有优先权。因此,如果在调用 env.getProperty("my-property") 时,my-property 属性恰好在两个地方都被设置了,那么系统属性值 "胜出" 并被返回。请注意,属性值不会被合并,而是被前面的条目完全覆盖。

对于一个普通的 StandardServletEnvironment 来说,完整的层次结构如下,最高优先级的条目在顶部。

  1. ServletConfig 参数(如果适用 - 例如,在 DispatcherServlet 上下文的情况下)。

  2. ServletContext 参数(web.xml的context-param条目).

  3. JNDI环境变量(java:comp/env/ 条目)。

  4. JVM系统属性(-D 命令行参数)。

  5. JVM系统环境(操作系统环境变量).

最重要的是,整个机制是可配置的。也许你有一个自定义的属性源,你想集成到这个搜索中。要做到这一点,实现并实例化你自己的 PropertySource,并将其添加到当前环境的 PropertySources 集合中。下面的例子展示了如何做到这一点。

Java

Kotlin

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

在前面的代码中,MyPropertySource 在搜索中被加入了最高优先级。如果它包含 my-property 属性,该属性将被检测并返回,而不是任何其他 PropertySource 中的任何 my-property 属性。 MutablePropertySources API暴露了许多方法,允许精确地操作属性源(property sources)的集合。

1.13.3. 使用 @PropertySource

@PropertySource 注解为向Spring的 Environment 添加 PropertySource 提供了一种方便的声明性机制。

给定一个包含键值对 testbean.name=myTestBean 的名为 app.properties 的文件,下面的 @Configuration 类以这样一种方式使用 @PropertySource,即调用 testBean.getName() 返回 myTestBean

Java

Kotlin

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

任何存在于 @PropertySource 资源位置的 ${…​} 占位符都会根据已经针对环境(environment)注册的属性源集合进行解析,如下例所示。

Java

Kotlin

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

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

假设 my.placeholder 存在于一个已经注册的属性源中(例如,系统属性或环境变量),那么该占位符将被解析为相应的值。如果没有,那么就使用 default/path 作为默认值。如果没有指定默认值,并且一个属性不能被解析,就会抛出一个 IllegalArgumentException

根据Java 8惯例,@PropertySource 注解是可重复的。然而,所有这些 @PropertySource 注解都需要在同一级别上声明,要么直接在配置类上声明,要么作为同一自定义注解中的元注解。不建议混合使用直接注解和元注解,因为直接注解会有效地覆盖元注解。
1.13.4. 声明中的占位符解析

历史上,元素中占位符的值只能通过JVM系统属性或环境变量来解析。现在的情况不再是这样了。因为 Environment 抽象被集成到整个容器中,所以很容易通过它来解决占位符的问题。这意味着你可以以任何你喜欢的方式配置解析过程。你可以改变通过系统属性和环境变量搜索的优先级,或者完全删除它们。你也可以酌情将你自己的属性源添加到组合中。

具体来说,无论 customer 属性在哪里定义,只要它在 Environment 中是可用的,下面的语句就能发挥作用。

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

1.14. 注册 LoadTimeWeaver

LoadTimeWeaver 被Spring用来在类被加载到Java虚拟机(JVM)时进行动态转换。

要启用 load-time 织入,你可以把 @EnableLoadTimeWeaving 添加到你的一个 @Configuration 类中,如下例所示。

Java

Kotlin

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

另外,对于XML配置,你可以使用 context:load-time-weaver 元素。

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

一旦为 ApplicationContext 配置,该 ApplicationContext 中的任何Bean都可以实现 LoadTimeWeaverAware,从而接收对 load-time 织入实例的引用。这在与 Spring的JPA支持 相结合时特别有用,因为JPA类的转换可能需要 load-time 织入。请参考 LocalContainerEntityManagerFactoryBean javadoc以了解更多细节。关于AspectJ load-time 织入的更多信息,请参见 在Spring框架中用AspectJ进行加载时织入(Load-time Weaving)

1.15. ApplicationContext 的附加功能

正如本章介绍中所讨论的,org.springframework.beans.factory 包提供了管理和操纵Bean的基本功能,包括以编程的方式。org.springframework.context 包增加了 ApplicationContext 接口,它扩展了 BeanFactory 接口,此外还扩展了其他接口,以更加面向应用框架的方式提供额外功能。许多人以完全声明的方式使用 ApplicationContext,甚至不以编程方式创建它,而是依靠诸如 ContextLoader 这样的支持类来自动实例化 ApplicationContext,作为Jakarta EE Web应用正常启动过程的一部分。

为了以更加面向框架的风格增强 BeanFactory 的功能,context包还提供了以下功能。

  • 通过 MessageSource 接口,以i18n风格访问消息。

  • 通过 ResourceLoader 接口访问资源,如URL和文件。

  • 事件发布,即通过使用 ApplicationEventPublisher 接口,向实现 ApplicationListener 接口的bean发布。

  • 通过 HierarchicalBeanFactory 接口,加载多个(分层的)上下文,让每个上下文都集中在一个特定的层上,例如一个应用程序的Web层。

1.15.1. 使用 MessageSource 进行国际化

ApplicationContext 接口扩展了一个名为 MessageSource 的接口,因此,它提供了国际化("i18n")功能。Spring还提供了 HierarchicalMessageSource 接口,它可以分层次地解析消息。这些接口共同提供了Spring实现消息解析的基础。在这些接口上定义的方法包括。

  • String getMessage(String code, Object[] args, String default, Locale loc): 用于从 MessageSource 检索消息的基本方法。当没有找到指定地区(locale)的消息时,将使用默认的消息。任何传入的参数都成为替换值,使用标准库提供的 MessageFormat 功能。

  • String getMessage(String code, Object[] args, Locale loc): 基本上与前一个方法相同,但有一点不同。不能指定默认消息。如果找不到消息,会抛出一个 NoSuchMessageException

  • String getMessage(MessageSourceResolvable resolvable, Locale locale): 在前面的方法中使用的所有属性(properties)也被包裹在一个名为 MessageSourceResolvable 的类中,你可以使用这个方法。

当 ApplicationContext 被加载时,它会自动搜索定义在上下文中的 MessageSource bean。这个Bean必须有 messageSource 这个名字。如果找到了这样的Bean,所有对前面的方法的调用都被委托给消息源。如果没有找到消息源,ApplicationContext 会尝试找到一个包含有相同名称的Bean的父类。如果找到了,它就使用该 bean 作为 MessageSource。如果 ApplicationContext 不能找到任何消息源,那么就会实例化一个空的 DelegatingMessageSource,以便能够接受对上面定义的方法的调用。

Spring提供了三种 MessageSource 实现:ResourceBundleMessageSource、 ReloadableResourceBundleMessageSource 和 StaticMessageSource。它们都实现了 HierarchicalMessageSource,以便进行嵌套消息传递。StaticMessageSource 很少被使用,但它提供了向消息源添加消息的程序化方法。下面的例子显示了 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>

这个例子假设你在classpath中定义了三个资源包,分别是 formatexceptions 和 windows。任何解析消息的请求都以 JDK 标准的方式处理,即通过 ResourceBundle 对象解析消息。就本例而言,假设上述两个资源包文件的内容如下。

# in format.properties
message=Alligators rock!

# in exceptions.properties
argument.required=The {0} argument is required.

下一个例子显示了一个运行 MessageSource 功能的程序。请记住,所有 ApplicationContext 的实现也是 MessageSource 的实现,所以可以强制转换为 MessageSource 接口。

Java

Kotlin

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

上述程序的结果输出如下。

Alligators rock!

简而言之,MessageSource 被定义在一个叫做 beans.xml 的文件中,它存在于你的classpath的根部。messageSource Bean定义通过它的 basenames 属性引用了一些资源包。在列表中传递给 basenames 属性的三个文件作为文件存在于你的 classpath 根部,分别被称为 format.propertiesexceptions.properties 和 windows.properties

下一个例子显示了传递给消息查询的参数。这些参数被转换为 String 对象,并被插入到查找消息的占位符中。

<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>

Java

Kotlin

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

调用 execute() 方法的结果输出如下。

The userDao argument is required.

关于国际化("i18n"),Spring的各种 MessageSource 实现遵循与标准JDK ResourceBundle 相同的区域划分和回退规则。简而言之,继续使用之前定义的 messageSource 示例,如果你想根据英国(en-GB)地区设置来解析消息,你将创建名为 format_en_GB.propertiesexceptions_en_GB.properties 和 windows_en_GB.properties 的文件。

通常情况下,地区的解析(locale resolution)是由应用程序的周围环境管理的。在下面的例子中,(英国)信息所依据的地方语言是手动指定的。

# in exceptions_en_GB.properties
argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.

Java

Kotlin

public static void main(final String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("argument.required",
        new Object [] {"userDao"}, "Required", Locale.UK);
    System.out.println(message);
}

运行上述程序的结果输出如下。

Ebagum lad, the 'userDao' argument is required, I say, required.

你也可以使用 MessageSourceAware 接口来获取对任何已定义的 MessageSource 的引用。任何定义在实现 MessageSourceAware 接口的 ApplicationContext 中的Bean,在Bean被创建和配置时都会被注入 ApplicationContext 的 MessageSource

因为Spring的 MessageSource 是基于Java的 ResourceBundle,它不会合并具有相同基名的bundle,而是只使用找到的第一个bundle。随后的具有相同基名的消息包会被忽略。
作为 ResourceBundleMessageSource 的替代品,Spring提供了一个 ReloadableResourceBundleMessageSource 类。这个变体支持相同的bundle文件格式,但比基于JDK的标准 ResourceBundleMessageSource 实现更灵活。特别是,它允许从任何Spring资源位置(不仅仅是classpath)读取文件,并支持bundle属性文件的热重载(同时在两者之间有效地缓存它们)。详情请参见 ReloadableResourceBundleMessageSource javadoc。
1.15.2. 标准和自定义事件

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

从Spring 4.2开始,事件基础架构得到了极大的改进,它提供了基于 注解的模型 以及发布任何任意事件的能力(也就是不一定从 ApplicationEvent 继承出来的对象)。当这样的对象被发布时,我们会为你把它包装成一个事件。

下表描述了Spring提供的标准事件。

Table 7. 内置事件
事件说明

ContextRefreshedEvent

当 ApplicationContext 被初始化或刷新时发布(例如,通过使用 ConfigurableApplicationContext 接口上的 refresh() 方法)。这里,"初始化" 意味着所有的Bean都被加载,后处理器Bean被检测和激活,单例被预先实例化,并且 ApplicationContext 对象可以使用。只要上下文没有被关闭,就可以多次触发刷新,前提是所选的 ApplicationContext 实际上支持这种 "热" 刷新。例如, XmlWebApplicationContext 支持热刷新,但 GenericApplicationContext 不支持。

ContextStartedEvent

当 ApplicationContext 通过使用 ConfigurableApplicationContext 接口上的 start() 方法启动时发布。在这里,"开始" 意味着所有的 Lifecycle Bean都收到一个显式的启动信号。通常,这个信号被用来在显式停止后重新启动Bean,但它也可能被用来启动那些没有被配置为自动启动的组件(例如,在初始化时还没有启动的组件)。

ContextStoppedEvent

当 ApplicationContext 通过使用 ConfigurableApplicationContext 接口上的 stop() 方法而停止时发布。在这里,"停止" 意味着所有的 Lifecycle Bean收到一个明确的停止信号。停止的上下文可以通过调用 start() 重新启动。

ContextClosedEvent

当 ApplicationContext 通过使用 ConfigurableApplicationContext 接口上的 close() 方法或通过 JVM shutdown hook 被关闭时发布。这里,"关闭" 意味着所有的单例Bean将被销毁。一旦上下文被关闭,它的生命就结束了,不能被刷新或重新启动。

RequestHandledEvent

一个针对Web的事件,告诉所有Bean一个HTTP请求已经被处理。该事件在请求完成后被发布。这个事件只适用于使用Spring的 DispatcherServlet 的Web应用程序。

ServletRequestHandledEvent

RequestHandledEvent 的一个子类,增加了 Servlet 特定的上下文信息。

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

Java

Kotlin

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来实现的。下面的例子展示了这样一个类。

Java

Kotlin

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...
    }
}

在配置时,Spring容器检测到 EmailService 实现了 ApplicationEventPublisherAware 并自动调用 setApplicationEventPublisher()。实际上,传入的参数是Spring容器本身。你正在通过它的 ApplicationEventPublisher 接口与 application context 进行交互。

为了接收自定义的 ApplicationEvent,你可以创建一个实现 ApplicationListener 的类并将其注册为Spring Bean。下面的例子展示了这样一个类。

Java

Kotlin

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 实现的 javadoc 配置选项。

下面的例子显示了用于注册和配置上述每个类的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() 方法时,如果有任何应该被阻止的电子邮件,就会发布一个 BlockedListEvent 类型的自定义事件。 blockedListNotifier Bean被注册为 ApplicationListener,并接收 BlockedListEvent,这时它可以通知相关方。

Spring的事件机制是为同一应用环境下的Spring Bean之间的简单通信而设计的。然而,对于更复杂的企业集成需求,单独维护的 Spring Integration 项目为构建轻量级、 面向模式、事件驱动的架构提供了完整的支持,这些架构建立在众所周知的Spring编程模型之上。
基于注解的事件监听器

你可以通过使用 @EventListener 注解在管理型Bean的任何方法上注册一个事件监听器。 BlockedListNotifier 可以被重写如下。

Java

Kotlin

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...
    }
}

方法签名再次声明了它所监听的事件类型,但是,这一次,有了一个灵活的名字,并且没有实现一个特定的监听接口。事件类型也可以通过泛型来缩小,只要实际的事件类型在其实现层次中解析你的泛型参数。

如果你的方法应该监听几个事件,或者你想在没有参数的情况下定义它,事件类型也可以在注解本身中指定。下面的例子展示了如何做到这一点。

Java

Kotlin

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}

也可以通过使用定义 SpEL表达式 的注解的 condition 属性来添加额外的运行时过滤,该表达式应该匹配以实际调用特定事件的方法。

下面的例子显示了我们的 notifier 如何被改写成只有在事件的 content 属性等于 my-event 时才会被调用。

Java

Kotlin

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
    // notify appropriate parties via notificationAddress...
}

每个 SpEL 表达式都针对一个专门的上下文进行评估。下表列出了提供给上下文的项目,以便你可以使用它们进行条件性事件处理。

Table 8. 事件SpEL可用的元数据
名称位置说明示例

事件

root 对象

实际的 ApplicationEvent

#root.event or event

参数数组

root 对象

用于调用方法的参数(作为一个对象数组)。

#root.args 或 argsargs[0] 用于访问第一个参数,等等。

参数名称

evaluation context

任何一个方法参数的名称。如果由于某种原因,这些名称无法使用(例如,因为在编译的字节码中没有debug信息),单个参数也可以使用 #a<#arg> 语法,其中 <#arg> 代表参数索引(从0开始)。

#blEvent 或 #a0(你也可以使用 #p0 或 #p<#arg> 参数符号作为别名)。

请注意,#root.event 让你可以访问底层事件,即使你的方法签名实际上指的是一个被发布的任意对象。

如果你需要发布一个事件作为处理另一个事件的结果,你可以改变方法签名以返回应该被发布的事件,如下例所示。

Java

Kotlin

@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}

异步监听器 不支持此功能。

handleBlockedListEvent() 方法为其处理的每个 BlockedListEvent 发布一个新的 ListUpdateEvent。如果你需要发布几个事件,你可以返回一个 Collection 或者一个事件数组来代替。

异步监听器

如果你想让一个特定的监听器异步处理事件,你可以重新使用 常规的 @Async 支持。下面的例子展示了如何做到这一点。

Java

Kotlin

@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
}

使用异步事件时要注意以下限制。

  • 如果一个异步事件监听器抛出一个异常,它不会被传播给调用者。参见 AsyncUncaughtExceptionHandler 获取更多细节。

  • 异步事件监听器方法不能通过返回一个值来发布后续的事件。如果你需要发布另一个事件作为处理的结果,请注入一个 ApplicationEventPublisher 来手动发布该事件。

监听顺序

如果你需要一个监听器在另一个之前被调用,你可以在方法声明中添加 @Order 注解,如下例所示。

Java

Kotlin

@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress...
}

一般性事件

你也可以使用泛型来进一步定义你的事件的结构。考虑使用 EntityCreatedEvent<T>,其中 T 是被创建的实际实体的类型。例如,你可以创建下面的监听器定义,只接收一个 Person 的 EntityCreatedEvent

Java

Kotlin

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    // ...
}

由于泛型擦除,只有当被触发的事件解析了事件监听器过滤的泛型参数时,这才起作用(也就是说,像 class PersonCreatedEvent extends EntityCreatedEvent<Person> { …​ })。

在某些情况下,如果所有的事件都遵循相同的结构,这可能会变得相当乏味(前面的例子中的事件应该就是这样)。在这种情况下,你可以实现 ResolvableTypeProvider,以引导框架超越运行时环境提供的内容。下面的事件展示了如何做到这一点。

Java

Kotlin

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,而且适用于任何你作为事件发送的任意对象。

1.15.3. 方便地获取低级别的资源

为了获得最佳的使用效果和对应用上下文的理解,你应该熟悉Spring的 Resource 抽象,如 资源(Resources) 所述。

一个 application context 是一个 ResourceLoader,它可以用来加载 Resource 对象。Resource 本质上是JDK java.net.URL 类的一个功能更丰富的版本。事实上,在适当的时候,Resource 的实现会包裹 java.net.URL 的实例。Resource 可以以透明的方式从几乎任何位置获取底层资源,包括从 classpath、文件系统位置、任何可以用标准URL描述的地方,以及其他一些变化。如果资源位置字符串是一个没有任何特殊前缀的简单路径,那么这些资源的来源是特定的,适合于实际的 application context 类型。

你可以配置部署在 application context 中的Bean,使其实现特殊的回调接口 ResourceLoaderAware,以便在初始化时自动回调,并将应用 application contex t本身作为 ResourceLoader 传递进来。你也可以公开 Resource 类型的属性,用于访问静态资源。它们像其他属性一样被注入其中。你可以将这些 Resource 属性指定为简单的 String 路径,并在Bean部署时依赖从这些文本字符串到实际 Resource 对象的自动转换。

提供给 ApplicationContext 构造函数的一个或多个位置路径实际上是资源字符串,以简单的形式,根据具体的上下文实现被适当地处理。例如,ClassPathXmlApplicationContext 将一个简单的位置路径视为 classpath 位置。你也可以使用带有特殊前缀的位置路径(资源字符串)来强制从 classpath 或URL加载定义,而不考虑实际的上下文类型。

1.15.4. 应用程序启动跟踪

ApplicationContext 管理Spring应用程序的生命周期,并围绕组件提供丰富的编程模型。因此,复杂的应用程序可以有同样复杂的组件图和启动阶段。

用具体的指标来跟踪应用程序的启动步骤,可以帮助了解在启动阶段花费的时间,但它也可以作为一种方式来更好地了解整个上下文生命周期。

AbstractApplicationContext(及其子类)被一个 ApplicationStartup 工具化,它收集关于各种启动阶段的 StartupStep 数据。

  • application context 生命周期(基础包扫描、配置类管理)。

  • bean生命周期(实例化、智能初始化、后处理)。

  • application 事件处理。

总结

以上就是今天的内容~

欢迎大家点赞👍,收藏⭐,转发🚀,
如有问题、建议,请您在评论区留言💬哦。

最后:转载请注明出处!!!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张小鱼༒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值