文章目录
1.4.5. 注入协作者(Autowiring Collaborators)
1.5.3. singleton Bean 和 prototype bean 依赖
1.5.4. Request、 Session、 Application 和 WebSocket Scope
1.6.2. ApplicationContextAware 和 BeanNameAware
1.8.1. 使用 BeanPostProcessor 自定义 Bean
示例: Hello World, BeanPostProcessor
示例: AutowiredAnnotationBeanPostProcessor
1.8.2. 用 BeanFactoryPostProcessor 定制配置元数据
示例: 类名替换 PropertySourcesPlaceholderConfigurer
1.9.2. 用 @Primary 对基于注解的自动注入进行微调
1.9.3. 用 Qualifiers 微调基于注解的自动注入
1.9.5. 使用 CustomAutowireConfigurer
1.9.8. 使用 @PostConstruct 和 @PreDestroy
1.10.1. @Component 和进一步的 Stereotype 注解
1.11.1. 用 @Inject 和 @Named 进行依赖注入
1.11.2. @Named 和 @ManagedBean:与 @Component 注解的标准对等物
1.12.1. 基本概念:@Bean 和 @Configuration
1.12.2. 通过使用 AnnotationConfigApplicationContext 实例化Spring容器
通过使用 register(Class…) 以编程方式构建容器。
用 AnnotationConfigWebApplicationContext 支持Web应用程序
有条件地包括 @Configuration 类或 @Bean 方法
@Configuration 以类为中心使用XML与 @ImportResource
1.15. ApplicationContext 的附加功能
1.15.1. 使用 MessageSource 进行国际化
前言
本文主要讲解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
被创建和初始化后,你就有了一个完全配置好的可执行系统或应用程序。
Figure 1. Spring IoC容器
1.2.1. 配置元数据
如上图所示,Spring IoC容器消费一种配置元数据。这种配置元数据代表了你,作为一个应用开发者,如何告诉Spring容器在你的应用中实例化、配置和组装对象。
配置元数据传统上是以简单直观的XML格式提供的,这也是本章大部分内容用来传达Spring IoC容器的关键概念和特性。
基于XML的元数据并不是配置元数据的唯一允许形式。Spring IoC容器本身与这种配置元数据的实际编写格式是完全解耦的。如今,许多开发者为他们的Spring应用程序选择 基于Java的配置。 |
关于在Spring容器中使用其他形式的元数据的信息,请参见。
-
基于注解的配置: 使用基于注解的配置元数据定义Bean。
-
Java-based configuration: 通过使用Java而不是XML文件来定义你的应用类外部的Bean。要使用这些特性,请参阅 @Configuration, @Bean, @Import, 和 @DependsOn 注解。
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的 |
下面的例子显示了 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.xml
、messageSource.xml
和 themeSource.xml
。所有的位置路径都是相对于进行导入的定义文件而言的,所以 services.xml
必须与进行导入的文件在同一目录或 classpath 位置,而 messageSource.xml
和 themeSource.xml
必须在导入文件的位置以下的 resources
位置。正如你所看到的,前导斜线会被忽略。然而,鉴于这些路径是相对的,最好不要使用斜线。被导入文件的内容,包括顶层的 <beans/>
元素,必须是有效的XML Bean定义,根据Spring Schema。
使用相对的 "../" 路径来引用父目录中的文件是可能的,但不推荐这样做。这样做会造成对当前应用程序之外的文件的依赖。特别是,这种引用不推荐用于 你总是可以使用完全限定的资源位置而不是相对路径:例如, |
命名空间本身提供了导入指令的功能。除了普通的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定义的一组属性。下表描述了这些属性。
属性 | 解释… |
---|---|
Class | |
Name | |
Scope | |
Constructor arguments | |
Properties | |
Autowiring mode | |
Lazy initialization mode | |
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的名字以小写字母开始,然后以驼峰字母开头。这种名称的例子包括 accountManager
、accountService
、userDao
、loginController
等等。
统一命名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 可以将以字符串格式提供的值转换为所有内置类型,如
int
、long
、String
、boolean
等等。
当容器被创建时,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
类型 List
、Set
、Map
和 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
集合类型相关的语义(也就是值的有序集合的概念)被保持。父列表的值在所有子列表的值之前。在 Map
、Set
和 Properties
集合类型的情况下,不存在排序。因此,对于容器在内部使用的相关的 Map
、Set
和 Properties
实现类型的基础上的集合类型,没有排序语义。
集合合并的限制
你不能合并不同的集合类型(例如 Map
和 List
)。如果你试图这样做,会抛出一个适当的 Exception
。merge
属性必须被指定在较低的、继承的、子定义上。在父级集合定义上指定 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.99
、2.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指定自动注入,从而选择哪些要自动注入。下表描述了四种自动注入模式。
模式 | 解释 |
---|---|
| (默认)没有自动注入。Bean引用必须由 |
| 通过属性名称进行自动注入。Spring寻找一个与需要自动注入的属性同名的Bean。例如,如果一个Bean定义被设置为按名称自动注入,并且它包含一个 |
| 如果容器中正好有一个 property 类型的 bean 存在,就可以自动注入该属性。如果存在一个以上的bean,就会抛出一个致命的 exception,这表明你不能对该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库的字节码生成来实现这种方法注入,动态地生成一个覆盖该方法的子类。
|
在前面代码片段中的 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的另一种方式是 你可能还会发现 |
任意方法替换
与查找方法注入相比,方法注入的一个不太有用的形式是用另一个方法实现替换托管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。
Scope | 说明 |
---|---|
(默认情况下)为每个Spring IoC容器将单个Bean定义的Scope扩大到单个对象实例。 | |
将单个Bean定义的Scope扩大到任何数量的对象实例。 | |
将单个Bean定义的Scope扩大到单个HTTP请求的生命周期。也就是说,每个HTTP请求都有自己的Bean实例,该实例是在单个Bean定义的基础上创建的。只在Web感知的Spring | |
将单个Bean定义的Scope扩大到一个HTTP | |
将单个Bean定义的 Scope 扩大到 | |
将单个Bean定义的 Scope 扩大到 |
一个 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 是如何工作的。
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。
(数据访问对象(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
request
、session
、application
和 websocket
scope只有在你使用Web感知的Spring ApplicationContext
实现(如 XmlWebApplicationContext
)时才可用。如果你将这些scope与常规的Spring IoC容器(如 ClassPathXmlApplicationContext
)一起使用,就会抛出一个 IllegalStateException
,抱怨有未知的Bean scope。
初始 Web 配置
为了支持Bean在 request
、 session
、application
和 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>
DispatcherServlet
、RequestContextListener
和 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 为 当针对scope 为 另外,scope代理并不是以生命周期安全的方式从较短的scope访问Bean的唯一方法。你也可以将你的注入点(也就是构造器或设置器参数或自动注入的字段)声明为 作为一个扩展变量,你可以声明 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 的配置)。为什么在 request
、session
和自定义 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
和 prototype
。registerScope(..)
方法的第二个参数是你希望注册和使用的自定义 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的 如果你不想使用JSR-250注解,但你仍然想消除耦合,可以考虑用 |
在内部,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的生命周期行为。
-
InitializingBean 和 DisposableBean callback 接口。
-
自定义
init()
anddestroy()
方法。 -
@PostConstruct 和 @PreDestroy 注解。你可以结合这些机制来控制一个特定的Bean。
如果为一个bean配置了多个生命周期机制,并且每个机制都配置了不同的方法名称,那么每个配置的方法都会按照本说明后面列出的顺序运行。然而,如果同一方法名称被配置—例如,init() 为一个初始化方法—用于多个这些生命周期机制,则该方法将被运行一次,如 上一节 所解释的。 |
为同一个Bean配置的多个生命周期机制,具有不同的初始化方法,其调用方式如下。
-
注解了
@PostConstruct
的方法。 -
afterPropertiesSet()
,如InitializingBean
回调接口所定义。 -
一个自定义配置的
init()
方法。
销毁方法的调用顺序是一样的。
-
注解了
@PreDestroy
的方法。 -
destroy()
,正如DisposableBean
回调接口所定义的那样。 -
一个自定义配置的
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 的刷新和关闭做出反应。
请注意,常规的 另外,请注意,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的 |
如果你在非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
接口。
接口名称 | 注入的依赖性 | 解释 |
---|---|---|
| 声明 | |
| 封装了 | |
| 用来加载Bean类的类加载器(Class loader)。 | |
| 声明 | |
| 声明Bean的名称。 | |
| 定义了用于在加载时处理类定义的织入点。 | |
| 配置解析消息的策略(支持参数化和国际化)。 | |
| Spring JMX notification publisher。 | |
| 配置的加载器用于低级别的资源访问。 | |
| 容器所运行的当前 |
请再次注意,使用这些接口会将你的代码与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 实例的程序化注册 的说明。
要改变实际的Bean定义(即定义Bean的蓝图),你需要使用 |
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 注册方法是通过 ApplicationContext 自动检测(如前所述),但你可以通过使用 addBeanPostProcessor 方法,针对 ConfigurableBeanFactory 以编程方式注册它们。当你需要在注册前评估条件逻辑或甚至在一个层次结构中跨上下文复制Bean Post处理器时,这可能很有用。然而,请注意,以编程方式添加的 BeanPostProcessor 实例并不尊重 Ordered 接口。这里,是注册的顺序决定了执行的顺序。还要注意的是,以编程方式注册的 BeanPostProcessor 实例总是在通过自动检测注册的实例之前被处理,而不考虑任何明确的顺序。 |
实现了 对于任何这样的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实例(即从配置元数据中创建的对象),那么你需要使用 另外, |
当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 属性设置为 true ,Bean(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。
你可以使用 如果该类在运行时不能被解析为一个有效的类,那么在即将创建Bean时,也就是在非 lazy-init 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)。
|
1.9.1. 使用 @Autowired
JSR 330的 |
你可以将 @Autowired
注解应用于构造函数,如下例所示。
Java
Kotlin
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;
@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
从Spring Framework 4.3开始,如果目标Bean一开始就只定义了一个构造函数,那么在这样的构造函数上就不再需要 |
你也可以将 @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;
}
// ...
}
确保你的目标组件(例如 对于通过classpath扫描找到的XML定义的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可以实现 你可以在目标类层面和 请注意,标准的 |
即使是类型化的 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)字段将根本不会被填充,而是将其默认值留在原地。 换句话说,将 |
注入的构造函数和工厂方法参数是一种特殊情况,因为由于Spring的构造函数解析算法有可能处理多个构造函数,所以 @Autowired
中的 required
属性有一些不同的含义。构造函数和工厂方法参数实际上是默认需要的,但在单构造函数的情况下有一些特殊的规则,比如多元素注入点(数组、collection、map)如果没有匹配的Bean,则解析为空实例。这允许一种常见的实现模式,即所有的依赖关系都可以在一个独特的多参数构造函数中声明—例如,声明为一个没有 @Autowired
注解的单一公共构造函数。
任何给定的Bean类中只有一个构造函数可以声明 |
另外,你可以通过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) {
...
}
}
你也可以对那些众所周知的可解析依赖的接口使用 @Autowired
。BeanFactory
、ApplicationContext
、Environment
、ResourceLoader
、ApplicationEventPublisher
和 MessageSource
。这些接口和它们的扩展接口,如 ConfigurableApplicationContext
或 ResourcePatternResolver
,将被自动解析,不需要特别的设置。下面的例子是自动注入一个 ApplicationContext
对象。
Java
Kotlin
public class MovieRecommender {
@Autowired
private ApplicationContext context;
public MovieRecommender() {
}
// ...
}
|
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名称进行选择,不需要在注入点上进行 |
也就是说,如果你打算通过名字来表达注解驱动的注入,请不要主要使用 @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的代理。
试图在同一个配置类上注入 |
@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,并解析众所周知的可解析的依赖:BeanFactory
、ApplicationContext
、 ResourceLoader
、ApplicationEventPublisher
和 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初始化失败。也可以使用 setPlaceholderPrefix
、setPlaceholderSuffix
或 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...
}
}
关于结合各种生命周期机制的效果,详见 结合生命周期机制。
与 |
1.10. Classpath扫描和管理的组件
本章中的大多数例子都使用XML来指定配置元数据,在Spring容器中产生每个 BeanDefinition
。上一节(基于注解的容器配置)演示了如何通过源级注解提供大量的配置元数据。然而,即使在这些例子中,"基础" Bean定义也是在XML文件中明确定义的,而注解只驱动依赖性注入。本节描述了一个通过扫描classpath来隐式检测候选组件的选项。候选组件是与过滤器标准相匹配的类,并且有一个在容器中注册的相应的bean定义。这样就不需要使用 XML 来执行 bean 注册了。相反,你可以使用注解(例如 @Component
)、AspectJ类型表达式或你自己的自定义过滤标准来选择哪些类在容器中注册了Bean定义。
你可以使用Java来定义 Bean,而不是使用XML文件。看看 |
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扫描一般都能按预期工作。但是,请确保你的组件类在你的 |
此外,当你使用组件扫描元素时,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
属性。下表描述了过滤选项。
Filter Type | 示例表达式 | 说明 |
---|---|---|
注解 (默认) |
| 一个注解在目标组件中的类型级别是 present 或 meta-present。 |
可指定 |
| 目标组件可分配给(继承或实现)的一个类(或接口)。 |
aspectj |
| 要被目标组件匹配的 AspectJ type 表达式。 |
regex |
| 一个与目标组件的类名相匹配的 regex expression。 |
自定义 |
|
|
下面的例子显示了配置忽略了所有 @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持有多个 |
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
属性。三个可能的值是:no
、interfaces
和 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, |
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 标准注解的局限性
当你使用标准注解工作时,你应该知道一些重要的功能是不可用的,如下表所示。
Spring | jakarta.inject.* | jakarta.inject 限制 / 说明 |
---|---|---|
@Autowired | @Inject |
|
@Component | @Named / @ManagedBean | JSR-330并没有提供一个可组合的模型,只是提供了一种识别命名组件的方法。 |
@Scope("singleton") | @Singleton | JSR-330的默认scope就像Spring的 |
@Qualifier | @Qualifier / @Named |
|
@Value | - | 没有对应的 |
@Lazy | - | 没有对应的 |
ObjectFactory | Provider |
|
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();
}
前面的例子假设 MyServiceImpl
、Dependency1
和 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的 |
在前面的例子中,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实现了 InitializingBean
、DisposableBean
或 Lifecycle
,它们各自的方法就会被容器调用。
标准的 *Aware
接口集(如 BeanFactoryAware、BeanNameAware、MessageSourceAware、ApplicationContextAware 等)也被完全支持。
@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 的 你可能想对你用JNDI获取的资源默认这样做,因为它的生命周期是在应用程序之外管理的。特别是,要确保总是对 下面的例子显示了如何阻止一个 Java Kotlin 另外,对于 |
就前文例子中的 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_CLASS
、ScopedProxyMode.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类被重新打包到 |
由于CGLIB在启动时动态地添加功能,所以有一些限制。特别是,配置类不能是 如果你想避免任何CGLIB施加的限制,可以考虑在非 |
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
注入以及其他功能。
请确保你用这种方式注入的依赖关系是最简单的那种。 另外,对于通过 |
下面的例子显示了一个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 。 |
在前面的场景中,使用 @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,请使用不同的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
现在我们已经更新了我们的配置,我们仍然需要指示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)比环境变量有优先权。因此,如果在调用 对于一个普通的
|
最重要的是,整个机制是可配置的。也许你有一个自定义的属性源,你想集成到这个搜索中。要做到这一点,实现并实例化你自己的 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中定义了三个资源包,分别是 format
、exceptions
和 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.properties
、exceptions.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.properties
、exceptions_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提供的标准事件。
事件 | 说明 |
---|---|
| 当 |
| 当 |
| 当 |
| 当 |
| 一个针对Web的事件,告诉所有Bean一个HTTP请求已经被处理。该事件在请求完成后被发布。这个事件只适用于使用Spring的 |
|
|
你也可以创建和发布你自己的自定义事件。下面的例子展示了一个简单的类,它扩展了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
表达式都针对一个专门的上下文进行评估。下表列出了提供给上下文的项目,以便你可以使用它们进行条件性事件处理。
名称 | 位置 | 说明 | 示例 |
---|---|---|---|
事件 | root 对象 | 实际的 |
|
参数数组 | root 对象 | 用于调用方法的参数(作为一个对象数组)。 |
|
参数名称 | evaluation context | 任何一个方法参数的名称。如果由于某种原因,这些名称无法使用(例如,因为在编译的字节码中没有debug信息),单个参数也可以使用 |
|
请注意,#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 事件处理。
总结
以上就是今天的内容~
欢迎大家点赞👍,收藏⭐,转发🚀,
如有问题、建议,请您在评论区留言💬哦。
最后:转载请注明出处!!!