Spring——Core
Version 5.3.22
本文涵盖了 Spring 框架绝对不可或缺的所有技术。
其中最重要的是 Spring 框架的控制反转( Inversion of Control,IoC )容器。在对 Spring 框架的 IoC 容器进行彻底讨论之后,紧接着全面介绍 Spring 的面向切面编程(Aspect-Oriented Programming,AOP)技术。Spring 框架有自己的 AOP 框架,它在概念上很容易理解,并且成功地解决了 Java 企业编程中 80% 的 AOP 需求。
还介绍了 Spring 与 AspectJ 的集成(就特性而言,目前 AspectJ 是最丰富的,当然也是 Java 企业领域中最成熟的 AOP 实现)。
1. IoC 容器
本章介绍了 Spring 的控制反转( IoC )容器。
1.1 Spring IoC 容器和 Bean 简介
本章介绍了控制反转( IoC )原理的 Spring 框架实现。IoC 也被称为依赖注入(dependency injection,DI)。在此过程中,对象仅通过构造函数参数、工厂方法的参数或在对象实例构造或从工厂方法返回后在对象实例上设置的属性来定义它们的依赖关系(即它们依赖的其他对象)。然后容器在创建 bean 时注入那些依赖项。这个过程从根本上说是 bean 本身的逆过程(因此得名“控制反转”),它通过使用类的直接构造或服务定位器模式(Service Locator pattern)等机制来控制依赖项的实例化或定位。
org.springframework.beans
和org.springframework.context
包是 Spring 框架 IoC 容器的基础。BeanFactory
接口提供了一种高级的配置机制,能够管理任何类型的对象。ApplicationContext
是BeanFactory
的子接口,它增加了:
- 更容易与 Spring 的 AOP 特性集成
- 消息资源处理(用于国际化)
- 事件发布
- 应用程序层特定的上下文,如用于 web 应用程序的
WebApplicationContext
。
简而言之,BeanFactory
提供了配置框架和基本功能,而ApplicationContext
添加了更多特定于企业的功能。ApplicationContext
是BeanFactory
的一个完整超集,在本章 Spring 的 IoC 容器的描述中被专门使用。
在 Spring 中,构成应用程序主干并由 Spring IoC 容器管理的对象称为 bean。bean是由 Spring IoC 容器实例化(instantiated)、组装(assembled)和管理的对象。否则,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 描述符 XML 模板通常就足够了。
下图显示了 Spring 如何工作的高级视图。你的应用程序类与配置元数据相结合,这样,在ApplicationContext
创建和初始化之后,你就拥有了一个配置完全和可执行的系统或应用程序。
1.2.1 配置元数据
如上图所示,Spring IoC容器使用某种形式的配置元数据。配置元数据表示着作为应用程序开发人员的你,如何告诉 Spring 容器实例化、配置和组装应用程序中的对象。
配置元数据传统上以简单直观的 XML 格式提供,本章的大部分内容都使用这种格式来传达 Spring IoC 容器的关键概念和特性。
基于 xml 的元数据不是唯一允许的配置元数据形式。Spring IoC 容器本身与实际编写此配置元数据的格式完全解耦。现在,许多开发人员为他们的 Spring 应用程序选择基于 Java 的配置。
有关在 Spring 容器中使用其他形式的元数据的信息,请参见:
- 基于注解的配置:Spring 2.5 引入了对基于注解的配置元数据的支持。
- 基于 Java 的配置:从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多特性成为核心 Spring 框架的一部分。因此你可以使用 Java 而不是 XML 文件来定义应用程序类外部的bean。要使用这些新特性,请参阅
@Configuration
、@Bean
、@Import
和@DependsOn
注释。
Spring 配置由容器必须管理的至少一个(通常不止一个)bean 定义组成。基于 XML 的配置元数据将这些 bean 配置为顶级元素<beans/>
中的元素<bean/>
。Java 配置通常在被@Configuration
注解的类中使用被@Bean
注解的方法。
这些 bean 定义对应于构成应用程序的实际对象。通常,你需要定义服务层对象、数据访问对象(DAOs)、表示对象(如Struts Action
实例)、基础设施对象(如Hibernate SessionFactories
)、JMS Queues
等等。通常,不需要在容器中配置细粒度的域对象,因为创建和加载域对象通常是 DAO 和业务逻辑的职责。
不过,你可以使用 Spring 与 AspectJ 的集成来配置在 IoC 容器控制之外创建的对象。
下面的例子展示了基于 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="...">
<!-- collaborators and configuration for this bean go here -->
</bean>
<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions go here -->
</beans>
id
属性是一个标识各个 bean 定义的字符串。class
属性定义 bean 的类型,并使用完全限定的类名。
1.2.2 实例化一个容器
提供给ApplicationContext
构造器的一个或多个位置路径实际上是资源字符串,它们允许容器从各种外部资源加载配置元数据,例如本地文件系统、Java CLASSPATH
等。
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")
服务层对象 (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>
下面的例子展示了数据访问对象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 配置文件代表体系结构中的一个逻辑层或模块。
可以使用应用程序上下文构造函数从所有这些 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
必须与执行导入的文件位于相同的目录或类路径位置,而messageSource.xml
和themeSource.xml
必须位于执行导入文件位置下的resources
位置。如你所见,前导斜杠会被忽略。然而,考虑到这些路径是相对的,最好完全不使用斜杠。根据Spring Schema,被导入的文件的内容,包括顶级<beans/>
元素,必须是有效的XML bean定义。
使用相对的
../
引用父目录中的文件是可能的,但不建议这样做。这样做会在当前应用程序之外的文件上创建一个依赖项。尤其是,不建议将此引用用于classpath:
URLs(例如,classpath:../services.xml
),其中运行时解析过程选择“最近的”类路径根,然后查看其父目录。类路径配置更改可能导致选择不同的、不正确的目录。
你总是可以使用完全限定的资源位置而不是相对路径:例如,file:C:/config/services.xml
或classpath:/config/services.xml
。但是,请注意,您正在将应用程序的配置耦合到特定的绝对位置。通常更可取的做法是对这种绝对位置有所保留——例如,通过在运行时根据 JVM 系统属性解析的${…}
占位符。
命名空间本身提供了导入指令特性。除了普通 bean 定义之外,Spring 提供的一系列 XML 名称空间还提供了更多的配置特性—例如,context
和util
名称空间。
Groovy Bean定义DSL
作为外部化配置元数据的进一步示例,bean 定义也可以在 Spring 的 Groovy bean 定义 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 定义并访问它们,如下面的示例所示:
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);
// use configured instance
List<String> userList = service.getUsernameList();
import org.springframework.beans.factory.getBean
// create and configure beans
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")
// retrieve configured instance
val service = context.getBean<PetStoreService>("petStore")
// use configured instance
var userList = service.getUsernameList()
使用 Groovy 配置,引导看起来非常类似。它有一个不同的上下文实现类,它支持groovy(但也理解XML bean定义)。下面的例子展示了Groovy的配置:
ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");
val context = GenericGroovyApplicationContext("services.groovy", "daos.groovy")
最灵活的变体是GenericApplicationContext
与Reader
委托的组合——例如,XML文件的XmlBeanDefinitionReader,如下所示:
GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();
val context = GenericApplicationContext()
XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml")
context.refresh()
你也可以为 Groovy 文件使用GroovyBeanDefinitionReader
,如下面的例子所示:
GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();
val context = GenericApplicationContext()
GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy")
context.refresh()
你可以在同一个ApplicationContext
上混合和匹配这样的Reader
委托,从不同的配置源读取 bean 定义。
然后可以使用getBean
检索 bean 的实例。ApplicationContext
接口还有一些用于检索 bean 的其他方法,但理想情况下,应用程序代码永远不应该使用它们。实际上,你的应用程序代码根本不应该调用getBean()
方法,因此根本不依赖Spring APIs。例如,Spring 与 web 框架的集成为各种 web 框架组件(如控制器和 JSF 管理的 bean)提供了依赖注入,允许你通过元数据(如自动装配注解)声明对特定bean的依赖。
1.3. Bean 概览
Spring IoC 容器管理一个或多个 bean。这些 bean 是用你提供给容器的配置元数据创建的(例如,以 XML <bean/>
定义的形式)。
在容器本身内,这些 bean 定义表示为BeanDefinition
对象,其中包含(除其他信息外)以下元数据:
- 包限定的类名:通常是被定义的 bean 的实际实现类。
- Bean 行为配置元素,它规定 bean 在容器中应该怎么做(范围(scope)、生命周期回调(lifecycle callbacks),等等)。
- 对 bean 执行其工作所需的其他 bean 的引用。这些引用也称为协作者或依赖项。
- 在新创建的对象中设置的其他配置设置——例如,池的大小限制或在管理连接池的bean中使用的连接数。
元数据转换为组成每个bean定义的一组属性。下表描述了这些属性:
属性 | 解释 |
---|---|
Class | 实例化Bean |
Name | 给bean命名 |
Scope | Bean的范围 |
Constructor arguments | 依赖注入 |
Properties | 依赖注入 |
Autowiring mode | 自动装配的合作者 |
Lazy initialization mode | 延迟初始化的Bean |
Initialization method | 初始化回调 |
Destruction method | 销毁回调 |
除了包含如何创建特定 bean 的信息的 bean 定义之外,ApplicationContext
实现还允许注册在容器外部(由用户)创建的现有对象。这是通过getBeanFactory()
方法访问ApplicationContext
的BeanFactory
来完成的,该方法返回DefaultListableBeanFactory
实现。DefaultListableBeanFactory
通过registerSingleton(..)
和registerBeanDefinition(..)
方法支持这种注册。但是,典型的应用程序只使用通过常规bean定义元数据定义的bean。
Bean 元数据和手动提供的单例实例需要尽早注册,以便容器在自动装配和其他自省步骤期间正确地对它们进行推理。虽然在某种程度上支持覆盖现有的元数据和现有的单例实例,但官方并不支持在运行时注册新 bean (与对工厂的实时访问同时进行),这可能会导致并发访问异常、bean 容器中的状态不一致,或两者都有。
1.3.1 命名bean
每个 bean 都有一个或多个标识符。这些标识符在承载 bean 的容器中必须是唯一的。一个 bean 通常只有一个标识符。但是,如果它需要一个以上的,额外的可以被认为是别名。
在基于 XML 的配置元数据中,可以使用id
属性、name
属性或两者来指定 bean 标识符。id
属性允许你指定一个id
。按照惯例,这些名称是字母数字(myBean
, someService
等),但它们也可以包含特殊字符。如果希望引入 bean 的其他别名,还可以在name
属性中指定它们,用逗号(,
)、分号(;
)或空格分隔。作为一个历史记录,在 Spring 3.1 之前的版本中,id
属性被定义为xsd:ID
类型,它限制了可能的字符。从3.1开始,它被定义为xsd:string
类型。请注意,bean id 唯一性仍然由容器强制执行,但不再由 XML 解析器强制执行。
你不一定需要为 bean 提供name
或id
。如果你没有显式地提供name
或id
,容器将为该 bean 生成一个惟一的名称。但是,如果您想通过名称引用该 bean,通过使用ref
元素或Service Locator 查找,则必须提供名称。不提供名称的动机是使用内部 bean 和自动装配协作器。
Bean 命名约定
在命名 bean 时使用用于对实例字段名称使用的标准Java约定。也就是说,bean 名称以小写字母开头,并从那里开始使用驼峰命名法。这类名称的示例包括accountManager
、accountService
、userDao
、loginController
等。
一致地命名 bean 使你的配置更容易阅读和理解。另外,如果你使用 Spring AOP,在将通知应用到一组与名称相关的 bean 时,它会有很大帮助。
通过在类路径中扫描组件,Spring 为未命名组件生成 bean 名,遵循前面描述的规则:本质上,取简单的类名,并将其第一个字符转换为小写。但是,在(不寻常的)特殊情况下,当有多个字符并且第一个和第二个字符都是大写字符时,将保留原来的大小写。这些规则与
java.beans.Introspector.decapitalize
(Spring在这里使用)定义的规则相同。
在 Bean 定义之外给一个 Bean 取别名
在 bean 定义本身中,通过使用id
属性指定的最多一个名称和name
属性中任意数量的其他名称的组合,你可以为 bean 提供多个名称。这些名称可以是同一个 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 Configuration
,@Bean
注解可以用来提供别名。
1.3.2. 实例化bean
bean 定义本质上是创建一个或多个对象的方法。容器在被请求时查看被命名的bean的创建方法,并使用该 bean 定义封装的配置元数据来创建(或获取)实际对象。
如果使用基于 XML 的配置元数据,则需要在<bean/>
元素的class
属性中指定要实例化的对象的类型(或类)。这个class
属性(在内部是BeanDefinition
实例上的Class
属性)通常是强制性的。使用Class
属性有两种方式:
- 通常,在容器本身通过反射调用其构造函数来直接创建bean的情况下,指定要构造的bean类,这在某种程度上相当于使用
new
操作符的 Java 代码。 - 指定包含用于创建对象的静态工厂方法的实际类,在较不常见的情况下,容器调用类上的静态工厂方法来创建bean。调用静态工厂方法返回的对象类型可以是同一个类,也可以完全是另一个类。
嵌套类的命名:如果你希望为嵌套类配置bean定义,你可以使用嵌套类的二进制名称或源名称。例如,如果你在
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
属性来指定包含静态工厂方法的类,并使用名为factory-method
的属性来指定工厂方法本身的名称。你应该能够调用这个方法(带有可选参数,稍后将进行描述)并返回一个活动对象,该对象随后将被视为通过构造函数创建的对象。这种bean定义的一个用途是在遗留代码中调用静态工厂。
下面的bean定义指定该bean将通过调用工厂方法来创建。定义没有指定返回对象的类型(类),而是指定包含工厂方法的类。在这个例子中,createInstance()
方法必须是一个静态方法。下面的例子展示了如何指定一个工厂方法:
<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>
下面的例子展示了一个可以与前面的bean定义一起工作的类:
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}
public static ClientService createInstance() {
return clientService;
}
}
class ClientService private constructor() {
companion object {
private val clientService = ClientService()
@JvmStatic
fun createInstance() = 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"/>
下面的例子显示了相应的类:
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
}
class DefaultServiceLocator {
companion object {
private val clientService = ClientServiceImpl()
}
fun createClientServiceInstance(): ClientService {
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"/>
下面的例子显示了相应的类:
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;
}
}
class DefaultServiceLocator {
companion object {
private val clientService = ClientServiceImpl()
private val accountService = AccountServiceImpl()
}
fun createClientServiceInstance(): ClientService {
return clientService
}
fun createAccountServiceInstance(): AccountService {
return accountService
}
}
这种方法表明工厂bean本身可以通过依赖注入(DI)进行管理和配置。参见依赖和配置细节。
在 Spring 文档中,“工厂bean”指的是在Spring容器中配置的bean,它通过实例或静态工厂方法创建对象。相比之下,
FactoryBean
(注意大写)指的是spring特定的FactoryBean
实现类。
确定Bean的运行时类型
特定bean的运行时类型的确定并不简单。bean元数据定义中的指定类只是一个初始类引用,可能与声明的工厂方法结合在一起,或者是可能导致bean的不同运行时类型的FactoryBean
类,或者在实例级工厂方法的情况下根本不设置(而是通过指定的factory-bean
名称解析)。此外,AOP代理可以用基于接口的代理包装一个bean实例,该代理有限地公开目标bean的实际类型(只公开其实现的接口)。
了解特定bean的实际运行时类型的建议方法是为指定的bean名称调用BeanFactory.getType
。这将考虑上述所有情况,并返回为相同的bean名称调用BeanFactory.getBean
返回的对象的类型。
查看源码发现方法名对不上,可能是文档没有更新吧。
1.4. 依赖项
典型的企业应用程序不是由单个对象(或Spring术语中的bean)组成的。即使是最简单的应用程序也有几个对象一起工作,以呈现终端用户眼中的一致应用程序。下一节将解释如何从定义许多独立的bean定义到完全实现的应用程序(其中对象协作以实现目标)。
1.4.1. 依赖注入
依赖注入(DI)是这样一个过程:对象仅通过构造函数参数、工厂方法的参数、或在对象实例构造或从工厂方法返回后在对象实例上设置的属性来定义它们的依赖项(即它们工作的其他对象)。然后容器在创建 bean 时注入这些依赖项。这个过程从根本上说是 bean 本身的逆过程(因此得名“控制反转”),它通过直接构造类或服务定位器模式(前面提到的实例工厂Bean定义)来控制依赖项的实例化或定位。
使用依赖注入原则,代码会更清晰,用依赖项提供对象时,解耦会更有效。对象不查找其依赖项,也不知道依赖项的位置或类。因此,你的类变得更容易测试,特别是当依赖关系在接口或抽象基类上时,这允许在单元测试中使用打桩(测试上的术语)或模拟实现。
依赖注入主要有两种变体:基于构造函数的依赖注入和基于setter
的依赖注入。
基于构造函数的依赖注入
基于构造函数的依赖注入是由容器调用带有许多参数的构造函数来完成的,每个参数代表一个依赖项。调用带有特定参数的静态工厂方法来构造bean几乎是等价的,本文将以类似的方式处理构造函数和静态工厂方法的参数。下面的例子展示了一个只能通过构造函数注入来进行依赖注入的类:
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on a MovieFinder
private final MovieFinder movieFinder;
// a constructor so that the Spring container can inject a MovieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
// a constructor so that the Spring container can inject a MovieFinder
class SimpleMovieLister(private val movieFinder: MovieFinder) {
// business logic that actually uses the injected MovieFinder is omitted...
}
注意,这个类没有什么特别之处。它是一个不依赖于容器特定接口、基类或注解的POJO
。
构造函数参数解析
构造函数参数解析通过使用参数的类型来匹配。如果bean定义的构造函数参数中不存在可能的歧义,那么在bean定义中定义构造函数参数的顺序就是在实例化bean时将这些参数提供给适当的构造函数的顺序。看下面的类:
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
package x.y
class 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无法确定值的类型,因此无法在没有帮助的情况下通过类型进行匹配。看下面的类:
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;
}
}
package examples
class ExampleBean(
private val years: Int, // Number of years to calculate the Ultimate Answer
private val ultimateAnswer: String // The Answer to Life, the Universe, and Everything
)
构造函数参数的类型匹配
在上述场景中,如果使用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>
除了解决多个简单值的不确定性外,指定索引还可以解决构造函数具有两个相同类型参数时的不确定性。
构造函数参数的名字
你还可以使用构造函数参数名来消除值歧义,如下示例所示:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
请记住,要使此功能开箱即用,必须在编译代码时启用调试标志,以便Spring可以从构造函数中查找参数名称。如果不能或不想使用调试标志编译代码,可以使用@ConstructorProperties
JDK注解显式地命名构造函数参数。然后,示例类必须如下所示:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
package examples
class ExampleBean
@ConstructorProperties("years", "ultimateAnswer")
constructor(val years: Int, val ultimateAnswer: String)
基于Setter
的依赖注入
基于setter
的依赖注入是由容器在调用无参数构造函数或无参数静态工厂方法来实例化 bean 之后调用 bean 上的setter
方法来完成的。
下面的例子展示了一个只能通过使用纯setter
注入来进行依赖注入的类。这是传统的Java类。它是一个不依赖于容器特定接口、基类或注解的POJO
。
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...
}
class SimpleMovieLister {
// a late-initialized property so that the Spring container can inject a MovieFinder
lateinit var movieFinder: MovieFinder
// business logic that actually uses the injected MovieFinder is omitted...
}
ApplicationContext
为其管理的 bean 支持基于构造函数和基于setter
的依赖注入。在已经通过构造函数方法注入了一些依赖项之后,它还支持基于setter
的依赖注入。你以BeanDefinition
的形式配置依赖项,你可以将其与PropertyEditor
实例一起使用,以将属性从一种格式转换为另一种格式。然而,大多数Spring用户并不直接(即通过编程方式)使用这些类,而是使用 XML bean 定义、带注解的组件(即使用@Component
、@Controller
等进行注解的类)或基于 Java 的@Configuration
类中的@Bean
方法。然后将这些源代码在内部转换为BeanDefinition
的实例,并用于加载整个 Spring IoC 容器实例。
基于构造函数还是基于setter
的依赖注入?
由于可以混合使用基于构造函数和基于setter
的依赖注入,对强制依赖项使用构造函数,对可选依赖项使用setter方法或配置方法,是一个不错的经验法则。注意,在setter
方法上使用@Required
注解可以使该属性成为必需的依赖项;但是,构造函数注入和参数的编程验证更可取。
Spring 团队通常提倡构造函数注入,因为它允许将应用程序组件实现为不可变对象,并确保所需的依赖项不为null
。此外,构造函数注入的组件总是以完全初始化的状态返回给客户端(调用)代码。顺便说一句,大量的构造函数参数是一种糟糕的代码味道,这意味着类可能有太多的责任,应该重构以更好地解决适当的关注点。
Setter
注入应该主要用于可选依赖项,这些依赖项可以在类中分配合理的默认值。否则,必须在代码使用依赖项的所有地方执行非空检查。setter
注入的一个好处是,setter
方法使该类的对象能够在以后重新配置或重新注入。因此,通过JMX MBean
进行管理是setter
注入的一个引人注目的用例。
使用对特定类最有意义的依赖注入方式。有时,在处理你没有源代码的第三方类时,可以为你做出选择。例如,如果第三方类不公开任何setter
方法,那么构造函数注入可能是惟一可用的依赖注入方式。
依赖解析过程
容器执行 bean 依赖解析如下:
- 使用描述所有 bean 的配置元数据创建和初始化
ApplicationContext
。配置元数据可以通过XML、Java代码或注解指定。 - 对于每个 bean,其依赖关系以属性、构造函数参数或静态工厂方法参数的形式表示(如果使用静态工厂方法而不是普通构造函数)。这些依赖项是在实际创建 bean 时提供给bean 的。
- 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个 bean 的引用。
- 若每个属性或构造函数参数都是值,将其从指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring可以将字符串格式提供的值转换为所有内置类型,例如
int
、long
、String
、boolean
等。
Spring 容器在创建容器时验证每个 bean 的配置。但是,直到实际创建bean时才设置 bean 属性本身。单例作用域并设置为预实例化(默认)的 bean 是在创建容器时创建的。作用域在Bean Scopes
中定义。否则,只有在请求bean时才创建它。创建一个bean可能会导致创建一个 bean 图,因为创建并分配了 bean 的依赖项及其依赖项的依赖项(等等)。请注意,这些依赖项之间的解析不匹配可能会比较迟出现——即在第一次创建受影响的 bean 时出现。
循环依赖
如果主要使用构造函数注入,可能会创建一个不可解析的循环依赖场景。
例如:类A通过构造函数注入需要类B的一个实例,而类B通过构造函数注入需要类A的一个实例。如果你配置了类A和类B相互注入的bean, Spring IoC容器将在运行时检测此循环引用,并抛出BeanCurrentlyInCreationException
。
一种可能的解决方案是编辑一些类的源代码,由setter
而不是构造函数来配置。或者,避免构造函数注入,只使用setter
注入。换句话说,尽管不推荐这样做,但你可以使用setter
注入来配置循环依赖项。
与典型的情况(没有循环依赖)不同,bean A和bean B之间的循环依赖强制在完全初始化自己之前将一个 bean 注入到另一个 bean 中(典型的鸡和蛋的场景)。
你通常可以相信Spring会做正确的事情。它在容器加载时检测配置问题,例如对不存在的 bean 的引用和循环依赖。Spring 在实际创建 bean 时设置属性并解析依赖项(尽可能迟)。这意味着,如果在创建对象或其依赖项时出现问题,正确加载的Spring容器稍后可以在请求对象时生成异常——例如,bean由于缺少或无效属性而抛出异常。某些配置问题的可见性可能会延迟,这就是ApplicationContext
实现默认预实例化单例 bean 的原因。在实际需要这些bean之前创建这些bean需要花费一些前期时间和内存,你会在创建ApplicationContext
时发现配置问题,而不是稍后。你仍然可以覆盖此默认行为,以便单例bean延迟初始化,而不是提前实例化。
如果不存在循环依赖项,那么当一个或多个协作bean被注入到依赖bean中时,每个协作bean在注入到依赖bean之前都要完全配置。这意味着,如果bean A对bean B有依赖,Spring IoC容器在调用bean A上的setter方法之前完全配置了bean B。换句话说,bean被实例化(如果它不是预实例化的单例),它的依赖项被设置,并且相关的生命周期方法(例如配置的init
方法或InitializingBean
回调方法)被调用。
依赖注入的例子
下面的示例使用基于 XML 的配置元数据进行基于setter
的依赖注入。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
类:
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;
}
}
class ExampleBean {
lateinit var beanOne: AnotherBean
lateinit var beanTwo: YetAnotherBean
var i: Int = 0
}
在前面的示例中,声明了与 XML 文件中指定的属性匹配的setter
。下面的例子使用了基于构造函数的依赖注入:
<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
类:
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;
}
}
class ExampleBean(
private val beanOne: AnotherBean,
private val beanTwo: YetAnotherBean,
private val i: Int)
bean定义中指定的构造函数参数被用作ExampleBean
构造函数的参数。
现在考虑这个例子的一个变体,其中,Spring被告知调用一个静态工厂方法来返回对象的一个实例,而不是使用构造函数:
<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
<constructor-arg ref="anotherExampleBean"/>
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
下面的例子显示了相应的ExampleBean
类:
public class ExampleBean {
// a private constructor
private ExampleBean(...) {
...
}
// a static factory method; the arguments to this method can be
// considered the dependencies of the bean that is returned,
// regardless of how those arguments are actually used.
public static ExampleBean createInstance (
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
// some other operations...
return eb;
}
}
class ExampleBean private constructor() {
companion object {
// 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.
@JvmStatic
fun createInstance(anotherBean: AnotherBean, yetAnotherBean: YetAnotherBean, i: Int): ExampleBean {
val eb = ExampleBean (...)
// some other operations...
return eb
}
}
}
静态工厂方法的参数由<constructor-arg/>
元素提供,与实际使用的构造函数完全相同。工厂方法返回的类的类型不必与包含静态工厂方法的类的类型相同(尽管在本例中是相同的)。可以以本质上相同的方式使用实例(非静态)工厂方法(除了使用工厂bean属性而不是类属性),因此我们在这里不讨论这些细节。
1.4.2. 依赖关系和配置的细节
如上一节所述,你可以将 bean 属性和构造函数参数定义为对其他被管理的 bean (合作者)的引用或内联定义的值。为此,Spring 基于 XML 的配置元数据在其<property/>
和<constructor-arg/>
元素中支持子元素类型。
直接值(基本类型(Primitives)、字符串等)
<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 更简洁。但是,输入错误是在运行时而不是在设计时发现的,除非你使用了在创建bean定义时支持自动补全属性的 IDE (如 IntelliJ IDEA 或用于 Eclipse 的 Spring Tools)。强烈推荐这种 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 容器通过使用 JavaBeans 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是一个原型bean,那么这个错误和产生的异常只有在容器部署很久之后才会被发现。
在 4.0 beans XSD 中不再支持
idref
元素上的local
属性,因为它不再提供超过常规bean引用的值。当升级到 4.0 schema 时,将现有的idref local
引用更改为idref bean
。
<idref/>
元素带来价值的一个常见地方(至少在Spring 2.0之前的版本中)是在ProxyFactoryBean
bean定义中的AOP拦截器的配置中。在指定拦截器名称时使用<idref/>
元素可以防止将拦截器ID拼错。
对其他 bean 的引用(合作者)
ref
元素是<constructor-arg/>
或<property/>
定义元素中的最后一个元素。在这里,你将一个bean
的指定属性的值设置为对容器管理的另一个 bean (协作者)的引用。被引用的bean是要设置其属性的bean的一个依赖项,并且在设置属性之前根据需要对其进行初始化(如果协作者是一个单例 bean,它可能已经被容器初始化了)。所有的引用最终都是对另一个对象的引用。作用域和验证取决于你是否通过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>
内部 Bean
<property/>
或<constructor-arg/>
元素中的<bean/>
元素定义了一个内部 bean,如下所示:
<bean id="outer" class="...">
<!-- instead of using a reference to a target bean, simply define the target bean inline -->
<property name="target">
<bean class="com.example.Person"> <!-- this is the inner bean -->
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
</bean>
</property>
</bean>
内部 bean 定义不需要被定义的ID或名称。如果指定了,容器不使用这样的值作为标识符。容器在创建时也会忽略scope
标志,因为内部bean总是匿名的,并且总是用外部bean创建的。不可能单独访问内部 bean,也不可能将它们注入到协作 bean 中。
作为一种极端情况,从自定义作用域(scope)接收销毁回调是可能的——例如,对于包含在单例bean中的请求范围内的内部bean。内部bean实例的创建与包含它的bean绑定在一起,但是销毁回调让它参与到请求范围的生命周期中。这种情况并不常见。内部bean通常只是共享包含它们的bean的范围。