本部分文档涵盖了 Spring Framework 框架中必不可少的所有技术
其中最重要的是 Spring 框架的控制反转(IoC) 容器. 之后,将全面介绍 Spring 的面向切面的编程 (AOP) 技术. Spring 框架拥有自己的 AOP 框架,该框架在概念上易于理解,并且成功解决了 Java 企业编程中 AOP 要求的 80% 的难题.
还提供了 Spring 与 AspectJ 的集成(目前,在功能上最丰富) 以及 Java 企业领域中最成熟的 AOP 实现) .
1. IOC 容器
本章介绍了 Spring 的 IoC 容器.
1.1. Spring IoC 容器和 bean 的介绍
本章介绍 Spring 框架中控制反转 IOC 的实现. IOC 与大家熟知的依赖注入同理,这是一个通过依赖注入对象的过程 也就是说,依赖的对象通过构造参数、工厂方法参数或者属性注入, 创建 bean 后容器注入这些依赖对象. 也就是 domain 对象.创建过程相对于普通创建对象的过程是反向的(因此称之为 IoC) ,它通过直接使用构造类来控制实例化, 或者定义它们之间的依赖,或者类似于服务定位模式的一种机制.
org.springframework.beans
和 org.springframework.context
包是实现 Spring IOC 容器的基础 .
BeanFactory
接口提供了一种更先进的配置机制来管理任意类型的对象.
ApplicationContext
是 BeanFactory
的子接口. 他提供了:
-
更容易与 Spring 的 AOP 特性集成
-
消息资源处理(用于国际化)
-
事件发布
-
应用层特定的上下文,如用于 web 应用程序的
WebApplicationContext
简而言之, BeanFactory
提供了配置框架的基本功能,ApplicationContext
添加了更多特定于企业的功能.
ApplicationContext
完全扩展了 BeanFactory
的功能,这些内容将在介绍 Spring IoC 容器的专门章节讲解.
有关使用 BeanFactory
代替 ApplicationContext,
的更多信息,请参考 BeanFactory
API.
在 Spring 中,由 Spring IOC 容器管理的,构成程序的骨架的对象称为 Bean. bean 对象是指经过 IoC 容器实例化,组装和管理的对象. 此外,bean 就是应用程序中众多对象之一 . bean 和 bean 的依赖是由容器所使用的配置元数据反射而来.
1.2. 容器概述
org.springframework.context.ApplicationContext
是 Spring IoC 容器实现的代表,它负责实例化,配置和组装 Bean. 容器通过读取配置元数据获取有关实例化、配置和组装哪些对象的说明 .
配置元数据可以使用 XML、Java 注解或 Java 代码来呈现. 它允许你处理应用程序的对象与其他对象之间的互相依赖.
Spring 提供了 ApplicationContext
接口的几个实现. 在独立应用程序中,通常创建
ClassPathXmlApplicationContext
或 FileSystemXmlApplicationContext
的实例. 虽然 XML 一直是定义配置元数据的传统格式, 但是您可以指定容器使用 Java 注解或编程的方式编写元数据格式,并通过提供少量的 XML 配置以声明对某些额外元数据的支持.
在大多数应用场景中,不需要用户显式的编写代码来实例化 IOC 容器的一个或者多个实例. 例如,在 Web 应用场景中,只需要在 web.xml
中添加大概 8 行简单的 web 描述样板就行了(see 快速对 Web 应用的 ApplicationContext 实例化).
如果你使用的是基于 Eclipse 的 Spring Tools for Eclipse 开发环境,该样板配置只需点击几下鼠标或按几下键盘就能创建了.
下图展示了 Spring 工作方式的高级视图,应用程序的类与元数据配置相互配合,这样,在 ApplicationContext
创建和初始化后,你立即拥有一个可配置的,可执行的系统或应用程序.
-
IOC容器

1.2.1. 配置元数据
如上图所示,Spring IOC 容器使用元数据配置这种形式,这个配置元数据表示了应用开发人员告诉 Spring 容器以何种方式实例化、配置和组装应用程序中的对象.
配置元数据通常以简单、直观的 XML 格式提供,本章的大部分内容都使用这种格式来说明 Spring IoC 容器的关键概念和特性.
XML 并不是配置元数据的唯一方式,Spring IoC 容器本身是完全与元数据配置的实际分离的. 现在,许多开发人员选择 基于 Java 配置 来开发应用程序. |
更多其他格式的元数据见:
-
基于注解的配置: Spring 2.5 支持基于注解的元数据配置.
-
基于 Java 的配置 : 从 Spring 3.0 开始, 由 Spring JavaConfig 项目提供的功能已经成为 Spring 核心框架的一部分. 因此,你可以使用 Java 配置来代替 XML 配置定义外部 bean . 要使用这些新功能,请参阅
@Configuration
,@Bean
,@Import
, 和@DependsOn
注解.
Spring 的 Bean (至少一个) 由容器来管理,基于 XML 的元数据配置将这些 bean 配置为 <beans/>
元素.并放置于 <beans/>
元素内部. 基于 Java 的配置通常是使用 @Configuration
注解过的类中,在它的方法上使用 @Bean
注解.
这些 bean 定义会对应到构成应用程序的实际对象. 通常你会定义服务层对象,数据访问对象(DAOs) ,表示对象(如Struts Action
的实例),基础对象(如 Hibernate 的 SessionFactories
, JMS Queues
,) .
通常,不会在容器中配置细粒度的 domain 对象,因为创建和加载 domain 对象通常是 DAO 和业务逻辑的职责.
但是,你可以使用 Spring 与 AspectJ 集成独立于 IoC 容器来创建的对象,请参阅 Spring 使用 AspectJ 进行依赖注入 domain 对象.
下面的示例显示了基于 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="..."> (1) (2)
<!-- 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>
1 | id 属性是字符串 ,用来识别唯一的bean定义. |
2 | class 属性定义了bean的类型,使用全类名. |
id
属性的值是指引用依赖对象(在这个例子没有声明用于引用依赖对象的XML) . 请参阅 依赖 获取更多信息
1.2.2. 实例化容器
提供给 ApplicationContext
构造函数的路径就是实际的资源字符串,使容器能从各种外部资源(如本地文件系统、Java CLASSPATH
等)装载元数据配置.
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")
当你了解 Spring IoC 容器之后,你可能想知道更多关于 Spring 的 |
下面的例子显示了服务层对象 (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 的元数据配置
使用 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>
上面的例子中,使用了3个文件: services.xml
, messageSource.xml
, 和 themeSource.xml
来加载外部Bean的定义. 导入文件采用的都是相对路径,因此 services.xml
必须和导入文件位于同一目录或类路径中,
而 messageSource.xml
和 themeSource.xml
必须在导入文件的资源位置中. 正如你所看到的,前面的斜线将会被忽略,但考虑到这些路径是相对的,最佳的使用是不用斜线的. 这个XML文件的内容都会被导入,包括顶级的
<beans/>
元素, 但必须遵循 Spring Schema 定义 XML bean 定义的规则.
这种相对路径的配置是可行的,但不推荐这样做. 在使用 "../" 引用目录时,这样做会对当前应用程序之外的文件产生依赖.
特别是对于 您可以使用完全限定的资源位置而不是相对路径:例如, |
import
是由 bean 命名空间本身提供的功能. 在 Spring 提供的 XML 命名空间中,如 context
和 util
命名空间,可以用于对普通 bean 定义进行更高级的功能配置.
DSL 定义Groovy Bean
作为从外部配置元数据的另一个示例, bean 定义也可以使用 Spring 的 Groovy DSL 来定义. Grails 框架有此配置实例,通常, 可以在具有以下结构的 ".groovy" 文件中配置 bean 定义. 例如:
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)
, 获取容器实例.
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();
// 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 定义) 如下:
ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");
val context = GenericGroovyApplicationContext("services.groovy", "daos.groovy")
最灵活的实现是 GenericApplicationContext
, 例如读取 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
,上混合使用,也可以自动匹配,如果需要可以从不同的配置源读取 bean 定义.
您可以使用 getBean
来获取 bean 实例, ApplicationContext
接口也可以使用其他的方法来获取 bean. 但是在理想情况下,应用程序代码永远不应该使用它们. 事实上,你的应用程序代码也不应该调用
getBean()
方法,因此对 Spring API 没有依赖. 例如,Spring 与 Web 框架的集成为各种 Web 框架组件(如控制器和 JSF 管理 bean) 提供了依赖注入功能,从而允许开发者通过元数据声明对特定 bean 的依赖(例如,自动注解) .
1.3. Bean 的概述
Spring IoC 容器管理一个或多个 bean. 这些 bean 是由您提供给容器的元数据配置创建的(例如,XML <bean/>
定义的形式).
在容器内部,这些 bean 定义表示为 BeanDefinition
对象,其中包含(其他信息) 以下元数据
-
限定包类名称: 通常,定义的 bean 的实际实现类.
-
bean 行为配置元素, 定义 Bean 的行为约束(例如作用域,生命周期回调等等)
-
bean 需要引用其他 bean 来完成工作. 这些引用也称为协作或依赖.
-
其他配置用于新对象的创建,例如使用 bean 的数量来管理连接池,或者限制池的大小.
以下是每个 bean 定义的属性:
属性 | 对应章节介绍… |
---|---|
Class |
|
Name |
|
Scope |
|
Constructor arguments |
|
Properties |
|
Autowiring mode |
|
Lazy initialization mode |
|
Initialization method |
|
Destruction method |
除了 bean 定义包含如何创建特定的 bean 的信息外, ApplicationContext
实现还允许用户在容器中注册现有的、已创建的对象. 这是通过 getBeanFactory()
方法访问 ApplicationContext
的 BeanFactory
来完成的,
该方法返回 BeanFactory DefaultListableBeanFactory
实现. DefaultListableBeanFactory
支持通过 registerSingleton(..)
和
registerBeanDefinition(..)
方法来注册对象. 然而,典型的应用程序只能通过元数据配置来定义 bean.
为了让容器正确推断它们在自动装配和其它内置步骤,需要尽早注册 Bean 的元数据和手动使用单例的实例. 虽然覆盖现有的元数据和现有的单例实例在某种程度上是支持的, 但是新 bean 在运行时(同时访问动态工厂) 注册官方并不支持,可能会导致并发访问异常、bean 容器中的不一致状态,或者两者兼有. |
1.3.1. 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 和 自动装配.
在类路径中进行组件扫描时, Spring 会根据上面的规则为未命名的组件生成 bean 名称,规则是: 采用简单的类名,并将其初始字符转化为小写字母. 然而,在特殊情况下,当有一个以上的字符,同时第一个和第二个字符都是大写时,原来的规则仍然应该保留. 这些规则与 Java 中定义实例的相同.
例如 Spring 使用的 java.beans.Introspector.decapitalize 类.
|
定义外部 Bean 的别名
在对 bean 定义时,除了使用 id
属性指定唯一的名称外,还可以提供多个别名,这需要通过 name
属性指定. 所有这个名称都会指向同一个 bean,在某些情况下提供别名非常有用,例如为了让应用每一个组件都能更容易的对公共组件进行引用.
然而,在定义 bean 时就指定所有的别名并不是很恰当的. 有时期望能够在当前位置为那些在别处定义的 bean 引入别名. 在 XML 配置文件中, 可以通过 <alias/>
元素来定义 bean 别名,例如:
<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.
1.3.2. 实例化 Bean
bean 定义基本上就是用来创建一个或多个对象的配置,当需要 bean 的时候,容器会查找配置并且根据 bean 定义封装的元数据来创建(或获取) 实际对象.
如果你使用基于 XML 的配置,那么可以在 <bean/>
元素中通过 class
属性来指定对象类型. class
属性实际上就是 BeanDefinition
实例中的 class
属性.
他通常是必需的(一些例外情况,通过实例工厂方法实例化 和 Bean 继承的定义). 有两种方式使用 Class
属性
-
通常情况下,会直接通过反射调用构造方法来创建 bean,这种方式与 Java 代码的 new 创建相似.
-
通过静态工厂方法创建,类中包含静态方法. 通过调用静态方法返回对象的类型可能和 Class 一样,也可能完全不一样.
通过构造器实例化
当通过构造器创建 Bean 时,Spring 兼容所有可以使用的普通类,也就是说,正在开发的类不需要实现任何特定接口或以特定方式编码. 只要指定 bean 类就足够了. 但是,根据您为该特定 bean 使用的 IoC 类型,您可能需要一个默认(空) 构造函数.
Spring IoC 容器几乎可以管理您希望它管理的任何类. 它不仅限于管理真正的 JavaBeans. 大多数 Spring 用户更喜欢管理那些只有一个默认构造函数(无参数) 和有合适的 setter 和 getter 方法的真实的 JavaBeans,还可以在容器中放置更多的外部非 bean 形式(non-bean-style)类,例如: 如果需要使用一个绝对违反 JavaBean 规范的遗留连接池时 Spring 也是可以管理它的.
使用基于 XML 的配置元数据,您可以按如下方式指定 bean 类:
<bean id="exampleBean" class="examples.ExampleBean"/>
<bean name="anotherExample" class="examples.ExampleBeanTwo"/>
给构造方法指定参数以及为 bean 实例化设置属性将在后面的 依赖注入 中说明.
通过静态工厂方法实例化
当采用静态工厂方法创建 bean 时,除了需要指定 class 属性外,还需要通过 factory-method
属性来指定创建 bean 实例的工厂方法. Spring 将会调用此方法(其可选参数接下来会介绍) 返回实例对象. 从这样看来,它与通过普通构造器创建类实例没什么两样.
下面的 bean 定义展示了如何通过工厂方法来创建 bean 实例. 注意,此定义并未指定对象的返回类型,只是指定了该类包含的工厂方法,在这个例中, createInstance()
必须是一个静态(static
) 的方法:
<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 实例设置属性的详细内容请查阅 依赖和配置细节.
通过实例工厂方法实例化
通过调用工厂实例的非静态方法进行实例化与 通过静态工厂方法实例化类似, 请将 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 类:
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"/>
以下示例显示了相应的 Java 类:
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
}
}
这种方法表明可以通过依赖注入(DI) 来管理和配置工厂 bean 本身. 请参阅详细信息中的 依赖和配置细节.
在 Spring 文档中, "factory bean" 是指在 Spring 容器中通过
实例 或
静态 工厂方法 创建对象的 bean.
相比之下,FactoryBean (注意大小写) 是指 Spring 特定的 FactoryBean .
|
确定 Bean 的运行时类型
想要确定 Bean 运行时的类型并不简单,在 bean 元数据定义中只是一个初始类引用,可能会因为声明的工厂方法组合或者 FactoryBean
而造成不用的运行时类型,或者在创建 bean 的
实例不设置 工厂方法(通过指定的 "factory-bean" 名称解析).此外,AOP 代理可以将 bean 的实例和基于接口的代理一起包装为目标 bean 的实际类型(仅是其实现的接口).
找出指定 bean 的实际运行时类型的推荐方法是通过 BeanFactory.getType
指定 bean 的名称,这需要考虑到大小写并且和 BeanFactory.getBean
调用对象返回相同的 bean 名称
1.4. 依赖
一般情况下企业应用不会只有一个对象(Spring Bean) ,甚至最简单的应用都需要多个对象协同工作. 下一部分将解释如何从定义单个 Bean 到让多个 Bean 协同工作.
1.4.1. 依赖注入
依赖注入 (DI) 是让对象只通过构造参数、工厂方法的参数或者配置的属性来定义他们的依赖的过程. 这些依赖也是其他对象所需要协同工作的对象, 容器会在创建 Bean 的时候注入这些依赖. 整个过程完全反转了由 Bean 自己控制实例化或者依赖引用,所以这个过程也称之为 "控制反转"
当使用了依赖注入的特性以后,会让开发者更容易管理和解耦对象之间的依赖,使代码变得更加简单. 对象之间不再关注依赖,也不需要知道依赖类的位置. 如此一来,开发的类更易于测试 尤其是当开发者的依赖是接口或者抽象类的情况时,开发者可以轻易地在单元测试中 mock 对象.
依赖注入主要使用两种方式: 基于构造函数的注入 and 基于 Setter 方法的依赖注入.
基于构造函数的注入
基于构造函数的依赖注入是由 IoC 容器来调用类的构造函数,构造函数的参数代表这个 Bean 所依赖的对象. 构造函数的依赖注入与调用带参数的静态工厂方法基本一样. 调用具有特定参数的静态工厂方法来构造 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 的构造函数参数不存在歧义,那么构造器参数的顺序也就是就是这些参数实例化以及装载的顺序. 参考如下代码:
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
class ThingOne(thingTwo: ThingTwo, thingThree: ThingThree)
假设 ThingTwo
和 ThingThree
不存在继承关系 也没有什么歧义. 下面的配置完全可以工作正常. 开发者无需再到 <constructor-arg/>
元素中指定构造函数参数的 index
或 type
<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 IoC 容器无法判断值的类型,所以也是无法匹配的,考虑代码:
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;
}
}
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>
除了解决多个简单值的歧义之外,指定索引还可以解决构造函数具有相同类型的两个参数的歧义.
index 从 0 开始. |
您还可以使用构造函数参数名称消除歧义,如以下示例所示:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
需要注意的是,解析这个配置的代码必须启用了 debug 来编译,这样 Spring 才可以从构造函数查找参数名称. 开发者也可以使用 @ConstructorProperties 注解来显式声明构造函数的名称. 例如下面代码:
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
class ExampleBean
@ConstructorProperties("years", "ultimateAnswer")
constructor(val years: Int, val ultimateAnswer: String)
基于 Setter 方法的依赖注入
基于 setter 函数的依赖注入是让容器调用 Bean 的无参构造函数,或者无参静态工厂方法,然后再来调用 setter 方法来实现依赖注入.
下面的例子展示了使用 setter 方法进行的依赖注入的过程. 其中类对象只是简单的 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
实例来自由选择注入方式. 然而,大多数的开发者并不直接使用这些类,而是更喜欢使用 XML 配置来进行 bean
定义, 或者基于注解的组件 (例如使用 @Component
,
@Controller
), 或者在配置了 @Configuration
类中使用 @Bean
的方法. 然后,这些会在 Spring 内部转换为 BeanDefinition
实例,并用于加载整个 Spring IoC 容器实例.
决定依赖的过程
容器解析 Bean 的过程如下:
-
创建并根据描述的元数据来实例化
ApplicationContext
元数据配置可以是 XML 文件、Java 代码或者注解. -
每一个 Bean 的依赖都通过构造函数参数或属性,或者静态工厂方法的参数等等来表示. 这些依赖会在 Bean 创建的时候装载和注入
-
每一个属性或者构造函数的参数都是真实定义的值或者引用容器其他的 Bean.
-
每一个属性或者构造参数可以根据指定的类型转换为所需的类型. Spring 也可以将 String 转成默认的 Java 内置类型. 例如
int
,long
,String
,boolean
,等.
Spring 容器会在容器创建的时候针对每一个 Bean 进行校验. 但是 Bean 的属性在 Bean 没有真正创建之前是不会进行配置的,单例类型的 Bean 是容器创建的时候配置成预实例状态的. Bean 的作用域 后面再说, 其他的 Bean 都只有在请求的时候,才会创建,显然创建 Bean 对象会有一个依赖顺序图,这个图表示 Bean 之间的依赖. 容器根据此来决定创建和配置 Bean 的顺序.
你可以信任 Spring 做正确的事. 它在容器加载时检测配置问题,例如对不存在的 bean 和循环依赖的引用. 当实际创建 bean 时,Spring 会尽可能晚地设置属性并解析依赖. 这也意味着 Spring 容器加载正确后会在 bean 注入依赖出错的时候抛出异常. 例如,bean 抛出缺少属性或者属性不合法的异常 ,这种延迟的解析也是
ApplicationContext
的实现会令单例 Bean 处于预实例化状态的原因. 这样,通过创建 bean,可以在真正使用 bean 之前消耗一些内存代价而发现配置的问题 . 开发者也可以覆盖默认的行为让单例 bean 延迟加载,而不总是处于预实例化状态.
如果不存在循环依赖的话,bean 所引用的依赖会预先全部构造. 举例来说,如果 bean A 依赖于 bean B,那么 Spring IoC 容器会先配置 bean B,然后调用 bean A 的 setter 方法来构造 bean A. 换言之,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
}
在前面的示例中,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
类:
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/>
元素提供,与实际使用的构造函数完全相同. 工厂方法返回类的类型不必与包含静态工厂方法 的类完全相同,
尽管在本例中是这样. 实例(非静态) 工厂方法的使用方式也是相似的(除了使用 factory-bean
属性而不是 class
属性. 因此此处不在展开讨论.
1.4.2. 依赖和配置细节
如上一节所述, 您可以将 bean 的属性和构造函数参数定义为对其他 bean 的引用,或者作为其内联定义的值. Spring 可以允许您在基于 XML 的配置元数据(定义 Bean) 中使用子元素 <property/>
和 <constructor-arg/>
来达到这种目的.
直接值(基本类型,String 等等)
<property/>
元素的 value
属性 将属性或构造函数参数指定为人类可读的字符串表示形式, Spring 的 conversion service 用于将这些值从 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(例如 IntelliJIDEA 或者 Spring Tools for Eclipse) . 所以,强烈建议使用此类 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 的容器会将 <value/>
里面的文本通过 JavaBean 的 PropertyEditor
机制转换成 java.util.Properties
实例, 这种嵌套 <value/>
元素的快捷方式也是 Spring 团队推荐使用的.
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>
Spring 团队更推荐第一种方式,因为使用了 idref
标签,它会让容器在部署阶段就对 bean 进行校验,以确保 bean 一定存在. 而使用第二种方式的话,是没有任何校验的. 只有实际上引用了 client
bean 的 targetName
属性
不对其值进行校验. 在实例化 client 的时候才会被发现. 如果 client
是 prototype 类型的 Bean 的话,那么类似拼写之类的错误会在容器部署以后很久才能发现.
idref 元素的 local 属性 属性在 Spring 4.0 以后的 xsd 中已经不再支持了,而是使用了 bean 引用. 如果更新了版本的话,只要将 idref local 引用都转换成 idref bean 即可.
|
在 Spring 2.0 之前的版本中,<idref/>
在 ProxyFactoryBean
bean定义中的 AOP interceptors 的配置中 常见,指定拦截器名称时使用 <idref/>
元素可防止您拼写错误的拦截器 ID.
引用其他的 Bean(装配)
ref
元素是 <constructor-arg/>
or <property/>
定义元素中的最后一个元素. 你可以通过这个标签配置一个 bean 来引用另一个 bean. 当需要引用一个 bean 的时候,被引用的 bean 会先实例化,
然后配置属性,也就是引用的依赖. 如果该 bean 是单例 bean 的话 ,那么该 bean 会早由容器初始化. 最终会引用另一个对象的所有引用,bean 的作用域以及校验取决于你是否有通过 bean
, 或 parent
这些属性来指定对象的 id
或者 name
属性. .
通过指定 bean 属性中的 <ref/>
元素来指定依赖是最常见的一种方式,可以引用容器或者父容器中的 bean,不在同一个 XML 文件定义也可以引用. 其中 bean 属性中的值可以和其他引用 bean 中的 id
属性一致,或者和其中的某个 name
属性一致,以下示例显示如何使用 ref
元素:
<ref bean="someBean"/>
通过指定 bean 的 parent
属性可以创建一个引用到当前容器的父容器之中. parent
属性的值可以与目标 bean 的 id
属性一致,或者和目标 bean 的 name
属性中的某个一致,目标 bean 必须是当前引用目标 bean 容器的父容器 .
开发者一般只有在具有层次化容器,并且希望通过代理来包裹父容器中一个存在的 bean 的时候才会用到这个属性. 以下一对列表显示了如何使用 parent
属性:
<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
<!-- insert dependencies as required as 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>
ref 元素中的 local 标签在 xsd 4.0,以后已经不再支持了,开发者可以通过将已存在的 ref local 改为 ref bean 来完成 Spring 版本升级.
|
内部 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 以外的其他 bean 中的.
作为一个极端情况,可以从自定义作用域接收销毁回调,例如: 请求作用域的内部 bean 包含了单例 bean,那么内部 bean 实例会绑定到包含的 bean,而包含的 bean 允许访问 request 的 scope 生命周期. 这种场景并不常见,内部 bean 通常只是供给它的外部 bean 使用.
集合
在 <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,或者集合的 value 都可以配置为下列元素之一:
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>
请注意,在 child
bean 定义的 adminEmails
中的 <props/>
使用 merge=true
属性. 当容器解析并实例化 child
bean时,
生成的实例有一个 adminEmails
属性集合, 其实例中包含的 adminEmails
集合就是child的 adminEmails
以及 parent 的 adminEmails
集合. 以下清单显示了结果:
administrator=administrator@example.com sales=sales@example.com support=support@example.co.uk
子属性集合的 Properties
集合继承父 <props/>
的所有属性元素,子值的支持值覆盖父集合中的值.
这个合并的行为和 <list/>
, <map/>
, 和 <set/>
之类的集合类型的行为是类似的. <list/>
在特定例子中,与 List
集合类型类似, 有着隐含的 ordered
概念. 所有的父元素里面的值,是在所有子元素的值之前配置的.
但是像 Map
, Set
, 和 Properties
的集合类型,是不存在顺序的.
集合合并的限制
您不能合并不同类型的集合(例如要将 Map
和 List
合并是不可能的) . 如果开发者硬要这样做就会抛出异常, merge
的属性是必须特指到更低级或者继承的子节点定义上, 特指 merge
属性到父集合的定义上是冗余的,而且在合并上也没有任何效果.
强类型的集合
感谢 Java 对泛型类型的支持. 也就是,开发者可以声明 Collection
类型,然后这个集合只包含 String
元素(举例来说) . 如果开发者通过 Spring 来注入强类型的 Collection
到 bean 中,开发者就可以利用 Spring 的类型转换支持来做到 以下 Java 类和 bean 定义显示了如何执行此操作:
public class SomeClass {
private Map<String, Float> accounts;
public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}
class SomeClass {
lateinit var accounts: Map<String, Float>
}
<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
的属性 accounts
准备注入的时候,accounts
的泛型信息 MapMap<String, Float>
就会通过反射拿到. 这样 Spring 的类型转换系统能够识别不同的类型,如上面的例子 Float
然后会将字符串的值 9.99
, 2.75
, 和 3.99
转换成对应的 Float
类型.
Null 和 空字符串
Strings
将属性的空参数视为空字符串. 下面基于XML的元数据配置就会将 email 属性配置 String
值("").
<bean class="ExampleBean">
<property name="email" value=""/>
</bean>
上面的示例等效于以下 Java 代码:
exampleBean.setEmail("");
exampleBean.email = ""
<null/>
将被处理为 null
值. 以下清单显示了一个示例:
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>
上述配置等同于以下 Java 代码:
exampleBean.setEmail(null);
exampleBean.email = null
使用 p 命名空间简化 XML 配置
p 命名空间让开发者可以使用 bean
的属性,而不必使用嵌套的 <property/>
元素.
Spring 是支持基于 XML 的格式化 命名空间扩展的. 本节讨论的 beans
配置都是基于 XML 的,p
命名空间是定义在 Spring Core 中的(不是在 XSD 文件) .
以下示例显示了两个 XML 片段(第一个使用标准XML格式,第二个使用 p
命名空间) ,它们解析为相同的结果:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<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 中定义了 email
的属性. 这种定义告知 Spring 这是一个属性声明. 如前面所描述的,p
命名空间并没有标准的定义模式,所以开发者可以将属性的名称配置为依赖名称.
下一个示例包括另外两个 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"
来作为指向 bean 的引用. 在这个例子中 spouse
是属性的名字,而 -ref
部分表名这个依赖不是直接的类型,而是引用另一个 bean.
p 命名空间并不如标准XML格式灵活. 例如,声明属性的引用可能和一些以 Ref 结尾的属性相冲突,而标准的 XML 格式就不会. Spring 团队推荐开发者能够和团队商量一下,协商使用哪一种方式,而不要同时使用三种方法.
|
使用 c 命名空间简化 XML
与 使用 p 命名空间简化 XML 配置 p
命名空间类似,c 命名空间是在 Spring 3.1 首次引入的,c 命名空间允许使用内联的属性来配置构造参数而不必使用 constructor-arg
.
以下示例使用 c:
命名空间的例子来执行与 基于构造函数的注入 基于 Constructor 的依赖注入相同的操作:
<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:
命名空间相类似的方式(使用了 -ref
来配置引用).而且,同样的,c 命名空间也是定义在 Spring Core 中的(不是 XSD 模式).
在少数的例子之中,构造函数的参数名字并不可用(通常,如果字节码没有 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> 元素,但并不常用,因为声明的普通顺序在那里就足够了.
|
实际上,机制 在匹配参数方面非常有效,因此除非您确实需要,否则我们建议在整个配置中使用名称表示法.
组合属性名
开发者可以配置混合的属性,只需所有的组件路径(除了最后一个属性名字) 不能为 null
即可. 参考如下定义:
<bean id="something" class="things.ThingOne">
<property name="fred.bob.sammy" value="123" />
</bean>
something
有 fred
属性, 而其中 fred
属性有 bob
属性,而 bob
属性之中有 sammy
属性,那么最后这个 sammy
属性会配置为 123
. 想要上述的配置能够生效,fred
属性需要有 bob
属性而且在 fred
构造之后不为 null
即可.
1.4.3. 使用 depends-on
如果一个 bean 是另一个 bean 的依赖,通常这个 bean 也就是另一个 bean 的属性之一. 多数情况下,开发者可以在配置 XML 元数据的时候使用 <ref/>
元素 然而,有时 bean 之间的依赖不是直接关联的. 例如: 需要调用类的静态实例化器来触发依赖,类似数据库驱动注册. depends-on
属性可以显式强制初始化一个或多个 bean. 以下示例使用 depends-on
属性表示对单个bean的依赖:
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
如果想要依赖多个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 属性既可以指定初始化时间依赖性,也可以仅在 singleton bean 的情况下指定相应的销毁时间依赖性. 独立定义了 depends-on 属性的 bean 会优先销毁 (相对于 depends-on 的 bean 销毁,这样 depends-on 可以控制销毁的顺序.
|
1.4.4. 懒加载 Bean
默认情况下, ApplicationContext
会在实例化的过程中创建和配置所有的单例singleton 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"/>
当将 bean 配置为上述 XML 的时候, ApplicationContext
之中的 lazy
bean 是不会随着 ApplicationContext
的启动而进入到预初始化状态的. 只有那些 not.lazy
加载的 bean 是处于预初始化的状态的.
然而,如果延迟加载的类是作为单例非延迟加载的 bean 的依赖而存在的话,ApplicationContext
仍然会在 ApplicationContext
启动的时候加载. 因为作为单例 bean 的依赖,会随着单例 bean 的实例化而实例化.
您还可以使用 <beans/>
元素上的 default-lazy-init
属性在容器级别控制延迟初始化,如下:
<beans default-lazy-init="true">
<!-- no beans will be pre-instantiated... -->
</beans>
1.4.5. 自动装配
Spring 容器可以根据 bean 之间的依赖自动装配,开发者可以让 Spring 通过 ApplicationContext
来自动解析这些关联,自动装载有很多优点:
-
自动装载能够明显的减少指定的属性或者是构造参数. (在 本章其他地方讨论 的其他机制,如 bean 模板,在这方面也很有价值. )
-
自动装载可以扩展开发者的对象,比如说,如果开发者需要加一个依赖,只需关心如何更改配置即可自动满足依赖关联. 这样,自动装载在开发过程中是极其高效的,无需明确选择装载的依赖会使系统更加稳定
使用基于 XML 的配置元数据(see 依赖注入), 可以使用 <bean/>
元素的 autowire
属性 为 bean 定义指定 autowire 模式. 自动装配功能有四种方式. 开发者可以指定每个 bean 的装配方式,这样 bean 就知道如何加载自己的依赖. 下表描述了四种自动装配模式:
Mode | Explanation |
---|---|
|
(默认) 不自动装配. Bean 引用必须由 |
|
按属性名称自动装配. Spring 查找与需要自动装配的属性同名的 bean. 例如,如果 bean 配置为根据名字装配,他包含 的属性名字为 |
|
如果需要自动装配的属性的类型在容器中只存在一个的话,他允许自动装配. 如果存在多个,则抛出致命异常,这表示您不能对该 bean 使用 |
|
类似于 |
通过 byType
或者 constructor
的自动装配方式,开发者可以装载数组和强类型集合. 在这样的例子中,所有容器中的匹配了指定类型的 bean 都会自动装配到 bean 上来完成依赖注入. 开发者可以自动装配 key 为 String
强类型的 Map
. 自动装配的 Map
值会包含所有的 bean 实例值来匹配指定的类型,Map
的 key
会包含关联的 bean 的名字.
自动装配的局限和缺点
自动装配在项目中一致使用时效果最佳. 如果一般不使用自动装配,那么开发人员使用它来装配一个或两个 bean 定义可能会让人感到困惑.
-
property
和constructor-arg
设置中的显式依赖始终覆盖自动装配. 开发者不能自动装配一些简单属性,您不能自动装配简单属性,例如基本类型 ,Strings
, 和Classes
(以及此类简单属性的数组) . 这种限制是按设计的. -
自动装配比显式的配置更容易歧义,尽管上表表明了不同自动配置的特点,Spring 也会尽可能避免不必要的装配错误. 但是 Spring 管理的对象关系仍然不如显式配置那样明确.
-
从 Spring 容器生成文档的工具可能无法有效的提供装配信息.
-
容器中的多个 bean 定义可能与 setter 方法或构造函数参数所指定的类型相匹配, 这有利于自动装配. 对于 arrays, collections, 或者 Map 实例来说这不是问题. 但是如果是对只有一个依赖的值是有歧义的话,那么这个项是无法解析的. 如果没有唯一的 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 的名字来限制自动装配的候选者. 最上层的 <beans/>
元素会在 default-autowire-candidates
属性中来配置多种模式. 例如,限制自动装配候选者的名字以 Repository
结尾,可以配置成 *Repository
.
如果需要配置多种模式,只需要用逗号分隔开即可. bean定义的 autowire-candidate
属性的显式值 true
或 false
始终优先. 对于此类 bean,模式匹配规则不适用.
上面的这些技术在配置那些无需自动装配的 bean 是相当有效的,当然这并不是说这类 bean 本身无法自动装配其他的 bean. 而是说这些 bean 不再作为自动装配的依赖候选者.
1.4.6. 查找方法注入
在大多数的应用场景下,多数的 bean 都是singletons的. 当这个单例的 bean 需要和另一个单例的或者非单例的 bean 协作使用的时候,开发者只需要配置依赖 bean 为这个 bean 的属性即可. 但是有时会因为 bean 具有不同的生命周期而产生问题. 假设单例的 bean A 在每个方法调用中使用了非单例的 bean B. 容器只会创建 bean A 一次,而只有一个机会来配置属性. 那么容器就无法为每一次创建 bean A 时都提供新的 bean B 实例.
一种解决方案就是放弃 IoC,开发者可以通过实现 ApplicationContextAware
接口 让bean A对 ApplicationContextAware
可见 . 从容器中调用 getBean("B")
调用来使 bean A 知道该容器,以便每次 bean A 需要它时都请求一个(通常是新的) bean B 实例. 参考下面例子.
// Spring-API imports
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-API imports
class CommandManager : ApplicationContextAware {
private lateinit var applicationContext: ApplicationContext
fun process(commandState: Map<*, *>): Any {
// grab a new instance of the appropriate Command
val command = createCommand()
// set the state on the (hopefully brand new) Command instance
command.state = commandState
return command.execute()
}
// notice the Spring API dependency!
protected fun createCommand() =
applicationContext.getBean("command", Command::class.java)
override fun setApplicationContext(applicationContext: ApplicationContext) {
this.applicationContext = applicationContext
}
}
上面的代码并不让人十分满意,因为业务的代码已经与 Spring 框架耦合在一起. 方法注入是 Spring IoC 容器的一个高级功能,可以让您处理这种问题. Spring 提供了一个稍微高级的注入方式来处理这种问题
查找方法注入
查找方法注入是容器覆盖管理 bean 上的方法的能力,以便返回容器中另一个命名 bean 的查找结果. 查找方法通常涉及原型 bean,如前面描述的场景. Spring 框架通过使用 CGLIB 库生成的字节码来生成动态子类重写的方法实现此注入.
|
对于前面代码片段中的 CommandManager
类,Spring 容器动态地覆盖 createCommand()
方法的实现. CommandManager
类不再拥有任何的 Spring 依赖,如下:
// 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();
}
// no more Spring imports!
abstract class CommandManager {
fun process(commandState: Any): Any {
// grab a new instance of the appropriate Command interface
val command = createCommand()
// set the state on the (hopefully brand new) Command instance
command.state = commandState
return command.execute()
}
// okay... but where is the implementation of this method?
protected abstract fun createCommand(): Command
}
在包含需要注入方法的客户端类中 (在本例中为 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为singletonbean. 如果所需的 bean 是单例的,那么每次都会返回相同的 myCommand
bean 实例.
另外,如果是基于注解的配置模式,你可以在查找方法上定义 @Lookup
注解,如下:
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
}
abstract class CommandManager {
fun process(commandState: Any): Any {
val command = createCommand()
command.state = commandState
return command.execute()
}
@Lookup("myCommand")
protected abstract fun createCommand(): Command
}
或者,更常见的是,开发者也可以根据查找方法的返回类型来查找匹配的 bean,如下
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup
protected abstract Command createCommand();
}
abstract class CommandManager {
fun process(commandState: Any): Any {
val command = createCommand()
command.state = commandState
return command.execute()
}
@Lookup
protected abstract fun createCommand(): Command
}
注意开发者可以通过创建子类实现 lookup 方法,以便使它们与 Spring 的组件扫描规则兼容,同时抽象类会在默认情况下被忽略. 这种限制不适用于显式注册 bean 或明确导入 bean 的情况.
另一种可以访问不同生命周期的方法是 您可能还会发现 |
替换任意方法
从前面的描述中,我们知道查找方法是有能力来覆盖任何由容器管理的 bean 方法的. 开发者最好跳过这一部分,除非一定需要用到这个功能.
通过基于 XML 的元数据配置,开发者可以使用 replaced-method
元素来替换已存在方法的实现. 考虑以下类,它有一个我们想要覆盖的名为 computeValue
的方法:
public class MyValueCalculator {
public String computeValue(String input) {
// some real code...
}
// some other methods...
}
class MyValueCalculator {
fun computeValue(input: String): String {
// some real code...
}
// some other methods...
}
实现 org.springframework.beans.factory.support.MethodReplacer
接口的类提供了新的方法定义,如以下示例所示:
/**
* meant to be used to override the existing computeValue(String)
* implementation in MyValueCalculator
*/
public class ReplacementComputeValue implements MethodReplacer {
public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
// get the input value, work with it, and return a computed result
String input = (String) args[0];
...
return ...;
}
}
/**
* meant to be used to override the existing computeValue(String)
* implementation in MyValueCalculator
*/
class ReplacementComputeValue : MethodReplacer {
override fun reimplement(obj: Any, method: Method, args: Array<out Any>): Any {
// get the input value, work with it, and return a computed result
val input = args[0] as String;
...
return ...;
}
}
如果需要覆盖 bean 方法的 XML 配置如下类似于以下示例:
<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 的作用域
创建 bean 定义时,同时也会定义该如何创建 Bean 实例. 这些具体创建的过程是很重要的,因为它意味着像创建类一样,您可以通过简单的定义来创建许多 bean 的实例.
您不仅可以将不同的依赖注入到 bean 中,还可以配置 bean 的作用域. 这种方法是非常强大而且也非常灵活,开发者可以通过配置来指定对象的作用域,无需在 Java 类的层次上配置.
bean 可以配置多种作用域,Spring 框架支持六种作用域,有四种作用域是当开发者使用基于 Web 的 ApplicationContext
的时候才有效的. 您还可以创建自定义作用域.
下表描述了支持的作用域:
Scope | Description |
---|---|
(默认) 每一 Spring IOC 容器都拥有唯一的实例对象. |
|
一个 Bean 定义可以创建任意多个实例对象. |
|
将单个 bean 作用域限定为单个 HTTP 请求的生命周期. 也就是说,每个 HTTP 请求都有自己的 bean 实例,它是在单个 bean 定义的后面创建的. 只有基于 Web 的 Spring |
|
将单个 bean 作用域限定为HTTP |
|
将单个 bean 作用域限定为 |
|
将单个 bean 作用域限定为 |
从 Spring 3.0 开始,线程作用域默认是可用的,但默认情况下未注册. 有关更多信息,请参阅 SimpleThreadScope 的文档. 有关如何注册此作用域或任何其他自定义作用域的说明,请参阅使用使用自定义作用域.
|
1.5.1. Singleton 作用域
单例 bean 在全局只有一个共享的实例,所有依赖单例 bean 的场景中,容器返回的都是同一个实例.
换句话说,当您定义一个 bean 并且它的作用域是一个单例时,Spring IoC 容器只会根据 bean 的定义来创建该 bean 的唯一实例. 这些唯一的实例会缓存到容器中,后续针对单例 bean 的请求和引用,都会从这个缓存中拿到这个唯一实例. 下图显示了单例作用域的工作原理:

Spring 的单例 bean 概念不同于设计模式(GoF) 之中所定义的单例模式. 设计模式中的单例模式是将一个对象的作用域硬编码的,一个 ClassLoader 只能有唯一的一个实例. 而 Spring 的单例作用域是以容器为前提的,每个容器每个 bean 只能有一个实例. 这意味着,如果在单个 Spring 容器中为特定类定义一个 bean,则 Spring 容器会根据 bean 定义创建唯一的 bean 实例. 单例作用域是 Spring 的默认作用域. 下面的例子是在 XML 中配置单例模式 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 作用域
非单例的、原型 bean 指的是每次请求 bean 实例时,返回的都是新的对象实例. 也就是说,每次注入到另外的 bean 或者通过调用 getBean()
方法来获得的 bean 都是全新的实例. 基于线程安全性的考虑,当 bean 对象有状态时使用原型作用域,而无状态时则使用单例作用域.
下图显示了原型作用域的工作原理:

(数据访问对象(DAO) 通常不配置为原型,因为典型的 DAO 不具有任何会话状态. 我们可以更容易重用单例图的核心. )
用下面的例子来说明 Spring 的原型作用域:
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
与其他作用域相比,Spring 不会完整地管理原型 bean 的生命周期. Spring 容器只会初始化、配置和装载这些 bean,然后传递给 Client. 但是之后就不会再有该原型实例的进一步记录. 也就是说,初始化生命周期回调方法在所有作用域的 bean 是都会调用的,但是销毁生命周期回调方法在原型 bean 是不会调用的. 所以,客户端代码必须注意清理原型 bean 以及释放原型 bean 所持有的资源. 可以通过使用自定义的bean post-processor(Bean的后置处理器) 来让 Spring 释放掉原型 bean 所持有的资源.
在某些方面,Spring 容器关于原型作用域的 bean 就是取代了 Java 的 new
操作符. 所有的生命周期的控制都由客户端来处理(有关 Spring 容器中 bean 的生命周期的详细信息,请参阅生命周期回调) .
1.5.3. 单例 bean 依赖原型 bean
当单例 bean 依赖原型 bean 时,请注意在实例化时解析依赖. 因此,如果将原型 bean 注入到单例的 bean 中,则会实例化一个新的原型 bean,然后将依赖注入到单例 bean 中. 这个依赖的原型 bean 仍然是同一个实例.
但是,假设您希望单例 bean 在运行时重复获取原型 bean 的新实例. 您不能将原型 bean 依赖注入到您的单例 bean 中, 因为当 Spring 容器实例化单例 bean 并解析注入其依赖时,该注入只发生一次. 如果您需要在运行时多次使用原型 bean 的新实例,请参阅查找方法注入.
1.5.4. Request, Session, Application, 和 WebSocket 作用域
request
, session
, application
, 和 websocket
作用域只有在 Web 中使用 Spring 的 ApplicationContext
的实现,(例如 ClassPathXmlApplicationContext
) 的情况下才用得上.
如果在普通的 Spring IoC 容器,例如 ClassPathXmlApplicationContext
中使用这些作用域,将会抛出 IllegalStateException
异常来说明使用了未知的作用域.
初始化 Web Configuration
为了能够使用 request
, session
, application
, 和
websocket
作用域 (Web 作用域的 bean) , 需要在配置 bean 之前作一些基础配置. 而对于标准的作用域,例如单例和原型作用域,这种基础配置是不需要的.
如何完成此初始设置取决于您的特定 Servlet 环境.
例如,如果开发者使用了 Spring Web MVC 框架,那么每一个请求都会通过 Spring 的 DispatcherServlet
来处理,那么也无需特殊的设置了. DispatcherServlet
和 DispatcherPortlet
已经包含了相应状态.
如果您使用 Servlet 2.5 Web 容器,并且在 Spring 的 DispatcherServlet
之外处理请求(例如,使用 JSF 或 Struts时) ,
则需要注册 org.springframework.web.context.request.RequestContextListener
或者 ServletRequestListener
.
对于 Servlet 3.0+,可以使用 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
所做的工作实际上是一样的,都是将 request 对象请求绑定到服务的 Thread
上. 这才使 bean 在之后的调用链上对请求和会话作用域可见.
Request 作用域
参考下面这个 XML 配置的 bean 定义:
<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
Spring 容器会在每次使用 LoginAction
来处理每个 HTTP 请求时都会创建新的 loginAction
实例. 也就是说,loginAction
bean 的作用域是 HTTP Request 级别的.
开发者可以随意改变实例的状态,因为其他通过 loginAction
请求来创建的实例根本看不到开发者改变的实例状态,所有创建的 Bean 实例都是根据独立的请求创建的. 当请求处理完毕,这个 bean 也将会销毁.
当使用注解配置或 Java 配置时,使用 @RequestScope
注解修饰的 bean 会被设置成 request
作用域. 以下示例显示了如何执行此操作:
@RequestScope
@Component
public class LoginAction {
// ...
}
@RequestScope
@Component
class LoginAction {
// ...
}
Session 作用域
参考下面 XML 配置的 bean 的定义:
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
Spring 容器通过在单个 HTTP 会话的生命周期中使用 UserPreferences
bean 定义来创建 UserPreferences
bean 的新实例. 换言之,UserPreferences
Bean 的作用域是 HTTP Session 级别的,在 request-scoped 作用域的 bean 上,
开发者可以随意的更改实例的状态,同样,其他 HTTP Session 的基本实例在每个 Session 中都会请求 userPreferences
来创建新的实例,所以,开发者更改 bean 的状态, 对于其他的 Bean 仍然是不可见的. 当 HTTP Session 被销毁时,根据这个 Session 来创建的 bean 也将会被销毁.
使用注解配置和 Java 配置时,使用 @SessionScope
注解修饰的 bean 会被设置成 session
作用域.
@SessionScope
@Component
public class UserPreferences {
// ...
}
@SessionScope
@Component
class UserPreferences {
// ...
}
Application 作用域
参考下面用 XML 配置的 bean 的定义:
<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
Spring 容器会在整个 Web 应用内使用到 appPreferences
的时候创建一个新的 AppPreferences
的实例. 也就是说,appPreferences
bean是在 ServletContext
级别的, 就像普通的 ServletContext
属性一样.
这种作用域和 Spring 的单例作用域有一些相似的地方,但是也有两个重要的不同之处,它对于每一个 ServletContext
来说是单例的,但是对 Spring ApplicationContext
来说不是的(某些 web 应用可能包含多个 ApplicationContext
) .
实际上它是被暴露在外的,并且作为 ServletContext
属性对外可见.
当使用注解配置或 Java 配置时,使用 @ApplicationScope
注解修饰的 bean 会被设置成 application
作用域 . 以下示例显示了如何执行此操作:
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
@ApplicationScope
@Component
class AppPreferences {
// ...
}
WebSocket 作用域
WebSocket 作用域与 WebSocket 会话的生命周期相关联,适用于 STOMP over WebSocket 应用程序,请参阅 WebSocket 作用域了解更多详情。
依赖有 Scope 的 Bean
Spring IoC 容器不仅仅管理对象(bean) 的实例化,同时也负责装配依赖. 如果开发者要将一个 bean 装配到比它作用域更广的 bean 时(例如 HTTP 请求返回的 bean) ,那么开发者应当选择注入 AOP 代理而不是使用带作用域的 bean. 也就是说,开发者需要注入代理对象,而这个代理对象既可以找到实际的 bean,还能够创建全新的 bean.
您还可以在作为单例的作用域的 bean 之间使用 当针对原型作用域的 bean 声明 此外,作用域代理并不是取得作用域 bean 的唯一安全方式. 开发者也可以通过简单的声明注入(即构造函数或 setter 参数或自动装配字段) 作为扩展变体,您可以声明 JSR-330 将这样的方法称为 Provider,它使用 |
以下示例中的配置只有一行,但了解 “why” 以及它背后的 “how” 非常重要:
<?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/> (1)
</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>
1 | 定义代理的行. |
要创建这样的一个代理,只需要在带作用域的 bean 定义中添加子节点 <aop:scoped-proxy/>
即可(具体查看选择要创建的代理类型 和
基于 XML Schema 的配置) .
为什么在 request
, session
和自定义作用域级别的bean定义需要 <aop:scoped-proxy/>
, 考虑以下单例 bean 定义,并与这些特殊的作用域定义的内容进行相比(请注意,以下 userPreferencesbean
定义不完整) :
<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
<bean id="userManager" class="com.something.UserManager">
<property name="userPreferences" ref="userPreferences"/>
</bean>
在上面的例子中,单例 bean(userManager
) 注入了 HTTP Session 级别的 userPreferences
依赖. 显然, 问题就是 userPreferences
在 Spring 容器中只会实例化一次.
它的依赖(在这种情况下只有一个,userPreferences
) 也只注入一次. 这意味着 userManager
每次使用的是完全相同的 userPreferences
对象(即最初注入它的对象) 进行操作.
这不是将短周期作用域 bean 注入到长周期作用域bean时所需的行为,例如将 HTTP Session 级别的作用域 bean 作为依赖注入到单例 bean 中. 相反,开发者需要一个 userManager
对象, 而在 HTTP Session 的生命周期中,
开发者需要一个特定于 HTTP Session 的 userPreferences
对象. 因此,容器创建一个对象,该对象暴露与 UserPreferences
类(理想情况下为 UserPreferences
实例的对象) 完全相同的公共接口,
该对象可以从作用域机制( HTTP Request、Session 等) 中获取真实的 UserPreferences
对象. 容器将这个代理对象注入到 userManager
中, 而不知道这个 UserPreferences
引用是一个代理.
在这个例子中,当一个 UserManager
实例在依赖注入的 UserPreferences
对象上调用一个方法时, 它实际上是在调用代理的方法,再由代理从 HTTP Session (本例) 获取真实的 UserPreferences
对象,并将方法调用委托给检索到的实际 UserPreferences
对象.
因此,在将 request-
and session-scoped
的bean来作为依赖时,您需要以下(正确和完整) 配置,如以下示例所示: 所以当开发者希望能够正确的使用配置请求、会话或者全局会话级别的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 代理只拦截 |
或者,您可以通过为 <aop:scoped-proxy/>
元素的 proxy-target-class
属性的值指定 false
来配置 Spring 容器, 以便为此类作用域 bean 创建基于 JDK 接口的标准代理.
使用基于接口的 JDK 代理意味着开发者无需引入第三方库即可完成代理. 但是,这也意味着带作用域的 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. 自定义 Scopes
bean 的作用域机制是可扩展的,开发者可以自定义作用域,甚至重新定义已经存在的作用域,但是 Spring 团队不推荐这样做,而且开发者也不能重写 singleton
和 prototype
作用域.
创建自定义作用域
为了能够使 Spring 可以管理开发者定义的作用域,开发者需要实现 org.springframework.beans.factory.config.Scope·
. 如何实现自定义的作用域, 可以参考 Spring 框架的一些实现或者有关 Scope
的javadoc
Scope 接口有四个方法用于操作对象,例如获取、移除或销毁等操作.
例如,传入 Session 作用域该方法将会返回一个 session-scoped 的 bean (如果它不存在,那么将会返回绑定 session 作用域的新实例) . 下面的方法返回相应作用域的对象:
Object get(String name, ObjectFactory<?> objectFactory)
fun get(name: String, objectFactory: ObjectFactory<*>): Any
下面的方法将从相应的作用域中移除对象. 同样,以会话为例,该函数会删除会话作用域的 Bean. 删除的对象会作为返回值返回,当无法找到对象时将返回 null. 以下方法从相应作用域中删除对象:
Object remove(String name)
fun remove(name: String): Any
以下方法注册作用域在销毁时或在 Scope 中的指定对象被销毁时应该执行的回调:
void registerDestructionCallback(String name, Runnable destructionCallback)
fun registerDestructionCallback(name: String, destructionCallback: Runnable)
有关销毁回调的更多信息,请参看 javadoc 或 Spring 的 Scope 实现部分.
下面的方法获取相应作用域的区分标识符:
String getConversationId()
fun getConversationId(): String
这个标识符在不同的作用域中是不同的. 例如对于会话作用域,这个标识符就是会话的标识符.
使用自定义作用域
在实现了自定义作用域后,开发者还需要让 Spring 容器能够识别发现所创建的新作用域. 下面的方法就是在 Spring 容器中用来注册新 Scope
的:
void registerScope(String scopeName, Scope scope);
fun registerScope(scopeName: String, scope: Scope)
这个方法是在 ConfigurableBeanFactory
的接口中声明的,可以用在多数的 ApplicationContext
实现,也可以通过 BeanFactory
属性来调用.
registerScope(..)
方法的第一个参数是相关作用域的唯一名称. 举例来说,Spring 容器中的单例和原型就以它本身来命名. 第二个参数就是开发者希望注册和使用的自定义 Scope
实现的实例对象
假定开发者实现了自定义 Scope
,然后可以按如下步骤来注册.
下一个示例使用 SimpleThreadScope ,这个例子在 Spring 中是有实现的,但没有默认注册. 您自定义的作用域也可以通过如下的方式来注册.
|
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
val threadScope = SimpleThreadScope()
beanFactory.registerScope("thread", threadScope)
然后,您可以创建符合自定义 Scope
的作用域规则的 bean 定义,如下所示:
<bean id="..." class="..." scope="thread">
在自定义作用域中,开发者也不限于仅仅通过编程的方式来注册作用域,还可以通过配置 CustomScopeConfigurer
类来实现. 如以下示例所示:
<?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>
在 FactoryBean 实现中添加了 <aop:scoped-proxy/> <bean> 元素时,它是工厂 bean 本身的作用域,而不是从 getObject() 方法返回的对象.
|
1.6. 自定义 Bean 的特性
Spring Framework 提供了许多可用于自定义 bean 特性的接口. 本节将它们分组如下:
1.6.1. 生命周期回调
你可以实现 InitializingBean
和 DisposableBean
接口,让容器里管理 Bean 的生命周期. 容器会在调用 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;
Spring 团队是不建议开发者使用 InitializingBean
接口,因为这样会将代码耦合到 Spring 的特殊接口上. 他们建议使用 @PostConstruct
注解或者指定一个 POJO 的实现方法, 这会比实现接口更好.
在基于 XML 的元数据配置上,开发者可以使用 init-method
属性来指定一个没有参数的方法,使用 Java 配置的开发者可以在 @Bean
上添加 initMethod
属性. 请参阅接收生命周期回调:
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
public class ExampleBean {
public void init() {
// do some initialization work
}
}
class ExampleBean {
fun init() {
// do some initialization work
}
}
前面的示例与以下示例(由两个列表组成) 具有几乎完全相同的效果:
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements InitializingBean {
@Override
public void afterPropertiesSet() {
// do some initialization work
}
}
class AnotherExampleBean : InitializingBean {
override fun afterPropertiesSet() {
// do some initialization work
}
}
但是,前面两个示例中的第一个没有将代码耦合到 Spring.
销毁方法的回调
实现 org.springframework.beans.factory.DisposableBean
接口的 Bean 就能让容器通过回调来销毁 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"/>
public class ExampleBean {
public void cleanup() {
// do some destruction work (like releasing pooled connections)
}
}
class ExampleBean {
fun cleanup() {
// do some destruction work (like releasing pooled connections)
}
}
前面的定义与以下定义几乎完全相同:
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements DisposableBean {
@Override
public void destroy() {
// do some destruction work (like releasing pooled connections)
}
}
class AnotherExampleBean : DisposableBean {
override fun destroy() {
// do some destruction work (like releasing pooled connections)
}
}
但是,前面两个定义中的第一个没有将代码耦合到 Spring. .
您可以为 <bean> 元素的 destroy-method 属性分配一个特殊的(推断的) 值,该值指示 Spring 自动检测特定 bean 类的 close 或者 shutdown 方法. (因此,任何实现 java.lang.AutoCloseable 或 java.io.Closeable 的类都将匹配. )
您还可以在 <bean> 元素的 default-destroy-method 属性上设置此特殊(推断) 值,用于让所有的 bean 都实现这个行为(参见默认初始化和销毁方法) .
请注意,这是 Java 配置的默认行为.
|
默认初始化和销毁方法
当您不使用 Spring 特有的 InitializingBean
和 DisposableBean
回调接口来实现初始化和销毁方法时,您定义方法的名称最好类似于 init()
, initialize()
, dispose()
.
这样可以在项目中标准化类方法,并让所有开发者都使用一样的名字来确保一致性.
您可以配置 Spring 容器来针对每一个 Bean 都查找这种名字的初始化和销毁回调方法. 也就是说, 任意的开发者都会在应用的类中使用一个叫 init()
的初始化回调. 而不需要在每个 bean 中都定义 init-method="init"
这种属性,
Spring IoC 容器会在 bean 创建的时候调用那个回调方法(如前面描述的标准生命周期一样) . 这个特性也将强制开发者为其他的初始化以及销毁回调方法使用同样的名字.
假设您的初始化回调方法名为 init()
,而您的 destroy
回调方法名为 destroy()
. 然后,您的类类似于以下示例中的类
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.");
}
}
}
class DefaultBlogService : BlogService {
private var blogDao: BlogDao? = null
// this is (unsurprisingly) the initialization callback method
fun init() {
if (blogDao == null) {
throw 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
属性,以类似方式(在 XML 中) 配置destroy方法回调.
当某些 bean 已有的回调方法与配置的默认回调方法不相同时,开发者可以通过特指的方式来覆盖掉默认的回调方法. 以 XML 为例,可以通过使用元素的 init-method
和 destroy-method
属性来覆盖掉 <bean/>
中的配置.
Spring 容器会做出如下保证,bean 会在装载了所有的依赖以后,立刻就开始执行初始化回调. 这样的话,初始化回调只会在直接的 bean 引用装载好后调用, 而此时 AOP 拦截器还没有应用到 bean 上. 首先目标的 bean 会先完全初始化, 然后 AOP 代理和拦截链才能应用. 如果目标 bean 和代理是分开定义的,那么开发者的代码甚至可以跳过 AOP 而直接和引用的 bean 交互. 因此,在初始化方法中应用拦截器会前后矛盾,因为这样做耦合了目标 bean 的生命周期和代理/拦截器,还会因为与 bean 产生了直接交互进而引发不可思议的现象.
组合生命周期策略
从 Spring 2.5 开始,您有三种选择用于控制 bean 生命周期行为:
-
InitializingBean
和DisposableBean
回调接口 -
自定义
init()
和destroy()
方法 -
@PostConstruct
和@PreDestroy
注解. 你也可以在 bean 上同时使用这些机制.
如果 bean 配置了多个生命周期机制,而且每个机制都配置了不同的方法名字时,每个配置的方法会按照以下描述的顺序来执行. 但是,如果配置了相同的名字, 例如初始化回调为 init() ,在不止一个生命周期机制配置为这个方法的情况下,这个方法只会执行一次. 如 上一节中所述.
|
为同一个 bean 配置的多个生命周期机制具有不同的初始化方法,如下所示:
-
包含
@PostConstruct
注解的方法 -
在
InitializingBean
接口中的afterPropertiesSet()
方法 -
自定义的
init()
方法
Destroy 方法以相同的顺序调用:
-
包含
@PreDestroy
注解的方法 -
在
DisposableBean
接口中的destroy()
方法 -
自定义的
destroy()
方法
开始和关闭回调
Lifecycle
接口中为所有具有自定义生命周期需求的对象定义了一些基本方法(例如启动或停止一些后台进程) :
public interface Lifecycle {
void start();
void stop();
boolean isRunning();
}
任何 Spring 管理的对象都可以实现 Lifecycle
接口. 然后,当 ApplicationContext
接收到启动和停止信号时(例如,对于运行时的停止/重启场景) ,
ApplicationContext
会通知到所有上下文中包含的生命周期对象. 它通过委托 LifecycleProcessor
完成此操作,如下面的清单所示:
public interface LifecycleProcessor extends Lifecycle {
void onRefresh();
void onClose();
}
请注意,LifecycleProcessor
是 Lifecycle
接口的扩展. 它还添加了另外两种方法来响应刷新和关闭的上下文.
注意,常规的 同时,停止通知并不能保证在销毁之前出现. 在正常的关闭情况下,所有的 |
启动和关闭调用的顺序非常重要. 如果任何两个对象之间存在 “depends-on” 关系,则依赖方在其依赖之后开始,并且在其依赖之前停止. 但是,有时,直接依赖性是未知的.
您可能只知道某种类型的对象应该在另一种类型的对象之前开始.
在这些情况下, SmartLifecycle
接口定义了另一个选项,即在其超级接口 Phased
上定义的 getPhase()
方法. 以下清单显示了 Phased
接口的定义
public interface Phased {
int getPhase();
}
以下清单显示了 SmartLifecycle
接口的定义:
public interface SmartLifecycle extends Lifecycle, Phased {
boolean isAutoStartup();
void stop(Runnable callback);
}
当启动时,拥有最低 phased
的对象会优先启动,而当关闭时,会相反的顺序执行. 因此,如果一个对象实现了 SmartLifecycle
,然后令其 getPhase()
方法返回 Integer.MIN_VALUE
值的话,
就会让该对象最早启动,而最晚销毁. 显然,如果 getPhase()
方法返回了 Integer.MAX_VALUE
值则表明该对象会最晚启动,而最早销毁.
当考虑到使用 phased
值时,也同时需要了解正常没有实现 SmartLifecycle
的 Lifecycle
对象的默认值,这个值是 0
. 因此,配置任意的负值都将表明将对象会在标准组件启动之前启动 ,
而在标准组件销毁以后再进行销毁.
SmartLifecycle
接口也定义了一个名为 stop
的回调方法,任何实现了 SmartLifecycle
接口的方法都必须在关闭流程完成之后调用回调中的 run()
方法.
这样做可以进行异步关闭,而 lifecycleProcessor
的默认实现 DefaultLifecycleProcessor
会等到配置的超时时间之后再调用回调.
默认的每一阶段的超时时间为 30
秒. 您可以通过在上下文中定义名为 lifecycleProcessor
的 bean 来覆盖默认生命周期处理器实例. 如果您只想修改超时,则定义以下内容就足够了:
<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
<!-- timeout value in milliseconds -->
<property name="timeoutPerShutdownPhase" value="10000"/>
</bean>
如前所述,LifecycleProcessor
接口还定义了用于刷新和关闭上下文的回调方法. 在关闭过程中,如果 stop()
方法已经被调用,则就会执行关闭流程. 但是如果上下文正在关闭中则不会在进行此流程, 而刷新的回调会使用到 SmartLifecycle
的另一个特性.
当上下文刷新完毕(所有的对象已经实例化并初始化) 后, 就会调用刷新回调,默认的生命周期处理器会检查每一个 SmartLifecycle
对象的 isAutoStartup()
方法返回的 Boolean
值.如果为真, 对象将会自动启动而不是等待明确的上下文调用,
或者调用自己的 start()
方法(不同于上下文刷新,标准的上下文实现是不会自动启动的) . phase
的值以及 "depends-on" 关系会决定对象启动和销毁的顺序.
在非 Web 应用中优雅地关闭 Spring IoC 容器
本节仅适用于非 Web 应用程序. Spring 的基于 Web 的 |
如果开发者在非 Web 应用环境使用 Spring IoC 容器的话(例如,在桌面客户端的环境下) 开发者需要在 JVM 上注册一个关闭的钩子,来确保在关闭 Spring IoC 容器的时候能够调用相关的销毁方法来释放掉引用的资源. 当然,开发者也必须正确配置和实现那些销毁回调.
要注册关闭钩子,请调用 ConfigurableApplicationContext
接口上声明的 registerShutdownHook()
方法,如以下示例所示:
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...
}
}
fun main() {
val ctx = 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
了. 通过 ApplicationContext
接口,或者通过将引用转换成已知的接口的子类, 例如 ConfigurableApplicationContext
就能够提供一些额外的功能. 其中的一个用法就是可以通过编程的方式来获取其他的 bean.有时候这个能力非常有用. 当然,Spring 团队并不推荐这样做,
因为这样会使代码与 Spring 框架耦合,同时也没有遵循 IoC 的风格. ApplicationContext
中其它的方法可以提供一些诸如资源的访问、发布应用事件或者添加 MessageSource
之类的功能. ApplicationContext
的ApplicationContext
的附加功能中描述了这些附加功能.
自动装配是另一种获取 ApplicationContext
引用的替代方法. 传统的的构造函数 和 byType
的装载方式自动装配模式(如 自动装配中所述)
可以通过构造函数或 setter
方法的方式注入,开发者也可以通过注解注入的方式. 为了更为方便,包括可以注入的字段和多个参数方法,请使用新的基于注解的自动装配功能.
这样,ApplicationContext
将自动装配字段、构造函数参数或方法参数,如果相关的字段,构造函数或方法带有 @Autowired
注解,则该参数需要 ApplicationContext
类型. 有关更多信息,请参阅使用 @Autowired
.
当 ApplicationContext
创建实现了 org.springframework.beans.factory.BeanNameAware
接口的类,那么这个类就可以针对其名字进行配置. 以下清单显示了 BeanNameAware
接口的定义:
public interface BeanNameAware {
void setBeanName(String name) throws BeansException;
}
这个回调的调用在属性配置完成之后,但是在初始化回调之前. 例如 InitializingBean.afterPropertiesSet()
方法以及自定义的初始化方法等.
1.6.3. 其他 Aware
接口
除了 ApplicationContextAware
和 BeanNameAware
(前面已讨论过) 之外,Spring还提供了一系列 Aware
回调接口,让 bean 告诉容器,它们需要一些具体的基础配置信息. 一些重要的 Aware
接口如下表:
Name | Injected Dependency | Explained in… |
---|---|---|
|
声明 |
|
|
|
|
|
用于加载bean类的类加载器 |
|
|
声明 |
|
|
声明bean的名称. |
|
|
定义的 weaver 用于在加载时处理类定义. |
|
|
用于解析消息的已配置策略(支持参数化和国际化) |
|
|
Spring JMX 通知发布者 |
|
|
配置的资源加载器 |
|
|
当前 |
|
|
容器运行的当前 |
请再次注意,使用这些接口会将您的代码绑定到 Spring API,而不会遵循 IoC 原则. 因此,我们建议将它们用于需要以编程方式访问容器的基础架构 bean.
1.7. Bean 继承的定义
bean 定义可以包含许多配置信息,包括构造函数参数,属性值和特定于容器的信息,例如初始化方法,静态工厂方法名称等. 子 bean 定义从父定义继承配置数据. 子定义可以覆盖某些值或根据需要添加其他值. 使用父子 bean 定义可以节省很多配置输入. 实际上,这是一种模板形式.
如果开发者编程式地使用 ApplicationContext
接口,子 bean 定义可以通过 ChildBeanDefinition
类来表示. 很多开发者不会使用这个级别的方法,
而是会在类似于 ClassPathXmlApplicationContext
中声明式地配置 bean 定义. 当你使用基于 XML 的配置时,你可以在子 bean 中使用 parent 属性,该属性的值用来识别父 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"> (1)
<property name="name" value="override"/>
<!-- the age property value of 1 will be inherited from parent -->
</bean>
1 | 请注意 parent 属性. |
子 bean 如果没有指定 class,它将使用父 bean 定义的 class. 但也可以进行重写. 在后一种情况中,子 bean 必须与父bean兼容,也就是说,它必须接受父bean的属性值.
子 bean 定义从父类继承作用域、构造器参数、属性值和可重写的方法,除此之外,还可以增加新值. 开发者指定任何作用域、初始化方法、销毁方法和/或者静态工厂方法设置都会覆盖相应的父bean设置.
剩下的设置会取子 bean 定义: 依赖、自动注入模式、依赖检查、单例、延迟加载.
前面的示例通过使用 abstract
属性将父 bean 定义显式标记为 abstract
. 如果父定义未指定类,则需要将父 bean 定义显式标记为 abstract
,如以下示例所示:
<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()
方法也会忽略定义为 abstract
的bean.
ApplicationContext 默认会预先实例化所有的单例 bean. 因此,如果开发者打算把(父) bean 定义仅仅作为模板来使用,同时为它指定了 class 属性, 那么必须确保设置 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 后置处理器.
ApplicationContex
会自动地检测所有定义在配置元文件中,并实现了 BeanPostProcessor
接口的 bean. ApplicationContext
会注册这些 beans 为后置处理器, 使他们可以在 bean 创建完成之后被调用. bean 后置处理器可以像其他 bean 一样部署到容器中.
当在配置类上使用 @Bean
工厂方法声明 BeanPostProcessor
时,工厂方法返回的类型应该是实现类自身. ,或至少也是 org.springframework.beans.factory.config.BeanPostProcessor
接口, 要清楚地表明这个 bean
的后置处理器的本质特点.
否则,在它完全创建之前,ApplicationContext
将不能通过类型自动探测它. 由于 BeanPostProcessor
在早期就需要被实例化, 以适应上下文中其他 bean 的实例化,因此这个早期的类型检查是至关重要的.
BeanPostProcessor 实例的编程注册BeanPostProcessor 实例,虽然 BeanPostProcessor 注册的推荐方法是通过 ApplicationContext 自动检测(如前所述) ,但您可以以编程的方式使用 ConfigurableBeanFactory 的 addBeanPostProcessor 方法进行注册. 这对于在注册之前需要对条件逻辑进行评估,或者是在继承层次的上下文之间复制 bean 的后置处理器中是有很有用的.
但请注意,以编程方式添加的 BeanPostProcessor 实例不遵循 Ordered 接口. 这里,注册顺序决定了执行的顺序. 另请注意,以编程方式注册的 BeanPostProcessor 实例始终在通过自动检测注册的实例之前处理,而不管任何显式排序.
|
BeanPostProcessor 实例 和 AOP 自动代理实现 对于所有这样的 bean,您应该看到一条信息性日志消息: 如果你使用自动装配或 |
以下示例显示如何在 ApplicationContext
中编写,注册和使用 BeanPostProcessor
实例.
Example: Hello World, BeanPostProcessor
-style
第一个例子说明了基本用法. 该示例显示了一个自定义 BeanPostProcessor
实现,该实现在容器创建时调用每个 bean 的 toString()
方法,并将生成的字符串输出到系统控制台.
以下清单显示了自定义 BeanPostProcessor
实现类定义:
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;
}
}
class InstantiationTracingBeanPostProcessor : BeanPostProcessor {
// simply return the instantiated bean as-is
override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? {
return bean // we could potentially return any object reference here...
}
override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? {
println("Bean '$beanName' created : $bean")
return bean
}
}
以下 beans
元素使用 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 应用执行了前面代码和配置:
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);
}
}
fun main() {
val ctx = ClassPathXmlApplicationContext("scripting/beans.xml")
val messenger = ctx.getBean<Messenger>("messenger")
println(messenger)
}
上述应用程序的输出类似于以下内容:
Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961 org.springframework.scripting.groovy.GroovyMessenger@272961
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
中声明时自动执行,这样就可以对定义在容器中的元数据配置进行修改. Spring 包含许多预定义的 bean 工厂后处理器,
例如 PropertyOverrideConfigurer
和 PropertySourcesPlaceholderConfigurer
. 您还可以使用自定义 BeanFactoryPostProcessor
.
例如,注册自定义属性编辑器 .
ApplicationContext
自动检测部署到其中的任何实现 BeanFactoryPostProcessor
接口的 bean. 它在适当的时候使用这些 bean 作为 bean 工厂后置处理器. 你可以部署这些后置处理器为你想用的任意其它 bean.
注意,和 BeanPostProcessor 一样,通常不应该配置 BeanFactoryPostProcessor 来进行延迟初始化. 如果没有其它 bean 引用 Bean(Factory)PostProcessor , 那么后置处理器就不会被初始化.
因此,标记它为延迟初始化就会被忽略,,即便你在 <beans /> 元素声明中设置 default-lazy-init=true 属性,Bean(Factory)PostProcessor 也会提前初始化 bean.
|
Example: 类名替换 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>
该示例显示了从外部属性文件配置的属性. 在运行时,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',并且同样适用于与属性文件中的键匹配的其他占位符值. PropertySourcesPlaceholderConfigurer
检查 bean 定义的大多数属性和属性中的占位符. 此外,您可以自定义占位符前缀和后缀.
使用 Spring 2.5 中引入的 context
命名空间,您可以使用专用配置元素配置属性占位符. 您可以在 location
属性中以逗号分隔列表的形式提供一个或多个位置,如以下示例所示:
<context:property-placeholder location="classpath:com/something/jdbc.properties"/>
PropertySourcesPlaceholderConfigurer
不仅在您指定的属性文件中查找属性. 默认情况下,如果它在指定的属性文件中找不到属性,则会检查 Spring Environment
属性和常规 Java System
属性.
你可以使用
如果在运行时无法将类解析为有效类,则在即将创建 bean 时,bean 的解析将失败,这是 |
Example: The PropertyOverrideConfigurer
PropertyOverrideConfigurer
, 另外一种 bean 工厂后置处理器,类似于 PropertyPlaceholderConfigurer
,但与后者不同的是: 对于所有的 bean 属性,原始定义可以有默认值或也可能没有值. 如果一个 Properties
覆盖文件没有配置特定的 bean 属性,则就会使用默认的上下文定义
注意,bean 定义是不知道是否被覆盖的,所以从 XML 定义文件中不能马上看到那个配置正在被使用. 在拥有多个 PropertyOverrideConfigurer
实例的情况下,为相同 bean 的属性定义不同的值时,基于覆盖机制只会有最后一个生效.
属性文件配置行采用以下格式:
beanName.property=value
例如:
dataSource.driverClassName=com.mysql.jdbc.Driver dataSource.url=jdbc:mysql:mydb
这个示例文件可以和容器定义一起使用,该容器定义包含一个名为 dataSource
的 bean,该 bean 具有 driver
和 url
属性
复合属性名称也是被支持的,只要被重写的最后一个属性以外的路径中每个组件都已经是非空时(假设由构造方法初始化) . 在下面的示例中,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 容器实例化逻辑的可插拔点,如果你的初始化代码非常复杂,那么相对于(潜在地) 大量详细的 XML 而言,最好是使用 Java 语言来表达. 你可以创建自定义的 FactoryBean
,在该类中编写复杂的初始化代码. 然后将自定义的 FactoryBean
插入到容器中.
FactoryBean<T>
接口提供下面三个方法
-
T getObject()
: 返回这个工厂创建的对象实例. 这个实例可能是共享的,这取决于这个工厂返回的是单例还是原型实例. -
boolean isSingleton()
: 如果FactoryBean
返回单例,那么这个方法就返回true
,否则返回false
. 此方法的默认实现返回true
。 -
`Class<?> getObjectType()
: 返回由getObject()
方法返回的对象类型,如果事先不知道的类型则会返回null
.
Spring 框架大量地使用了 FactoryBean
的概念和接口,FactoryBean
接口的 50 多个实现都随着 Spring 一同提供.
当开发者需要向容器请求一个真实的 FactoryBean
实例(而不是它生产的bean) 时,调用 ApplicationContext
的 getBean()
方法时在 bean 的 id 之前需要添加连字符(&
) 所以对于一个给定 id
为 myBean
的 FactoryBean
,
调用容器的 getBean("myBean")
方法返回的是 FactoryBean
的代理,而调用 getBean("&myBean")
方法则返回 FactoryBean
实例本身
1.9. 基于注解的容器配置
XML 配置的替代方法是基于注解的配置,它依赖于字节码元数据来连接组件进而替代 XML 声明. 开发人员通过使用相关类、方法或字段上声明的注解来将配置移动到组件类本身. 而不是使用 XML bean
来配置.
如 Example: AutowiredAnnotationBeanPostProcessor
所述,AutowiredAnnotationBeanPostProcessor
,将 BeanPostProcessor
与注解混合使用是扩展 Spring IoC 容器的常用方法.
例如,Spring 2.0 引入了使用 @Required
注解强制属性必须在配置的时候被填充, Spring 2.5 使用同样的方式来驱动 Spring 的依赖注入.
本质上,@Autowired
注解提供的功能与 自动装配 中描述的相同,但具有更细粒度的控制和更广泛的适用性. Spring 2.5 还增加了对 JSR-250 注解的支持,例如 @PostConstruct
和 @PreDestroy
. Spring 3.0 增加了对 JSR-330 (Dependency
Injection for Java) 包含在 javax.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/>
元素隐式注册如下后置处理器:
|
1.9.1. @Required
@Required
注解适用于 bean 属性 setter 方法,如下例所示:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Required
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
class SimpleMovieLister {
@set:Required
lateinit var movieFinder: MovieFinder
// ...
}
被注解的 bean 属性必须在配置时通过 bean 定义中的显式赋值或自动注入值. 如果受影响的 bean 属性尚未指定值,容器将抛出异常; 这导致及时的、明确的失败,避免在运行后再抛出 NullPointerException
或类似的异常.
在这里,建议开发者将断言放入 bean 类本身,例如放入 init 方法. 这样做强制执行那些必需的引用和值,即使是在容器外使用这个类.
如果要启用对 |
|
1.9.2. @Autowired
JSR 330 的 |
开发者可以在构造器上使用 @Autowired
注解:
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;
@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
class MovieRecommender @Autowired constructor(
private val customerPreferenceDao: CustomerPreferenceDao)
从 Spring Framework 4.3 开始,如果目标 bean 仅定义一个构造函数,则不再需要 |
您还可以将 @Autowired
注解应用于 传统 setter 方法,如以下示例所示:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
class SimpleMovieLister {
@set:Autowired
lateinit var movieFinder: MovieFinder
// ...
}
您还可以将注解应用于具有任意名称和多个参数的方法,如以下示例所示:
public class MovieRecommender {
private MovieCatalog movieCatalog;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public void prepare(MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
class MovieRecommender {
private lateinit var movieCatalog: MovieCatalog
private lateinit var customerPreferenceDao: CustomerPreferenceDao
@Autowired
fun prepare(movieCatalog: MovieCatalog,
customerPreferenceDao: CustomerPreferenceDao) {
this.movieCatalog = movieCatalog
this.customerPreferenceDao = customerPreferenceDao
}
// ...
}
还可以将 @Autowired
应用于字段,甚至可以和构造函数混合使用:
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;
@Autowired
private MovieCatalog movieCatalog;
@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
class MovieRecommender @Autowired constructor(
private val customerPreferenceDao: CustomerPreferenceDao) {
@Autowired
private lateinit var movieCatalog: MovieCatalog
// ...
}
确保您的组件(例如, 对于通过类路径扫描找到的 XML 定义的 bean 或组件类,容器通常预先知道具体类型. 但是,对于 |
您还可以通过将 @Autowired
注解添加到需要该类型数组的字段或方法来指示 Spring 从 ApplicationContext
提供特定类型的所有 bean,如以下示例所示:
public class MovieRecommender {
@Autowired
private MovieCatalog[] movieCatalogs;
// ...
}
class MovieRecommender {
@Autowired
private lateinit var movieCatalogs: Array<MovieCatalog>
// ...
}
也可以应用于集合类型,如以下示例所示:
public class MovieRecommender {
private Set<MovieCatalog> movieCatalogs;
@Autowired
public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}
// ...
}
class MovieRecommender {
@Autowired
lateinit var movieCatalogs: Set<MovieCatalog>
// ...
}
如果想让数组元素或集合元素按特定顺序排列, 可以实现 您可以在类级别和 请注意,标准的 |
只要键类型是 String,Map
类型就可以自动注入. Map 值将包含所有类型的 bean,并且键将包含相应的 bean 名称. 如以下示例所示:
public class MovieRecommender {
private Map<String, MovieCatalog> movieCatalogs;
@Autowired
public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}
// ...
}
class MovieRecommender {
@Autowired
lateinit var movieCatalogs: Map<String, MovieCatalog>
// ...
}
默认情况下,当没有候选的 bean 可用时,自动注入将会失败; 对于声明的数组,集合或映射,至少应有一个匹配元素.
默认的处理方式是将带有注解的方法、构造函数和字段标明为必须依赖,也可以使用 @Autowired
中的 required=false
属性. 来标明这种依赖不是必须的,如下:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired(required = false)
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
class SimpleMovieLister {
@Autowired(required = false)
var movieFinder: MovieFinder? = null
// ...
}
如果不需要的方法(或在多个参数的情况下,其中一个依赖) 不可用,则根本不会调用该方法. 在这种情况下,完全不需要填充非必需字段,而将其默认值保留在适当的位置.
构造函数和工厂方法参数注入是一种特殊情况,因为由于 Spring 的构造函数解析算法可能会处理多个构造函数,因此 @Autowired
中的 required
属性的含义有所不同. 默认情况下,需要构造函数和工厂方法参数,但是在单构造函数场景中有一些特殊规则,
例如,如果没有可用的匹配 bean,则多元素注入点(数组,集合,映射) 解析为空实例. 这允许一种通用的实现模式,其中所有依赖都可以在唯一的多参数构造函数中声明-例如,声明为没有 @Autowired
注解的单个公共构造函数.
每个类仅可以有一个带 推荐使用 |
或者,您可以通过 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
)
public class SimpleMovieLister {
@Autowired
public void setMovieFinder(@Nullable MovieFinder movieFinder) {
...
}
}
class SimpleMovieLister {
@Autowired
var movieFinder: MovieFinder? = null
// ...
}
您也可以使用 @Autowired
作为常见的可解析依赖的接口,BeanFactory
, ApplicationContext
, Environment
, ResourceLoader
, ApplicationEventPublisher
, 和 MessageSource
这些接口及其扩展接口
(例如 ConfigurableApplicationContext
或 ResourcePatternResolver
) 会自动解析,无需特殊设置. 以下示例自动装配 ApplicationContext
对象:
public class MovieRecommender {
@Autowired
private ApplicationContext context;
public MovieRecommender() {
}
// ...
}
class MovieRecommender {
@Autowired
lateinit var context: ApplicationContext
// ...
}
|
1.9.3. @Primary
由于按类型的自动注入可能匹配到多个候选者,所以通常需要对选择过程添加更多的约束. 使用 Spring 的 @Primary
注解是实现这个约束的一种方法.
它表示如果存在多个候选者且另一个 bean 只需要一个特定类型的 bean 依赖时,就明确使用标记有 @Primary
注解的那个依赖. 如果候选中只有一个"Primary" bean,那么它就是自动注入的值
请考虑以下配置,将 firstMovieCatalog
定义为主要 MovieCatalog
:
@Configuration
public class MovieConfiguration {
@Bean
@Primary
public MovieCatalog firstMovieCatalog() { ... }
@Bean
public MovieCatalog secondMovieCatalog() { ... }
// ...
}
@Configuration
class MovieConfiguration {
@Bean
@Primary
fun firstMovieCatalog(): MovieCatalog { ... }
@Bean
fun secondMovieCatalog(): MovieCatalog { ... }
// ...
}
使用上述配置,以下 MovieRecommender
将与 firstMovieCatalog
一起自动装配:
public class MovieRecommender {
@Autowired
private MovieCatalog movieCatalog;
// ...
}
class MovieRecommender {
@Autowired
private lateinit var 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.4. @Qualifier
@Primary
是一种用于解决自动装配多个值的注入的有效的方法,当需要对选择过程做更多的约束时,可以使用 Spring 的 @Qualifier
注解,可以为指定的参数绑定限定的值. 缩小类型匹配集,以便为每个参数选择特定的 bean. 在最简单的情况下,这可以是一个简单的描述性值,如以下示例所示:
public class MovieRecommender {
@Autowired
@Qualifier("main")
private MovieCatalog movieCatalog;
// ...
}
class MovieRecommender {
@Autowired
@Qualifier("main")
private lateinit var movieCatalog: MovieCatalog
// ...
}
您还可以在各个构造函数参数或方法参数上指定 @Qualifier
注解,如以下示例所示:
public class MovieRecommender {
private MovieCatalog movieCatalog;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public void prepare(@Qualifier("main") MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
class MovieRecommender {
private lateinit var movieCatalog: MovieCatalog
private lateinit var customerPreferenceDao: CustomerPreferenceDao
@Autowired
fun 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"/> (1)
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<qualifier value="action"/> (2)
<!-- inject any dependencies required by this bean -->
</bean>
<bean id="movieRecommender" class="example.MovieRecommender"/>
</beans>
1 | 带有限定符 main 的 bean 会被装配到拥有相同值的构造方法参数上. |
2 | 带有限定符 action 的 bean 会被装配到拥有相同值的构造方法参数上. |
bean 的 name 会作为备用的 qualifier 值,因此可以定义 bean 的 id
为 main
替代内嵌的 qualifier 元素.这种匹配方式同样有效. 但是,虽然可以使用这个约定来按名称引用特定的 bean, 但是 @Autowired
默认是由带限定符的类型驱动注入的. 这就意味着 qualifier 值,甚至是 bean 的 name 作为备选项,只是为了缩小类型匹配的范围.
它们在语义上不表示对唯一 bean id
的引用. 良好的限定符值是像 main
或 EMEA
或 persistent
这样的,能表示与 bean id
无关的特定组件的特征,在匿名 bean 定义的情况下可以自动生成.
Qualifiers 也可以用于集合类型,如上所述,例如 Set<MovieCatalog>
. 在这种情况下,根据声明的限定符,所有匹配的 bean 都作为集合注入.
这意味着限定符不必是唯一的. 相反,它们构成过滤标准. 例如,您可以使用相同的限定符值 “action” 定义多个 MovieCatalog
bean,所有这些 bean 都注入到使用 @Qualifier("action")
注解的 Set<MovieCatalog>
中.
在类型匹配候选项中,根据目标 bean 名称选择限定符值,在注入点不需要 |
如果打算 by name
来驱动注解注入,那么就不要使用 @Autowired
(多数情况) ,即使在技术上能够通过 @Qualifier
值引用 bean 名字. 相反,应该使用 JSR-250 @Resource
注解,该注解在语义上定义为通过其唯一名称标识特定目标组件,
其中声明的类型与匹配进程无关. @Autowired
具有多种不同的语义,在 by type
选择候选 bean 之后,指定的 String 限定的值只会考虑这些被选择的候选者. 例如将 account
限定符与标有相同限定符标签的 bean 相匹配.
对于自身定义为 collection
, Map
, 或者 array
类型的 bean, @Resource
是一个很好的解决方案,通过唯一名称引用特定的集合或数组 bean. 也就是说,从 Spring4.3 开始,
只要元素类型信息保存在 @Bean
返回类型签名或集合(或其子类) 中,您就可以通过 Spring 的 @Autowired
类型匹配算法匹配 Map 和数组类型. 在这种情况下,可以使用限定的值来选择相同类型的集合,如上一段所述.
从 Spring4.3 开始,@Autowired
也考虑了注入的自引用,即引用当前注入的 bean. 自引用只是一种后备选项,还是优先使用正常的依赖注入操作其它 bean. 在这个意义上,自引用不参与到正常的候选者选择中,并且总是次要的,
相反,它们总是拥有最低的优先级. 在实践中,自引用通常被用作最后的手段. 例如,通过 bean 的事务代理在同一实例上调用其他方法 在这种情况下,考虑将受影响的方法分解为单独委托的 bean,或者使用 @Resource
,它可以通过其唯一名称获取代理返回到当前的 bean 上.
尝试将 |
@Autowired
可以应用在字段、构造函数和多参数方法上,允许在参数上使用 qualifier 限定符注解缩小取值范围. 相比之下,@Resource
仅支持具有单个参数的字段和 bean 属性 setter 方法. 因此,如果注入目标是构造函数或多参数方法,请使用 qualifiers 限定符.
开发者也可以创建自定义的限定符注解,只需定义一个注解,在其上提供了 @Qualifier
注解即可. 如以下示例所示:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {
String value();
}
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class Genre(val value: String)
然后,您可以在自动装配的字段和参数上提供自定义限定符,如以下示例所示:
public class MovieRecommender {
@Autowired
@Genre("Action")
private MovieCatalog actionCatalog;
private MovieCatalog comedyCatalog;
@Autowired
public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) {
this.comedyCatalog = comedyCatalog;
}
// ...
}
class MovieRecommender {
@Autowired
@Genre("Action")
private lateinit var actionCatalog: MovieCatalog
private lateinit var comedyCatalog: MovieCatalog
@Autowired
fun setComedyCatalog(@Genre("Comedy") comedyCatalog: MovieCatalog) {
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>
在 类路径扫描和组件管理,将展示一个基于注解的替代方法,可以在 XML 中提供 qualifier 元数据, 请参阅为注解提供Qualifier元数据.
在某些情况下,使用没有值的注解可能就足够了. 当注解用于更通用的目的并且可以应用在多种不同类型的依赖上时,这是很有用的. 例如,您可以提供可在没有 Internet 连接时搜索的 Offline 目录. 首先,定义简单注解,如以下示例所示:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {
}
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class Offline
然后将注解添加到需要自动注入的字段或属性中:
public class MovieRecommender {
@Autowired
@Offline (1)
private MovieCatalog offlineCatalog;
// ...
}
1 | This line adds the @Offline annotation. |
class MovieRecommender {
@Autowired
@Offline (1)
private lateinit var offlineCatalog: MovieCatalog
// ...
}
1 | 此行添加 @Offline 注解. |
现在 bean 定义只需要一个限定符类型,如下例所示:
<bean class="example.SimpleMovieCatalog">
<qualifier type="Offline"/> (1)
<!-- inject any dependencies required by this bean -->
</bean>
1 | 此元素指定限定符 |
开发者还可以为自定义限定名 qualifier 注解增加属性,用于替代简单的 value
属性. 如果在要自动注入的字段或参数上指定了多个属性值,则 bean 的定义必须全部匹配这些属性值才能被视为自动注入候选者.
例如,请考虑以下注解定义:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {
String genre();
Format format();
}
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class MovieQualifier(val genre: String, val format: Format)
在这种情况下, Format
是一个枚举类型,定义如下:
public enum Format {
VHS, DVD, BLURAY
}
enum class Format {
VHS, DVD, BLURAY
}
要自动装配的字段使用自定义限定符进行注解,并包含两个属性的值: genre
和 format
,如以下示例所示:
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;
// ...
}
class MovieRecommender {
@Autowired
@MovieQualifier(format = Format.VHS, genre = "Action")
private lateinit var actionVhsCatalog: MovieCatalog
@Autowired
@MovieQualifier(format = Format.VHS, genre = "Comedy")
private lateinit var comedyVhsCatalog: MovieCatalog
@Autowired
@MovieQualifier(format = Format.DVD, genre = "Action")
private lateinit var actionDvdCatalog: MovieCatalog
@Autowired
@MovieQualifier(format = Format.BLURAY, genre = "Comedy")
private lateinit var comedyBluRayCatalog: MovieCatalog
// ...
}
最后,bean 定义应包含匹配的限定符值. 此示例还演示了可以使用 bean meta 属性而不是使用 <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.5. 使用泛型作为自动装配限定符
除了 @Qualifier
注解之外,您还可以使用 Java 泛型类型作为隐式的限定形式. 例如,假设您具有以下配置:
@Configuration
public class MyConfiguration {
@Bean
public StringStore stringStore() {
return new StringStore();
}
@Bean
public IntegerStore integerStore() {
return new IntegerStore();
}
}
@Configuration
class MyConfiguration {
@Bean
fun stringStore() = StringStore()
@Bean
fun integerStore() = IntegerStore()
}
假设上面的 bean 都实现了泛型接口,即 Store<String>
和 Store<Integer>
,那么可以用 @Autowire
来注解 Store
接口, 并将泛型用作限定符,如下例所示:
@Autowired
private Store<String> s1; // <String> qualifier, injects the stringStore bean
@Autowired
private Store<Integer> s2; // <Integer> qualifier, injects the integerStore bean
@Autowired
private lateinit var s1: Store<String> // <String> qualifier, injects the stringStore bean
@Autowired
private lateinit var s2: Store<Integer> // <Integer> qualifier, injects the integerStore bean
通用限定符也适用于自动装配列表,Map
实例和数组. 以下示例自动装配通用 List
:
// 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;
// Inject all Store beans as long as they have an <Integer> generic
// Store<String> beans will not appear in this list
@Autowired
private lateinit var s: List<Store<Integer>>
1.9.6. CustomAutowireConfigurer
CustomAutowireConfigurer
是一个 BeanFactoryPostProcessor
,它允许开发者注册自定义的 qualifier 注解类型,而无需指定 @Qualifier
注解,以下示例显示如何使用 CustomAutowireConfigurer
:
<bean id="customAutowireConfigurer"
class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
<property name="customQualifierTypes">
<set>
<value>example.CustomQualifier</value>
</set>
</property>
</bean>
AutowireCandidateResolver
通过以下方式确定自动注入的候选者:
-
每个bean定义的
autowire-candidate
值 -
在
<beans/>
元素上使用任何可用的default-autowire-candidates
模式 -
存在
@Qualifier
注解以及使用CustomAutowireConfigurer
注册的任何自定义注解
当多个 bean 有资格作为自动注入的候选项时,primary
的确定如下: 如果候选者中只有一个 bean 定义的 primary
属性设置为 true
,则选择它.
1.9.7. @Resource
Spring 还通过在字段或 bean 属性 setter 方法上使用 JSR-250 @Resource
(javax.annotation.Resource
) 注解来支持注入. 这是 Java EE 中的常见模式(例如,JSF-managed beans 和 JAX-WS 端点中) .
Spring 也为 Spring 管理对象提供这种模式.
@Resource
接受一个 name
属性.. 默认情况下,Spring 将该值解释为要注入的 bean 名称. 换句话说,它遵循按名称的语义,如以下示例所示:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource(name="myMovieFinder") (1)
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
1 | 这行注入一个 @Resource . |
class SimpleMovieLister {
@Resource(name="myMovieFinder") (1)
private lateinit var movieFinder:MovieFinder
}
1 | 这行注入一个 @Resource . |
如果未明确指定名称,则默认名称是从字段名称或 setter
方法生成的. 如果是字段,则采用字段名称. 在 setter
方法的情况下,它采用 bean 属性名称. 下面的例子将把名为 movieFinde
r的 bean 注入其 setter 方法:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
class SimpleMovieLister {
@set:Resource
private lateinit var movieFinder: MovieFinder
}
ApplicationContext 若使用了 CommonAnnotationBeanPostProcessor ,注解提供的name名字将被解析为 bean 的 name 名字. 如果配置了 Spring 的 SimpleJndiBeanFactory ,
这些 name 名称就可以通过 JNDI 解析. 但是,推荐使用默认的配置,简单地使用 Spring 的 JNDI,这样可以保持逻辑引用. 而不是直接引用.
|
@Resource
在没有明确指定 name 时,其行为类似于 @Autowired
,对于特定bean(Spring API内的 bean) , @Resource
找到主要类型匹配而不是特定的命名 bean, 并解析众所周知的可解析依赖: ApplicationContext
, ResourceLoader
, ApplicationEventPublisher
, 和 MessageSource
接口.
因此,在以下示例中,customerPreferenceDao
字段首先查找名为 customerPreferenceDao
的 bean,如果未找到,则会使用类型匹配 CustomerPreferenceDao
类的实例:
public class MovieRecommender {
@Resource
private CustomerPreferenceDao customerPreferenceDao;
@Resource
private ApplicationContext context; (1)
public MovieRecommender() {
}
// ...
}
1 | context 将会注入 ApplicationContext |
class MovieRecommender {
@Resource
private lateinit var customerPreferenceDao: CustomerPreferenceDao
@Resource
private lateinit var context: ApplicationContext (1)
// ...
}
1 | context 将会注入 ApplicationContext |
1.9.8. @Value
@Value
通常用于注入外部属性:
@Component
public class MovieRecommender {
private final String catalog;
public MovieRecommender(@Value("${catalog.name}") String catalog) {
this.catalog = catalog;
}
}
@Component
class MovieRecommender(@Value("\${catalog.name}") private val catalog: String)
使用以下配置:
@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig { }
@Configuration
@PropertySource("classpath:application.properties")
class AppConfig
以及以下 application.properties
文件:
catalog.name=MovieCatalog
在这种情况下,catalog
参数和字段将等于 MovieCatalog
值.
Spring 提供了一个默认的内嵌值解析器. 它将尝试解析属性值,如果无法解析,则将属性名称(例如 ${catalog.name}
) 作为值注入. 如果要严格控制不存在的值,则应声明一个 PropertySourcesPlaceholderConfigurer
bean,如以下示例所示:
@Configuration
public class AppConfig {
@Bean
public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
@Configuration
class AppConfig {
@Bean
fun propertyPlaceholderConfigurer() = PropertySourcesPlaceholderConfigurer()
}
使用 JavaConfig 配置 PropertySourcesPlaceholderConfigurer 时,@Bean 方法必须是 static 的.
|
如果无法解析任何 ${}
占位符,则使用上述配置 Spring 初始化会失败. 也可以使用 setPlaceholderPrefix
,setPlaceholderSuffix
或 setValueSeparator
之类的方法来自定义占位符.
Spring Boot 默认配置一个 PropertySourcesPlaceholderConfigurer bean,它将从 application.properties 和 application.yml 文件获取属性.
|
Spring 提供的内置转换器支持允许自动处理简单的类型转换(例如,转换为 Integer
或 int
) . 多个逗号分隔的值可以自动转换为 String
数组,而无需付出额外的努力.
可以提供如下默认值:
@Component
public class MovieRecommender {
private final String catalog;
public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) {
this.catalog = catalog;
}
}
@Component
class MovieRecommender(@Value("\${catalog.name:defaultCatalog}") private val catalog: String)
Spring BeanPostProcessor
在后台使用 ConversionService
处理将 @Value
中的 String
值转换为目标类型的过程. 如果要为自己的自定义类型提供转换支持,则可以提供自己的 ConversionService
bean 实例,如以下示例所示:
@Configuration
public class AppConfig {
@Bean
public ConversionService conversionService() {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
conversionService.addConverter(new MyCustomConverter());
return conversionService;
}
}
@Configuration
class AppConfig {
@Bean
fun conversionService(): ConversionService {
return DefaultFormattingConversionService().apply {
addConverter(MyCustomConverter())
}
}
}
当 @Value
包含 SpEL
表达式 时,该值将在运行时动态计算,如以下示例所示:
@Component
public class MovieRecommender {
private final String catalog;
public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) {
this.catalog = catalog;
}
}
@Component
class MovieRecommender(
@Value("#{systemProperties['user.catalog'] + 'Catalog' }") private val catalog: String)
SpEL 还可以使用更复杂的数据结构:
@Component
public class MovieRecommender {
private final Map<String, Integer> countOfMoviesPerCatalog;
public MovieRecommender(
@Value("#{{'Thriller': 100, 'Comedy': 300}}") Map<String, Integer> countOfMoviesPerCatalog) {
this.countOfMoviesPerCatalog = countOfMoviesPerCatalog;
}
}
@Component
class MovieRecommender(
@Value("#{{'Thriller': 100, 'Comedy': 300}}") private val countOfMoviesPerCatalog: Map<String, Int>)
1.9.9. @PostConstruct
和 @PreDestroy
CommonAnnotationBeanPostProcessor
不仅仅识别 @Resource
注解,还识别 JSR-250 生命周期注解 javax.annotation.PostConstruct
和 javax.annotation.PreDestroy
,在 Spring 2.5 中引入了这些注解, 它们提供了另一个代初始化回调 和销毁回调.
如果 CommonAnnotationBeanPostProcessor
在Spring ApplicationContext
中注册,它会在相应的 Spring bean 生命周期中调用相应的方法,就像是 Spring 生命周期接口方法,或者是明确声明的回调函数那样. 在以下示例中,缓存在初始化时预先填充并在销毁时清除:
public class CachingMovieLister {
@PostConstruct
public void populateMovieCache() {
// populates the movie cache upon initialization...
}
@PreDestroy
public void clearMovieCache() {
// clears the movie cache upon destruction...
}
}
class CachingMovieLister {
@PostConstruct
fun populateMovieCache() {
// populates the movie cache upon initialization...
}
@PreDestroy
fun clearMovieCache() {
// clears the movie cache upon destruction...
}
}
有关组合各种生命周期机制的影响的详细信息,请参阅组合生命周期策略.
和 |
1.10. 类路径扫描和管理组件类路径扫描和管理组件
本章中的大多数示例会使用 XML 配置指定在 Spring 容器中生成每个 BeanDefinition
的元数据,上一节(基于注解的容器配置) 演示了如何通过源代码注解提供大量的元数据配置.
然而,即使在这些示例中,注解也仅仅用于驱动依赖注入. "base" bean依然会显式地在XML文件中定义. 本节介绍通过扫描类路径隐式检测候选组件的选项.
候选者组件是 class 类, 这些类经过过滤匹配,由 Spring 容器注册的 bean 定义会成为 Spring bean. 这消除了使用 XML 执行 bean 注册的需要(也就是没有 XML 什么事儿了),可以使用注解(例如 @Component
), AspectJ 类型表达式或开发者自定义过滤条件来选择哪些类将在容器中注册 bean 定义.
从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多功能都是核心 Spring 框架的一部分. 这允许开发者使用 Java 而不是使用传统的 XML 文件来定义 bean. 有关如何使用这些新功能的示例,请查看 |
1.10.1. @Component
注解和更多模板注解
@Repository
注解用于满足存储库(也称为数据访问对象或DAO)的情况,这个注解的用途是自动转换异常. 如异常转换中所述.
Spring 提供了更多的构造型注解: @Component
,@Service
, 和 @Controller
. @Component
可用于管理任何 Spring 的组件. @Repository
, @Service
, 或 @Controller
是 @Component
的特殊化. 用于更具体的用例(分别在持久性,服务和表示层中) . 因此,您可以使用 @Component
注解组件类,但是,通过使用 @Repository
, @Service
, 和 @Controller
注解它们,能够让你的类更易于被合适的工具处理或与相应的切面关联.
例如,这些注解可以使目标组件变成切入点. 在 Spring 框架的未来版本中,@Repository
, @Service
, 和 @Controller
也可能带有附加的语义. 因此,如果在使用 @Component
或 @Service
来选择服务层时,@Service
显然是更好的选择. 同理,在持久化层要选择 @Repository
,它能自动转换异常.
1.10.2. 使用元注解和组合注解
Spring 提供的许多注解都可以在您自己的代码中用作元注解. 元注解是可以应用于另一个注解的注解. 例如, 前面提到的 @Service
注解是使用 @Component
进行元注解的,如下例所示:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component (1)
public @interface Service {
// ...
}
1 | @Component 使 @Service 以与 @Component 相同的方式处理. |
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Component (1)
annotation class Service {
// ...
}
1 | @Component 使 @Service 以与 @Component 相同的方式处理. |
元注解也可以进行组合,进而创建组合注解. 例如,来自 Spring MVC 的 @RestController
注解是由 @Controller
和 @ResponseBody
组成的
此外,组合注解也可以重新定义来自元注解的属性. 这在只想暴露元注解的部分属性时非常有用. 例如,Spring 的 @SessionScope
注解将它的作用域硬编码为 session
,但仍允许自定义 proxyMode
.
以下清单显示了 SessionScope
注解的定义:
@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;
}
@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Scope(WebApplicationContext.SCOPE_SESSION)
annotation class SessionScope(
@get:AliasFor(annotation = Scope::class)
val proxyMode: ScopedProxyMode = ScopedProxyMode.TARGET_CLASS
)
然后,您可以使用 @SessionScope
而不声明 proxyMode
,如下所示:
@Service
@SessionScope
public class SessionScopedService {
// ...
}
@Service
@SessionScope
class SessionScopedService {
// ...
}
您还可以覆盖 proxyMode
的值,如以下示例所示:
@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
// ...
}
@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
class SessionScopedUserService : UserService {
// ...
}
有关更多详细信息,请参阅 Spring Annotation Programming Model wiki 页面.
1.10.3. 自动探测类并注册 bean 定义
Spring 可以自动检测各代码层中被注解的类,并使用 ApplicationContext
内注册相应的 BeanDefinition
. 例如,以下两个类就可以被自动探测:
@Service
public class SimpleMovieLister {
private MovieFinder movieFinder;
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
@Service
class SimpleMovieLister(private val movieFinder: MovieFinder)
@Repository
public class JpaMovieFinder implements MovieFinder {
// implementation elided for clarity
}
@Repository
class JpaMovieFinder : MovieFinder {
// implementation elided for clarity
}
想要自动检测这些类并注册相应的 bean,需要在 @Configuration
配置中添加 @ComponentScan
注解,其中 basePackages
属性是两个类的父包路径. (或者,您可以指定以逗号或分号或空格分隔的列表,其中包含每个类的父包) .
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
// ...
}
@Configuration
@ComponentScan(basePackages = ["org.example"])
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> 元素.
|
类路径扫描的包必须保证这些包出现在类路径中. 当使用 Ant 构建 JAR 时,请确保你没有激活 JAR 任务的纯文件开关. 此外在某些环境装由于安全策略,类路径目录可能不能访问. JDK 1.7.0_45 及更高版本上的独立应用程序(需要在清单中设置 'Trusted-Library') - 请参阅 stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources) . 在 JDK 9 的模块路径(Jigsaw) 上,Spring 的类路径扫描通常按预期工作. ,但是,请确保在模块信息描述符中导出组件类. 如果您希望 Spring 调用类的非公共成员,请确保它们已 "打开"(即,它们在 |
在使用 component-scan
元素时, AutowiredAnnotationBeanPostProcessor
和 CommonAnnotationBeanPostProcessor
都会隐式包含. 意味着这两个组件也是自动探测和注入的. 所有这些都无需 XML 配置.
您可以通过 annotation-config=false 属性来禁用 AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 的注册.
|
1.10.4. 在自定义扫描中使用过滤器
默认情况下,使用 @Component
, @Repository
, @Service
,@Controller
@Configuration
注解的类或者注解为 @Component
的自定义注解类才能被检测为候选组件. 但是,开发者可以通过应用自定义过滤器来修改和扩展此行为.
将它们添加为 @ComponentScan
注解的 includeFilters
或 excludeFilters
参数(或作为 component-scan
元素. 元素的 include-filter
或 exclude-filter
子元素. 每个 filter 元素都需要包含 type
和 expression
属性. 下表介绍了筛选选项:
过滤类型 | 表达式例子 | 描述 |
---|---|---|
annotation (default) |
|
要在目标组件中的类级别出现的注解. |
assignable |
|
目标组件可分配给(继承或实现) 的类(或接口) . |
aspectj |
|
要由目标组件匹配的 AspectJ 类型表达式. |
regex |
|
要由目标组件类名匹配的正则表达式. |
custom |
|
|
以下示例显示忽略所有 @Repository
注解并使用 “stub” 存储库的配置:
@Configuration
@ComponentScan(basePackages = "org.example",
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
excludeFilters = @Filter(Repository.class))
public class AppConfig {
// ...
}
@Configuration
@ComponentScan(basePackages = ["org.example"],
includeFilters = [Filter(type = FilterType.REGEX, pattern = [".*Stub.*Repository"])],
excludeFilters = [Filter(Repository::class)])
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 , 或 @Configuration .
|
1.10.5. 在组件中定义bean的元数据
Spring 组件也可以向容器提供 bean 定义元数据. 在 @Configuration
注解的类中使用 @Bean
注解定义 bean 元数据(也就是 Spring bean),以下示例显示了如何执行此操作:
@Component
public class FactoryMethodComponent {
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
public void doWork() {
// Component method implementation omitted
}
}
@Component
class FactoryMethodComponent {
@Bean
@Qualifier("public")
fun publicInstance() = TestBean("publicInstance")
fun doWork() {
// Component method implementation omitted
}
}
这个类是一个 Spring 组件,它有个 doWork()
方法. 然而,它还有一个工厂方法 publicInstance()
用于产生 bean 定义. @Bean
注解了工厂方法, 还设置了其他 bean 定义的属性,例如通过 @Qualifier
注解的 qualifier
值. 可以指定的其他方法级别的注解是 @Scope
, @Lazy
以及自定义的 qualifier 注解.
除了用于组件初始化的角色之外,@Lazy 注解也可以在 @Autowired 或者 @Inject 注解上,在这种情况下,该注入将会变成延迟注入代理 lazy-resolution proxy(也就是懒加载) 对于复杂的惰性交互,尤其是组合对于可选依赖项,我们推荐使用 ObjectProvider<MyTargetBean> 。.
|
自动注入的字段和方法也可以像前面讨论的一样被支持,也支持 @Bean
方法的自动注入. 以下示例显示了如何执行此操作:
@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);
}
}
@Component
class FactoryMethodComponent {
companion object {
private var i: Int = 0
}
@Bean
@Qualifier("public")
fun publicInstance() = TestBean("publicInstance")
// use of a custom qualifier and autowiring of method parameters
@Bean
protected fun protectedInstance(
@Qualifier("public") spouse: TestBean,
@Value("#{privateInstance.age}") country: String) = TestBean("protectedInstance", 1).apply {
this.spouse = spouse
this.country = country
}
@Bean
private fun privateInstance() = TestBean("privateInstance", i++)
@Bean
@RequestScope
fun requestScopedInstance() = TestBean("requestScopedInstance", 3)
}
该示例将方法参数为 String
,名称为 country
的 bean 自动装配为另一个名为 privateInstance
的 bean 的 age 属性值. Spring 表达式语言元素通过记号 #{ <expression> }
来定义属性的值. 对于 @Value
注解,表达式解析器在解析表达式后,会查找 bean 的名字并设置 value 值.
从 Spring4.3 开始,您还可以声明一个类型为 InjectionPoint
的工厂方法参数(或其更具体的子类: DependencyDescriptor
) 以访问触发创建当前 bean 的请求注入点. 请注意,这仅适用于真实创建的 bean 实例,而不适用于注入现有实例.
因此,这个特性对 prototype scope 的 bean 最有意义. 对于其他作用域,工厂方法将只能看到触发在给定 scope 中创建新 bean 实例的注入点. 例如,触发创建一个延迟单例 bean 的依赖. 在这种情况下,使用提供的注入点元数据拥有优雅的语义. 以下示例显示了如何使用 InjectionPoint
:
@Component
public class FactoryMethodComponent {
@Bean @Scope("prototype")
public TestBean prototypeInstance(InjectionPoint injectionPoint) {
return new TestBean("prototypeInstance for " + injectionPoint.getMember());
}
}
@Component
class FactoryMethodComponent {
@Bean
@Scope("prototype")
fun prototypeInstance(injectionPoint: InjectionPoint) =
TestBean("prototypeInstance for ${injectionPoint.member}")
}
在 Spring 组件中处理 @Bean
和在 @Configuration
中处理是不一样的,区别在于,在 @Component
中,不会使用 CGLIB 增强去拦截方法和属性的调用. 在 @Configuration
注解的类中, @Bean
注解创建的 bean 对象会使用 CGLIB 代理对方法和属性进行调用.
方法的调用不是常规的 Java 语法,而是通过容器来提供通用的生命周期管理和代理 Spring bean, 甚至在通过编程的方式调用 @Bean
方法时也会产生对其它 bean 的引用. 相比之下,在一个简单的 @Component
类中调用 @Bean
方法中的方法或字段具有标准 Java 语义,这里没有用到特殊的 CGLIB 处理或其他约束.
开发者可以将 对静态 由于 Java 语言的可见性,
最后,请注意,单个类可以为同一个 bean 保存多个 |
1.10.6. 命名自动注册组件
扫描处理过程,其中一步就是自动探测组件,扫描器使用 BeanNameGenerator
对探测到的组件命名. 默认情况下,各代码层注解( @Component
, @Repository
, @Service
, 和 @Controller
)所包含的 name 值,将会作为相应的 bean 定义的名字.
如果这些注解没有 name 值,或者是其他一些被探测到的组件(比如使用自定义过滤器探测到的),默认会又 bean name 生成器生成,使用小写类名作为 bean 名字. 例如,如果检测到以下组件类,则名称为 myMovieLister
和 movieFinderImpl
:
@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
@Service("myMovieLister")
class SimpleMovieLister {
// ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
@Repository
class MovieFinderImpl : MovieFinder {
// ...
}
如果您不想依赖默认的 bean 命名策略,则可以提供自定义 bean 命名策略. 首先,实现 BeanNameGenerator
接口,并确保包括一个默认的无参构造函数. 然后,在配置扫描程序时提供完全限定的类名,如以下示例注解和 bean 定义所示:
如果由于多个自动检测到的组件具有相同的非限定类名称(即,具有相同名称但位于以下位置的类不同的软件包) ,则可能需要配置一个默认为 BeanNameGenerator 的 生成的bean名称的完全限定的类名称. 从 Spring Framework 5.2.3 开始, 位于 org.springframework.context.annotation 包中的 FullyQualifiedAnnotationBeanNameGenerator 可以用于这种目的.
|
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
// ...
}
@Configuration
@ComponentScan(basePackages = ["org.example"], nameGenerator = MyNameGenerator::class)
class AppConfig {
// ...
}
<beans>
<context:component-scan base-package="org.example"
name-generator="org.example.MyNameGenerator" />
</beans>
作为一般规则,考虑在其他组件可能对其进行显式引用时使用注解指定名称. 另一方面,只要容器负责装配时,自动生成的名称就足够了.
1.10.7. 为自动检测的组件提供作用域
与 Spring 管理组件一样,自动检测组件的默认和最常见的作用域是 singleton
. 但是,有时您需要一个可由 @Scope
注解指定的不同作用域. 您可以在注解中提供作用域的名称,如以下示例所示:
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
@Scope("prototype")
@Repository
class MovieFinderImpl : MovieFinder {
// ...
}
@Scope 注解仅在具体 bean 类(用于带注解的组件) 或工厂方法(用于 @Bean 方法) 上进行关联. 与 XML bean 定义相比,没有 bean 继承的概念,并且 类级别的继承结构与元数据无关.
|
有关特定于 Web 的作用域(如 Spring 上下文中的 “request” 或 “session” ) 的详细信息,请参阅 Request, Session, Application, 和 WebSocket 作用域. 请求,会话,应用程序和 WebSocket 作用域, 这些作用域与构建注解一样,您也可以使用 Spring 的元注解方法编写自己的作用域注解: 例如,使用 @Scope("prototype")
进行元注解的自定义注解,可能还会声明自定义作用域代理模式.
想要提供自定义作用域的解析策略,而不是依赖于基于注解的方法,那么需要实现 ScopeMetadataResolver 接口,并确保包含一个默认的无参数构造函数. 然后,在配置扫描程序时提供完全限定类名. 以下注解和 bean 定义示例显示:
|
@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
// ...
}
@Configuration
@ComponentScan(basePackages = ["org.example"], scopeResolver = MyScopeResolver::class)
class AppConfig {
// ...
}
<beans>
<context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>
当使用某个非单例作用域时,为作用 domain 对象生成代理可能非常必要,原因参看 依赖有 Scope 的 Bean 作为依赖的作用域 bean. 为此,组件扫描元素上提供了 scoped-proxy 属性. 三个可能的值是: no
, interfaces
, 和 targetClass
. 例如,以下配置导致标准 JDK 动态代理:
@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
// ...
}
@Configuration
@ComponentScan(basePackages = ["org.example"], scopedProxy = ScopedProxyMode.INTERFACES)
class AppConfig {
// ...
}
<beans>
<context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>
1.10.8. 为注解提供Qualifier元数据
在前面使用 @Qualifier 讨论过 @Qualifier
注解. 该部分中的示例演示了在解析自动注入候选者时使用 @Qualifier
注解和自定义限定符注解以提供细粒度控制.
因为这些示例基于 XML bean 定义,所以使用 XML 中的 bean
元素的 qualifier
或 meta
子元素在候选 bean 定义上提供了限定符元数据. 当依靠类路径扫描并自动检测组件时, 可以在候选类上提供具有类型级别注解的限定符元数据. 以下三个示例演示了此技术:
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Qualifier("Action")
class ActionMovieCatalog : MovieCatalog
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Genre("Action")
class ActionMovieCatalog : MovieCatalog {
// ...
}
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Offline
class CachingMovieCatalog : MovieCatalog {
// ...
}
与大多数基于注解的替代方法一样,注解元数据绑定到类定义本身,而使用在 XML 配置时,允许同一类型的 beans 在 qualifier 元数据中提供变量, 因为元数据是依据实例而不是类来提供的. |
1.10.9. 生成候选组件的索引
虽然类路径扫描非常快,但通过在编译时创建候选的静态列表. 可以提高大型应用程序的启动性能. 在此模式下,应用程序的所有模块都必须使用此机制.
将 @ComponentScan 或 <context: component-scan/> 指令保留为是请求上下文扫描某些程序包中的候选项. 当 ApplicationContext 检测到此类索引时,它将自动使用它,而不是扫描类路径.
|
若要生成索引, 只需向包含组件扫描指令目标组件的每个模块添加一个附加依赖. 以下示例显示了如何使用 Maven 执行此操作:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<version>5.3.22</version>
<optional>true</optional>
</dependency>
</dependencies>
在 Gradle 4.5 及更早版本中,依赖应在 compileOnly
中声明配置,如以下示例所示:
dependencies {
compileOnly "org.springframework:spring-context-indexer:5.3.22"
}
对于 Gradle 4.6 和更高版本,应在 annotationProcessor
配置中声明依赖,如以下示例所示:
dependencies {
annotationProcessor "org.springframework:spring-context-indexer:5.3.22"
}
spring-context-indexer
artifact 将产生一个名为 META-INF/spring.components
的文件,并将包含在 jar 包中.
在 IDE 中使用此模式时,必须将 spring-context-indexer 注册为注解处理器, 以确保更新候选组件时索引是最新的.
|
如果在类路径中找到 META-INF/spring.components 文件时,将自动启用索引. 如果某个索引对于某些库(或用例)是不可用的, 但不能为整个应用程序构建,则可以将 spring.index.ignore 设置为 true ,从而将其回退到常规类路径的排列(即根本不存在索引), 或者作为 JVM 系统属性或在 SpringProperties 文件位于类路径的根目录中.
|
1.11. 使用 JSR 330 标准注解
从 Spring 3.0 开始,Spring 提供对 JSR-330 标准注解(依赖注入) 的支持. 这些注解的扫描方式与 Spring 注解相同. 要使用它们,您需要在类路径中包含相关的 jar.
如果使用 Maven 工具,那么
|
1.11.1. 使用 @Inject
和 @Named
注解实现依赖注入
@javax.inject.Inject
可以使用以下的方式来替代 @Autowired
注解:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.findMovies(...);
// ...
}
}
class SimpleMovieLister {
@Inject
lateinit var movieFinder: MovieFinder
fun listMovies() {
movieFinder.findMovies(...)
// ...
}
}
与 @Autowired
一样,您可以在字段,方法和构造函数参数级别使用 @Inject
注解. 此外,还可以将注入点声明为 Provider
. 它允许按需访问作用域较小的 bean 或通过 Provider.get()
调用对其他 bean 进行延迟访问. 以下示例提供了前面示例的变体:
public class SimpleMovieLister {
private Provider<MovieFinder> movieFinder;
@Inject
public void setMovieFinder(Provider<MovieFinder> movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.get().findMovies(...);
// ...
}
}
class SimpleMovieLister {
@Inject
lateinit var movieFinder: Provider<MovieFinder>
fun listMovies() {
movieFinder.get().findMovies(...)
// ...
}
}
如果想要为注入的依赖使用限定名称,则应该使用 @Named
注解. 如下所示:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
class SimpleMovieLister {
private lateinit var movieFinder: MovieFinder
@Inject
fun 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) {
// ...
}
}
public class SimpleMovieLister {
@Inject
public void setMovieFinder(@Nullable MovieFinder movieFinder) {
// ...
}
}
class SimpleMovieLister {
@Inject
var movieFinder: MovieFinder? = null
}
1.11.2. @Named
和 @ManagedBean
注解: 标准与 @Component
注解相同
可以使用 @javax.inject.Named
或 javax.annotation.ManagedBean
来替代 @Component
注解:
@Named("movieListener") // @ManagedBean("movieListener") could be used as well
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
@Named("movieListener") // @ManagedBean("movieListener") could be used as well
class SimpleMovieLister {
@Inject
lateinit var movieFinder: MovieFinder
// ...
}
在不指定组件名称的情况下使用 @Component
是很常见的. @Named
可以以类似的方式使用,如下例所示:
@Named
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
@Named
class SimpleMovieLister {
@Inject
lateinit var movieFinder: MovieFinder
// ...
}
当使用 @Named
或 @ManagedBean
时,可以与 Spring 注解完全相同的方式使用组件扫描. 如以下示例所示:
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
// ...
}
@Configuration
@ComponentScan(basePackages = ["org.example"])
class AppConfig {
// ...
}
与 @Component 相反,JSR-330 @Named 和 JSR-250 @ManagedBean 注解不可组合. 请使用 Spring 的原型模型(stereotype mode)来构建自定义组件注解.
|
1.11.3. 使用 JSR-330标准注解的限制
使用标准注解时,需要知道哪些重要功能是不可用的. 如下表所示:
Spring | javax.inject.* | javax.inject restrictions / comments |
---|---|---|
@Autowired |
@Inject |
|
@Component |
@Named / @ManagedBean |
JSR-330 不提供可组合模型,只是一种识别命名组件的方法. |
@Scope("singleton") |
@Singleton |
JSR-330 的默认作用域就像 Spring 的 |
@Qualifier |
@Qualifier / @Named |
|
@Value |
- |
无 |
@Required |
- |
无 |
@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
注解方法 ,但大多数情况下,@Bean
是配合 @Configuration
使用的.
使用 @Configuration
注解类时,这个类的目的就是作为 bean 定义的地方. 此外,@Configuration
类允许通过调用同一个类中的其他 @Bean
方法来定义 bean 间依赖. 最简单的 @Configuration
类如下所示:
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
@Configuration
class AppConfig {
@Bean
fun myService(): MyService {
return MyServiceImpl()
}
}
前面的 AppConfig
类等效于以下 Spring <beans/>
XML:
<beans>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>
@Bean
和 @Configuration
注解将在下面的章节深入讨论,首先,我们将介绍使用基于 Java 代码的配置来创建 Spring 容器的各种方法.
1.12.2. 使用 AnnotationConfigApplicationContext
初始化 Spring 容器
以下部分介绍了 Spring 的 AnnotationConfigApplicationContext
,它是在 Spring 3.0 中引入的. 这是一个强大的(versatile) ApplicationContext
实现,它不仅能解析 @Configuration
注解类 ,也能解析 @Component
注解的类和使用 JSR-330 注解的类.
当使用 @Configuration
类作为输入时,@Configuration
类本身被注册为一个 bean 定义,类中所有声明的 @Bean
方法也被注册为 bean 定义.
当提供 @Component
和 JSR-330 类时,它们被注册为 bean 定义,并且假定在必要时在这些类中使用 DI 元数据,例如 @Autowired
或 @Inject
.
简单结构
与实例化 ClassPathXmlApplicationContext
时 Spring XML 文件用作输入的方式大致相同, 在实例化 AnnotationConfigApplicationContext
时可以使用 @Configuration
类作为输入. 这允许完全无 XML 使用 Spring 容器,如以下示例所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
fun main() {
val ctx = AnnotationConfigApplicationContext(AppConfig::class.java)
val myService = ctx.getBean<MyService>()
myService.doStuff()
}
如前所述,AnnotationConfigApplicationContext
不仅限于使用 @Configuration
类. 任何 @Component
或 JSR-330 带注解的类都可以作为输入提供给构造函数,如以下示例所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
fun main() {
val ctx = AnnotationConfigApplicationContext(MyServiceImpl::class.java, Dependency1::class.java, Dependency2::class.java)
val myService = ctx.getBean<MyService>()
myService.doStuff()
}
上面假设 MyServiceImpl
, Dependency1
, 和 Dependency2
使用 Spring 依赖注入注解,例如 @Autowired
.
使用 register(Class<?>…)
编程构建容器
AnnotationConfigApplicationContext
可以通过无参构造函数实例化,然后调用 register()
方法进行配置. 这种方法在以编程的方式构建 AnnotationConfigApplicationContext
时特别有用. 下列示例显示了如何执行此操作
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();
}
fun main() {
val ctx = AnnotationConfigApplicationContext()
ctx.register(AppConfig::class.java, OtherConfig::class.java)
ctx.register(AdditionalConfig::class.java)
ctx.refresh()
val myService = ctx.getBean<MyService>()
myService.doStuff()
}
使用 scan(String…)
扫描组件
要启用组件扫描,可以按如下方式注解 @Configuration
类:
@Configuration
@ComponentScan(basePackages = "com.acme") (1)
public class AppConfig {
// ...
}
1 | 此注解可启用组件扫描. |
@Configuration
@ComponentScan(basePackages = ["com.acme"]) (1)
class AppConfig {
// ...
}
1 | 此注解可启用组件扫描. |
有经验的用户可能更熟悉使用 XML 的
|
上面的例子中,com.acme
包会被扫描,只要是使用了 @Component
注解的类,都会被注册进容器中. 同样地,AnnotationConfigApplicationContext
暴露的 scan(String…)
方法也允许扫描类完成同样的功能 如以下示例所示:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
}
fun main() {
val ctx = AnnotationConfigApplicationContext()
ctx.scan("com.acme")
ctx.refresh()
val myService = ctx.getBean<MyService>()
}
请记住 @Configuration 类是使用 @Component 进行 元注解的,因此它们是组件扫描的候选者. 在前面的示例中, 假设 AppConfig 在 com.acme 包(或下面的任何包) 中声明,它在 scan() 调用期间被拾取. 在 refresh() 之后,它的所有 @Bean 方法都被处理并在容器中注册为 bean 定义.
|
使用 AnnotationConfigWebApplicationContext
支持Web应用程序
WebApplicationContext
与 AnnotationConfigApplicationContext
的结合是 AnnotationConfigWebApplicationContext
配置.
这个实现可以用于配置 Spring ContextLoaderListener
servlet 监听器 ,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 名称将与方法名称相同. 以下示例显示了 @Bean
方法声明:
@Configuration
public class AppConfig {
@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
@Configuration
class AppConfig {
@Bean
fun transferService() = TransferServiceImpl()
}
前面的配置完全等同于以下 Spring XML:
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>
这两个声明都在 ApplicationContext
中创建一个名为 transferService
的 bean,并且绑定了 TransferServiceImpl
的实例. 如下图所示:
transferService -> com.acme.TransferServiceImpl
您还可以使用默认方法来定义 bean。 这允许 bean 的组合通过在默认方法上实现带有 bean 定义的接口来进行配置。
public interface BaseConfig {
@Bean
default TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
@Configuration
public class AppConfig implements BaseConfig {
}
您还可以使用接口(或基类) 返回类型声明 @Bean
方法,如以下示例所示:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
@Configuration
class AppConfig {
@Bean
fun transferService(): TransferService {
return TransferServiceImpl()
}
}
但是,这会将预先类型预测的可见性限制为指定的接口类型(TransferService
),然后在实例化受影响的单一 bean 时,只知道容器的完整类型(TransferServiceImpl
) .
非延迟的单例 bean 根据它们的声明顺序进行实例化,因此开发者可能会看到不同类型的匹配结果,这具体取决于另一个组件尝试按未类型匹配的时间(如 @Autowired TransferServiceImpl
, 一旦 transferService
bean 已被实例化,这个问题就被解决了).
如果通过声明的服务接口都是引用类型,那么 @Bean 返回类型可以安全地加入该设计决策.但是,对于实现多个接口的组件或可能由其实现类型引用的组件, 更安全的方法是声明可能的最具体的返回类型(至少按照注入点所要求的特定你的bean) .
|
Bean 的依赖
一个使用 @Bean
注解的方法可以具有任意数量的参数描述构建该 bean 所需的依赖,例如,如果我们的 TransferService
需要 AccountRepository
, 我们可以使用方法参数来实现该依赖,如以下示例所示:
@Configuration
public class AppConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
class AppConfig {
@Bean
fun transferService(accountRepository: AccountRepository): TransferService {
return TransferServiceImpl(accountRepository)
}
}
这个解析机制与基于构造函数的依赖注入非常相似. 有关详细信息,请参阅相关部分
接收生命周期回调
使用 @Bean
注解定义的任何类都支持常规的生命周期回调,并且可以使用 JSR-250 的 @PostConstruct
和 @PreDestroy
注解. 有关更多详细信息,请参阅 JSR-250 注解 .
完全支持常规的 Spring 生命周期回调. 如果 bean 实现 InitializingBean
, DisposableBean
, 或 Lifecycle
,则它们各自的方法由容器调用.
同样地,还完全支持标准的 *Aware
,如 BeanFactoryAware,
BeanNameAware,
MessageSourceAware,
ApplicationContextAware.
@Bean
注解支持指定任意初始化和销毁回调方法,就像 bean 元素上的 Spring XML 的 init-method
和 destroy-method
属性一样,如下例所示:
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();
}
}
class BeanOne {
fun init() {
// initialization logic
}
}
class BeanTwo {
fun cleanup() {
// destruction logic
}
}
@Configuration
class AppConfig {
@Bean(initMethod = "init")
fun beanOne() = BeanOne()
@Bean(destroyMethod = "cleanup")
fun beanTwo() = BeanTwo()
}
默认情况下,使用 Java Config 定义的 bean 中 开发者可能希望对通过 JNDI 获取的资源执行此操作,因为它的生命周期是在应用程序外部管理的. 更进一步,使用 以下示例说明如何防止 Java
Kotlin
同样地,使用 |
对于前面注解中上面示例中的 BeanOne
,在构造期间直接调用 init()
方法同样有效,如下例所示:
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
BeanOne beanOne = new BeanOne();
beanOne.init();
return beanOne;
}
// ...
}
@Configuration
class AppConfig {
@Bean
fun beanOne() = BeanOne().apply {
init()
}
// ...
}
当您直接使用 Java( new 对象那种) 工作时,您可以使用对象执行任何您喜欢的操作,并且不必总是依赖于容器生命周期. |
指定 Bean 的作用域
Spring包含 @Scope
注解,以便您可以指定 bean 的作用域.
使用 @Scope
注解
可以使用任意标准的方式为 @Bean
注解的 bean 指定一个作用域,你可以使用Bean Scopes中的任意标准作用域
默认作用域是 singleton
的,但是可以使用 @Scope
注解来覆盖. 如下例所示:
@Configuration
public class MyConfiguration {
@Bean
@Scope("prototype")
public Encryptor encryptor() {
// ...
}
}
@Configuration
class MyConfiguration {
@Bean
@Scope("prototype")
fun encryptor(): Encryptor {
// ...
}
}
@Scope
和 scoped-proxy
Spring 提供了一种通过scoped proxies处理作用域依赖的便捷方法. 使用 XML 配置时创建此类代理的最简单方法是 <aop:scoped-proxy/>
元素.
使用 @Scope
注解在 Java 中配置 bean 提供了与 proxyMode
属性的等效支持. 默认值为不应创建作用域代理(ScopedProxyMode.DEFAULT
) ,但您可以指定 ScopedProxyMode.TARGET_CLASS
, ScopedProxyMode.INTERFACES
或 ScopedProxyMode.NO
。.
如果使用 Java 将 XML 参考文档(请参阅scoped proxies) 的作用域代理示例移植到我们的 @Bean
,它类似于以下内容:
// 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;
}
// an HTTP Session-scoped bean exposed as a proxy
@Bean
@SessionScope
fun userPreferences() = UserPreferences()
@Bean
fun userService(): Service {
return SimpleUserService().apply {
// a reference to the proxied userPreferences bean
setUserPreferences(userPreferences())
}
}
自定义 Bean 名字
默认情况下,配置类使用 @Bean
方法的名称作为结果 bean 的名称. 但是,可以使用 name
属性覆盖此功能,如以下示例所示:
@Configuration
public class AppConfig {
@Bean("myThing")
public Thing thing() {
return new Thing();
}
}
@Configuration
class AppConfig {
@Bean("myThing")
fun thing() = Thing()
}
Bean 的别名
正如 Bean 的命名 中所讨论的,有时需要为单个 bean 提供多个名称,也称为 bean 别名. @Bean
注解的 name
属性为此接受 String
数组. 以下示例显示如何为 bean 设置多个别名:
@Configuration
public class AppConfig {
@Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
public DataSource dataSource() {
// instantiate, configure and return DataSource bean...
}
}
@Configuration
class AppConfig {
@Bean("dataSource", "subsystemA-dataSource", "subsystemB-dataSource")
fun dataSource(): DataSource {
// instantiate, configure and return DataSource bean...
}
}
Bean 的描述
有时,提供更详细的 bean 文本描述会很有帮助. 当 bean 被暴露(可能通过 JMX) 用于监视目的时,这可能特别有用.
要向 @Bean
添加描述,可以使用 @Description
注解,如以下示例所示:
@Configuration
public class AppConfig {
@Bean
@Description("Provides a basic example of a bean")
public Thing thing() {
return new Thing();
}
}
@Configuration
class AppConfig {
@Bean
@Description("Provides a basic example of a bean")
fun thing() = Thing()
}
1.12.4. 使用 @Configuration
注解
@Configuration
是一个类级别的注解,表明该类将作为 bean 定义的元数据配置. @Configuration
类会将有 @Bean
注解的暴露方法声明为 bean .在 @Configuration
类上调用 @Bean
方法也可以用于定义 bean 间依赖, 有关一般介绍,请参阅 基本概念:基本概念: @Bean
和 @Configuration
注入内部bean依赖
当 bean 彼此有依赖时,表示依赖就像调用另一个 bean 方法一样简单.如下例所示:
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
@Configuration
class AppConfig {
@Bean
fun beanOne() = BeanOne(beanTwo())
@Bean
fun beanTwo() = BeanTwo()
}
在前面的示例中,beanOne
通过构造函数注入接收对 beanTwo
的引用.
这种声明 bean 间依赖的方法只有在 @Configuration 类中声明 @Bean 方法时才有效. 您不能使用普通的 @Component 类声明 bean 间依赖.
|
查找方法注入
如前所述,查找方法注入 是一项很少使用的高级功能. 在单例作用域的 bean 依赖于原型作用域的 bean 的情况下,它很有用. Java 提供了很友好的 API 来实现这种模式. 以下示例显示了如何使用查找方法注入:
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();
}
abstract class CommandManager {
fun process(commandState: Any): Any {
// grab a new instance of the appropriate Command interface
val 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 fun createCommand(): Command
}
通过使用 Java 配置,您可以创建 CommandManager
的子类,其中抽象的 createCommand()
方法被覆盖,以便查找新的(prototype)对象. 以下示例显示了如何执行此操作:
@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();
}
}
}
@Bean
@Scope("prototype")
fun asyncCommand(): AsyncCommand {
val command = AsyncCommand()
// inject dependencies here as required
return command
}
@Bean
fun commandManager(): CommandManager {
// return new anonymous implementation of CommandManager with createCommand()
// overridden to return a new prototype Command object
return object : CommandManager() {
override fun createCommand(): Command {
return asyncCommand()
}
}
}
有关基于 Java 的配置如何在内部工作的更多信息
请考虑以下示例,该示例显示了被调用两次的 @Bean
注解方法:
@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();
}
}
@Configuration
class AppConfig {
@Bean
fun clientService1(): ClientService {
return ClientServiceImpl().apply {
clientDao = clientDao()
}
}
@Bean
fun clientService2(): ClientService {
return ClientServiceImpl().apply {
clientDao = clientDao()
}
}
@Bean
fun clientDao(): ClientDao {
return ClientDaoImpl()
}
}
clientDao()
在 clientService1()
中调用一次,在 clientService2()
中调用一次. 由于此方法创建了 ClientDaoImpl
的新实例并将其返回,因此通常希望有两个实例(每个服务一个) .
这肯定会有问题: 在 Spring 中,实例化的 bean 默认具有 singleton
作用域. 这就是它的神奇之处:所有 @Configuration
类在启动时都使用 CGLIB
进行子类化. 在子类中,子方法在调用父方法并创建新实例之前,首先检查容器是否有任何缓存(作用域) bean.
这种行为可以根据 bean 的作用域而变化,我们这里只是讨论单例. |
从 Spring 3.2 开始,不再需要将 CGLIB 添加到类路径中,因为 CGLIB 类已经在 |
由于 CGLIB 在启动时动态添加功能,因此存在一些限制. 特别是,配置类不能是 final 的. 但是,从 4.3 开始,配置类允许使用任何构造函数,包括使用 如果想避免因 CGLIB 带来的限制,请考虑声明非 |
1.12.5. 编写基于 Java 的配置
Spring 的基于 Java 的配置功能允许您撰写注解,这可以降低配置的复杂性.
使用 @Import
注解
就像在 Spring XML 文件中使用 <import/>
元素来帮助模块化配置一样,@Import
注解允许从另一个配置类加载 @Bean
定义,如下例所示:
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B b() {
return new B();
}
}
@Configuration
class ConfigA {
@Bean
fun a() = A()
}
@Configuration
@Import(ConfigA::class)
class ConfigB {
@Bean
fun b() = B()
}
现在,在实例化上下文时,不需要同时指定 ConfigA.class
和 ConfigB.class
,只需要显式提供 ConfigB
,如下例所示:
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);
}
fun main() {
val ctx = AnnotationConfigApplicationContext(ConfigB::class.java)
// now both beans A and B will be available...
val a = ctx.getBean<A>()
val b = ctx.getBean<B>()
}
这种方法简化了容器实例化,因为只需要处理一个类,而不是要求您在构造期间记住可能大量的 @Configuration
类.
从 Spring Framework 4.2 开始,@Import 还支持引用常规组件类,类似于 AnnotationConfigApplicationContext.register 方法. 如果要避免组件扫描,这一点特别有用,可以使用一些配置类作为明确定义所有组件的入口点.
|
在导入的 @Bean
定义上注入依赖
上面的例子可以运行,但是太简单了. 在大多数实际情况下, bean 将在配置类之间相互依赖.在使用XML时,这本身不是问题,因为没有涉及到编译器. 可以简单地声明 ref="someBean"
,并且相信 Spring 将在容器初始化期间可以很好地处理它. 当然,当使用 @Configuration
类时,Java编译器会有一些限制 ,即需符合Java的语法.
幸运的是,解决这个问题很简单. 正如我们 已经讨论过 的,@Bean
方法可以有任意数量的参数来描述 bean 的依赖. 考虑以下更多真实场景,其中包含几个 @Configuration
类,每个类都取决于其他类中声明的 bean :
@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
class ServiceConfig {
@Bean
fun transferService(accountRepository: AccountRepository): TransferService {
return TransferServiceImpl(accountRepository)
}
}
@Configuration
class RepositoryConfig {
@Bean
fun accountRepository(dataSource: DataSource): AccountRepository {
return JdbcAccountRepository(dataSource)
}
}
@Configuration
@Import(ServiceConfig::class, RepositoryConfig::class)
class SystemTestConfig {
@Bean
fun dataSource(): DataSource {
// return new DataSource
}
}
fun main() {
val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
// everything wires up across configuration classes...
val transferService = ctx.getBean<TransferService>()
transferService.transfer(100.00, "A123", "C456")
}
还有另一种方法可以达到相同的效果. 请记住,@Configuration
类最终只是容器中的另一个 bean : 这意味着它们可以利用 @Autowired
和 @Value
注入以及与任何其他 bean 相同的其他功能.
确保以这种方式注入的依赖只是最简单的. 另外,要特别注意通过 |
以下示例显示了如何将一个 bean 自动连接到另一个 bean :
@Configuration
public class ServiceConfig {
@Autowired
private AccountRepository accountRepository;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
private final DataSource dataSource;
public RepositoryConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
@Configuration
class ServiceConfig {
@Autowired
lateinit var accountRepository: AccountRepository
@Bean
fun transferService(): TransferService {
return TransferServiceImpl(accountRepository)
}
}
@Configuration
class RepositoryConfig(private val dataSource: DataSource) {
@Bean
fun accountRepository(): AccountRepository {
return JdbcAccountRepository(dataSource)
}
}
@Configuration
@Import(ServiceConfig::class, RepositoryConfig::class)
class SystemTestConfig {
@Bean
fun dataSource(): DataSource {
// return new DataSource
}
}
fun main() {
val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
// everything wires up across configuration classes...
val transferService = ctx.getBean<TransferService>()
transferService.transfer(100.00, "A123", "C456")
}
仅在 Spring Framework 4.3 中支持 @Configuration 类中的构造函数注入. 另请注意,如果目标 bean 仅定义了一个构造函数,则无需指定 @Autowired . 在前面的示例中,RepositoryConfig 构造函数中不需要 @Autowired .
|
在上面的场景中,@Autowired
可以很好的工作,使设计更具模块化,但是自动注入哪个 bean 依然有些模糊不清.例如, 作为一个开发者查看 ServiceConfig
类时,你怎么知道 @Autowired AccountRepository
在哪定义的呢?代码中并未明确指出,
还好, Spring Tools for Eclipse 提供的工具可以呈现图表,显示所有内容的连线方式,这可能就是您所需要的. 此外,您的Java IDE可以轻松找到 AccountRepository
类型的所有声明和用法,并快速显示返回该类型的 @Bean
方法的位置.
万一需求不允许这种模糊的装配,并且您希望从 IDE 中从一个 @Configuration
类直接导航到另一个 @Configuration
类,请考虑自动装配配置类本身. 以下示例显示了如何执行此操作:
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
// navigate 'through' the config class to the @Bean method!
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
@Configuration
class ServiceConfig {
@Autowired
private lateinit var repositoryConfig: RepositoryConfig
@Bean
fun transferService(): TransferService {
// navigate 'through' the config class to the @Bean method!
return TransferServiceImpl(repositoryConfig.accountRepository())
}
}
在前面的情况中,定义 AccountRepository
是完全明确的. 但是,ServiceConfig
现在与 RepositoryConfig
紧密耦合. 这是一种权衡的方法. 通过使用基于接口的或基于类的抽象 @Configuration
类,可以在某种程度上减轻这种紧密耦合. 请考虑以下示例:
@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");
}
@Configuration
class ServiceConfig {
@Autowired
private lateinit var repositoryConfig: RepositoryConfig
@Bean
fun transferService(): TransferService {
return TransferServiceImpl(repositoryConfig.accountRepository())
}
}
@Configuration
interface RepositoryConfig {
@Bean
fun accountRepository(): AccountRepository
}
@Configuration
class DefaultRepositoryConfig : RepositoryConfig {
@Bean
fun accountRepository(): AccountRepository {
return JdbcAccountRepository(...)
}
}
@Configuration
@Import(ServiceConfig::class, DefaultRepositoryConfig::class) // import the concrete config!
class SystemTestConfig {
@Bean
fun dataSource(): DataSource {
// return DataSource
}
}
fun main() {
val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
val transferService = ctx.getBean<TransferService>()
transferService.transfer(100.00, "A123", "C456")
}
现在,ServiceConfig
与具体的 DefaultRepositoryConfig
松散耦合,内置的IDE工具仍然很有用: 您可以很容易获取 RepositoryConfig
实现类的继承体系. 以这种方式,操作 @Configuration
类及其依赖与操作基于接口的代码的过程没有什么区别
如果要影响某些 bean 的启动创建顺序,可以考虑将其中一些声明为 @Lazy (用于在首次访问时创建而不是在启动时) 或 @DependsOn 某些其他 bean (确保在创建之前创建特定的其他 bean (当前的 bean ,超出后者的直接依赖性所暗示的) ) .
|
有条件地包含 @Configuration
类或 @Bean
方法
基于某些任意系统状态,有条件地启用或禁用完整的 @Configuration
类甚至单独的 @Bean
方法通常很有用. 一个常见的例子是, 只有在 Spring 环境中启用了特定的配置文件时才使用 @Profile
注解来激活 bean (有关详细信息,请参阅 BeanBean 定义 Profiles) .
@Profile
注解实际上是通过使用更灵活的注解 @Conditional
实现的. @Conditional
注解表示特定的 org.springframework.context.annotation.Condition
实现. 它表明 @Bean
被注册之前会先"询问 @Conditional
注解.
Condition
接口的实现提供了一个返回 true
或 false
的 matches(…)
方法. 例如,以下清单显示了用于 @Profile
的实际 Condition
实现:
@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;
}
override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
// Read the @Profile annotation attributes
val attrs = metadata.getAllAnnotationAttributes(Profile::class.java.name)
if (attrs != null) {
for (value in attrs["value"]!!) {
if (context.environment.acceptsProfiles(Profiles.of(*value as Array<String>))) {
return true
}
}
return false
}
return true
}
有关更多详细信息,请参阅 @Conditional
javadoc.
结合 Java 和 XML 配置
Spring的 @Configuration
类支持但不一定成为 Spring XML 的 100% 完全替代品. 某些工具(如 Spring XML 命名空间) 仍然是配置容器的理想方法.
在 XML 方便或必要的情况下,您可以选择: 通过使用例如 ClassPathXmlApplicationContext
以 “XML-centric” 的方式实例化容器, 或者通过使用 AnnotationConfigApplicationContext
以 “Java-centric” 的方式实例化它. @ImportResource
注解,根据需要导入XML.
以XML为中心使用 @Configuration
类
更受人喜爱的方法是从包含 @Configuration
类的 XML 启动容器.例如,在使用 Spring 的现有系统中,大量使用的是 Spring XML 配置,所以很容易根据需要创建 @Configuration
类 ,并将他们到包含 XML 文件中. 我们将介绍在这种 “XML-centric” 的情况下使用 @Configuration
类的选项.
请记住,@Configuration
类最终也只是容器中的 bean 定义. 在本系列示例中,我们创建一个名为 AppConfig
的 @Configuration
类,并将其作为 <bean/>
定义包含在 system-test-config.xml
中. 由于 <context:annotation-config/>
已打开,容器会识别 @Configuration
注解并正确处理 AppConfig
中声明的 @Bean
方法.
以下示例显示了 Java 中的普通配置类:
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
@Bean
public TransferService transferService() {
return new TransferService(accountRepository());
}
}
@Configuration
class AppConfig {
@Autowired
private lateinit var dataSource: DataSource
@Bean
fun accountRepository(): AccountRepository {
return JdbcAccountRepository(dataSource)
}
@Bean
fun transferService() = 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=
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
fun main() {
val ctx = ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml")
val transferService = ctx.getBean<TransferService>()
// ...
}
在 system-test-config.xml 文件中, AppConfig <bean/> 不声明 id 元素. 虽然这样做是可以的,但是没有必要,因为没有其他 bean 引用它,并且不太可能通过名称从容器中明确地获取它. 类似地,DataSource bean 只是按类型自动装配,因此不严格要求显式的 bean id .
|
因为 @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-centric” 的方法来配置容器并尽可能少的使用 XML. 以下示例(包括配置类,定义 bean 的 XML 文件,属性文件和主类) 显示了如何使用 @ImportResource
注解来实现根据需要使用 XML 的 “Java-centric” 的配置:
@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);
}
}
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
class AppConfig {
@Value("\${jdbc.url}")
private lateinit var url: String
@Value("\${jdbc.username}")
private lateinit var username: String
@Value("\${jdbc.password}")
private lateinit var password: String
@Bean
fun dataSource(): DataSource {
return 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=
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
fun main() {
val ctx = AnnotationConfigApplicationContext(AppConfig::class.java)
val transferService = ctx.getBean<TransferService>()
// ...
}
Environment
接口是集成在容器中的抽象,它模拟了应用程序环境的两个关键方面: profiles 和 properties
profile 配置是一个被命名的, bean 定义的逻辑组,这些 bean 只有在给定的 profile 配置激活时才会注册到容器.无论是以 XML 还是通过注解定义, bean 都可以分配给配置文件 .
Environment
对象在 profile 中的角色是判断哪一个 profile 应该在当前激活和哪一个 profile 应该在默认情况下激活.
属性在几乎所有应用程序中都发挥着重要作用,可能源自各种源: 属性文件,JVM 系统属性,系统环境变量,JNDI,servlet 上下文参数,ad-hoc 属性对象,Map
对象等.
与属性相关的 Environment
对象的作用是为用户提供方便的服务接口,用于配置属性源和从中解析属性.
1.12.6. Bean 定义 Profiles
bean 定义 profiles 是核心容器内的一种机制,该机制能在不同环境中注册不同的 bean.“environment” 这个词对不同的用户来说意味着不同的东西,这个功能可以帮助解决许多用例,包括:
-
在 QA 或生产环境中,针对开发中的内存数据源而不是从JNDI查找相同的数据源.
-
开发期使用监控组件,当部署以后则关闭监控组件,使应用更高效
-
为用户各自注册自定义 bean 实现.
考虑 DataSource
的实际应用程序中的第一个用例. 在测试环境中,配置可能类似于以下内容:
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build()
}
现在考虑如何将此应用程序部署到 QA 或生产环境中,假设应用程序的数据源已注册到生产应用程序服务器的 JNDI 目录. 我们的 dataSource
bean 现在看起来如下:
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
val ctx = InitialContext()
return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}
问题是如何根据当前环境在使用这两种变体之间切换. 随着时间的推移,Spring 用户已经设计了许多方法来完成这项工作,通常依赖于系统环境变量和包含 ${placeholder}
标记的 XML <import/>
语句的组合,
这些标记根据值解析为正确的配置文件路径一个环境变量. bean 定义 profiles 是核心容器功能,可为此问题提供解决方案.
概括一下上面的场景,环境决定 bean 定义,最后发现,我们需要在某些上下文环境中使用某些 bean ,在其他环境中则不用这些 bean .或者说, 在场景 A 中注册一组 bean 定义,而在场景B中注册另外一组. 先看看如何通过修改配置来完成此需求:
使用 @Profile
@Profile
注解用于当一个或多个配置文件激活的时候,用来指定组件是否有资格注册. 使用前面的示例,我们可以重写 dataSource
配置,如下所示:
@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();
}
}
@Configuration
@Profile("development")
class StandaloneDataConfig {
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build()
}
}
@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");
}
}
@Configuration
@Profile("production")
class JndiDataConfig {
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
val ctx = InitialContext()
return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}
}
如前所述,使用 @Bean 方法,您通常选择使用 Spring 的 JndiTemplate/JndiLocatorDelegate 帮助程序或前面显示的 直接 JNDI InitialContext 用法但不使用 JndiObjectFactoryBean 变量来使用编程 JNDI 查找,这会强制您将返回类型声明为 FactoryBean 类型.
|
profile 字符串可以包含简单的 profile 名称(例如,production) 或 profile 表达式. profile 表达式允许表达更复杂的概要逻辑(例如,production & us-east
) . profile 表达式支持以下运算符:
-
!
: 逻辑 非 -
&
: 逻辑 与 -
|
: 逻辑 或
你必须使用括号混合 & 和 | . 例如,production & us-east | eu-central ,它不是一个有效的表达. 它必须表示为 production & (us-east | eu-central) .
|
您可以将 @Profile
用作 元注解,以创建自定义组合注解. 以下示例定义了一个自定义 @Production
注解,您可以将其用作 @Profile("production")
的替代品:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Profile("production")
annotation class Production
如果 @Configuration 类标有 @Profile ,类中所有 @Bean 和 @Import 注解相关的类都将被忽略,除非该 profile 被激活.
如果一个 @Component 或 @Configuration 类被标记为 @Profile({"p1", "p2"}) . 那么除非 profile 'p1' or 'p2' 已被激活.
否则该类将不会注册/处理. 如果给定的配置文件以 NOT 运算符( ! )为前缀,如果配置文件为没有激活,则注册的元素将被注册. 例如,给定 @Profile({"p1", "!p2"}) ,如果配置文件 p1 处于 active 状态或配置文件 p2 未激活,则会进行注册.
|
@Profile
也能注解方法,用于配置一个配置类中的指定 bean . 如以下示例所示:
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("development") (1)
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") (2)
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
1 | standaloneDataSource 方法仅在 development 环境可用. |
2 | jndiDataSource 方法仅在 production 环境可用. |
@Configuration
class AppConfig {
@Bean("dataSource")
@Profile("development") (1)
fun standaloneDataSource(): DataSource {
return 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") (2)
fun jndiDataSource() =
InitialContext().lookup("java:comp/env/jdbc/datasource") as DataSource
}
1 | standaloneDataSource 方法仅在 development 环境可用. |
2 | jndiDataSource 方法仅在 production 环境可用. |
在 如果想定义具有不同配置文件条件的备用 bean ,请使用不同的 Java 方法名称,通 XML bean 定义 profiles 过 |
XML bean 定义profiles
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>
也可以不用分开 2 个文件,在同一个 XML 中配置 2 个 <beans/>
,<beans/>
元素也有 profile
属性. 如以下示例所示:
<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
强制允许将 profile
元素定义在文件的最后面,这有助于在 XML 文件中提供灵活的方式而又不引起混乱.
对应 XML 不支持前面描述的
在前面的示例中,如果 |
激活 Profile
现在已经更新了配置,但仍然需要指定要激活哪个配置文件, 如果我们现在开始我们的示例应用程序, 我们会看到抛出 NoSuchBeanDefinitionException
,因为容器找不到名为 dataSource
的Spring bean.
激活配置文件可以通过多种方式完成,但最直接的方法是以编程方式对可通过 ApplicationContext
提供的 Environment
API 进行操作. 以下示例显示了如何执行此操作:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
val ctx = AnnotationConfigApplicationContext().apply {
environment.setActiveProfiles("development")
register(SomeConfig::class.java, StandaloneDataConfig::class.java, JndiDataConfig::class.java)
refresh()
}
此外,配置文件也可以通过 spring.profiles.active
属性声明式性地激活,可以通过系统环境变量,JVM系统属性,web.xml
中的 Servlet上下文参数指定, 甚至作为JNDI中的一个条目设置(PropertySource
抽象) .
在集成测试中,可以通过 spring-test
模块中的 @ActiveProfiles
注解来声明 active 配置文件(参见使用环境配置文件的上下文配置)
配置文件不是 "二选一" 的. 开发者可以一次激活多个配置文件. 使用编程方式,您可以为 setActiveProfiles()
方法提供多个配置文件名称,该方法接受 String…
变量参数. 以下示例激活多个配置文件:
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
ctx.getEnvironment().setActiveProfiles("profile1", "profile2")
声明性地,spring.profiles.active
可以接受以逗号分隔的 profile 名列表,如以下示例所示:
-Dspring.profiles.active="profile1,profile2"
默认 Profile
default 配置文件表示默认开启的 profile 配置. 考虑以下配置:
@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();
}
}
@Configuration
@Profile("default")
class DefaultDataConfig {
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build()
}
}
如果没有配置文件激活,上面的 dataSource
就会被创建. 这提供了一种默认的方式,如果有任何一个配置文件启用,default 配置就不会生效.
默认配置文件的名字(default) 可以通过 Environment
的 setDefaultProfiles()
方法或者 spring.profiles.default
属性修改.
1.12.7. PropertySource
抽象
Spring的 Environment
抽象提供用于一系列的属性配置文件的搜索操作.请考虑以下列表:
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);
val ctx = GenericApplicationContext()
val env = ctx.environment
val containsMyProperty = env.containsProperty("my-property")
println("Does my environment contain the 'my-property' property? $containsMyProperty")
在上面的代码段中,一个高级别的方法用于访问 Spring 是否为当前环境定义了 my-property
属性. 为了回答这个问题,Environment
对象对一组 PropertySource
对象进行搜索.
PropertySource
是对任何键值对的简单抽象,Spring 的 StandardEnvironment
配置有两个 PropertySource
对象 ,一个表示JVM系统属性(System.getProperties()
),一个表示系统环境变量(System.getenv()
).
这些默认 property 源位于 StandardEnvironment 中,用于独立应用程序. StandardServletEnvironment 用默认的 property 配置源填充. 默认配置源包括 Servlet 配置和 Servlet 上下文参数,它可以选择启用 JndiPropertySource . 有关详细信息,请参阅它的 javadocs
|
具体地说,当您使用 StandardEnvironment
时,如果在运行时存在 my-property
系统属性或 my-property
环境变量,则对 env.containsProperty("my-property")
的调用将返回 true
.
执行的搜索是分层的. 默认情况下,系统属性优先于环境变量,因此如果在调用 对于常见的
|
最重要的是,整个机制都是可配置的. 也许开发者需要一个自定义的 properties 源,并将该源整合到这个检索层级中. 为此,请实现并实例化您自己的 PropertySource
,并将其添加到当前 Environment
的 PropertySource
集合中. 以下示例显示了如何执行此操作:
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
val ctx = GenericApplicationContext()
val sources = ctx.environment.propertySources
sources.addFirst(MyPropertySource())
在上面的代码中, MyPropertySource
在搜索中添加了最高优先级. 如果它包含 my-property
属性,则会检测并返回该属性, 优先于其他 PropertySource
中的任何 my-property
属性.
MutablePropertySources
API 暴露了许多方法,允许你显式操作 property 属性源.
1.12.8. 使用 @PropertySource
@PropertySource
注解提供了便捷的方式,用于增加 PropertySource
到Spring的 Environment
中.
给定一个名为 app.properties
的文件,其中包含键值对 testbean.name=myTestBean
, 以下 @Configuration
类使用 @PropertySource
,以便调用 testBean.getName()
返回 myTestBean:
@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;
}
}
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
class AppConfig {
@Autowired
private lateinit var env: Environment
@Bean
fun testBean() = TestBean().apply {
name = env.getProperty("testbean.name")!!
}
}
任何的存在于 @PropertySource
中的 ${…}
占位符,将会被解析为定义在环境中的属性配置文件中的属性值. 如以下示例所示:
@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;
}
}
@Configuration
@PropertySource("classpath:/com/\${my.placeholder:default/path}/app.properties")
class AppConfig {
@Autowired
private lateinit var env: Environment
@Bean
fun testBean() = TestBean().apply {
name = env.getProperty("testbean.name")!!
}
}
假设 my.placeholder
存在于已注册的其中一个属性源中(例如,系统属性或环境变量) ,则占位符将解析为相应的值. 如果不是,则 default/path
用作默认值. 如果未指定默认值且无法解析属性,则抛出 IllegalArgumentException
.
根据Java 8惯例,@PropertySource 注解是可重复的. 但是,所有这些 @PropertySource 注解都需要在同一级别声明,可以直接在配置类上声明, 也可以在同一自定义注解中作为元注解声明. 不建议混合直接注解和元注解,因为直接注解有效地覆盖了元注解.
|
1.12.9. 在声明中的占位符
之前,元素中占位符的值只能针对 JVM 系统属性或环境变量进行解析. 现在已经打破了这种情况. 因为环境抽象集成在整个容器中,所以很容易通过它来对占位符进行解析. 这意味着开发者可以以任何喜欢的方式来配置这个解析过程,可以改变是优先查找系统 properties 或者是有限查找环境变量,或者删除它们; 增加自定义 property 源,使之成为更合适的配置
具体而言,只要在 Environment
中可用,无论 customer
属性在何处定义,以下语句都可以工作:
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>
1.13. 注册 LoadTimeWeaver
LoadTimeWeaver
被 Spring 用来在将类加载到 Java 虚拟机(JVM) 中时动态地转换成 字节码 文件
若要开启加载时织入,要在 @Configuration
类中增加 @EnableLoadTimeWeaving
注解,如以下示例所示:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
@Configuration
@EnableLoadTimeWeaving
class AppConfig
或者,对于 XML 配置,您可以使用 context:load-time-weaver
元素:
<beans>
<context:load-time-weaver/>
</beans>
一旦配置为 ApplicationContext
,该 ApplicationContext
中的任何 bean 都可以实现 LoadTimeWeaverAware
,从而接收对 load-time weaver 实例的引用. 这特别适用于 Spring 对 JPA 支持.
其中 JPA 类转换可能需要加载时织入. 有关更多详细信息,请参阅 LocalContainerEntityManagerFactoryBean
. 有关 AspectJ 加载时编织的更多信息,请参阅在 Spring 框架中使用 AspectJ 的加载时织入.
1.14. ApplicationContext
的附加功能
正如 前面章节中讨论的,org.springframework.beans.factory
包提供了管理和操作 bean 的基本功能,包括以编程方式. org.springframework.context
包添加了 ApplicationContext
接口,该接口扩展了 BeanFactory
接口,此外还扩展了其他接口,
以更面向应用程序框架的方式提供其他功能. 许多人以完全声明的方式使用 ApplicationContext
, 甚至不以编程方式创建它,而是依赖于诸如 ContextLoader
之类的支持类来自动实例化 ApplicationContext
,作为 Java EE Web 应用程序的正常启动过程的一部分.
为了增强 BeanFactory
的功能,上下文包还提供了以下功能.
-
通过
MessageSource
接口访问 i18n 风格的消息. -
通过
ResourceLoader
接口访问 URL 和文件等资源. -
事件发布,即通过使用
ApplicationEventPublishe
r接口实现ApplicationListener
接口的 bean . -
通过
HierarchicalBeanFactory
接口,加载多级 contexts,允许关注某一层级 context,比如应用的 Web 层.
1.14.1. 使用 MessageSource
实现国际化
ApplicationContext
接口扩展了一个名为 MessageSource
的接口,因此提供了国际化(“i18n”)功能. Spring还提供了 HierarchicalMessageSource
接口,该接口可以分层次地解析消息. 这些接口共同提供了Spring影响消息解析的基础. 这些接口上定义的方法包括:
-
String getMessage(String code, Object[] args, String default, Locale loc)
: 用于从MessageSource
检索消息的基本方法. 如果未找到指定区域设置的消息,则使用默认消息. 传入的任何参数都使用标准库提供的MessageFormat
功能成为替换值. -
String getMessage(String code, Object[] args, Locale loc)
: 基本上与前一个方法相同,但有一个区别: 不能指定默认消息. 如果找不到该消息,则抛出NoSuchMessageException
. -
String getMessage(MessageSourceResolvable resolvable, Locale locale)
: 前面方法中使用的所有属性也包装在名为MessageSourceResolvable
的类中,您可以将此方法用于此类.
当一个 ApplicationContext
被加载时,它会自动搜索在上下文中定义的一个 MessageSource
, 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>
该示例假定您在类路径中定义了三个资源包,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
接口.
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);
}
fun main() {
val resources = ClassPathXmlApplicationContext("beans.xml")
val message = resources.getMessage("message", null, "Default", Locale.ENGLISH)
println(message)
}
上述程序产生的结果如下:
Alligators rock!
总而言之,MessageSource
在名为 beans.xml
的文件中定义,该文件存在于类路径的根目录中. messageSourcebean
定义通过其 basenames
属性引用许多资源包.
在列表中传递给 basenames
属性的三个文件作为类路径根目录下的文件存在,分别称为 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>
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);
}
}
class Example {
lateinit var messages: MessageSource
fun execute() {
val message = messages.getMessage("argument.required",
arrayOf("userDao"), "Required", Locale.ENGLISH)
println(message)
}
}
调用 execute()
方法得到的结果如下:
The userDao argument is required.
关于国际化 (“i18n”),Spring 的各种 MessageSource
实现遵循与标准 JDK ResourceBundle
相同的区域设置解析和回退规则.
简而言之,继续前面定义的示例 messageSource
,如果要根据 British(en-GB
)语言环境解析消息,则应分别创建名为 format_en_GB.properties
,exceptions_en_GB.properties
和 windows_en_GB.properties
的文件.
通常,区域设置解析由应用程序的环境配置管理. 在以下示例中,手动指定解析(英国) 消息的区域设置:
# in exceptions_en_GB.properties argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.
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);
}
fun main() {
val resources = ClassPathXmlApplicationContext("beans.xml")
val message = resources.getMessage("argument.required",
arrayOf("userDao"), "Required", Locale.UK)
println(message)
}
运行上述程序产生的结果如下:
Ebagum lad, the 'userDao' argument is required, I say, required.
您还可以使用 MessageSourceAware
接口获取对已定义的任何 MessageSource
的引用. 在创建和配置 bean 时,应用程序上下文的 MessageSource
会注入实现 MessageSourceAware
接口的 ApplicationContext
中定义的任何 bean .
因为 Spring 的 MessageSource 是基于 Java 的 ResourceBundle 的,所以没有合并具有相同名称的捆绑包,但只会使用找到的第一个捆绑包。
后面具有相同名称的包将被忽略。
|
- NOTE
-
作为
ResourceBundleMessageSource
的替代,Spring 提供了一个ReloadableResourceBundleMessageSource
类. 此变体支持相同的 bundle 文件格式,但比基于标准 JDK 的ResourceBundleMessageSource
实现更灵活. 特别是,它允许从任何 Spring 资源位置(不仅从类路径) 读取文件,并支持 bundle 属性文件的热重新加载(同时在其间有效地缓存它们) . 有关详细信息,请参阅ReloadableResourceBundleMessageSource
javadoc.
1.14.2. 标准和自定义事件
ApplicationContext
中的事件处理是通过 ApplicationEvent
类和 ApplicationListener
接口提供的. 如果将实现 ApplicationListener
接口的 bean 部署到上下文中,则每次将 ApplicationEvent
发布到 ApplicationContext
时,都会通知该 bean . 从本质上讲,这是标准的 Observer 设计模式.
从 Spring 4.2 开始,事件架构已经得到显着改进,并提供了一个基于注解的模型 使其有发布任意事件的能力(即,不一定从 ApplicationEvent 扩展的对象) . 当发布这样的对象时,我们将它包装在一个事件中.
|
下表描述了 Spring 提供的标准事件:
事件 | 说明 |
---|---|
|
初始化或刷新 |
|
通过使用 |
|
通过使用 |
|
通过使用 |
|
一个特定于Web的事件,告诉所有 bean 已经为 HTTP 请求提供服务. 请求完成后发布此事件. 此事件仅适用于使用 Spring 的 DispatcherServlet 的 Web 应用程序. |
|
|
您还可以创建和发布自己的自定义事件. 以下示例显示了一个扩展 Spring 的 ApplicationEvent
基类的简单类:
public class BlockedListEvent extends ApplicationEvent {
private final String address;
private final String content;
public BlockedListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}
// accessor and other methods...
}
class BlockedListEvent(source: Any,
val address: String,
val content: String) : ApplicationEvent(source)
要发布自定义 ApplicationEvent
,请在 ApplicationEventPublisher
上调用 publishEvent()
方法. 通常,这是通过创建一个实现 ApplicationEventPublisherAware
并将其注册为 Spring bean 的类来完成的. 以下示例显示了这样一个类:
public class EmailService implements ApplicationEventPublisherAware {
private List<String> blockedList;
private ApplicationEventPublisher publisher;
public void setBlockedList(List<String> blockedList) {
this.blockedList = blockedList;
}
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEmail(String address, String content) {
if (blockedList.contains(address)) {
publisher.publishEvent(new BlockedListEvent(this, address, content));
return;
}
// send email...
}
}
class EmailService : ApplicationEventPublisherAware {
private lateinit var blockedList: List<String>
private lateinit var publisher: ApplicationEventPublisher
fun setBlockedList(blockedList: List<String>) {
this.blockedList = blockedList
}
override fun setApplicationEventPublisher(publisher: ApplicationEventPublisher) {
this.publisher = publisher
}
fun sendEmail(address: String, content: String) {
if (blockedList!!.contains(address)) {
publisher!!.publishEvent(BlockedListEvent(this, address, content))
return
}
// send email...
}
}
在配置时,Spring 容器检测到 EmailService
实现 ApplicationEventPublisherAware
并自动调用 setApplicationEventPublisher()
. 实际上,传入的参数是 Spring 容器本身. 您正在通过其 ApplicationEventPublisher
接口与应用程序上下文进行交互.
要接收自定义 ApplicationEvent
,您可以创建一个实现 ApplicationListener
的类并将其注册为 Spring bean. 以下示例显示了这样一个类:
public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
class BlockedListNotifier : ApplicationListener<BlockedListEvent> {
lateinit var notificationAddres: String
override fun onApplicationEvent(event: BlockedListEvent) {
// 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()
方法时,如果有任何应列入 blocked 的电子邮件消息,则会发布 BlockedListEvent
类型的自定义事件. blockedListNotifier
bean 注册为 ApplicationListener
并接收 BlockedListEvent
,此时它可以通知相关方.
Spring 的事件机制是为在同一应用程序上下文中的 Spring bean 之间的简单通信而设计的. 但是,对于更复杂的企业集成需求,单独维护的 Spring Integration 项目提供了完整的支持并可用于构建轻量级, pattern-oriented(面向模式) ,依赖 Spring 编程模型的事件驱动架构. |
基于注解的事件监听器
从 Spring 4.2 开始,您可以使用 @EventListener
注解在托管 bean 的任何 public 方法上注册事件监听器. BlockedListNotifier
可以重写如下:
public class BlockedListNotifier {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
@EventListener
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
class BlockedListNotifier {
lateinit var notificationAddress: String
@EventListener
fun processBlockedListEvent(event: BlockedListEvent) {
// notify appropriate parties via notificationAddress...
}
}
方法参数为它监听的事件类型,但这次使用灵活的名称并且没有实现特定的监听器接口. 只要实际事件类型在其实现层次结构中解析通用参数,也可以通过泛型缩小事件类型.
如果您的方法应该监听多个事件,或者您想要根据任何参数进行定义,那么也可以在注解本身上指定事件类型. 以下示例显示了如何执行此操作:
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
// ...
}
@EventListener(ContextStartedEvent::class, ContextRefreshedEvent::class)
fun handleContextStart() {
// ...
}
还可以通过使用定义 SpEL
表达式的注解的 condition 属性来添加额外的运行时过滤,该表达式应匹配以实际调用特定事件的方法.
以下示例显示了仅当事件的 content
属性等于 my-event
时才能重写我们的通知程序以进行调用:
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blockedListEvent) {
// notify appropriate parties via notificationAddress...
}
@EventListener(condition = "#blEvent.content == 'my-event'")
fun processBlockedListEvent(blockedListEvent: BlockedListEvent) {
// notify appropriate parties via notificationAddress...
}
每个 SpEL
表达式都针对专用上下文进行评估. 下表列出了可用于上下文的项目,以便您可以将它们用于条件事件处理:
-
事件 SpEL 可用的元数据
名字 | 位置 | 描述 | 例子 |
---|---|---|---|
Event |
root object |
真实的 |
|
Arguments array |
root object |
用于调用目标的参数(作为数组) |
|
Argument name |
evaluation context |
任何方法参数的名称. 如果由于某种原因,名称不可用(例如,因为没有调试信息) ,参数名称也可以在 |
|
请注意,即使您的方法签名实际引用已发布的任意对象,#root.event
也允许您访问基础事件.
如果需要发布一个事件作为处理另一个事件的结果,则可以更改方法签名以返回应发布的事件,如以下示例所示:
@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
@EventListener
fun handleBlockedListEvent(event: BlockedListEvent): ListUpdateEvent {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
异步监听器不支持此功能. |
这将通过 handleBlockedListEvent
方法处理每个 BlockedListEvent
并发布一个新的 ListUpdateEvent
,如果需要发布多个 Collection
,则可以返回事件 集合.
异步的监听器
如果希望特定监听器异步处理事件,则可以使用常规 @Async
支持.. 以下示例显示了如何执行此操作:
@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
// BlockedListEvent is processed in a separate thread
}
@EventListener
@Async
fun processBlockedListEvent(event: BlockedListEvent) {
// BlockedListEvent is processed in a separate thread
}
使用异步事件时请注意以下限制:
-
如果事件监听器抛出
Exception
,则不会将其传播给调用者. 有关更多详细信息,请参阅AsyncUncaughtExceptionHandler
. -
此类事件监听器无法发送回复. 如果您需要作为处理结果发送另一个事件,请注入
ApplicationEventPublisher
以手动发送事件.
监听器的排序
如果需要在另一个监听器之前调用一个监听器,则可以将 @Order
注解添加到方法声明中,如以下示例所示:
@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
@EventListener
@Order(42)
fun processBlockedListEvent(event: BlockedListEvent) {
// notify appropriate parties via notificationAddress...
}
泛型事件
您还可以使用泛型来进一步定义事件的结构. 考虑使用 EntityCreatedEvent<T>
,其中 T
是创建的实际实体的类型. 例如,您可以创建以下监听器定义以仅接收 Person
的 EntityCreatedEvent
:
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
// ...
}
@EventListener
fun onPersonCreated(event: EntityCreatedEvent<Person>) {
// ...
}
由于泛型擦除,只有此事件符合事件监听器所过滤的通用参数条件那么才会触发相应的处理事件(有点类似于 class PersonCreatedEvent extends EntityCreatedEvent<Person> { … }
)
在某些情况下,如果所有事件遵循相同的结构(如上述事件的情况) ,这可能变得相当乏味. 在这种情况下,开发者可以实现 ResolvableTypeProvider
来引导框架超出所提供的运行时环境范围.
public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {
public EntityCreatedEvent(T entity) {
super(entity);
}
@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
}
}
class EntityCreatedEvent<T>(entity: T) : ApplicationEvent(entity), ResolvableTypeProvider {
override fun getResolvableType(): ResolvableType? {
return ResolvableType.forClassWithGenerics(javaClass, ResolvableType.forInstance(getSource()))
}
}
这不仅适用于 ApplicationEvent ,也适用于作为事件发送的任意对象.
|
1.14.3. 通过便捷的方式访问底层资源
为了最佳地使用和理解应用程序上下文,您应该熟悉 Spring 的抽象 Resource
,如参考资料中所述资源(Resources).
应用程序上下文是 ResourceLoader
,可用于加载 Resource
对象. Resource
本质上是 JDK java.net.URL
类的功能更丰富的版本. 实际上 Resource
的实现类中大多含有 java.net.URL
的实例.
Resource
几乎能从任何地方透明的获取底层资源,可以是 classpath
类路径、文件系统、标准的 URL 资源及变种 URL 资源. 如果资源定位字串是简单的路径, 没有任何特殊前缀,就适合于实际应用上下文类型.
可以配置一个 bean 部署到应用上下文中,用以实现特殊的回调接口,ResourceLoaderAware
它会在初始化期间自动回调. 应用程序上下文本身作为 ResourceLoader
传入. 可以暴露 Resource
的 type
属性,这样就可以访问静态资源 静态资源可以像其他 properties
那样被注入 Resource
.
可以使用简单的字串路径指定资源,这需要依赖于特殊的 JavaBean PropertyEditor
(由上下文自动注册) ,当 bean 部署时候它将转换资源中的字串为实际的资源对象.
提供给 ApplicationContext
构造函数的一个或多个位置路径实际上是资源字符串,并且以简单形式对特定上下文实现进行适当处理. ClassPathXmlApplicationContext
将一个简单的定位路径视为类路径位置. 开发者还可以使用带有特殊前缀的定位路径,这样就可以强制从 classpath
或者 URL
定义加载路径, 而不用考虑实际的上下文类型.
1.14.4. 应用程序启动跟踪
ApplicationContext
管理 Spring 应用程序的生命周期, 并提供丰富的编程模型.因此, 复杂的应用程序可以同时拥有
复杂的组件依赖和启动阶段.
使用特定指标跟踪应用程序的启动步骤可以帮助您了解在启动阶段的那块花了一些时间, 它也是了解整体上下文生命周期更好的方法.
AbstractApplicationContext
(及其子类)配有一个 ApplicationStartup
, 它收集有关各个启动阶段的 StartupStep
数据:
-
应用程序上下文生命周期(基本软件包扫描, 配置类管理)
-
bean生命周期 (instantiation, smart initialization, post processing)
-
应用程序事件处理
这是 AnnotationConfigApplicationContext
中的检测示例:
// create a startup step and start recording
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages);
// end the current step
scanPackages.end();
// create a startup step and start recording
val scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan")
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages))
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages)
// end the current step
scanPackages.end()
应用程序上下文已经通过多个步骤进行了检测. 记录后, 可以使用特定工具收集, 显示和分析这些启动步骤. 有关现有启动步骤的完整列表, 您可以查看 此部分.
默认的 ApplicationStartup
实现没有做任何操作.这意味着默认情况下, 在应用程序启动期间不会收集任何指标.
Spring Framework 附带了一个用于使用 Java Flight Recorder 跟踪启动步骤的实现:
FlightRecorderApplicationStartup
. 要使用此实现, 必须配置它的一个实例,创建后立即添加到 ApplicationContext
.
如果开发人员提供自己的应用程序, 那么他们也可以使用 ApplicationStartup
基础结构 AbstractApplicationContext
子类, 或者如果他们希望收集更精确的数据.
ApplicationStartup 只能在应用程序启动期间使用, 用于核心容器;这绝不是 Java profilers 的替代品, 也不是
metrics libraries, 例如 Micrometer.
|
要开始收集自定义的 StartupStep
, 组件可以获取 ApplicationStartup
直接从应用程序上下文实例, 使其组件实现 ApplicationStartupAware
, 或在任何注入点上请求 ApplicationStartup
类型.
创建自定义启动步骤时, 开发人员不应使用 "spring.*" 命名空间,该名称空间保留给 Spring 内部使用, 并且可能会发生变化.
|
1.14.5. 快速对 Web 应用的 ApplicationContext 实例化
开发者可以通过使用 ContextLoader
来声明性地创建 ApplicationContext
实例,当然也可以通过使用 ApplicationContext
的实现来编程实现 ApplicationContext
.
您可以使用 ContextLoaderListener
注册 ApplicationContext
,如以下示例所示:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
监听器检查 contextConfigLocation
参数. 如果参数不存在,则监听器将 /WEB-INF/applicationContext.xml
用作默认值. 当参数确实存在时,监听器使用预定义的分隔符(逗号,分号和空格) 分隔 String,并将值用作搜索应用程序上下文的位置.
还支持 Ant 样式的路径模式. 示例是 /WEB-INF/*Context.xml
(对于所有名称以 Context.xml
结尾且位于 WEB-INF
目录中的文件) 和 /WEB-INF/**/*Context.xml
(对于所有这样的文件) WEB-INF
的任何子目录中的文件.
1.14.6. 使用Java EE RAR文件部署Spring的 ApplicationContext
可以将 Spring ApplicationContext 部署为 RAR 文件,将上下文及其所有必需的 bean 类和库 JAR 封装在 Java EE RAR 部署单元中,这相当于引导一个独立的 ApplicationContext
. 只是托管在 Java EE 环境中,能够访问 Java EE 服务器设施. 在部署无头WAR文件(实际上,没有任何 HTTP 入口点,仅用于在 Java EE 环境中引导 Spring ApplicationContext
的 WAR 文件) 的情况下RAR部署是更自然的替代方案
RAR 部署非常适合不需要 HTTP 入口点但仅由消息端点和调度作业组成的应用程序上下文. 在这种情况下,Bean 可以使用应用程序服务器资源, 例如 JTA 事务管理器和 JNDI 绑定的 JDBC DataSource
和 JMS ConnectionFactory
实例,
并且还可以通过 Spring 的标准事务管理和 JNDI 和 JMX 支持设施向平台的JMX服务器注册. 应用程序组件还可以通过 Spring 的 TaskExecutor
抽象实现与应用程序服务器的 JCA WorkManager
交互
有关 RAR 部署中涉及的配置详细信息,请参阅 SpringContextResourceAdapter
类的javadoc
对于将 Spring ApplicationContext 简单部署为 Java EE RAR 文件:
-
将所有应用程序类打包到一个 RAR 文件(这是一个具有不同文件扩展名的标准JAR文件) . .
-
将所有必需的库 JAR 添加到 RAR 存档的根目录中.
-
添加
META-INF/ra.xml
部署描述符 (如SpringContextResourceAdapter
的 javadoc 所示) 和相应的 Spring XML bean 定义文件(通常是META-INF/applicationContext.xml
) . -
将生成的RAR文件放入应用程序服务器的部署目录中.
这种 RAR 部署单元通常是独立的. 它们不会将组件暴露给外部世界,甚至不会暴露给同一应用程序的其他模块. 与基于 RAR 的 ApplicationContext 的交互通常通过与其他模块共享的 JMS 目标进行. 例如,基于 RAR 的 ApplicationContext 还可以调度一些作业或对文件系统(等等) 中的新文件作出响应. 如果它需要允许来自外部的同步访问,它可以(例如) 导出RMI端点,这可以由同一台机器上的其他应用程序模块使用.
|
1.15. BeanFactory
API
BeanFactory
API为 Spring 的 IoC 功能提供了基础. 它的特定契约主要用于与 Spring 的其他部分和相关的第三方框架集成其 DefaultListableBeanFactory
实现是更高级别 GenericApplicationContext
容器中的密钥委托.
BeanFactory
和相关接口(例如 BeanFactoryAware
, InitializingBean
,DisposableBean
) 是其他框架组件的重要集成点. 通过不需要任何注解或甚至反射,它们允许容器与其组件之间的非常有效的交互. 应用程序级 bean 可以使用相同的回调接口,但通常更喜欢通过注解或通过编程配置进行声明性依赖注入.
请注意,核心 BeanFactory
API 级别及其 DefaultListableBeanFactory
实现不会对配置格式或要使用的任何组件注解做出假设. 所有这些风格都通过扩展(例如 XmlBeanDefinitionReader
和 AutowiredAnnotationBeanPostProcessor
) 进行,并作为核心元数据表示在共享 BeanDefinition
对象上运行. 这是使 Spring 的容器如此灵活和可扩展的本质.
1.15.1. 选择 BeanFactory
还是 ApplicationContext
?
本节介绍 BeanFactory
和 ApplicationContext
容器级别之间的差异以及影响.
您应该使用 ApplicationContext
,除非您有充分的理由不这样做,使用 GenericApplicationContext
及其子类 AnnotationConfigApplicationContext
作为自定义引导的常见实现.
这些是 Spring 用于所有常见目的的核心容器的主要入口点: 加载配置文件,触发类路径扫描,以编程方式注册 bean 定义和带注解的类,以及(从 5.0 开始) 注册功能 bean 定义.
因为 ApplicationContext
包括 BeanFactory
的所有功能,和 BeanFactory
相比更值得推荐,除了一些特定的场景,例如在资源受限的设备上运行的内嵌的应用.
在 ApplicationContext
(例如 GenericApplicationContext
实现) 中,按照约定(即通过 bean 名称或 bean 类型 - 特别是后处理器) 检测到几种 bean , 而普通的 DefaultListableBeanFactory
对任何特殊 bean 都是不可知的.
对于许多扩展容器功能,例如注解处理和 AOP 代理, BeanPostProcessor
的扩展点是必不可少的. 如果仅使用普通的 DefaultListableBeanFactory
,则默认情况下不会检测到并激活此类后置处理器.
这种情况可能令人困惑,因为您的 bean 配置实际上没有任何问题. 相反,在这种情况下,容器需要至少得多一些额外的处理.
下表列出了 BeanFactory
和 ApplicationContext
接口和实现提供的功能.
特性 | BeanFactory |
ApplicationContext |
---|---|---|
Bean实例化/装配 |
Yes |
Yes |
集成生命周期管理 |
No |
Yes |
自动注册 |
No |
Yes |
自动注册 |
No |
Yes |
便利的 |
No |
Yes |
内置 |
No |
Yes |
要使用 DefaultListableBeanFactory
显式注册 bean 的后置处理器,您需要以编程方式调用 addBeanPostProcessor
,如以下示例所示:
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions
// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());
// now start using the factory
val factory = DefaultListableBeanFactory()
// populate the factory with bean definitions
// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(AutowiredAnnotationBeanPostProcessor())
factory.addBeanPostProcessor(MyBeanPostProcessor())
// now start using the factory
要将 BeanFactoryPostProcessor
应用于普通的 DefaultListableBeanFactory
,需要调用其 postProcessBeanFactory
方法,如以下示例所示:
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));
// bring in some property values from a Properties file
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));
// now actually do the replacement
cfg.postProcessBeanFactory(factory);
val factory = DefaultListableBeanFactory()
val reader = XmlBeanDefinitionReader(factory)
reader.loadBeanDefinitions(FileSystemResource("beans.xml"))
// bring in some property values from a Properties file
val cfg = PropertySourcesPlaceholderConfigurer()
cfg.setLocation(FileSystemResource("jdbc.properties"))
// now actually do the replacement
cfg.postProcessBeanFactory(factory)
在这两种情况下,显示注册步骤都不方便,这就是为什么各种 ApplicationContext
变体优先于 Spring 支持的应用程序中的普通 DefaultListableBeanFactory
, 尤其是在典型企业设置中依赖 BeanFactoryPostProcessor
和 BeanPostProcessor
实例来扩展容器功能时.
|
2. 资源(Resources)
本章介绍 Spring 如何处理资源以及如何在 Spring 中使用资源. 它包括以下主题:
2.1. 介绍
遗憾的是,Java 的标准 java.net.URL
类和各种 URL 前缀的标准处理程序不足以完全访问底层资源. 例如,没有标准化的 URL
实现可用于访问需要从类路径或相对于 ServletContext
获取的资源.
虽然可以为专用 URL
前缀注册新的处理程序(类似于 http:
)这样的前缀的现有处理程序,但这通常非常复杂,并且 URL
接口仍然缺少一些理想的功能,例如检查当前资源是否存在的方法.
2.2. Resource
接口
位于 org.springframework.core.io.
包中的 Spring Resource
接口的目标是成为一个更强大的接口,用于抽象对底层资源的访问. 以下清单显示了 Resource
接口定义,见 Resource
Javadoc 了解更多详细信息:
public interface Resource extends InputStreamSource {
boolean exists();
boolean isReadable();
boolean isOpen();
boolean isFile();
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFile() throws IOException;
ReadableByteChannel readableChannel() throws IOException;
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename();
String getDescription();
}
正如 Resource
接口的定义所示,它扩展了 InputStreamSource
接口. 以下清单显示了 InputStreamSource
接口的定义:
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
Resource
接口中一些最重要的方法是:
-
getInputStream()
: 用于定位和打开当前资源, 返回当前资源的InputStream
,预计每一次调用都会返回一个新的InputStream
. 因此调用者必须自行关闭当前的输出流. -
exists()
: 返回boolean
值,表示当前资源是否存在. -
isOpen()
:返回boolean
值,表示当前资源是否有已打开的输入流. 如果为true
,那么InputStream
不能被多次读取 ,只能在一次读取后即关闭以防止内存泄漏. 除了InputStreamResource
外,其他常用Resource
实现都会返回false
. -
getDescription()
: 返回当前资源的描述,当处理资源出错时,资源的描述会用于输出错误的信息. 一般来说,资源的描述是一个完全限定的文件名称,或者是当前资源的真实 URL.
其他方法允许您获取表示资源的实际 URL
或 File
对象(如果底层实现兼容并支持该功能) .
Resource
接口的某些实现也实现了扩展 WritableResource
接口,用于支持对资源的写入.
在 Spring 里, Resource
抽象有着相当广泛的使用,例如,当需要某个资源时, Resource
可以当作方法签名里的参数类型被使用. 在 Spring API 中,有些方法(例如各种 ApplicationContext
实现的构造函数) 会直接采用普通格式的 String 路径来创建合适的 Resource
,调用者也可以通过在路径里带上指定的前缀来创建特定的 Resource
实现.
不但 Spring 内部和使用 Spring 的应用都大量地使用了 Resource
接口,而且开发者在应用代码中将它作为一个通用的工具类也是非常通用的. 当你仅需要使用到 Resource
接口实现时,
可以直接忽略 Spring 的其余部分.虽然这样会与 Spring 耦合,但是也只是耦合一部分而已. 使用这些 Resource
实现代替底层的访问是极其美好的. 这与开发者引入其他库的目的也是一样的
Resource 抽象不会取代功能. 它尽可能地包裹它. 例如,UrlResource 包装URL并使用包装的 URL 来完成其工作.
|
2.3. 内置 Resource
实现
有关 Spring中 可用的 Resource
实现的完整列表, 请参阅的 "All Known Implementing Classes" 部分 Resource
Javadoc.
2.3.1. UrlResource
UrlResource
封装了 java.net.URL
用来访问正常 URL 的任意对象. 例如 file:
,HTTPS 目标,FTP 目标等.
所有的 URL 都可以用标准化的字符串来表示,例如通过正确的标准化前缀. 可以用来表示当前 URL 的类型. 这包括 file:
,用于访问文件系统路径,https:
用于通过 HTTPS 协议访问资源,ftp:
用于通过 FTP 访问资源,以及其他.
通过 java 代码可以显式地使用 UrlResource
构造函数来创建 UrlResource
,但也可以调用 API 方法来使用代表路径的 String 参数来隐式创建 UrlResource
.
对于后一种情况,JavaBeans PropertyEditor
最终决定要创建哪种类型的 Resource
. 如果路径字符串包含众所周知的(对于它,那么) 前缀(例如 classpath:
),它会为该前缀创建适当的专用 Resource
.但是,如果它无法识别前缀,则假定该字符串是标准URL字符串并创建 UrlResource
.
2.3.2. ClassPathResource
ClassPathResource
代表从类路径中获取资源,它使用线程上下文加载器,指定类加载器或给定 class 类来加载资源.
当类路径上资源存于文件系统中时,ClassPathResource
支持使用 java.io.File
来访问. 但是当类路径上的资源位于未解压(没有被 Servlet
引擎或其他可解压的环境解压) 的 jar 包中时,
ClassPathResource
就不再支持以 java.io.File
的形式访问. 鉴于此,Spring中各种 Resource
的实现都支持以 java.net.URL
的形式访问资源.
可以显式使用 ClassPathResource
构造函数来创建 ClassPathResource
,但是更多情况下,是调用 API 方法使用的. 即使用一个代表路径的 String 参数来隐式创建 ClassPathResource
.
对于后一种情况,将会由JavaBeans的 PropertyEditor
来识别路径中 classpath:
前缀,并创建 ClassPathResource
.
2.3.3. FileSystemResource
FileSystemResource
是用于处理 java.io.File
和 java.nio.file.Path
的实现.Spring 的 String 的标准路径
字符串转换, 但通过 java.nio.file.Files
API 执行所有操作. 对于纯基于 java.nio.path.Path
的支持改为使用 PathResource
. 显然,它同时能解析作为 File
和作为 URL
的资源.
2.3.4. PathResource
这是 Resource
用于处理 java.nio.file.Path
的实现, 执行所有通过 Path
API 进行操作和转换. 它支持解析为 File
, 并且
作为 URL
, 并且实现了扩展的 WritableResource
接口. PathResource
实际上是 FileSystemResource
的纯基于 java.nio.path.Path
的替代品, 它具有不同的 createRelative
行为.
2.3.5. ServletContextResource
这是 ServletContext
资源的 Resource
实现,用于解释相关 Web 应用程序根目录中的相对路径.
ServletContextResource
完全支持以流和 URL 的方式访问资源,但只有当 Web 项目是解压的(不是以 war 等压缩包形式存在) ,而且该 ServletContext
资源必须位于文件系统中,
它支持以 java.io.File
的方式访问资源. 无论它是在文件系统上扩展还是直接从 JAR 或其他地方(如数据库) (可以想象) 访问,实际上都依赖于 Servlet 容器.
2.4. ResourceLoader
ResourceLoader
接口用于加载 Resource
对象,换句话说,就是当一个对象需要获取 Resource
实例时,可以选择实现 ResourceLoader
接口,以下清单显示了 ResourceLoader
接口定义: .
public interface ResourceLoader {
Resource getResource(String location);
ClassLoader getClassLoader();
}
所有应用程序上下文都实现 ResourceLoader
接口. 因此,可以使用所有应用程序上下文来获取 Resource
实例.
当在特殊的应用上下文中调用 getResource()
方法以及指定的路径没有特殊前缀时,将返回适合该特定应用程序上下文的 Resource
类型. 例如,假设针对 ClassPathXmlApplicationContext
实例执行了以下代码片段:
Resource template = ctx.getResource("some/resource/path/myTemplate.txt");
val template = ctx.getResource("some/resource/path/myTemplate.txt")
针对 ClassPathXmlApplicationContext
,该代码返回 ClassPathResource
. 如果对 FileSystemXmlApplicationContext
实例执行相同的方法,它将返回 FileSystemResource
.
对于 WebApplicationContext
,它将返回 ServletContextResource
. 它同样会为每个上下文返回适当的对象.
因此,您可以以适合特定应用程序上下文的方式加载资源.
另一方面,您可以通过指定特殊的 classpath:
前缀来强制使用 ClassPathResource
,而不管应用程序上下文类型如何,如下例所示:
Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");
val template = ctx.getResource("classpath:some/resource/path/myTemplate.txt")
同样,您可以通过指定任何标准 java.net.URL
前缀来强制使用 UrlResource
. 以下对示例使用 file
和 https
前缀:
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");
val template = ctx.getResource("file:///some/resource/path/myTemplate.txt")
Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt");
val template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt")
下表总结了将: String
对象转换为 Resource
对象的策略:
前缀 | 示例 | 解释 |
---|---|---|
classpath: |
|
从类路径加载 |
file: |
|
从文件系统加载为 |
https: |
|
作为 |
(none) |
|
取决于底层的 |
2.5. ResourcePatternResolver
接口
ResourcePatternResolver
接口是对 ResourceLoader
接口的扩展.
它定义了一种解决位置模式的策略 (例如, Ant
样式的路径模式) 转换为 Resource
对象.
public interface ResourcePatternResolver extends ResourceLoader {
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
Resource[] getResources(String locationPattern) throws IOException;
}
如上所示, 该接口还定义了一个特殊的 classpath*:
资源前缀,用于类路径中的所有匹配资源. 请注意, 在这种情况下, 应该是没有占位符的路径-例如, classpath*:/config/beans.xml
类路径中的 JAR 文件或其他目录可以
包含具有相同路径和相同名称的多个文件. 请查看 使用通配符构造应用上下文 及其子小节, 以获取更多详细信息,支持带有 classpath*:
资源前缀的通配符.
传入的 ResourceLoader
(例如, 可以通过检查 ResourceLoaderAware
语义)
它也实现了这个扩展接口.
PathMatchingResourcePatternResolver
是一个独立的实现, 可以使用 在 ApplicationContext
之外, 并且 ResourceArrayPropertyEditor
还用于 填充 Resource[]
bean 属性. PathMatchingResourcePatternResolver
能够 将指定的资源位置路径解析为一个或多个匹配的 Resource
对象. 源路径可以是简单路径, 具有与目标一一对应的映射`Resource`, 或者可能包含特殊的 classpath*:
前缀 和/或 内部 Ant 风格的正则表达式(使用 Spring 的 org.springframework.util.AntPathMatcher
匹配). 后者都是有效的通配符.
实际上, 任何标准 |
2.6. ResourceLoaderAware
接口
ResourceLoaderAware
是一个特殊的标识接口,用来提供 ResourceLoader
引用的对象. 以下清单显示了 ResourceLoaderAware
接口的定义:
public interface ResourceLoaderAware {
void setResourceLoader(ResourceLoader resourceLoader);
}
当类实现 ResourceLoaderAware
并部署到应用程序上下文(作为 Spring 管理的 bean) 时,它被应用程序上下文识别为 ResourceLoaderAware
.
然后,应用程序上下文调用 setResourceLoader(ResourceLoader)
,将其自身作为参数提供(请记住,Spring 中的所有应用程序上下文都实现了 ResourceLoader
接口) .
由于 ApplicationContext
实现了 ResourceLoader
,因此 bean 还可以实现 ApplicationContextAware
接口并直接使用提供的应用程序上下文来加载资源. 但是,通常情况下,如果您需要,最好使用专用的 ResourceLoader
接口.
代码只能耦合到资源加载接口(可以被认为是实用程序接口) ,而不能耦合到整个Spring ApplicationContext
接口.
除了实现 ResourceLoaderAware
接口,还可以采取另外一种替代方案-依赖 ResourceLoader
的自动装配. "传统" 构造函数和 byType 自动装配模式都支持对ResourceLoader 的装配. 前者是以构造参数的形式装配,
后者作为 setter 方法的参数参与装配. 如果为了获得更大的灵活性(包括属性注入的能力和多参方法) ,可以考虑使用基于注解的新型注入方式. 使用注解@Autowired标识 ResourceLoader
变量,便可将其注入到成员属性、构造参数或方法参数中.
这些参数需要 ResourceLoader
类型. 有关更多信息,请参阅使用@Autowired.
为包含通配符的资源路径加载一个或多个 Resource 对象或使用特殊的 classpath*: 资源前缀, 请考虑使用以下实例:ResourcePatternResolver 自动连接到您的应用程序组件而不是 ResourceLoader .
|
2.7. 资源依赖
如果 bean 本身要通过某种动态过程来确定和提供资源路径,那么 bean 使用 ResourceLoader
或 ResourcePatternResolver
接口来加载资源就变得更有意义了. 假如需要加载某种类型的模板,其中所需的特定资源取决于用户的角色 .
如果资源是静态的,那么完全可以不使用 ResourceLoader
(or ResourcePatternResolver
interface) 接口,只需让 bean 暴露它需要的 Resource
属性,并按照预期注入属性即可.
是什么使得注入这些属性变得如此简单? 是因为所有应用程序上下文注册和使用一个特殊的 PropertyEditor
JavaBean,它可以将 String
paths
转换为 Resource
对象. 因此,如果 myBean
有一个类型为 Resource
的 template
属性. 如下所示:
public class MyBean {
private Resource template;
public setTemplate(Resource template) {
this.template = template;
}
// ...
}
class MyBean(var template: Resource)
在 XML 配置文件中, 它可以用一个简单的字符串配置该资源, 如以下示例所示:
<bean id="myBean" class="example.MyBean">
<property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>
请注意,资源路径没有前缀. 因此,因为应用程序上下文本身将用作 ResourceLoader
, 所以资源本身通过 ClassPathResource
,FileSystemResource
或 ServletContextResource
加载,具体取决于上下文的确切类型.
如果需要强制使用特定的 Resource
类型,则可以使用前缀. 以下两个示例显示如何强制 ClassPathResource
和 UrlResource
(后者用于访问文件系统文件) :
<property name="template" value="classpath:some/resource/path/myTemplate.txt">
<property name="template" value="file:///some/resource/path/myTemplate.txt"/>
如果将 MyBean
类重构为与注解驱动的配置一起使用, 则 myTemplate.txt
的路径可以存储在名为 template.path
的 key 下-例如, 在可用于 Spring Environment
的属性文件中 (请参见[beans-environment]) . 然后可以通过 @Value
引用模板路径.
使用属性占位符的注解 (请参见 @Value
) . Spring 会以字符串形式获取模板路径的值, 特殊的 PropertyEditor
将字符串转换为 Resource
对象, 以注入到 MyBean
构造函数中.
下面的示例演示如何实现此目的.
@Component
public class MyBean {
private final Resource template;
public MyBean(@Value("${template.path}") Resource template) {
this.template = template;
}
// ...
}
@Component
class MyBean(@Value("\${template.path}") private val template: Resource)
如果我们要支持在多个路径下的同一路径下发现的多个模板类路径中的位置-例如, 类路径中的多个 jar 中-我们可以使用特殊的 classpath*:
前缀和通配符将 templates.path
key 定义为 classpath*:/config/templates/*.txt
. 如果我们按照以下方式重新定义 MyBean
类, Spring 会将模板路径模式转换为一系列的 Resource
对象可以注入 MyBean
的构造函数中.
@Component
public class MyBean {
private final Resource[] templates;
public MyBean(@Value("${templates.path}") Resource[] templates) {
this.templates = templates;
}
// ...
}
@Component
class MyBean(@Value("\${templates.path}") private val templates: Resource[])
2.8. 应用上下文和资源路径
本节介绍如何使用资源创建应用程序上下文,包括使用XML的快捷方式,如何使用通配符以及其他详细信息.
2.8.1. 构造应用上下文
应用程序上下文构造函数(对于特定的应用程序上下文类型) 通常将字符串或字符串数组作为资源的位置路径,例如构成上下文定义的 XML 文件.
当指定的位置路径没有带前缀时,那么从指定位置路径创建 Resource
类型(用于后续加载 bean 定义) ,具体取决于所使用应用上下文. 例如,请考虑以下示例,该示例创建 ClassPathXmlApplicationContext
:
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");
val ctx = ClassPathXmlApplicationContext("conf/appContext.xml")
bean 定义是从类路径加载的,因为使用了 ClassPathResource
. 但是,请考虑以下示例,该示例创建 FileSystemXmlApplicationContext
:
ApplicationContext ctx =
new FileSystemXmlApplicationContext("conf/appContext.xml");
val ctx = FileSystemXmlApplicationContext("conf/appContext.xml")
现在,bean 定义是从文件系统位置加载的(在这种情况下,相对于当前工作目录) .
若位置路径带有 classpath
前缀或 URL
前缀,会覆盖默认创建的用于加载 bean 定义的 Resource
类型. 请考虑以下示例:
ApplicationContext ctx =
new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");
val ctx = FileSystemXmlApplicationContext("classpath:conf/appContext.xml")
使用 FileSystemXmlApplicationContext
从类路径加载 bean 定义. 但是,它仍然是 FileSystemXmlApplicationContext
. 如果它随后用作 ResourceLoader
,则任何未加前缀的路径仍被视为文件系统路径.
构造 ClassPathXmlApplicationContext
实例的快捷方式
ClassPathXmlApplicationContext
提供了多个构造函数,以利于快捷创建 ClassPathXmlApplicationContext
的实例. 基础的想法是, 使用只包含多个 XML 文件名(不带路径信息) 的字符串数组和一个 Class
参数的构造器,所省略路径信息 ClassPathXmlApplicationContext
会从 Class
参数中获取.
请考虑以下目录布局:
com/ example/ services.xml repositories.xml MessengerService.class
以下示例显示如何实例化由名为 services.xml
和 repositories.xml
(位于类路径中) 的文件中定义的 bean 组成的 ClassPathXmlApplicationContext
实例:
ApplicationContext ctx = new ClassPathXmlApplicationContext(
new String[] {"services.xml", "repositories.xml"}, MessengerService.class);
val ctx = ClassPathXmlApplicationContext(arrayOf("services.xml", "repositories.xml"), MessengerService::class.java)
有关各种构造函数的详细信息,请参阅 ClassPathXmlApplicationContext
javadoc.
2.8.2. 使用通配符构造应用上下文
从前文可知,应用上下文构造器的资源路径可以是单一的路径(即一对一地映射到目标资源) . 也可以使用高效的通配符. 可以包含特殊的 "classpath*:"
前缀或 ant 风格的正则表达式(使用Spring的 PathMatcher
来匹配) .
通配符机制可用于组装应用程序的组件,应用程序里所有组件都可以在一个公用的位置路径发布自定义的上下文片段,那么最终的应用上下文可使用 classpath*:
. 在同一路径前缀(前面的公用路径) 下创建,这时所有组件上下文的片段都会被自动装配.
请注意,此通配符特定于在应用程序上下文构造函数中使用资源路径(或直接使用 PathMatcher
实用程序类层次结构时) ,并在构造时解析. 它与 Resource
类型本身无关. 您不能使用 classpath*:
前缀来构造实际的 Resource
,,因为资源一次只指向一个资源.
Ant 风格模式
路径位置可以包含 Ant 样式模式,如以下示例所示:
/WEB-INF/*-context.xml com/mycompany/**/applicationContext.xml file:C:/some/path/*-context.xml classpath:com/mycompany/**/applicationContext.xml
当路径位置包含 Ant 样式模式时,解析程序遵循更复杂的过程来尝试解析通配符. 解释器会先从位置路径里获取最靠前的不带通配符的路径片段, 并使用这个路径片段来创建一个 Resource
,并从中获取一个 URL
.
如果此 URL 不是 jar:
URL 或特定于容器的变体(例如,在 WebLogic 中为 zip:
,在WebSphere中为 wsjar
,等等) 则从 Resource
里获取 java.io.File
对象,并通过其遍历文件系统. 进而解决位置路径里通配符. 对于 jar URL,解析器要么从中获取 java.net.JarURLConnection
, 要么手动解析 jar URL,然后遍历 jar 文件的内容以解析通配符.
可移植性所带来的影响
如果指定的路径定为 file
URL(不管是显式还是隐式的) ,首先默认的 ResourceLoader
就是文件系统,其次通配符使用程序可以完美移植.
如果指定的路径是 classpath
位置,则解析器必须通过 Classloader.getResource()
方法调用获取最后一个非通配符路径段URL. 因为这只是路径的一个节点(而不是末尾的文件) ,实际上它是未定义的(在 ClassLoader
javadoc 中) ,在这种情况下并不能确定返回什么样的URL.
实际上,它始终会使用 java.io.File
来解析目录,其中类路径资源会解析到文件系统的位置或某种类型的jar URL,其中类路径资源解析为jar包的位置. 但是,这个操作就碰到了可移植的问题了.
如果获取了最后一个非通配符段的 jar 包 URL,解析器必须能够从中获取 java.net.JarURLConnection
,或者手动解析 jar 包的 URL,以便能够遍历 jar 的内容. 并解析通配符,这适用于大多数工作环境,但在某些其他特定环境中将会有问题,最后会导致解析失败,所以强烈建议在特定环境中彻底测试来自 jar 资源的通配符解析,测试成功之后再对其作依赖使用.
classpath*:
前缀
当构造基于 XML 文件的应用上下文时,位置路径可以使用 classpath*:
前缀. 如以下示例所示:
ApplicationContext ctx =
new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");
val ctx = ClassPathXmlApplicationContext("classpath*:conf/appContext.xml")
classpath*:
的使用表示该类路径下所有匹配文件名称的资源都会被获取(本质上就是调用了 ClassLoader.getResources(…)
方法,接着将获取到的资源装配成最终的应用上下文.
通配符类路径依赖于底层类加载器的 getResources() 方法. 由于现在大多数应用程序服务器都提供自己的类加载器实现,因此行为可能会有所不同,尤其是在处理 jar 文件时.
要在指定服务器测试 classpath* 是否有效,简单点可以使用 getClass().getClassLoader().getResources("<someFileInsideTheJar>") 来加载类路径 jar 包里的文件.
尝试在两个不同的路径加载相同名称的文件,如果返回的结果不一致,就需要查看一下此服务器中与 ClassLoader 设置相关的文档.
|
您还可以将 classpath*:
前缀与位置路径的其余部分中的 PathMatcher 模式组合在一起(例如,classpath*:META-INF/*-beans.xml
) .
这种情况的解析策略非常简单,取位置路径最靠前的无通配符片段,然后调用 ClassLoader.getResources()
获取所有匹配到的类层次加载器加载资源,随后将 PathMatcher
的策略应用于每一个得到的资源.
通配符的补充说明
请注意,除非所有目标资源都存在文件系统中,否则 classpath*:
与Ant样式模式结合,都只能在至少有一个确定了根路径的情况下,才能达到预期的效果. 这意味着 classpath*:*.xml
等模式可能无法从 jar 文件的根目录中检索文件,而只能从根目录中的扩展目录中检索文件.
问题的根源是 JDK 的 ClassLoader.getResources()
方法的局限性. 当向 ClassLoader.getResources()
传入空串时(表示搜索潜在的根目录) , 只能获取的文件系统的位置路径,即获取不了 jar 中文件的位置路径.
Spring 也会评估 URLClassLoader
运行时配置和 jar 文件中的 java.class.path
清单,但这不能保证导致可移植行为.
扫描类路径包需要在类路径中存在相应的目录条目. 使用 Ant 构建 JAR 时,请不要激活 JAR 任务的 在 JDK 9 的模块路径(Jigsaw) 上,Spring 的类路径扫描通常按预期工作. 此处强烈建议将资源放入专用目录,避免上述搜索 jar 文件根级别的可移植性问题. |
如果有多个类路径上都用搜索到的根包,那么使用 classpath:
和ant风格模式一起指定资源并不保证会找到匹配的资源. 请考虑以下资源位置示例:
com/mycompany/package1/service-context.xml
现在考虑一个人可能用来尝试查找该文件的 Ant 风格路径:
classpath:com/mycompany/**/service-context.xml
这样的资源可能只在一个位置,但是当使用前面例子之类的路径来尝试解析它时,解析器会处理 getResource("com/mycompany")
;返回的(第一个) URL. 当在多个类路径存在基础包节点 "com/mycompany"
时(如在多个 jar 存在这个基础节点) ,解析器就不一定会找到指定资源.
因此,这种情况下建议结合使用 classpath*:
和 ant 风格模式,classpath*:
会让解析器去搜索所有包含以下基础包节点所有的类路径: classpath*:com/mycompany/**/service-context.xml
.
2.8.3. FileSystemResource
的警告
当 FileSystemResource
与 FileSystemApplicationContext
之间没有联系(即,当 FileSystemApplicationContext
不是实际的 ResourceLoader
时) 时会按预期处理绝对路径和相对路径. 相对路径是相对与当前工作目录而言的,而绝对路径则是相对文件系统的根目录而言的.
但是,出于向后兼容性(历史) 的原因,当 FileSystemApplicationContext
是 ResourceLoader
时,这会发生变化. FileSystemApplicationContext
强制所有有联系的 FileSystemResource
实例将所有位置路径视为相对路径, 无论它们是否以 '/' 开头. 实际上,这意味着以下示例是等效的:
ApplicationContext ctx =
new FileSystemXmlApplicationContext("conf/context.xml");
val ctx = FileSystemXmlApplicationContext("conf/context.xml")
ApplicationContext ctx =
new FileSystemXmlApplicationContext("/conf/context.xml");
val ctx = FileSystemXmlApplicationContext("/conf/context.xml")
以下示例也是等效的(即使它们有所不同,因为一个案例是相对的而另一个案例是绝对的) :
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");
val ctx: FileSystemXmlApplicationContext = ...
ctx.getResource("some/resource/path/myTemplate.txt")
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");
val ctx: FileSystemXmlApplicationContext = ...
ctx.getResource("/some/resource/path/myTemplate.txt")
实际上,如果确实需要使用绝对路径,建议放弃使用 FileSystemResource
和 FileSystemXmlApplicationContext
,而强制使用 file:
的 UrlResource
.
// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt");
// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt")
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
ApplicationContext ctx =
new FileSystemXmlApplicationContext("file:///conf/context.xml");
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
val ctx = FileSystemXmlApplicationContext("file:///conf/context.xml")
3. 验证, 数据绑定和类型转换
在业务逻辑中添加数据验证利弊参半,Spring 提供的验证(和数据绑定) 方案也未必能解决这个问题. 具体来说,验证不应该与 Web 层绑定,并且应该易于本地化,并且应该可以插入任何可用的验证器.
考虑到这些问题,Spring 提出了一个基本的,非常有用的 Validator
联系,它在应用程序的每一层都可用.
Spring 为开发者提供了一个称作 DataBinder
的对象来处理数据绑定,所谓的数据绑定就是将用户输入自动地绑定到相应的领域模型(或者说用来处理用户所输入的任何对象) . Spring 的 Validator
和 DataBinder
构成了验证包,这个包主要用在 web 层使用,但绝不限于此.
BeanWrapper
是 Spring 框架中的一个基本概念,并且在很多地方使用. 但是,在使用过程中可能从未碰过它. 鉴于这是一份参考文档,那么对 BeanWrapper
的解释很有必要. 我们将在本章中解释 BeanWrapper
,在开发者尝试将数据绑定到对象的时候一定会使用到它.
Spring的 DataBinder
和底层的 BeanWrapper
都使用 PropertyEditorSupport
实现来解析和格式化属性值. PropertyEditor
和PropertyEditorSupport
接口是 JavaBeans 规范的一部分,本章也对其进行了解释. Spring 3 引入了一个 core.convert
包,它提供了一个
通用类型转换工具,以及更高级别的 “format” 包格式化 UI 字段值。 您可以将这些包用作更简单的替代方案 PropertyEditorSupport
实现。 本章还将讨论它们。
Spring 可以通过一些基础设置来支持 Java Bean 的验证. 也可以统一个适配器来兼容 Spring 的 Validator
验证接口
应用程序既可以一次性开启控制全局的 Bean Validation,如 Spring Validation中所述,并专门用于所有验证需求.
应用程序还可以为每个 DataBinder
实例注册其他 Spring Validator
实例,如配置配置 DataBinder
中所述.
3.1. 使用 Spring 的 Validator 接口来进行数据验证
Spring 提供了 Validator
接口用来进行对象的数据验证. Validator
接口在进行数据验证的时候会要求传入一个 Errors
对象,当有错误产生时会将错误信息放入该对象.
考虑以下对象的示例:
public class Person {
private String name;
private int age;
// the usual getters and setters...
}
class Person(val name: String, val age: Int)
下一个示例通过实现 org.springframework.validation.Validator
接口的以下两个方法为 Person
类提供验证行为:
-
supports(Class)
: 判断该Validator
是否能验证提供的Class
的实例? -
validate(Object, org.springframework.validation.Errors)
: 验证给定的对象,如果有验证失败信息,则将其放入Errors
对象.
实现一个 Validator
相当简单,尤其是使用 Spring 提供的 ValidationUtils
工具类时. 以下示例为 Person
实例实现 Validator
:
public class PersonValidator implements Validator {
/**
* This Validator validates only Person instances
*/
public boolean supports(Class clazz) {
return Person.class.equals(clazz);
}
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) {
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
e.rejectValue("age", "too.darn.old");
}
}
}
class PersonValidator : Validator {
/*
* This Validator validates only Person instances
*/
override fun supports(clazz: Class<>): Boolean {
return Person::class.java == clazz
}
override fun validate(obj: Any, e: Errors) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty")
val p = obj as Person
if (p.age < 0) {
e.rejectValue("age", "negativevalue")
} else if (p.age > 110) {
e.rejectValue("age", "too.darn.old")
}
}
}
ValidationUtils
中的静态方法 rejectIfEmpty(..)
方法用于拒绝 name
属性(如果它为 null
或空字符串) . 更多信息可以参考 ValidationUtils
的javadocs.
虽然可以实现一个 Validator
类来验证富对象中的每个嵌套对象,但最好将每个嵌套对象类的验证逻辑封装在自己的 Validator
实现中. 例如,有一个名为 Customer
的复杂对象,它有两个 String
类型的属性(first name和second name) ,另外还有一个 Address
对象. 它与 Customer
毫无关系,
它还实现了名为 AddressValidator
的验证器. 如果考虑在 CustomerValidator
验证器类中重用 AddressValidator
验证器的功能(这种重用不是通过简单的代码拷贝) , 那么可以将 AddressValidator
验证器的实例通过依赖注入的方式注入到 CustomerValidator
验证器中. 如以下示例所示:
public class CustomerValidator implements Validator {
private final Validator addressValidator;
public CustomerValidator(Validator addressValidator) {
if (addressValidator == null) {
throw new IllegalArgumentException("The supplied [Validator] is " +
"required and must not be null.");
}
if (!addressValidator.supports(Address.class)) {
throw new IllegalArgumentException("The supplied [Validator] must " +
"support the validation of [Address] instances.");
}
this.addressValidator = addressValidator;
}
/**
* This Validator validates Customer instances, and any subclasses of Customer too
*/
public boolean supports(Class clazz) {
return Customer.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
Customer customer = (Customer) target;
try {
errors.pushNestedPath("address");
ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
} finally {
errors.popNestedPath();
}
}
}
class CustomerValidator(private val addressValidator: Validator) : Validator {
init {
if (addressValidator == null) {
throw IllegalArgumentException("The supplied [Validator] is required and must not be null.")
}
if (!addressValidator.supports(Address::class.java)) {
throw IllegalArgumentException("The supplied [Validator] must support the validation of [Address] instances.")
}
}
/*
* This Validator validates Customer instances, and any subclasses of Customer too
*/
override fun supports(clazz: Class<>): Boolean {
return Customer::class.java.isAssignableFrom(clazz)
}
override fun validate(target: Any, errors: Errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required")
val customer = target as Customer
try {
errors.pushNestedPath("address")
ValidationUtils.invokeValidator(this.addressValidator, customer.address, errors)
} finally {
errors.popNestedPath()
}
}
}
验证错误信息会上报给作为参数传入的 Errors
对象,如果使用 Spring Web MVC. 您可以使用 <spring:bind/>
标记来检查错误消息,但您也可以自己检查 Errors
对象. 有关它提供的方法的更多信息可以在 javadoc javadoc 中找到.
3.2. 通过错误编码得到错误信息
上一节介绍了数据绑定和数据验证,如何拿到验证错误信息是最后需要讨论的问题. 在上一个的例子中,验证器拒绝了 name
和 age
属性. 如果我们想通过使用 MessageSource
输出错误消息, 可以在验证失败时设置错误编码(本例中就是 name
和 age
) .
当调用(直接或间接地,通过使用 ValidationUtils
类) Errors
接口中的 rejectValue
方法或者它的任意一个方法时,它的实现不仅仅注册传入的错误编码参数, 还会注册一些遵循一定规则的错误编码.
注册哪些规则的错误编码取决于开发者使用的 MessageCodesResolver
. 当使用默认的 DefaultMessageCodesResolver
时, 除了会将错误信息注册到指定的错误编码上,这些错误信息还会注册到包含属性名的错误编码上. 假如调用 rejectValue("age", "too.darn.old")
方法,
Spring 除了会注册 too.darn.old
错误编码外, 还会注册 too.darn.old.age
和 too.darn.old.age.int
这两个错误编码(即一个是包含属性名,另外一个既包含属性名还包含类型的) . 在 Spring 中这种注册称为注册约定,这样所有的开发者都能按照这种约定来定位错误信息.
有关 MessageCodesResolver
和默认策略的更多信息可分别在 MessageCodesResolver
和 DefaultMessageCodesResolver
, 的 javadoc 中找到.
3.3. 操作 bean 和 BeanWrapper
org.springframework.beans
包遵循 Oracle 提供的 JavaBeans 标准,JavaBean 只是一个包含默认无参构造器的类,
它遵循命名约定(举例来说) 名为 bingoMadness
属性将拥有设置方法 setBingoMadness(..)
和获取方法 getBingoMadness()
. 有关 JavaBeans 和规范的更多信息,请参考 Oracle 的网站( javabeans) .
beans 包里一个非常重要的类是 BeanWrapper
接口和它的相应实现(BeanWrapperImpl
). 引自 javadoc: BeanWrapper
提供了设置和获取属性值(单个或批量) 、 获取属性描述符以及查询属性以确定它们是可读还是可写的功能.
BeanWrapper
还提供对嵌套属性的支持,能够不受嵌套深度的限制启用子属性的属性设置. BeanWrapper
还提供了无需目标类代码的支持就能够添加标准 JavaBeans 的 PropertyChangeListeners
和 VetoableChangeListeners
的能力.
最后但同样重要的是, BeanWrapper
支持设置索引属性. 应用程序代码通常不会直接使用 BeanWrapper
,而是提供给 DataBinder
和 BeanFactory
使用.
BeanWrapper
顾名思义,它包装了 bean 并对其执行操作. 例如设置和获取属性.
3.3.1. 设置并获取基本和嵌套的属性
设置和获取属性是通过使用 setPropertyValue
, 和 getPropertyValues
方法完成的,这些方法重载了 BeanWrapper
. Springs javadoc 更详细地描述了它们. JavaBeans 规范具有指示对象属性的约定. 下表显示了这些约定的一些示例:
Expression | Explanation |
---|---|
|
表示属性 |
|
表示 |
|
表示索引属性 |
|
表示映射属性 |
(如果您不打算直接使用 BeanWrapper ,那么下一节对您来说并不重要. 如果您只使用 DataBinder
和 BeanFactory
及其默认实现,那么您应该跳到有关PropertyEditors
的部分. )
以下两个示例类使用 BeanWrapper
来获取和设置属性:
public class Company {
private String name;
private Employee managingDirector;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Employee getManagingDirector() {
return this.managingDirector;
}
public void setManagingDirector(Employee managingDirector) {
this.managingDirector = managingDirector;
}
}
class Company {
var name: String? = null
var managingDirector: Employee? = null
}
public class Employee {
private String name;
private float salary;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public float getSalary() {
return salary;
}
public void setSalary(float salary) {
this.salary = salary;
}
}
class Employee {
var name: String? = null
var salary: Float? = null
}
以下代码段显示了如何检索和操作实例化 Companies
和 Employees
的某些属性的一些示例:
BeanWrapper company = new BeanWrapperImpl(new Company());
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.");
// ... can also be done like this:
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);
// ok, let's create the director and tie it to the company:
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());
// retrieving the salary of the managingDirector through the company
Float salary = (Float) company.getPropertyValue("managingDirector.salary");
val company = BeanWrapperImpl(Company())
// setting the company name..
company.setPropertyValue("name", "Some Company Inc.")
// ... can also be done like this:
val value = PropertyValue("name", "Some Company Inc.")
company.setPropertyValue(value)
// ok, let's create the director and tie it to the company:
val jim = BeanWrapperImpl(Employee())
jim.setPropertyValue("name", "Jim Stravinsky")
company.setPropertyValue("managingDirector", jim.wrappedInstance)
// retrieving the salary of the managingDirector through the company
val salary = company.getPropertyValue("managingDirector.salary") as Float?
3.3.2. 内置 PropertyEditor
实现
Spring 使用 PropertyEditor
的概念来实现 Object
和 String
之间的转换,有时使用不同于对象本身的方式来表示属性显得更方便. 例如,Date
可以使用易于阅读的方式(如 String : '2007-14-09'
).
还能将易于阅读的形式转换回原来的 Date
(甚至做得更好: 转换任何以易于阅读形式输入的日期,然后返回日期对象) . 可以通过注册 java.beans.PropertyEditor
类型的自定义编辑器来实现此行为.
在 BeanWrapper
上注册自定义编辑器,或者在特定的 IoC 容器中注册自定义编辑器(如前一章所述) ,使其了解如何将属性转换为所需类型. 有关 PropertyEditor
的更多信息,请参阅 Oracle的java.beans包的 javadoc
在 Spring 中使用属性编辑的几个示例:
-
通过使用
PropertyEditor
实现来设置 bean 的属性. 当您使用java.lang.String
作为您在 XML 文件中声明的某个 bean 的属性的值时, Spring 将(如果相应属性的 setter 具有类参数) 使用ClassEditor
尝试将参数解析为类对象. -
在 Spring 的 MVC 框架中解析 HTTP 请求参数是通过使用各种
PropertyEditor
实现来完成的,您可以在CommandController
的所有子类中手动绑定它们.
Spring 内置了许多 PropertyEditor
用于简化处理. 它们都位于 org.springframework.beans.propertyeditors
包中.
大多数(但不是全部,如下表所示) 默认情况下由 BeanWrapperImpl
注册. 当属性编辑器以某种方式进行配置时,开发者仍可以注册自定义的变体用于覆盖默认的变量. 下表描述了 Spring 提供的各种 PropertyEditor
实现:
类 | 说明 |
---|---|
|
字节数组的编辑器. 将字符串转换为其对应的字节表示形式. |
|
将表示类的字符串解析为实际的类,反之亦然. 找不到类时,抛出 |
|
|
|
|
|
|
|
任何 |
|
将字符串解析为 |
|
单向属性编辑器,可以获取字符串并生成(通过中间 |
|
可以将字符串解析为 |
|
可以将字符串解析为 |
|
可以将字符串(使用 |
|
修剪字符串的属性编辑器. (可选) 允许将空字符串转换为 |
|
可以将URL的字符串表示形式解析为实际的 |
Spring 使用 java.beans.PropertyEditorManager
设置属性编辑器(可能需要) 的搜索路径. 搜索路径还包括 sun.bean.editors
,其中包括 Font
, Color
和大多数基本类型等类型的 PropertyEditor
实现.
注意,标准的 JavaBeans 架构可以自动发现 PropertyEditor
类(无需显式注册) ,前提是此类与需处理的类位于同一个包,并且与该类具有相同的名称. 并以 Editor
单词结尾. 可以使用以下类和包结构,这足以使 SomethingEditor
类被识别并用作 Something
类型属性的 PropertyEditor
.
com chank pop Something SomethingEditor // the PropertyEditor for the Something class
请注意,您也可以在此处使用标准 BeanInfo
JavaBeans机制(这里描述的是无关紧要的细节) . 以下示例使用 BeanInfo
机制使用关联类的属性显式注册一个或多个 PropertyEditor
实例:
com chank pop Something SomethingBeanInfo // the BeanInfo for the Something class
以下引用的 SomethingBeanInfo
类的 Java 源代码将 CustomNumberEditor
与 Something
类的 age
属性相关联:
public class SomethingBeanInfo extends SimpleBeanInfo {
public PropertyDescriptor[] getPropertyDescriptors() {
try {
final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);
PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {
@Override
public PropertyEditor createPropertyEditor(Object bean) {
return numberPE;
}
};
return new PropertyDescriptor[] { ageDescriptor };
}
catch (IntrospectionException ex) {
throw new Error(ex.toString());
}
}
}
class SomethingBeanInfo : SimpleBeanInfo() {
override fun getPropertyDescriptors(): Array<PropertyDescriptor> {
try {
val numberPE = CustomNumberEditor(Int::class.java, true)
val ageDescriptor = object : PropertyDescriptor("age", Something::class.java) {
override fun createPropertyEditor(bean: Any): PropertyEditor {
return numberPE
}
}
return arrayOf(ageDescriptor)
} catch (ex: IntrospectionException) {
throw Error(ex.toString())
}
}
}
注册额外的自定义 PropertyEditor
将 bean 属性设置为字符串值时,Spring IoC 容器最终使用标准 JavaBeans PropertyEditor
实现将这些字符串转换为属性的复杂类型. Spring 预先注册了许多自定义 PropertyEditor
实现(例如,将表示为字符串的类名转换为 Class
对象) .
此外,Java 的标准 JavaBeans PropertyEditor
查找机制允许对类的 PropertyEditor
进行适当的命名,并将其放置在与其提供支持的类相同的包中,以便可以自动找到它.
如果需要注册其他自定义 PropertyEditors
,可以使用多种机制. 通常最麻烦也不推荐的策略是手动、简单的使用 ConfigurableBeanFactory
接口的 registerCustomEditor()
方法,
假设有一个 BeanFactory
引用,另一种(稍微更方便) 机制是使用一个名为 CustomEditorConfigurer
的特殊 bean 工厂后置处理器. 尽管您可以将 bean 工厂后置处理器与 BeanFactory
实现一起使用,但 CustomEditorConfigurer
具有嵌套属性设置,
因此我们强烈建议您将它与 ApplicationContext
一起使用,您可以在其中以类似的方式将其部署到任何其他 bean 以及它可以在哪里 自动检测并应用.
请注意,所有的 bean 工厂和应用程序上下文都自动使用了许多内置属性编辑器,在其内部都是使用 BeanWrapper
来进行属性转换的. BeanWrapper
注册的标准属性编辑器列在上一节中 此外,ApplicationContexts
还会覆盖或添加其他编辑器,以适合特定应用程序上下文类型的方式处理资源查找.
标准的 PropertyEditor
JavaBeans 实例用于将以字符串表示的属性值转换为属性的实际复杂类型. CustomEditorConfigurer
是一个 bean 后置处理工厂,可用于方便地在 ApplicationContext
中添加额外的 PropertyEditor
实例.
请考虑以下示例,该示例定义名为 ExoticType
的用户类和另一个名为 DependsOnExoticType
的类,该类需要将 ExoticType
设置为属性:
public class ExoticType {
private String name;
public ExoticType(String name) {
this.name = name;
}
}
public class DependsOnExoticType {
private ExoticType type;
public void setType(ExoticType type) {
this.type = type;
}
}
class ExoticType(val name: String)
class DependsOnExoticType {
var type: ExoticType? = null
}
当创建好后,希望将 type
属性指定为一个字符串,PropertyEditor
会在幕后将其转换成实际的 ExoticType
实例. 以下 bean 定义显示了如何设置此关系:
<bean id="sample" class="example.DependsOnExoticType">
<property name="type" value="aNameForExoticType"/>
</bean>
PropertyEditor
实现如下:
public class ExoticTypeEditor extends PropertyEditorSupport {
public void setAsText(String text) {
setValue(new ExoticType(text.toUpperCase()));
}
}
class ExoticTypeEditor : PropertyEditorSupport() {
override fun setAsText(text: String) {
value = ExoticType(text.toUpperCase())
}
}
最后,以下示例显示如何使用 CustomEditorConfigurer
向 ApplicationContext
注册新的 PropertyEditor
,然后可以根据需要使用它:
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
</map>
</property>
</bean>
使用 PropertyEditorRegistrar
使用 Spring 容器注册属性编辑器的另一个策略是创建和使用 PropertyEditorRegistrar
. 当需要在多种不同的情况下使用相同的属性编辑器集时,这个接口特别有用,编写相应的注册器并在每个案例中重用.
PropertyEditorRegistrar
与另外一个称为 PropertyEditorRegistry
的接口一起工作. 它使用 Spring BeanWrapper
(和DataBinder
)实现. PropertyEditorRegistrar
在与 CustomEditorConfigurer
(本节介绍的)一起使用时特别方便,
它暴露 setPropertyEditorRegistrars(..)
的属性. PropertyEditorRegistrar
和 CustomEditorConfigurer
结合使用可以简单的在 DataBinder
和 Spring MVC 控制之间共享. 它避免了在自定义编辑器上进行同步的需要: PropertyEditorRegistrar需要为每个bean创建尝试创建新的
PropertyEditor
实例.
以下示例显示如何创建自己的 PropertyEditorRegistrar
实现:
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
public void registerCustomEditors(PropertyEditorRegistry registry) {
// it is expected that new PropertyEditor instances are created
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
// you could register as many custom property editors as are required here...
}
}
class CustomPropertyEditorRegistrar : PropertyEditorRegistrar {
override fun registerCustomEditors(registry: PropertyEditorRegistry) {
// it is expected that new PropertyEditor instances are created
registry.registerCustomEditor(ExoticType::class.java, ExoticTypeEditor())
// you could register as many custom property editors as are required here...
}
}
有关 PropertyEditorRegistrar
实现的示例,另请参见 org.springframework.beans.support.ResourceEditorRegistrar
. 请注意,在实现 registerCustomEditors(..)
方法时,它会创建每个属性编辑器的新实例.
下一个示例显示如何配置 CustomEditorConfigurer
并将 CustomPropertyEditorRegistrar
的实例注入其中:
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="propertyEditorRegistrars">
<list>
<ref bean="customPropertyEditorRegistrar"/>
</list>
</property>
</bean>
<bean id="customPropertyEditorRegistrar"
class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>
最后(与本章的重点有所不同,对于那些使用Spring’s MVC web framework框架的人来说) ,使用 PropertyEditorRegistrars
和数据绑定控制器(SimpleFormController
) 可以非常方便. 以下示例在 @InitBinder
方法的实现中使用 PropertyEditorRegistrar
:
@Controller
public class RegisterUserController {
private final PropertyEditorRegistrar customPropertyEditorRegistrar;
RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
this.customPropertyEditorRegistrar = propertyEditorRegistrar;
}
@InitBinder
void initBinder(WebDataBinder binder) {
this.customPropertyEditorRegistrar.registerCustomEditors(binder);
}
// other methods related to registering a User
}
@Controller
class RegisterUserController(
private val customPropertyEditorRegistrar: PropertyEditorRegistrar) {
@InitBinder
fun initBinder(binder: WebDataBinder) {
this.customPropertyEditorRegistrar.registerCustomEditors(binder)
}
// other methods related to registering a User
}
这种类型的 PropertyEditor
注册方式可以让代码更加简洁(@InitBinder
的实现只有一行) ,并允许将通用 PropertyEditor
注册代码封装在一个类中,然后根据需要在尽可能多的 Controllers
之间共享.
3.4. Spring 类型转换
Spring 3 引入了一个 core.convert
包,它提供了一个通用的类型转换系统. 系统定义了一个用于实现类型转换逻辑的 SPI 和一个用于在运行时执行类型转换的 API.
在 Spring 的容器中,此系统可以用作 PropertyEditor
的替代方法,它将外部 bean 属性值字符串转换为所需的属性类型. 您还可以在需要进行类型转换的应用程序中的任何位置使用公共 API.
3.4.1. SPI 转换器
实现类型转换逻辑的 SPI 是简易的,而且是强类型的. 如以下接口定义所示:
public interface Converter<S, T> {
T convert(S source);
}
创建自定义转换器都需要实现 Converter
接口,参数 S
是需要转换的类型,T
是转换后的类型. 这个转换器也可以应用在集合或数组上将 S
参数转换为 T
参数. 前提是已经注册了委托数组或集合转换器(DefaultConversionService
默认情况下也是如此) .
对于要 convert(S)
的每个调用,source
参数需保证不为 null
. 转换失败时,Converter
可能会引发任意的 unchecked 异常. 具体来说,它应抛出 IllegalArgumentException
以报告无效的 source
值. 请注意确保您的 Converter
实现是线程安全的.
为方便起见,core.convert.support
包中提供了几个转换器实现. 这些包括从字符串到数字和其他常见类型的转换器. 以下清单显示了 StringToInteger
类,它是典型的 Converter
实现:
final class StringToInteger implements Converter<String, Integer> {
public Integer convert(String source) {
return Integer.valueOf(source);
}
}
3.4.2. 使用 ConverterFactory
当需要集中整个类层次结构的转换逻辑时(例如,从 String
转换为 Enum
对象时) ,您可以实现 ConverterFactory
,如以下示例所示:
public interface ConverterFactory<S, R> {
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
参数化 S
为您要转换的类型,R
是需要转换后的类型的基类. 然后实现 getConverter(Class)
,其中 T
是 R
的子类.
以 StringToEnumConverterFactory
为例:
final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToEnumConverter(targetType);
}
private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {
private Class<T> enumType;
public StringToEnumConverter(Class<T> enumType) {
this.enumType = enumType;
}
public T convert(String source) {
return (T) Enum.valueOf(this.enumType, source.trim());
}
}
}
3.4.3. 使用 GenericConverter
当您需要复杂的 Converter
实现时,请考虑使用 GenericConverter
接口. GenericConverter
具有比 Converter
更灵活但不太强类型的签名,支持在多种源和目标类型之间进行转换.
此外,GenericConverter
可以在实现转换逻辑时使用可用的源和目标字段上下文. 此上下文类允许通过字段注解或在字段签名上声明的一般信息来驱动类型转换. 以下清单显示了 GenericConverter
的接口定义:
public interface GenericConverter {
public Set<ConvertiblePair> getConvertibleTypes();
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
要实现 GenericConverter
,请使用 getConvertibleTypes()
返回支持的 source→target 类型对,然后实现 convert(Object, TypeDescriptor, TypeDescriptor)
方法并编写转换逻辑. 源 TypeDescriptor
提供对保存要转换的值的源字段的访问,目标 TypeDescriptor
提供对要设置转换值的目标字段的访问.
Java 数组和集合之间转换的转换器是 GenericConverter
应用的例子. 其中 ArrayToCollectionConverter
内部声明目标集合类型用于解析集合元素类型的字段. 它允许在目标字段上设置集合之前,将源数组中的每个元素转换为集合元素类型.
因为 GenericConverter 是一个更复杂的SPI接口,所以只有在需要时才应该使用它. 一般使用 Converter 或 ConverterFactory 足以满足基本的类型转换需求.
|
使用 ConditionalGenericConverter
有时可能只想在特定条件为真时才执行 Converter
,例如,在特定注解的目标上使用 Converter
,或者,在一个特定的目标类方法(例如 static valueOf
方法) 中执行 Converter
. ConditionalGenericConverter
是 GenericConverter
和 ConditionalConverter
接口的组合. 允许自定义匹配条件
public interface ConditionalConverter {
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}
用于持久实体标识符和实体引用之间转换的 IdToEntityConverter
是 ConditionalGenericConverter
应用的例子.
如果目标实体类型声明静态查找器方法(如 findAccount(Long)
), 那么 IdToEntityConverter
只对匹配的生效. 开发者可以实现 matches(TypeDescriptor, TypeDescriptor)
以执行 finder
方法来检查是否匹配.
3.4.4. ConversionService
API
ConversionService
定义了一个统一的 API,用于在运行时执行类型转换逻辑. 转换器通常在以下 Facade 接口后面执行:
public interface ConversionService {
boolean canConvert(Class<?> sourceType, Class<?> targetType);
<T> T convert(Object source, Class<T> targetType);
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
大多数 ConversionService
实现还实现了 ConverterRegistry
,它提供了一个用于注册转换器的SPI. 在内部,ConversionService
实现委托其注册的转换器执行类型转换逻辑.
core.convert.support
包中提供了强大的 ConversionService
实现. GenericConversionService
是适用于大多数环境的通用实现. ConversionServiceFactory
提供了一个方便的工厂,用于创建常见的 ConversionService
配置.
3.4.5. 配置 ConversionService
ConversionService
是一个无状态对象,在应用程序启动时就会实例化,可以被多个线程共享.
在 Spring 应用程序中,通常每个 Spring 容器(或 ApplicationContext
) 配置一个 ConversionService
实例. 该 ConversionService
将被 Spring 获取,然后在框架需要执行类型转换时使用. 也可以将 ConversionService
插入任意 bean 并直接调用它.
如果没有向 Spring 注册 ConversionService ,则使用基于 PropertyEditor 的原始系统.
|
要使用 Spring 注册默认的 ConversionService
,请添加以下 bean 定义,其 id
为 conversionService
:
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean"/>
默认的 ConversionService
可以在字符串,数字,枚举,集合,映射和其他常见类型之间进行转换. 要使用您自己的自定义转换器补充或覆盖默认转换器,请设置 converters
属性. 属性值可以任何实现了 Converter
, ConverterFactory
, 或 GenericConverter
接口的类.
<bean id="conversionService"
class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="example.MyCustomConverter"/>
</set>
</property>
</bean>
在 Spring MVC 应用程序中使用 ConversionService
也很常见. 请参阅Spring MVC章节中的转换和格式化 .
在某些情况下,您可能希望在转换期间应用格式. 有关使用 FormattingConversionServiceFactoryBean
的详细信息,请参阅 FormatterRegistry
SPI.
3.4.6. 使用 ConversionService
编程
要以编程方式使用 ConversionService
实例,您可以像对任何其他 bean 一样注入对它的引用. 以下示例显示了如何执行此操作:
@Service
public class MyService {
public MyService(ConversionService conversionService) {
this.conversionService = conversionService;
}
public void doIt() {
this.conversionService.convert(...)
}
}
@Service
class MyService(private val conversionService: ConversionService) {
fun doIt() {
conversionService.convert(...)
}
}
对于大多数用例,您可以使用指定 targetType
的 convert
方法,但它不适用于更复杂的类型,例如参数化元素的集合. 例如,如果想使用编程的方式将整数列表转换为字符串列表,则需要提供源和目标类型的正规定义.
幸运的是,TypeDescriptor
提供了各种选项,使得这样做非常简单,如下例所示:
DefaultConversionService cs = new DefaultConversionService();
List<Integer> input = ...
cs.convert(input,
TypeDescriptor.forObject(input), // List<Integer> type descriptor
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));
val cs = DefaultConversionService()
val input: List<Integer> = ...
cs.convert(input,
TypeDescriptor.forObject(input), // List<Integer> type descriptor
TypeDescriptor.collection(List::class.java, TypeDescriptor.valueOf(String::class.java)))
请注意, DefaultConversionService
会自动注册适合大多数环境的转换器. 这包括集合转换器,基本类型转换器和基本的对象到字符串转换器. 您可以使用 DefaultConversionService
类上的静态 addDefaultConverters
方法向任何 ConverterRegistry
注册相同的转换器.
值类型的转换器可以重用于数组和集合,因此无需创建特定的转换器即可将 S
的 Collection
转换为 T
的 Collection
,前提是标准集合处理是合适的.
3.5. Spring 字段格式化
如前一节所述, core.convert
是一种通用类型转换系统. 它提供统一的 ConversionService
API 以及强类型转换器 SPI,用于实现从一种类型到另一种类型的转换逻辑.
Spring 容器使用此系统绑定 bean 属性值. 此外,Spring Expression Language(SpEL) 和 DataBinder
都使用此系统绑定字段值. 此外,当 SpEL 需要将 Short 类型强转为 Long 类型, 用于试图完成 expression.setValue(Object bean, Object value)
时,那么 core.convert
系统也可以提供这种功能.
现在考虑典型客户端环境(例如 Web 或桌面应用程序) 的类型转换要求. 在这种环境中,在这种环境中,还包括转换成为 String
用于支持视图呈现程序. 此外,还通常需要本地化字符串值.
普通的转化器 SPI 没有提供按照直接进行格式转换的功能. 更通用的 core.convert
Converter
SPI不能解决此类要求. 为了实现这个功能,Spring 3 添加了方便的 Formatter
SPI,它提供了简单强健的的 PropertyEditor
专供客户端环境.
通常, 当需要使用通用类型转换时可以用 Converter
SPI. 例如,在 java.util.Date
和 java.lang.Long
之间进行转换. 在客户端环境(例如Web应用程序) 中工作时,可以使用 Formatter
SPI, 并且需要解析和打印本地化的字段值. ConversionService
为两个 SPI 提供统一的类型转换 API.
3.5.1. Formatter
SPI
Formatter
SPI 实现字段格式化逻辑是简单的,强类型的. 以下清单显示了 Formatter
接口定义:
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
Formatter
继承了内置的 Printer
和 Parser
接口. 以下清单显示了这两个接口的定义:
public interface Printer<T> {
String print(T fieldValue, Locale locale);
}
public interface Parser<T> {
T parse(String clientValue, Locale locale) throws ParseException;
}
如果需要创建自定义的 Formatter
,需要实现 Formatter
接口. 参数 T
类型是你需要格式化的类型. 例如,java.util.Date
. 实现 print()
操作在客户端本地设置中打印显示的 T
实例.
实现 parse()
操作以从客户端本地设置返回的格式化表示形式分析T的实例. 如果尝试分析失败,Formatter
会抛出 ParseException
或 IllegalArgumentException
异常. 注意确保自定义的 Formatter
是线程安全的.
format
子包提供了多种 Formatter
实现方便使用. number
子包中提供了 NumberStyleFormatter
, CurrencyStyleFormatter
, 和 PercentStyleFormatter
用于格式化 java.lang.Number
(使用 java.text.NumberFormat
) .
datetime
子包中提供了 DateFormatter
用于格式化 java.util.Date
(使用 java.text.DateFormat
) .
以下 DateFormatter
是 Formatter
实现的示例:
public final class DateFormatter implements Formatter<Date> {
private String pattern;
public DateFormatter(String pattern) {
this.pattern = pattern;
}
public String print(Date date, Locale locale) {
if (date == null) {
return "";
}
return getDateFormat(locale).format(date);
}
public Date parse(String formatted, Locale locale) throws ParseException {
if (formatted.length() == 0) {
return null;
}
return getDateFormat(locale).parse(formatted);
}
protected DateFormat getDateFormat(Locale locale) {
DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
dateFormat.setLenient(false);
return dateFormat;
}
}
class DateFormatter(private val pattern: String) : Formatter<Date> {
override fun print(date: Date, locale: Locale)
= getDateFormat(locale).format(date)
@Throws(ParseException::class)
override fun parse(formatted: String, locale: Locale)
= getDateFormat(locale).parse(formatted)
protected fun getDateFormat(locale: Locale): DateFormat {
val dateFormat = SimpleDateFormat(this.pattern, locale)
dateFormat.isLenient = false
return dateFormat
}
}
更多内容上 Spring 社区查看 Formatter
的版本信息,请参阅 GitHub Issues 进行贡献.
3.5.2. 基于注解的格式化
字段格式也可以通过字段类型或注解进行配置. 如果要将注解绑定到 Formatter
,请实现 AnnotationFormatterFactory
. 以下清单显示了 AnnotationFormatterFactory
接口的定义:
public interface AnnotationFormatterFactory<A extends Annotation> {
Set<Class<?>> getFieldTypes();
Printer<?> getPrinter(A annotation, Class<?> fieldType);
Parser<?> getParser(A annotation, Class<?> fieldType);
}
创建一个实现:
. 参数 A
是要与之关联的字段 annotationType
逻辑格式(例如: org.springframework.format.annotation.DateTimeFormat
)
. Have getFieldTypes()
返回可以在其上使用注解的字段的类型.
. Have getPrinter()
返回 Printer
以打印带注解的字段的值.
. Have getParser()
返回 Parser
解析注解字段的 clientValue
参数化 A 是将格式逻辑与(例如 org.springframework.format.annotation.DateTimeFormat
关联到字段 annotationType
. getFieldTypes()
返回注解可用的字段类型. 使 getPrinter()
返回 Printer
以打印注解字段值. getParser()
返回一个 Parser 以分析注解字段的 clientValue
.
下面的示例 AnnotationFormatterFactory
实现,将 @NumberFormat
注解绑定到格式化程序. 此注解允许指定数字样式或模式
public final class NumberFormatAnnotationFormatterFactory
implements AnnotationFormatterFactory<NumberFormat> {
public Set<Class<?>> getFieldTypes() {
return new HashSet<Class<?>>(asList(new Class<?>[] {
Short.class, Integer.class, Long.class, Float.class,
Double.class, BigDecimal.class, BigInteger.class }));
}
public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}
public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
return configureFormatterFrom(annotation, fieldType);
}
private Formatter<Number> configureFormatterFrom(NumberFormat annotation, Class<?> fieldType) {
if (!annotation.pattern().isEmpty()) {
return new NumberStyleFormatter(annotation.pattern());
} else {
Style style = annotation.style();
if (style == Style.PERCENT) {
return new PercentStyleFormatter();
} else if (style == Style.CURRENCY) {
return new CurrencyStyleFormatter();
} else {
return new NumberStyleFormatter();
}
}
}
}
class NumberFormatAnnotationFormatterFactory : AnnotationFormatterFactory<NumberFormat> {
override fun getFieldTypes(): Set<Class<*>> {
return setOf(Short::class.java, Int::class.java, Long::class.java, Float::class.java, Double::class.java, BigDecimal::class.java, BigInteger::class.java)
}
override fun getPrinter(annotation: NumberFormat, fieldType: Class<*>): Printer<Number> {
return configureFormatterFrom(annotation, fieldType)
}
override fun getParser(annotation: NumberFormat, fieldType: Class<*>): Parser<Number> {
return configureFormatterFrom(annotation, fieldType)
}
private fun configureFormatterFrom(annotation: NumberFormat, fieldType: Class<*>): Formatter<Number> {
return if (annotation.pattern.isNotEmpty()) {
NumberStyleFormatter(annotation.pattern)
} else {
val style = annotation.style
when {
style === NumberFormat.Style.PERCENT -> PercentStyleFormatter()
style === NumberFormat.Style.CURRENCY -> CurrencyStyleFormatter()
else -> NumberStyleFormatter()
}
}
}
}
想要触发格式化,只需在在字段上添加 @NumberFormat
注解即可.
public class MyModel {
@NumberFormat(style=Style.CURRENCY)
private BigDecimal decimal;
}
class MyModel(
@field:NumberFormat(style = Style.CURRENCY) private val decimal: BigDecimal
)
格式注解 API
org.springframework.format.annotation
包中存在可移植格式注解 API. 您可以使用 @NumberFormat
格式化 java.lang.Number
字段,例如 Double
and
Long
. 使用 @DateTimeFormat
格式化 java.util.Date
, java.util.Calendar
,java.util.Long
JSR-310 java.time
字段.
下面的示例使用 @DateTimeFormat
将 java.util.Date
化为 ISO Date(yyyy-MM-dd)
:
public class MyModel {
@DateTimeFormat(iso=ISO.DATE)
private Date date;
}
class MyModel(
@DateTimeFormat(iso= ISO.DATE) private val date: Date
)
3.5.3. FormatterRegistry
SPI
FormatterRegistry
是一个用于注册格式化程序和转换器的 SPI. FormattingConversionService
适用于大多数环境的 FormatterRegistry
实现. 此实现可以通过编程或以声明的方式配置为可用 FormattingConversionServiceFactoryBean
的 Spring bean.
由于它也实现了 ConversionService
,因此可以直接配置用于 Spring 的 DataBinder
和 Spring 的表达式语言(SpEL) .
以下清单显示了 FormatterRegistry
:
public interface FormatterRegistry extends ConverterRegistry {
void addPrinter(Printer<?> printer);
void addParser(Parser<?> parser);
void addFormatter(Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
}
如上所示, Formatters
通过 fieldType
或注解进行注册.
FormatterRegistry
SPI 可以集中配置格式规则,避免跨控制器的重复配置. 例如,想要强制所有日期字段都以特定方式格式化,或者具有特定注解的字段以某种方式格式化. 使用共享的 FormatterRegistry
,开发者只需一次定义这些规则,即可到处使用.
3.5.4. FormatterRegistrar
SPI
FormatterRegistrar
是用于注册格式化器和通过 FormatterRegistry 转换的 SPI:
public interface FormatterRegistrar {
void registerFormatters(FormatterRegistry registry);
}
FormatterRegistrar
用于注册多个相关的转换器或格式化器(根据给定的格式化目录注册,例如 Date 格式化) . 在直接注册不能实现时 FormatterRegistrar
就派上用场了,
例如,当格式化程序需要在不同于其自身 <T>
的特定字段类型下进行索引时,或者在注册 Printer/Parser
对时. 下一节提供了有关转换器和格式化器注册的更多信息.
3.5.5. 在Spring MVC中配置格式化
请参阅 Spring MVC 章节中的转换和格式化.
3.6. 配置全局日期和时间格式
默认情况下,不带 @DateTimeFormat
注解的日期和时间字段使用 DateFormat.SHORT
(短日期) 的格式转换字符串. 开发者也可以使用自定义的全局格式覆盖默认格式.
此时需要确保 Spring 不注册默认格式化器,而应该手动注册所有格式化器,可以借助以下方法手动注册格式器:
-
org.springframework.format.datetime.standard.DateTimeFormatterRegistrar
-
org.springframework.format.datetime.DateFormatterRegistrar
.
例如,以下 Java 配置注册全局 yyyyMMdd
格式(此示例不依赖于 Joda-Time 库) :
@Configuration
public class AppConfig {
@Bean
public FormattingConversionService conversionService() {
// Use the DefaultFormattingConversionService but do not register defaults
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
// Ensure @NumberFormat is still supported
conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
// Register date conversion with a specific global format
DateFormatterRegistrar registrar = new DateFormatterRegistrar();
registrar.setFormatter(new DateFormatter("yyyyMMdd"));
registrar.registerFormatters(conversionService);
return conversionService;
}
}
@Configuration
class AppConfig {
@Bean
fun conversionService(): FormattingConversionService {
// Use the DefaultFormattingConversionService but do not register defaults
return DefaultFormattingConversionService(false).apply {
// Ensure @NumberFormat is still supported
addFormatterForFieldAnnotation(NumberFormatAnnotationFormatterFactory())
// Register date conversion with a specific global format
val registrar = DateFormatterRegistrar()
registrar.setFormatter(DateFormatter("yyyyMMdd"))
registrar.registerFormatters(this)
}
}
}
如果您更喜欢基于 XML 的配置,则可以使用 FormattingConversionServiceFactoryBean
. 以下示例显示了如何执行此操作:
<?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="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="registerDefaultFormatters" value="false" />
<property name="formatters">
<set>
<bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
</set>
</property>
<property name="formatterRegistrars">
<set>
<bean class="org.springframework.format.datetime.standard.DateTimeFormatterRegistrar">
<property name="dateFormatter">
<bean class="org.springframework.format.datetime.standard.DateTimeFormatterFactoryBean">
<property name="pattern" value="yyyyMMdd"/>
</bean>
</property>
</bean>
</set>
</property>
</bean>
</beans>
请注意,在 Web 中配置日期和时间格式时需要考虑其他一些注意事项 应用程序. 请参阅 WebMVC 格式转换 或 WebFlux 格式转换
3.7. Java Bean 验证
Spring 框架提供了 Java Bean验证 API.
3.7.1. Bean验证概述
Bean 验证为 Java 应用程序提供了通过约束声明和元数据进行验证的通用方法. 要使用它,您需要使用声明性验证约束对 domain 模型属性进行注解,然后由运行时强制实施. 有内置的约束,您也可以定义自己的自定义约束.
请考虑以下示例,该示例显示了具有两个属性的简单 PersonForm
模型:
public class PersonForm {
private String name;
private int age;
}
class PersonForm(
private val name: String,
private val age: Int
)
Bean 允许您为这些属性定义声明性验证约束,如以下示例所示:
public class PersonForm {
@NotNull
@Size(max=64)
private String name;
@Min(0)
private int age;
}
class PersonForm(
@get:NotNull @get:Size(max=64)
private val name: String,
@get:Min(0)
private val age: Int
)
然后,Bean 验证验证器根据声明的约束来验证此类的实例.
有关 API 的常规信息,请参阅 Bean Validation 网站. 有关默认引用实现的特定功能的信息,请参阅 Hibernate Validator 文档. 要了解如何将 bean 验证提供程序设置为 Spring bean,请继续阅读以下内容.
3.7.2. 配置 bean Validation 提供者
Spring 提供了对 Bean 验证 API 的全面支持,包括将 Bean 验证提供程序作为 Spring Bean 进行引导. 这样,您就可以在应用程序需要验证的任何地方注入 javax.validation.ValidatorFactory
或 javax.validation.Validator
.
您可以使用 LocalValidatorFactoryBean
将默认 Validator
配置为 Spring bean,如以下示例所示:
@Configuration
public class AppConfig {
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
<bean id="validator"
class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
上面的基本配置将触发 Bean 验证以使用其默认的引导机制进行初始化,Bean Validation 提供程序(例如 Hibernate Validator) 应该存在于类路径中并自动检测.
注入Validator
LocalValidatorFactoryBean
实现了 javax.validation.ValidatorFactory
和 javax.validation.Validator
, 以及 Spring 的 org.springframework.validation.Validator
. 您可以将这些接口中的任何一个引用注入到需要调用验证逻辑的 bean 中.
如果您希望直接使用 Bean Validation API,则可以注入对 javax.validation.Validator
的引用,如以下示例所示:
@Service
public class MyService {
@Autowired
private Validator validator;
}
@Service
class MyService(@Autowired private val validator: Validator)
如果您的 bean 需要 Spring Validation API,则可以注入对 org.springframework.validation.Validator
的引用,如以下示例所示:
@Service
public class MyService {
@Autowired
private Validator validator;
}
@Service
class MyService(@Autowired private val validator: Validator)
配置自定义约束
每个 bean 验证约束由两部分组成:
-
首先是声明约束及其可配置属性的
@Constraint
注解 -
实现约束行为的
javax.validation.ConstraintValidator
接口实现.
如果要将声明与实现关联,每个 @Constraint
注解都会引用相应的 ConstraintValidator
实现类. 在运行中,当在 domain 模型中遇到约束注解时,ConstraintValidatorFactory
会将引用的实现实例化.
默认情况下,LocalValidatorFactoryBean
会配置 SpringConstraintValidatorFactory
,它会使用 Spring 去创建 ConstraintValidator
实例. 这允许自定义 ConstraintValidators
, 就像任何其他 Spring bean 一样,从依赖注入中获益.
下面是自定义 @Constraint
声明的例子,使用 Spring 的依赖注入来管理 ConstraintValidator
的实现:
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator::class)
annotation class MyConstraint
public class MyConstraintValidator implements ConstraintValidator {
@Autowired;
private Foo aDependency;
// ...
}
class MyConstraintValidator(private val aDependency: Foo) : ConstraintValidator {
// ...
}
如前面的示例所示,ConstraintValidator
实现可以将其依赖 @Autowired
与任何其他 Spring bean 一样.
Spring驱动的方法验证
Bean Validation 1.1 支持的方法验证,Hibernate Validator 4.3 支持的自定义扩展都可以通过 MethodValidationPostProcessor
定义集成到 Spring 上下文中. 如下所示:
@Configuration
public class AppConfig {
@Bean
public MethodValidationPostProcessor validationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>
为了符合 Spring 驱动方法验证的条件,所有目标类都需要使用 Spring 的 @Validated
进行注解,还可以选择声明要使用的验证组. 使用 Hibernate Validator 和 Bean Validation 1.1 提供验证的步骤可以查看 MethodValidationPostProcessor
的 javadocs
方法验证依赖于 AOP Proxies 目标类,接口上的方法的 JDK 动态代理或 CGLIB 代理。 使用代理有一定的限制,其中一些在 了解 AOP 代理。 另外记得 始终在代理类上使用方法和访问器; 直接访问字段将不起作用。 |
额外的配置选项
对于大多数情况,默认的 LocalValidatorFactoryBean
配置就足够了. 从消息插入到遍历解析,各种 Bean Validation 构造有许多配置选项. 有关这些选项的更多信息,请参阅 LocalValidatorFactoryBean
javadoc.
3.7.3. 配置 DataBinder
从 Spring 3 开始,您可以使用 Validator
配置 DataBinder
实例. 配置完成后,您可以通过调用 binder.validate()
来调用 Validator
. 任何验证 Errors
都会自动添加到 binder 的 BindingResult
中.
以下示例说明如何在绑定到目标对象后以编程方式使用 DataBinder
来调用验证逻辑:
Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());
// bind to the target object
binder.bind(propertyValues);
// validate the target object
binder.validate();
// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();
val target = Foo()
val binder = DataBinder(target)
binder.validator = FooValidator()
// bind to the target object
binder.bind(propertyValues)
// validate the target object
binder.validate()
// get BindingResult that includes any validation errors
val results = binder.bindingResult
DataBinder
还可以通过 dataBinder.addValidators
和 dataBinder.replaceValidators
来配置多个 Validator
实例. 将全局配置的 Bean Validation
与本地在 DataBinder
实例上配置的Spring Validator
相结合,这非常有用. 请参阅 webmvc.html.
3.7.4. Spring MVC 3 验证
请在 Spring MVC 章节 查看 验证.
4. Spring 的表达式语言(SpEL)
Spring Expression Language(简称 "SpEL") 是一种强大的表达式语言,支持在运行时查询和操作对象. 语言语法类似于 Unified EL,但提供了其他功能,最有名的是方法调用和基本字符串模板功能.
尽管还有其他几种 Java 表达式语言,例如 OGNL, MVEL, 和 JBoss,但 SpEL 只是为了向 Spring 社区提供一种支持良好的表达式语言,开发者可以在所有用到 Spring 框架的产品中使用 SpEL. 其语言特性是由使用 Spring 框架项目的需求所驱动的,包括基于 Eclipse 的 Spring Tools for Eclipse 代码支持的需求. 也就是说,SpEL 基于一种抽象实现的技术 API, 允许在需要时集成其他表达式语言来实现功能.
虽然 SpEL 作为 Spring 产品组合中的表达式运算操作的基础,但它并不直接与 Spring 耦合,可以独立使用. 要自包含,本章中的许多示例都使用 SpEL,就像它是一种独立的表达式语言一样. 这时需要创建一些引导作用的基础实现类,例如解释器. 大多数 Spring 用户不需要处理这种基础实现类,只需将表达式字符串作为运算操作即可. SpEL 典型用途的是集成到创建XML或基于注解的 bean 定义中, 例如bean 定义的表达式支持.
本章介绍表达式语言的功能,API 及其语言语法. 在一些地方,Inventor
和 Society
类作为表达式运算操作的目标对象. 这些类声明和用于填充它们的数据列在本章末尾.
表达式语言支持以下功能:
-
文字表达
-
B 布尔和关系运算符
-
正则表达式
-
类表达式
-
访问属性,数组,list 和 maps
-
方法调用
-
关系运算符
-
Assignment
-
调用构造器
-
bean 的引用
-
数组的构造
-
内嵌的 list
-
内嵌的map
-
三元表达式
-
变量
-
用户自定义函数
-
集合映射
-
集合选择
-
模板表达式
4.1. 使用 Spring 表达式接口的表达式运算
本节介绍 SpEL 接口及其表达式语言的简单使用. 完整的语言参考可以在语言参考中找到.
以下代码介绍使用 SpEL API 运算操作文字字符串表达式 Hello World
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); (1)
String message = (String) exp.getValue();
1 | 变量的值为 'Hello World' . |
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'") (1)
val message = exp.value as String
1 | 变量的值为 'Hello World' . |
您最有可能使用的 SpEL 类和接口位于 org.springframework.expression
包及其子包中,例如 spel.support
.
ExpressionParser
接口负责解析表达式字符串. 在前面的示例中,表达式字符串是由周围的单引号表示的字符串文字. 接口 Expression
负责运算操作先前定义的表达式字符串.
当分别调用 parser.parseExpression
和 exp.getValue
时,可能抛出两个异常: ParseException
和 EvaluationException
.
SpEL 支持广泛的功能,例如调用方法,访问属性和调用构造函数.
在下面的方法调用示例中,我们在字符串文字上调用 concat
方法:
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); (1)
String message = (String) exp.getValue();
1 | message 现在的值为 'Hello World!'. |
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'.concat('!')") (1)
val message = exp.value as String
1 | message 现在的值为 'Hello World!'. |
以下调用 JavaBean 属性的示例调用 String
Bytes
:
ExpressionParser parser = new SpelExpressionParser();
// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); (1)
byte[] bytes = (byte[]) exp.getValue();
1 | 该行将文字转换为字节数组 |
val parser = SpelExpressionParser()
// invokes 'getBytes()'
val exp = parser.parseExpression("'Hello World'.bytes") (1)
val bytes = exp.value as ByteArray
1 | 该行将文字转换为字节数组 |
SpEL 还支持嵌套属性,使用标准的点符号. 即 prop1.prop2.prop3
链式写法和设置属性值. 也可以访问公共字段. 以下示例显示如何使用点表示法来获取文字的长度:
ExpressionParser parser = new SpelExpressionParser();
// invokes 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); (1)
int length = (Integer) exp.getValue();
1 | 'Hello World'.bytes.length 给出了字符串的长度. |
val parser = SpelExpressionParser()
// invokes 'getBytes().length'
val exp = parser.parseExpression("'Hello World'.bytes.length") (1)
val length = exp.value as Int
1 | 'Hello World'.bytes.length 给出了字符串的长度. |
可以调用 String 的构造函数而不是使用字符串文字,如以下示例所示:
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); (1)
String message = exp.getValue(String.class);
1 | 从构造一个新的 String 对象并使其成为大写 |
val parser = SpelExpressionParser()
val exp = parser.parseExpression("new String('hello world').toUpperCase()") (1)
val message = exp.getValue(String::class.java)
1 | 从构造一个新的 String 对象并使其成为大写 |
请注意泛型方法的使用: public <T> T getValue(Class<T> desiredResultType)
. 使用此方法不需要将表达式的值转换为所需的结果类型. 如果该值不能转换为类型 T
或使用注册的类型转换器转换, 则将抛出 EvaluationException
异常.
SpEL 的更常见用法是提供针对特定对象实例(称为根对象) 计算的表达式字符串. 以下示例显示如何从 Inventor
类的实例检索 name
属性或创建布尔条件:
// Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);
// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name"); // Parse name as an expression
String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla"
exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true
// Create and set a calendar
val c = GregorianCalendar()
c.set(1856, 7, 9)
// The constructor arguments are name, birthday, and nationality.
val tesla = Inventor("Nikola Tesla", c.time, "Serbian")
val parser = SpelExpressionParser()
var exp = parser.parseExpression("name") // Parse name as an expression
val name = exp.getValue(tesla) as String
// name == "Nikola Tesla"
exp = parser.parseExpression("name == 'Nikola Tesla'")
val result = exp.getValue(tesla, Boolean::class.java)
// result == true
4.1.1. 了解 EvaluationContext
在评估表达式以解析属性,方法或字段以及帮助执行类型转换时,将使用 EvaluationContext
接口. Spring 提供了两种实现.
-
SimpleEvaluationContext
: 为不需要 SpEL 语言语法的完整范围的表达式类别暴露必要的 SpEL 语言特性和配置选项的子集, 并且应该进行有意义的限制. 示例包括但不限于数据绑定表达式和基于属性的过滤器. -
StandardEvaluationContext
: 暴露全套 SpEL 语言功能和配置选项. 您可以使用它来指定默认根对象并配置每个可用的与评估相关的策略.
SimpleEvaluationContext
旨在仅支持 SpEL 语言语法的子集. 它排除了 Java 类型引用,构造函数和 bean 引用. 它还要求您明确选择表达式中属性和方法的支持级别. 默认情况下,create()
静态工厂方法仅启用对属性的读访问权限. 您还可以获取构建器以配置所需的确切支持级别,定位以下一个或多个组合:
-
仅限自定义
PropertyAccessor
(no reflection) -
只读访问的数据绑定属性
-
读写的数据绑定属性
类型转换
默认情况下,SpEL 使用 Spring 核心类( org.springframework.core.convert.ConversionService
)提供的转换服务. 此转换服务附带许多转换器,内置很多常用转换,但也支持扩展. 因此可以添加类型之间的自定义转换. 此外,它具有泛型感知的关键功能. 这意味着在使用表达式中的泛型类型时,SpEL 将尝试转换以维护遇到的任何对象的类型正确性.
这在实践中能得到什么好处? 假设使用 setValue()
的赋值被用于设置 List
属性. 属性的类型实际上是 List<Boolean>
,SpEL 会识别列表的元素需要在被放置在其中之前被转换为 Boolean
. 以下示例显示了如何执行此操作:
class Simple {
public List<Boolean> booleanList = new ArrayList<Boolean>();
}
Simple simple = new Simple();
simple.booleanList.add(true);
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// "false" is passed in here as a String. SpEL and the conversion service
// will recognize that it needs to be a Boolean and convert it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");
// b is false
Boolean b = simple.booleanList.get(0);
class Simple {
var booleanList: MutableList<Boolean> = ArrayList()
}
val simple = Simple()
simple.booleanList.add(true)
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
// "false" is passed in here as a String. SpEL and the conversion service
// will recognize that it needs to be a Boolean and convert it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false")
// b is false
val b = simple.booleanList[0]
4.1.2. 解析器配置
可以使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration
)来配置 SpEL 表达式解释器. 该配置对象控制表达式组件的行为. 例如,如果索引到数组或集合,
并且指定索引处的元素为 null
,则可以自动创建该元素. 当使用由一组属性引用组成的表达式时,这是非常有用的. 如果创建数组或集合的索引,并指定了超出数组或列表的当前大小的结尾的索引时,它将自动增大数组或列表大小以适应索引.为了在指定的索引, SpEL 将尝试使用元素类型的默认值创建元素设置指定值之前的构造函数. 如果元素类型没有 默认构造函数 null
将被添加到数组或列表中. 如果没有内置或知道如何设置值的自定义转换器, null
将保留在数组中, 或者 在指定索引处列出. 以下示例演示如何自动增长列表:
class Demo {
public List<String> list;
}
// Turn on:
// - auto null reference initialization
// - auto collection growing
SpelParserConfiguration config = new SpelParserConfiguration(true,true);
ExpressionParser parser = new SpelExpressionParser(config);
Expression expression = parser.parseExpression("list[3]");
Demo demo = new Demo();
Object o = expression.getValue(demo);
// demo.list will now be a real collection of 4 entries
// Each entry is a new empty String
class Demo {
var list: List<String>? = null
}
// Turn on:
// - auto null reference initialization
// - auto collection growing
val config = SpelParserConfiguration(true, true)
val parser = SpelExpressionParser(config)
val expression = parser.parseExpression("list[3]")
val demo = Demo()
val o = expression.getValue(demo)
// demo.list will now be a real collection of 4 entries
// Each entry is a new empty String
4.1.3. SpEL 编译
Spring Framework 4.1 包含一个基本的表达式编译器. 通常,由于表达式在操作过程中提供的大量动态性、灵活性的运算能够被解释,但不能保证提供最佳性能. 对于不常使用的表达式使用这是非常好的, 但是当被其他并不真正需要动态灵活性的组件(例如 Spring Integration) 使用时,性能可能成为瓶颈.
新的 SpEL 编译器旨在满足这一需求. 编译器将在表达行为运算操作期间即时生成一个真正的 Java 类,并使用它来实现更快的表达式求值. 由于缺少对表达式按类型归类,编译器在执行编译时会使用在表达式解释运算期间收集的信息来编译. 例如,它不仅仅需要从表达式中知道属性引用的类型,而且需要在第一个解释运算过程中发现它是什么. 当然,如果各种表达式元素的类型随着时间的推移而变化,那么基于此信息的编译可能会发生问题. 因此, 编译最适合于重复运算操作而类型信息不会改变的表达式.
请考虑以下基本表达式:
someArray[0].someProperty.someOtherProperty < 0.1
这涉及到数组访问,某些属性的取消和数字操作,所以性能增益非常明显. 在 50000 次迭代的微基准测试示例中,使用解析器评估需要 75ms,使用表达式的编译版本只需 3ms.
编译器配置
编译器在默认情况下是关闭的,有两种方法可以打开. 您可以使用解析器配置过程(前面讨论的) 或在将 SpEL 用法嵌入到另一个组件中时使用 Spring 属性来打开它. 本节讨论这两个选项.
编译器可以以三种模式之一操作,这些模式在 org.springframework.expression.spel.SpelCompilerMode
枚举中获取. 模式如下:
-
OFF
(default): 编译器已关闭. -
IMMEDIATE
: 在即时模式下,表达式将尽快编译. 这通常在第一次解释运算之后,如果编译的表达式失败(通常是由于类型更改引起的,参看上一节) ,则表达式运算操作的调用者将收到异常. -
MIXED
: 在混合模式下,表达式随着时间的推移在解释模式和编译模式之间静默地切换. 经过一些解释运行后,它们将切换到编译模式,如果编译后的表单出现问题(如上所述改变类型) , 表达式将自动重新切换回解释模式. 稍后,它可能生成另一个编译表单并切换. 基本上,用户进入IMMEDIATE
模式的异常是内部处理的.
推荐 IMMEDIATE
即时模式,因为 MIXED
模式可能会导致副作用,使得表达式出错. 如果编译的表达式在部分成功之后崩掉,此时可能已经影响了系统状态. 如果发生这种情况,调用者可能不希望它在解释模式下静默地重新运行,这样的话表达式的某部分可能会运行两次.
选择模式后,使用 SpelParserConfiguration
配置解析器. 以下示例显示了如何执行此操作:
SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
this.getClass().getClassLoader());
SpelExpressionParser parser = new SpelExpressionParser(config);
Expression expr = parser.parseExpression("payload");
MyMessage message = new MyMessage();
Object payload = expr.getValue(message);
val config = SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
this.javaClass.classLoader)
val parser = SpelExpressionParser(config)
val expr = parser.parseExpression("payload")
val message = MyMessage()
val payload = expr.getValue(message)
指定编译器模式时,还可以指定类加载器(允许传递 null
) . 编译表达式将在任何提供的子类加载器中被定义. 重要的是确保是否指定了类加载器,它可以看到表达式运算操作过程中涉及的所有类型. 如果没有指定,那么将使用默认的类加载器(通常是在表达式计算期间运行的线程的上下文类加载器) .
配置编译器的第二种方法是将 SpEL 嵌入其他组件内部使用,并且可能无法通过配置对象进行配置. 在这种情况下,可以使用 JVM 系统属性配置 (或者通过 SpringProperties
). 属性 spring.expression.compiler.mode
可以设置为 SpelCompilerMode
枚举值之一(off
, immediate
, 或 mixed
) .
4.2. bean 定义的表达式支持
SpEL 表达式可以通过XML或基于注解的配置用于定义 BeanDefinition
实例. 在这两种情况下,定义表达式的语法是 #{ <expression string> }
.
4.2.1. XML 配置
可以使用表达式设置属性或构造函数参数值,如以下示例所示:
<bean id="numberGuess" class="org.spring.samples.NumberGuess">
<property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
<!-- other properties -->
</bean>
应用程序上下文中的所有 bean 都可以作为预定义变量使用,通过他们的 bean 名称调用. 这包括标准的上下文 Bean,例如 environment
(类型为
org.springframework.core.env.Environment
)以及 systemProperties
和用于访问运行时环境的 systemEnvironment
(类型为 Map<String,Object>
).
以下示例显示了对 SpEL 变量对 systemProperties
bean 的访问:
<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
<property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>
<!-- other properties -->
</bean>
请注意,您不必在此上下文中使用 #
符号为预定义变量添加前缀.
您还可以按名称引用其他 bean 属性,如以下示例所示:
<bean id="numberGuess" class="org.spring.samples.NumberGuess">
<property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
<!-- other properties -->
</bean>
<bean id="shapeGuess" class="org.spring.samples.ShapeGuess">
<property name="initialShapeSeed" value="#{ numberGuess.randomNumber }"/>
<!-- other properties -->
</bean>
4.2.2. 注解 配置
要指定默认值,可以在字段,方法和方法或构造函数参数上放置 @Value
注解.
以下示例设置字段的默认值:
public class FieldValueTestBean {
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}
public String getDefaultLocale() {
return this.defaultLocale;
}
}
class FieldValueTestBean {
@Value("#{ systemProperties['user.region'] }")
var defaultLocale: String? = null
}
下面显示了属性 setter 方法的相同配置:
public class PropertyValueTestBean {
private String defaultLocale;
@Value("#{ systemProperties['user.region'] }")
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}
public String getDefaultLocale() {
return this.defaultLocale;
}
}
class PropertyValueTestBean {
@Value("#{ systemProperties['user.region'] }")
var defaultLocale: String? = null
}
使用 @Autowired
方法注解的构造方法也可以使用 @Value
注解:
public class SimpleMovieLister {
private MovieFinder movieFinder;
private String defaultLocale;
@Autowired
public void configure(MovieFinder movieFinder,
@Value("#{ systemProperties['user.region'] }") String defaultLocale) {
this.movieFinder = movieFinder;
this.defaultLocale = defaultLocale;
}
// ...
}
class SimpleMovieLister {
private lateinit var movieFinder: MovieFinder
private lateinit var defaultLocale: String
@Autowired
fun configure(movieFinder: MovieFinder,
@Value("#{ systemProperties['user.region'] }") defaultLocale: String) {
this.movieFinder = movieFinder
this.defaultLocale = defaultLocale
}
// ...
}
public class MovieRecommender {
private String defaultLocale;
private CustomerPreferenceDao customerPreferenceDao;
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
@Value("#{systemProperties['user.country']}") String defaultLocale) {
this.customerPreferenceDao = customerPreferenceDao;
this.defaultLocale = defaultLocale;
}
// ...
}
class MovieRecommender(private val customerPreferenceDao: CustomerPreferenceDao,
@Value("#{systemProperties['user.country']}") private val defaultLocale: String) {
// ...
}
4.3. 语言引用
本节介绍 Spring 表达式语言的工作原理. 它涵盖以下主题:
4.3.1. 文字表达
支持的文字表达式的类型是字符串,数值(int,real,hex) ,boolean 和 null. 字符串由单引号分隔. 要在字符串中放置单引号,请使用两个单引号字符.
以下清单显示了文字的简单用法. 通常,它们不是像这样单独使用,而是作为更复杂表达式的一部分使用 - 例如,在逻辑比较运算符的一侧使用文字.
ExpressionParser parser = new SpelExpressionParser();
// evals to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();
double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();
// evals to 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();
boolean trueValue = (Boolean) parser.parseExpression("true").getValue();
Object nullValue = parser.parseExpression("null").getValue();
val parser = SpelExpressionParser()
// evals to "Hello World"
val helloWorld = parser.parseExpression("'Hello World'").value as String
val avogadrosNumber = parser.parseExpression("6.0221415E+23").value as Double
// evals to 2147483647
val maxValue = parser.parseExpression("0x7FFFFFFF").value as Int
val trueValue = parser.parseExpression("true").value as Boolean
val nullValue = parser.parseExpression("null").value
数字支持使用负号,指数表示法和小数点. 默认情况下,使用 Double.parseDouble()
解析实数.
4.3.2. Properties, Arrays, Lists, Maps, 和 Indexers
调用属性的引用是很简单的,只要指定内置的属性值即可. Inventor
类(pupin
和 tesla
) 的实例填充了例子中用到的类 中使用的类中列出的数据. 下面的表达式用于获得 Tesla 的出生年和 Pupin 的出生城市:
// evals to 1856
int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context);
String city = (String) parser.parseExpression("placeOfBirth.city").getValue(context);
// evals to 1856
val year = parser.parseExpression("birthdate.year + 1900").getValue(context) as Int
val city = parser.parseExpression("placeOfBirth.city").getValue(context) as String
属性名称的第一个字母允许不区分大小写. 就这样上例中的表达式可以写成 |
数组和列表的内容是使用方括号表示法获得的,如下例所示:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// Inventions Array
// evaluates to "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
context, tesla, String.class);
// Members List
// evaluates to "Nikola Tesla"
String name = parser.parseExpression("members[0].name").getValue(
context, ieee, String.class);
// List and Array navigation
// evaluates to "Wireless communication"
String invention = parser.parseExpression("members[0].inventions[6]").getValue(
context, ieee, String.class);
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
// Inventions Array
// evaluates to "Induction motor"
val invention = parser.parseExpression("inventions[3]").getValue(
context, tesla, String::class.java)
// Members List
// evaluates to "Nikola Tesla"
val name = parser.parseExpression("members[0].name").getValue(
context, ieee, String::class.java)
// List and Array navigation
// evaluates to "Wireless communication"
val invention = parser.parseExpression("members[0].inventions[6]").getValue(
context, ieee, String::class.java)
maps 的内容通过方括号包着文字的键/值定义. 在这种情况下, 由于 officers
的 keys
是字符串,则可以定义字符字面值:
// Officer's Dictionary
Inventor pupin = parser.parseExpression("officers['president']").getValue(
societyContext, Inventor.class);
// evaluates to "Idvor"
String city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue(
societyContext, String.class);
// setting values
parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue(
societyContext, "Croatia");
// Officer's Dictionary
val pupin = parser.parseExpression("officers['president']").getValue(
societyContext, Inventor::class.java)
// evaluates to "Idvor"
val city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue(
societyContext, String::class.java)
// setting values
parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue(
societyContext, "Croatia")
4.3.3. 内嵌的 Lists
您可以使用 {}
表示法直接在表达式中表达列表.
// evaluates to a Java list containing the four numbers
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);
List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);
// evaluates to a Java list containing the four numbers
val numbers = parser.parseExpression("{1,2,3,4}").getValue(context) as List<*>
val listOfLists = parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context) as List<*>
{}
本身就是一个空列表. 出于性能原因,如果列表本身完全由固定文字组成,则会创建一个常量列表来表示表达式(而不是在每个计算上构建新列表) .
4.3.4. 内嵌 Maps
您还可以使用 {key:value}
表示法直接在表达式中表达 map. 以下示例显示了如何执行此操作:
// evaluates to a Java map containing the two entries
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);
Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);
// evaluates to a Java map containing the two entries
val inventorInfo = parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context) as Map<*, >
val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<, *>
{:}
本身就是一张空 map. 出于性能原因,如果 map 本身由固定文字或其他嵌套常量结构(列表或 map) 组成, 则会创建一个常量来表示表达式(而不是在每次计算时构建新 map) . map 的双引号是可选的(除非 key 包含句点 (.
)).上面的示例没有使用双引号的 key.
4.3.5. 数组的构造
您可以使用熟悉的 Java 语法构建数组,可选择提供初始化程序以在构造时填充数组. 以下示例显示了如何执行此操作:
int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);
// Array with initializer
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);
// Multi dimensional array
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);
val numbers1 = parser.parseExpression("new int[4]").getValue(context) as IntArray
// Array with initializer
val numbers2 = parser.parseExpression("new int[]{1,2,3}").getValue(context) as IntArray
// Multi dimensional array
val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array<IntArray>
目前不支持创建多维数组的初始化器.
4.3.6. 方法
方法是使用典型的 Java 编程语法调用的,还可以对文本调用方法. 也支持对参数的调用.
// string literal, evaluates to "bc"
String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);
// evaluates to true
boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
societyContext, Boolean.class);
// string literal, evaluates to "bc"
val bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String::class.java)
// evaluates to true
val isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
societyContext, Boolean::class.java)
4.3.7. 运算符
Spring Expression Language 支持以下类型的运算符:
关系运算符
使用标准运算符表示法支持关系运算符(等于,不等于,小于,小于或等于,大于,等于或等于) . 以下清单显示了一些运算符示例:
// evaluates to true
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);
// evaluates to false
boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class);
// evaluates to true
boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);
// evaluates to true
val trueValue = parser.parseExpression("2 == 2").getValue(Boolean::class.java)
// evaluates to false
val falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean::class.java)
// evaluates to true
val trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean::class.java)
大于和小于 如果您更喜欢数字比较,请避免基于数字的 |
除了标准的关系运算符之外,SpEL 支持 instanceof
和基于 matches
的正则表达式运算符,以下列表显示了两者的示例:
// evaluates to false
boolean falseValue = parser.parseExpression(
"'xyz' instanceof T(Integer)").getValue(Boolean.class);
// evaluates to true
boolean trueValue = parser.parseExpression(
"'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
//evaluates to false
boolean falseValue = parser.parseExpression(
"'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
// evaluates to false
val falseValue = parser.parseExpression(
"'xyz' instanceof T(Integer)").getValue(Boolean::class.java)
// evaluates to true
val trueValue = parser.parseExpression(
"'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java)
//evaluates to false
val falseValue = parser.parseExpression(
"'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java)
使用原始类型的时候留意他们会直接被包装成包装类,因此 1 instanceof T(int) 是 false . 而 1 instanceof T(Integer) 是 true .
|
每一个符号运算符可以使用直接的单词字母(前缀) 来定义,这样可以避免在某些特定的表达式会在文件类型中出现问题(例如 XML 文档) . 现在列出文本的替换规则:
-
lt
(<
) -
gt
(>
) -
le
(<=
) -
ge
(>=
) -
eq
(==
) -
ne
(!=
) -
div
(/
) -
mod
(%
) -
not
(!
).
所有文本运算符都不区分大小写.
逻辑运算符
SpEL 支持以下逻辑运算符:
-
and
(&&
) -
or
(||
) -
not
(!
)
以下示例显示如何使用逻辑运算符
// -- AND --
// evaluates to false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);
// evaluates to true
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
// -- OR --
// evaluates to true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);
// evaluates to true
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
// -- NOT --
// evaluates to false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);
// -- AND and NOT --
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);
// -- AND --
// evaluates to false
val falseValue = parser.parseExpression("true and false").getValue(Boolean::class.java)
// evaluates to true
val expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')"
val trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java)
// -- OR --
// evaluates to true
val trueValue = parser.parseExpression("true or false").getValue(Boolean::class.java)
// evaluates to true
val expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')"
val trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java)
// -- NOT --
// evaluates to false
val falseValue = parser.parseExpression("!true").getValue(Boolean::class.java)
// -- AND and NOT --
val expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')"
val falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java)
数学运算符
(+
) 可以用在数值和字符串之间. (-
)、(*
) 和 (/
) 只能用在数值上. 其他算术运算符支持取余(%
) 和乘方(^
) . 标准的运算符是支持优先级的. 以下示例显示了正在使用的数学运算符:
// Addition
int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2
String testString = parser.parseExpression(
"'test' + ' ' + 'string'").getValue(String.class); // 'test string'
// Subtraction
int four = parser.parseExpression("1 - -3").getValue(Integer.class); // 4
double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); // -9000
// Multiplication
int six = parser.parseExpression("-2 * -3").getValue(Integer.class); // 6
double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); // 24.0
// Division
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); // -2
double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); // 1.0
// Modulus
int three = parser.parseExpression("7 % 4").getValue(Integer.class); // 3
int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); // 1
// Operator precedence
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -21
// Addition
val two = parser.parseExpression("1 + 1").getValue(Int::class.java) // 2
val testString = parser.parseExpression(
"'test' + ' ' + 'string'").getValue(String::class.java) // 'test string'
// Subtraction
val four = parser.parseExpression("1 - -3").getValue(Int::class.java) // 4
val d = parser.parseExpression("1000.00 - 1e4").getValue(Double::class.java) // -9000
// Multiplication
val six = parser.parseExpression("-2 * -3").getValue(Int::class.java) // 6
val twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double::class.java) // 24.0
// Division
val minusTwo = parser.parseExpression("6 / -3").getValue(Int::class.java) // -2
val one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double::class.java) // 1.0
// Modulus
val three = parser.parseExpression("7 % 4").getValue(Int::class.java) // 3
val one = parser.parseExpression("8 / 5 % 2").getValue(Int::class.java) // 1
// Operator precedence
val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21
赋值运算符
要设置属性,请使用赋值运算符(=
). 这通常在调用 setValue
时完成,但也可以在调用 getValue
时完成. 以下清单显示了使用赋值运算符的两种方法:
Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
parser.parseExpression("name").setValue(context, inventor, "Aleksandar Seovic");
// alternatively
String aleks = parser.parseExpression(
"name = 'Aleksandar Seovic'").getValue(context, inventor, String.class);
val inventor = Inventor()
val context = SimpleEvaluationContext.forReadWriteDataBinding().build()
parser.parseExpression("name").setValue(context, inventor, "Aleksandar Seovic")
// alternatively
val aleks = parser.parseExpression(
"name = 'Aleksandar Seovic'").getValue(context, inventor, String::class.java)
4.3.8. 类型
特殊 T
运算符可用于指定 java.lang.Class
的实例类型. 也可以使用此运算符调用静态方法. StandardEvaluationContext
使用 TypeLocator
来查找类型, 而 StandardTypeLocator
(可以替换)是通过对 java.lang
包的解释而生成的.
这意味着 T()
对 java.lang
中的类型的引用不需要完全限定,但所有其他类型引用都是必须的. 以下示例显示如何使用 T
运算符:
Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
boolean trueValue = parser.parseExpression(
"T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
.getValue(Boolean.class);
val dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class::class.java)
val stringClass = parser.parseExpression("T(String)").getValue(Class::class.java)
val trueValue = parser.parseExpression(
"T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
.getValue(Boolean::class.java)
4.3.9. 构造器
可以使用 new
运算符调用构造函数. 除了位于 java.lang
包中的类型 (Integer
, Float
, String
,等等是可以直接使用的) 外, 所有类型需要使用全限定类名. 以下示例显示如何使用 new
运算符来调用构造函数:
Inventor einstein = p.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
.getValue(Inventor.class);
// create new Inventor instance within the add() method of List
p.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor(
'Albert Einstein', 'German'))").getValue(societyContext);
val einstein = p.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
.getValue(Inventor::class.java)
// create new Inventor instance within the add() method of List
p.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
.getValue(societyContext)
4.3.10. 变量
在表达式中,变量通过 #variableName
模式来表示. 变量的设置用到 EvaluationContext
的 setVariable
方法.
有效的变量名称必须由以下一种或多种支持的组成字符.
|
以下示例显示了如何使用变量.
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("newName", "Mike Tesla");
parser.parseExpression("name = #newName").getValue(context, tesla);
System.out.println(tesla.getName()) // "Mike Tesla"
val tesla = Inventor("Nikola Tesla", "Serbian")
val context = SimpleEvaluationContext.forReadWriteDataBinding().build()
context.setVariable("newName", "Mike Tesla")
parser.parseExpression("name = #newName").getValue(context, tesla)
println(tesla.name) // "Mike Tesla"
#this
和 #root
变量
#this
变量始终指向当前的对象(处理没有全限定的引用) . #root
变量使用指向根上下文对象. 尽管 #this
可能根据表达式而不同. 但是,#root
一直指向根引用. 以下示例显示了如何使用 #this
和 #root
变量:
// create an array of integers
List<Integer> primes = new ArrayList<Integer>();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));
// create parser and set variable 'primes' as the array of integers
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess();
context.setVariable("primes", primes);
// all prime numbers > 10 from the list (using selection ?{...})
// evaluates to [11, 13, 17]
List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression(
"#primes.?[#this>10]").getValue(context);
// create an array of integers
val primes = ArrayList<Int>()
primes.addAll(listOf(2, 3, 5, 7, 11, 13, 17))
// create parser and set variable 'primes' as the array of integers
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataAccess()
context.setVariable("primes", primes)
// all prime numbers > 10 from the list (using selection ?{...})
// evaluates to [11, 13, 17]
val primesGreaterThanTen = parser.parseExpression(
"#primes.?[#this>10]").getValue(context) as List<Int>
4.3.11. 函数
可以通过用户自定义函数来扩展 SpEL,它可以在表达式字符串中使用,函数使用 EvaluationContext
的方法来注册:
Method method = ...;
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);
val method: Method = ...
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
context.setVariable("myFunction", method)
例如,请考虑以下实用程序方法来反转字符串:
public abstract class StringUtils {
public static String reverseString(String input) {
StringBuilder backwards = new StringBuilder(input.length());
for (int i = 0; i < input.length(); i++) {
backwards.append(input.charAt(input.length() - 1 - i));
}
return backwards.toString();
}
}
fun reverseString(input: String): String {
val backwards = StringBuilder(input.length)
for (i in 0 until input.length) {
backwards.append(input[input.length - 1 - i])
}
return backwards.toString()
}
然后,您可以注册并使用上述方法,如以下示例所示:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("reverseString",
StringUtils.class.getDeclaredMethod("reverseString", String.class));
String helloWorldReversed = parser.parseExpression(
"#reverseString('hello')").getValue(context, String.class);
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
context.setVariable("reverseString", ::reverseString::javaMethod)
val helloWorldReversed = parser.parseExpression(
"#reverseString('hello')").getValue(context, String::class.java)
4.3.12. Bean 的引用
如果已使用 bean 解析器配置了评估上下文,则可以使用 @
符号从表达式中查找 bean. 以下示例显示了如何执行此操作:
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());
// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@something").getValue(context);
val parser = SpelExpressionParser()
val context = StandardEvaluationContext()
context.setBeanResolver(MyBeanResolver())
// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
val bean = parser.parseExpression("@something").getValue(context)
要访问工厂 bean 本身,bean 名称应改为( &
) 前缀符号. 以下示例显示了如何执行此操作:
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());
// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("&foo").getValue(context);
val parser = SpelExpressionParser()
val context = StandardEvaluationContext()
context.setBeanResolver(MyBeanResolver())
// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
val bean = parser.parseExpression("&foo").getValue(context)
4.3.13. 三元运算符(If-Then-Else)
您可以使用三元运算符在表达式中执行 if-then-else 条件逻辑. 以下清单显示了一个最小的示例:
String falseString = parser.parseExpression(
"false ? 'trueExp' : 'falseExp'").getValue(String.class);
val falseString = parser.parseExpression(
"false ? 'trueExp' : 'falseExp'").getValue(String::class.java)
在这种情况下,布尔值 false
会返回字符串值 'falseExp'
. 一个更复杂的例子如下:
parser.parseExpression("name").setValue(societyContext, "IEEE");
societyContext.setVariable("queryName", "Nikola Tesla");
expression = "isMember(#queryName)? #queryName + ' is a member of the ' " +
"+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'";
String queryResultString = parser.parseExpression(expression)
.getValue(societyContext, String.class);
// queryResultString = "Nikola Tesla is a member of the IEEE Society"
parser.parseExpression("name").setValue(societyContext, "IEEE")
societyContext.setVariable("queryName", "Nikola Tesla")
expression = "isMember(#queryName)? #queryName + ' is a member of the ' " + "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'"
val queryResultString = parser.parseExpression(expression)
.getValue(societyContext, String::class.java)
// queryResultString = "Nikola Tesla is a member of the IEEE Society"
有关三元运算符的更短语法,请参阅 Elvis 运算符的下一节.
4.3.14. Elvis 运算符
Elvis 运算符是三元运算符语法的缩写,用于http://www.groovy-lang.org/operators.html#_elvis_operator[Groovy] 语言. 使用三元运算符语法,您通常必须重复两次变量,如以下示例所示:
String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");
可以使用 Elvis 运算符来实现,上面例子的也可以使用如下的形式展现:
ExpressionParser parser = new SpelExpressionParser();
String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class);
System.out.println(name); // 'Unknown'
val parser = SpelExpressionParser()
val name = parser.parseExpression("name?:'Unknown'").getValue(Inventor(), String::class.java)
println(name) // 'Unknown'
以下列表显示了一个更复杂的示例:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
String name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name); // Nikola Tesla
tesla.setName(null);
name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name); // Elvis Presley
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
val tesla = Inventor("Nikola Tesla", "Serbian")
var name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
println(name) // Nikola Tesla
tesla.setName(null)
name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
println(name) // Elvis Presley
您可以使用 Elvis 运算符在表达式中应用默认值. 以下示例显示如何在
如果已定义,则将注入系统属性 |
4.3.15. 安全的引导运算符
安全的引导运算符用于避免 NullPointerException
异常,这种观念来自 Groovy 语言. 当需要引用一个对象时, 可能需要在访问对象的方法或属性之前验证它是否为 null
. 为避免出现这种情况, 安全引导运算符将简单地返回 null
,而不是引发异常.
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));
String city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class);
System.out.println(city); // Smiljan
tesla.setPlaceOfBirth(null);
city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class);
System.out.println(city); // null - does not throw NullPointerException!!!
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
val tesla = Inventor("Nikola Tesla", "Serbian")
tesla.setPlaceOfBirth(PlaceOfBirth("Smiljan"))
var city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java)
println(city) // Smiljan
tesla.setPlaceOfBirth(null)
city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java)
println(city) // null - does not throw NullPointerException!!!
4.3.16. 集合的选择
Selection 是一种功能强大的表达语言功能,通过从条目中进行选择,可以将某些源集合转换为另一种集合.
Selection 使用语法是 .?[selectionExpression]
. 它会过滤集合并返回一个新的集合,其包含一个原始数据的子集合. 例如,Selection 可以简单地获取 Serbian inventors 的list:
List<Inventor> list = (List<Inventor>) parser.parseExpression(
"members.?[nationality == 'Serbian']").getValue(societyContext);
val list = parser.parseExpression(
"members.?[nationality == 'Serbian']").getValue(societyContext) as List<Inventor>
Selection 可以使用在数组,或实现了 java.lang.Iterable
接口的任何类和 map 上.对于数组和列表 选择标准是针对每一个元素,而当选择一个 map 时将会处理每个 map 的 entry(Java 类型 Map.Entry
的对象) ,Map
的 entry 有他的 key
和 value
作为属性访问在 Selection 中使用.
以下表达式将返回一个新的 map,包括原有 map 中所有值小于 27 的条目:
Map newMap = parser.parseExpression("map.?[value<27]").getValue();
val newMap = parser.parseExpression("map.?[value<27]").getValue()
除了返回所有选定元素外, 还可以只检索第一个或最后一个值. 要获得与所选内容匹配的第一个元素语法是 .^[selectionExpression]
. 而获取最后一个匹配的选择语法是 .$[selectionExpression]
.
4.3.17. 集合投影
投影允许集合被一个子表达式处理而且结果是一个新的集合. 投影的语法是 .![projectionExpression]
. 通过例子可便于理解,假设有一个 invertors 的 list 并且希望其生产一个叫 cities 的 list, 有效的做法是对每个在 invertor 的 list 调用 'placeOfBirth.city'. 使用投影:
// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("members.![placeOfBirth.city]");
// returns ['Smiljan', 'Idvor' ]
val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*>
Projection 可以使用在数组,或实现了 java.lang.Iterable
接口的任何类和 map 上
当使用 map 用于处理投影,在这种情况下投影表达式可以对 map 中的每个 entry 进行处理(作为一个 Java 的 Map.Entry
) . map 投影的结果是一个 list,包含对每一个 map 条目处理的投影表达式.
4.3.18. 表达式模板
表达式模板允许将文字文本与一个或多个评估块混合使用. 每个计算块都可以定义的前缀和后缀字符分隔,一般选择使用 #{ }
作为分隔符. 如下例所示:
String randomPhrase = parser.parseExpression(
"random number is #{T(java.lang.Math).random()}",
new TemplateParserContext()).getValue(String.class);
// evaluates to "random number is 0.7038186818312008"
val randomPhrase = parser.parseExpression(
"random number is #{T(java.lang.Math).random()}",
TemplateParserContext()).getValue(String::class.java)
// evaluates to "random number is 0.7038186818312008"
字符串包含文本 'random number is '
和在 #{ }
中的表达式的处理结果. 这个例子的结果调用了 random()
方法. 第二个参数对于 parseExpression()
方法是 ParserContext
的类型. ParserContext
接口可以控制表达式的解释,用于支持表达式模板功能. TemplateParserContext
的定义如下:
public class TemplateParserContext implements ParserContext {
public String getExpressionPrefix() {
return "#{";
}
public String getExpressionSuffix() {
return "}";
}
public boolean isTemplate() {
return true;
}
}
class TemplateParserContext : ParserContext {
override fun getExpressionPrefix(): String {
return "#{"
}
override fun getExpressionSuffix(): String {
return "}"
}
override fun isTemplate(): Boolean {
return true
}
}
4.4. 例子中用到的类
本节列出了本章示例中使用的类
public class Inventor {
private String name;
private String nationality;
private String[] inventions;
private Date birthdate;
private PlaceOfBirth placeOfBirth;
public Inventor(String name, String nationality) {
GregorianCalendar c= new GregorianCalendar();
this.name = name;
this.nationality = nationality;
this.birthdate = c.getTime();
}
public Inventor(String name, Date birthdate, String nationality) {
this.name = name;
this.nationality = nationality;
this.birthdate = birthdate;
}
public Inventor() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNationality() {
return nationality;
}
public void setNationality(String nationality) {
this.nationality = nationality;
}
public Date getBirthdate() {
return birthdate;
}
public void setBirthdate(Date birthdate) {
this.birthdate = birthdate;
}
public PlaceOfBirth getPlaceOfBirth() {
return placeOfBirth;
}
public void setPlaceOfBirth(PlaceOfBirth placeOfBirth) {
this.placeOfBirth = placeOfBirth;
}
public void setInventions(String[] inventions) {
this.inventions = inventions;
}
public String[] getInventions() {
return inventions;
}
}
class Inventor(
var name: String,
var nationality: String,
var inventions: Array<String>? = null,
var birthdate: Date = GregorianCalendar().time,
var placeOfBirth: PlaceOfBirth? = null)
public class PlaceOfBirth {
private String city;
private String country;
public PlaceOfBirth(String city) {
this.city=city;
}
public PlaceOfBirth(String city, String country) {
this(city);
this.country = country;
}
public String getCity() {
return city;
}
public void setCity(String s) {
this.city = s;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
}
class PlaceOfBirth(var city: String, var country: String? = null) {
public class Society {
private String name;
public static String Advisors = "advisors";
public static String President = "president";
private List<Inventor> members = new ArrayList<Inventor>();
private Map officers = new HashMap();
public List getMembers() {
return members;
}
public Map getOfficers() {
return officers;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isMember(String name) {
for (Inventor inventor : members) {
if (inventor.getName().equals(name)) {
return true;
}
}
return false;
}
}
class Society {
val Advisors = "advisors"
val President = "president"
var name: String? = null
val members = ArrayList<Inventor>()
val officers = mapOf<Any, Any>()
fun isMember(name: String): Boolean {
for (inventor in members) {
if (inventor.name == name) {
return true
}
}
return false
}
}
5. 使用 Spring 面向切面编程
面向切面编程(Aspect-oriented Programming 简称 AOP) ,是相对面向对象编程(Object-oriented Programming 简称 OOP )的框架,作为 OOP 的一种功能补充. OOP 主要的模块单元是类(class). 而 AOP 则是切面(aspect) . 切面会将诸如事务管理这样跨越多个类型和对象的关注点模块化(在 AOP 的语义中,这类关注点被称为横切关注点(crosscutting) ) .
AOP 是 Spring 框架重要的组件,虽然 Spring IoC 容器没有依赖 AOP,因此 Spring 不会强迫开发者使用 AOP. 但 AOP 提供了非常棒的功能,用做对 Spring IoC 的补充.
AOP 在 Spring Framework 中用于:
-
提供声明式企业服务,特别是用于替代 EJB 的声明式服务. 最重要的服务是声明式事务管理,这个服务建立在 Spring 的抽象事务管理之上.
-
允许开发者实现自定义切面,使用 AOP 来完善 OOP 的功能.
如果只打算使用通用的声明式服务或者已有的声明式中间件服务,例如缓冲池(pooling) 那么可以不直接使用 AOP,也可以忽略本章大部分内容. |
5.1. AOP 概念
让我们从定义一些核心 AOP 概念和术语开始. 这些术语不是特定于 Spring 的. 不幸的是,AOP 术语不是特别直观. 但是,如果 Spring 使用自己的术语,那将更加令人困惑.
-
Aspect(切面): 指关注点模块化,这个关注点可能会横切多个对象. 事务管理是企业级 Java 应用中有关横切关注点的例子. 在 Spring AOP 中,切面可以使用通用类 基于 schema 的方式的方式或者在普通类中以
@AspectJ
注解来实现. -
Join point(连接点):在程序执行过程中某个特定的点,例如某个方法调用的时间点或者处理异常的时间点. 在 Spring AOP 中,一个连接点总是代表一个方法的执行.
-
Advice(通知): 在切面的某个特定的连接点上执行的动作. 通知有多种类型,包括 “around”, “before” 和 “after” 等等. 通知的类型将在后面的章节进行讨论. 许多 AOP 框架,包括 Spring 在内,都是以拦截器做通知模型的,并维护着一个以连接点为中心的拦截器链.
-
Pointcut(切点): 匹配连接点的断言. 通知和切点表达式相关联,并在满足这个切点的连接点上运行(例如,当执行某个特定名称的方法时) . 切点表达式如何和连接点匹配是 AOP 的核心: Spring 默认使用 AspectJ 切点语义.
-
Introduction(引入): 声明额外的方法或者某个类型的字段. Spring 允许引入新的接口(以及一个对应的实现) 到任何被通知的对象上. 例如,可以使用引入来使 bean 实现
IsModified
接口, 以便简化缓存机制(在 AspectJ 社区,引入也被称为内部类型声明(inter) ) . -
Target object(目标对象): 被一个或者多个切面所通知的对象. 也被称作被通知(advised) 对象. 既然 Spring AOP 是通过运行时代理实现的,那么这个对象永远是一个被代理(proxied) 的对象.
-
AOP proxy(AOP代理): AOP 框架创建的对象,用来实现切面契约(aspect contract) (包括通知方法执行等功能) . 在 Spring 中,AOP 代理可以是 JDK 动态代理或 CGLIB 代理.
-
Weaving(织入): 把切面连接到其它的应用程序类型或者对象上,并创建一个被被通知的对象的过程. 这个过程可以在编译时(例如使用 AspectJ 编译器) 、类加载时或运行时中完成. Spring 和其他纯 Java AOP 框架一样,是在运行时完成织入的.
Spring AOP 包含以下类型的通知:
-
Before advice(前置通知): 在连接点之前运行但无法阻止执行流程进入连接点的通知(除非它引发异常) .
-
After returning advice(后置返回通知): 在连接点正常完成后执行的通知(例如,当方法没有抛出任何异常并正常返回时) .
-
After throwing advice(后置异常通知): 在方法抛出异常退出时执行的通知.
-
After (finally) advice(后置通知(总会执行) ): 当连接点退出的时候执行的通知(无论是正常返回还是异常退出) .
-
Around advice(环绕通知): 环绕连接点的通知,例如方法调用. 这是最强大的一种通知类型,. 环绕通知可以在方法调用前后完成自定义的行为. 它可以选择是否继续执行连接点或直接返回自定义的返回值又或抛出异常将执行结束.
环绕通知是最常用的一种通知类型. 与 AspectJ 一样,在选择 Spring 提供的通知类型时,团队推荐开发者尽量使用简单的通知类型来实现需要的功能. 例如, 如果只是需要使用方法的返回值来作缓存更新,虽然使用环绕通知也能完成同样的事情,但是仍然推荐使用后置返回通知来代替.
使用最合适的通知类型可以让编程模型变得简单, 还能避免很多潜在的错误. 例如,开发者无需调用于环绕通知(用 JoinPoint
) 的 proceed()
方法,也就不会产生调用的问题.
所有通知参数都是静态类型的,因此您可以使用相应类型的通知参数(例如,方法执行的返回值的类型) 而不是 Object
数组.
切点和连接点匹配是 AOP 的关键概念,这个概念让 AOP 不同于其它仅仅提供拦截功能的旧技术. 切入点使得通知可以独立于面向对象的层次结构进行定向. 例如,您可以将一个提供声明式事务管理的通知应用于跨多个对象(例如服务层中的所有业务操作) 的一组方法.
5.2. Spring AOP 的功能和目标
Spring AOP 是用纯 Java 实现的. 不需要特殊的编译过程. Spring AOP 不需要控制类加载器层次结构,因此适合在 servlet 容器或应用程序服务器中使用.
Spring 目前仅支持方法调用的方式作为连接点(在 Spring bean 上通知方法的执行) . 虽然可以在不影响到 Spring AOP 核心 API 的情况下加入对成员变量拦截器支持, 但 Spring 并没有实现成员变量拦截器. 如果需要通知对成员变量的访问和更新连接点,可以考虑其它语言,例如 AspectJ.
Spring 实现 AOP 的方法与其他的框架不同,Spring 并不是要尝试提供最完整 的AOP 实现(尽管 Spring AOP 有这个能力) ,相反地,它其实侧重于提供一种 AOP 与 Spring IoC 容器的整合的实现,用于帮助解决在企业级开发中的常见问题.
因此,例如,Spring Framework 的 AOP 功能通常与 Spring IoC 容器一起使用. 通过使用普通 bean 定义语法来配置切面(尽管 Spring 提供了强大的 "自动代理" 功能) . 这是与其他AOP实现的重要区别. 使用 Spring AOP 无法轻松或高效地完成某些操作,例如建议非常细粒度的对象(通常是域对象) . 在这种情况下,AspectJ 是最佳选择. 但是,我们的经验是,Spring AOP 为适合 AOP 的企业 Java 应用程序中的大多数问题提供了出色的解决方案.
Spring AOP从来没有打算通过提供一种全面的 AOP 解决方案用于取代 AspectJ,我们相信,基于代理的框架(如 Spring AOP) 和完整的框架(如 AspectJ) 都很有价值,而且它们是互补的,而不是竞争. Spring 将 Spring AOP 和 IoC 与 AspectJ 无缝集成,使得所有的 AOP 功能完全融入基于 Spring 的应用体系. 这样的集成不会影响 Spring AOP API 或者 AOP Alliance API. Spring AOP 仍然向后兼容. 有关Spring AOP API的讨论,请参阅 以下章节.
Spring 框架的一个核心原则是非侵入性. 这意味着开发者无需在自身的 业务/domain 模型上被迫引入框架特定的类和接口. 然而,有些时候,Spring 框架可以让开发者选择引入 Spring 框架特定的依赖到业务代码. 给予这种选择的理由是因为在某些情况下它可能是更易读或易于编写某些特定功能. Spring 框架(几乎) 总能给出这样的选择,开发者可以自由地做出明智的决定,选择最适合的特定用例或场景. 与本章相关的一个选择是选择哪种 AOP 框架(以及哪种AOP样式) . 您可以选择 AspectJ,Spring AOP 或两者. 也可以选择 请参阅选择要使用的 AOP 声明样式,以更全面地讨论每种样式的 "为什么和如何进行". |
5.3. AOP 代理
Spring 默认使用标准的 JDK 动态代理来作为AOP的代理. 这样任何接口(或者接口的 set) 都可以被代理.
Spring 也支持使用 CGLIB 代理. 对于需要代理类而不是代理接口的时候 CGLIB 代理是很有必要的. 如果业务对象并没有实现接口,默认就会使用 CGLIB 代理 . 此外,面向接口编程也是最佳实践,业务对象通常都会实现一个或多个接口. 此外,还可以强制的使用 CGLIB 代理, 在那些(希望是罕见的) 需要通知没有在接口中声明的方法时,或者当需要传递一个代理对象作为一种具体类型到方法的情况下.
掌握 Spring AOP 是基于代理的这一事实非常重要. 请参阅 AOP 代理,以全面了解此实现细节的实际含义. .
5.4. @AspectJ 注解支持
@AspectJ
会将切面声明为常规 Java 类的注解类型. AspectJ project 引入了 @AspectJ
风格,并作为 AspectJ 5 发行版的一部分. Spring 使用的注解类似于 AspectJ 5, 使用 AspectJ 提供的库用来解析和匹配切点. AOP 运行时仍然是纯粹的 Spring AOP,并不依赖 AspectJ 编译器或编织器.
使用 AspectJ 编译器和织入并允许使用全部基于 AspectJ 语言,并在在 Spring 应用中使用 AspectJ进行了讨论. |
5.4.1. 启用 @AspectJ 支持
要在 Spring 配置中使用 @AspectJ
切面,需要启用 Spring 支持,用于根据 @AspectJ
切面配置 Spring AOP,并根据这些切面自动代理 bean (事先判断是否在通知的范围内) . 通过自动代理的意思是: 如果 Spring 确定一个 bean 是由一个或多个切面处理的,将据此为 bean 自动生成代理 bean ,并以拦截方法调用并确保需要执行的通知.
可以使用 XML 或 Java 配置的方式启用 @AspectJ
支持. 不管哪一种方式,您还需要确保 AspectJ 的 aspectjweaver.jar
库位于应用程序的类路径中(版本 1.8 或更高版本) . 此库可在 AspectJ 分发的 lib
目录中或 Maven Central 仓库中找到.
使用 Java 配置启用 @AspectJ
支持
要使用 Java @Configuration
启用 @AspectJ
支持,请添加 @EnableAspectJAutoProxy
注解,如以下示例所示:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
@Configuration
@EnableAspectJAutoProxy
class AppConfig
使用 XML 配置启用 @AspectJ
支持
要使用基于 XML 的配置启用 @AspectJ
支持,请使用 aop:aspectj-autoproxy
元素,如以下示例所示:
<aop:aspectj-autoproxy/>
这假设您使用基于 XML Schema 配置中描述的 schema 支持. 有关如何在 aop
命名空间中导入标签,请参阅 AOP schema.
5.4.2. 声明切面
启用了 @AspectJ
支持后,在应用程序上下文中定义的任意 bean (有 @Aspect
注解) 的类都将被 Spring 自动检测,并用于配置 Spring AOP. 接下来的两个示例显示了非常有用的方面所需的最小定义.
这两个示例中的第一个示例在应用程序上下文中显示了一个常规 bean 定义,该定义指向具有 @Aspect
注解的 bean 类:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
这两个示例中的第二个显示了 NotVeryUsefulAspect
类定义,该定义使用 org.aspectj.lang.annotation.Aspect
注解进行注解:
@Aspect
public class NotVeryUsefulAspect {
}
@Aspect
class NotVeryUsefulAspect
切面(使用 @Aspect
的类) 可以拥有方法和属性,与其他类并无不同. 也可以包括切点、通知和内置类型(即引入) 声明.
通过组件扫描自动检测切面
您可以在 Spring XML 配置中将切面类注册为常规 bean 或者在 @Configuration 类中配置 @Bean 方法 ,Spring 会通过类路径扫描自动检测它们 - 与任何其他 Spring 管理的 bean 相同. 然而注意到 @Aspect 注解对于类的自动探测是不够的, 为此,需要单独添加 @Component ,注解(或自定义注解声明,用作 Spring 组件扫描器的规则之一) .
|
是否可以作为其他切面的切面通知?
在 Spring AOP 中,不可能将切面本身被作为其他切面的目标. 类上的 @Aspect 注解表明他是一个切面并且排除在自动代理的范围之外.
|
5.4.3. 声明切点
切点决定了匹配的连接点,从而使我们能够控制通知何时执行. Spring AOP 只支持使用 Spring bean 的方法执行连接点,所以可以将切点看出是匹配 Spring bean 上方法的执行. 切点的声明包含两个部分: 包含名称和任意参数的签名,以及明确需要匹配的方式执行的切点表达式.
在 @AspectJ
注解方式的 AOP 中,一个切点的签名由常规方法定义来提供, 并且切点表达式使用 @Pointcut
注解指定(方法作为切点签名必须有类型为 void
的返回) .
使用例子有助于更好地区分切点签名和切点表达式之间的关系. 以下示例定义名为 anyOldTransfer
的切点,该切点与名为 transfer
的任何方法的执行相匹配:
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
@Pointcut("execution(* transfer(..))") // the pointcut expression
private fun anyOldTransfer() {} // the pointcut signature
切点表达式由 @Pointcut
注解的值是常规的 AspectJ 切点表达式. 关于 AspectJ 切点语言的描述,见 AspectJ
Programming Guide (作为扩展, 请参考 AspectJ 5 Developer’s Notebook) 或者 Colyer 著的关于 AspectJ 的书籍. 例如, Eclipse AspectJ,或者参看 Ramnivas Laddad的 AspectJ in Action.
支持切点标识符
Spring AOP 支持使用以下 AspectJ 切点标识符(PCD),用于切点表达式:
-
execution
: 用于匹配方法执行连接点. 这是使用 Spring AOP 时使用的主要切点标识符. -
within
: 限制匹配特定类型中的连接点(在使用 Spring AOP 时,只需执行在匹配类型中声明的方法) . -
this
: 在 bean 引用(Spring AOP 代理) 是给定类型的实例的情况下,限制匹配连接点(使用 Spring AOP 时方法的执行) . -
target
: 限制匹配到连接点(使用 Spring AOP 时方法的执行) ,其中目标对象(正在代理的应用程序对象) 是给定类型的实例. -
args
: 限制与连接点的匹配(使用 Spring AOP 时方法的执行) ,其中变量是给定类型的实例. -
@target
: 限制与连接点的匹配(使用 Spring AOP 时方法的执行) ,其中执行对象的类具有给定类型的注解. -
@args
: 限制匹配连接点(使用 Spring AOP 时方法的执行) ,其中传递的实际参数的运行时类型具有给定类型的注解. -
@within
: 限制与具有给定注解的类型中的连接点匹配(使用 Spring AOP 时在具有给定注解的类型中声明的方法的执行) . -
@annotation
: 限制匹配连接点(在 Spring AOP 中执行的方法具有给定的注解) .
因为 Spring AOP 限制了只匹配方法的连接点执行,所以上面的切点标识符的讨论比在 AspectJ 编程指南中找到的定义要窄. 另外,AspectJ 本身具有基于类型的语义,
并且在执行连接点上,this
和 target
都指向同一个对象-即执行方法的对象. Spring AOP 是一个基于代理的系统,区分代理对象本身(绑定到 this
) 和代理(绑定到 target
) 后的目标对象.
由于 Spring AOP 框架是基于代理的特性,定义的 protected 方法将不会被处理,不管是 JDK 的代理(做不到) 还是 CGLIB 代理(有技术可以实现但是不建议) . 因此,任何给定的切点将只能与 public 方法匹配. 请注意,切点定义通常与任何截获的方法匹配. 如果切点严格意义上是暴露的,即使在通过代理进行潜在非公共交互的 CGLIB 代理方案中,也需要相应地定义切点. 如果需要拦截包括 protected 和 private 方法甚至是构造函数,请考虑使用基于 Spring 驱动的本地 AspectJ 织入而不是 Spring 的基于代理的 AOP 框架. 这构成了不同特性的 AOP 使用模式,所以在做出决定之前一定要先熟悉一下编织. |
Spring AOP 支持更多的 PCD 命名 bean
. PCD允许将连接点的匹配限制为特定的 Spring bean
或一系列 Spring bean
. bean PCD 具有以下形式:
bean(idOrNameOfBean)
bean(idOrNameOfBean)
idOrNameOfBean
标识可以是任意符合 Spring bean 的名字, 提供了使用 *
字符的有限通配符支持,因此,如果为 Spring bean
建立了一些命名约定,则可以编写 bean PCD 表达式来选择它们. 与其他切点标识符的情况一样,PCD bean 可以是 &&
(and), ||
(or), and !
(negation).
|
合并切点表达式
您可以使用 &&,
||
和 !
等符号进行合并操作. 也可以通过名字来指向切点表达式. 以下示例显示了三个切入点表达式:
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} (1)
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} (2)
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {} (3)
1 | anyPublicOperation 如果方法执行连接点表示任何公共方法的执行,则匹配 |
2 | inTrading 如果方法执行在 trading 中,则匹配. |
3 | tradingOperation 如果方法执行表示 trading 中的任何公共方法,则匹配. |
@Pointcut("execution(public * *(..))")
private fun anyPublicOperation() {} (1)
@Pointcut("within(com.xyz.myapp.trading..*)")
private fun inTrading() {} (2)
@Pointcut("anyPublicOperation() && inTrading()")
private fun tradingOperation() {} (3)
1 | anyPublicOperation 如果方法执行连接点表示任何公共方法的执行,则匹配 |
2 | inTrading 如果方法执行在 trading 中,则匹配. |
3 | tradingOperation 如果方法执行表示 trading 中的任何公共方法,则匹配. |
如上所示,用更小的命名组件构建更复杂的切入点表达式是最佳实践. 当按名称引用切点时,将应用普通的 Java 可见性规则(可以看到相同类型的私有切点,层次结构中受保护的切点,任何位置的公共切点等) . 可见性并不影响切点匹配.
共享通用的切点定义
在处理企业应用程序时,通常需要从几个切面来引用应用程序的模块和特定的操作集. 建议定义一个 CommonPointcuts
切面,以此为目的捕获通用的切点表达式. 这样的切面通常类似于以下示例:
@Aspect
public class CommonPointcuts {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.myapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.myapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.service..*)")
public void inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.myapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.myapp.dao..*)")
public void inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then
* the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.myapp..service.*.*(..))")
public void businessService() {}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.myapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
@Aspect
class CommonPointcuts {
/
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.myapp.web package or any sub-package
* under that.
/
@Pointcut("within(com.xyz.myapp.web..)")
fun inWebLayer() {
}
/
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.myapp.service package or any sub-package
* under that.
/
@Pointcut("within(com.xyz.myapp.service..)")
fun inServiceLayer() {
}
/
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.myapp.dao package or any sub-package
* under that.
/
@Pointcut("within(com.xyz.myapp.dao..)")
fun inDataAccessLayer() {
}
/
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then
* the pointcut expression "execution(* com.xyz.myapp..service..(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution( com.xyz.myapp..service..(..))")
fun businessService() {
}
/*
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution( com.xyz.myapp.dao..(..))")
fun dataAccessOperation() {
}
}
像这样定义的切点可以用在任何需要切点表达式的地方, 例如,要使服务层具有事务性,您可以编写以下内容:
<aop:config>
<aop:advisor
pointcut="com.xyz.myapp.CommonPointcuts.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
Examples
Spring AOP 用户可能最常使用 execution
切点标识符 ,执行表达式的格式为:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
除返回类型模式(上面片段中的 ret-type-pattern
) 以外的所有部件、名称模式和参数模式都是可选的. 返回类型模式确定要匹配的连接点的方法的返回类型必须是什么. 通常,可以使用 *
作为返回类型模式,它匹配任何返回类型. 只有当方法返回给定类型时,完全限定的类型名称才会匹配. 名称模式与方法名称匹配,可以将 *
通配符用作名称模式的全部或部分. 如果指定声明类型模式,则需要有后缀 .将其加入到名称模式组件中.
参数模式稍微复杂一点. ()
匹配没有参数的方法. (..)
匹配任意个数的参数(0个或多个) . ( *
)匹配任何类型的单个参数. (*,String)
匹配有两个参数而且第一个参数是任意类型,第二个必须是 String
的方法. 有关更多信息,请参阅AspectJ编程指南的 Language
Semantics部分.
以下示例显示了一些常见的切点表达式:
-
匹配任意公共方法的执行:
execution(public * *(..))
-
匹配任意以
set
开始的方法:execution(* set*(..))
-
匹配定义了
AccountService
接口的任意方法:execution(* com.xyz.service.AccountService.*(..))
-
匹配定义在
service
包中的任意方法:execution(* com.xyz.service.*.*(..))
-
匹配定义在
service
包和其子包中的任意方法:execution(* com.xyz.service..*.*(..))
-
匹配在
service
包中的任意连接点(只在 Spring AOP 中的方法执行) :within(com.xyz.service.*)
-
匹配在
service
包及其子包中的任意连接点(只在 Spring AOP 中的方法执行)within(com.xyz.service..*)
-
匹配代理实现了
AccountService
接口的任意连接点(只在 Spring AOP 中的方法执行) :this(com.xyz.service.AccountService)
this
常常以捆绑的形式出现. 见后续的章节讨论如何在 声明通知 中使用代理对象. -
匹配当目标对象实现了
AccountService
接口的任意连接点(只在 Spring AOP 中的方法执行) :target(com.xyz.service.AccountService)
target
常常以捆绑的形式出现. 见后续的章节讨论如何在 声明通知 中使用目标对象. -
匹配使用了单一的参数,并且参数在运行时被传递时可以
Serializable
的任意连接点(只在 Spring 的 AOP 中的方法执行) :args(java.io.Serializable)
args
常常以捆绑的形式出现.见后续的章节讨论如何在 声明通知 中使用方法参数.注意在这个例子中给定的切点不同于
execution(* *(java.io.Serializable))
. 如果在运行时传递的参数是可序列化的,则与execution
匹配,如果方法签名声明单个参数类型为Serializable
,则与 args 匹配. -
匹配当目标对象有
@Transactional
注解时的任意连接点(只在 Spring AOP 中的方法执行) .@target(org.springframework.transaction.annotation.Transactional)
@target
也可以以捆绑的形式使用.见后续的章节讨论如何在声明通知中使用注解对象. -
匹配当目标对象的定义类型有
@Transactional
注解时的任意连接点(只在 Spring 的 AOP 中的方法执行)@within(org.springframework.transaction.annotation.Transactional)
@within
也可以以捆绑的形式使用.见后续的章节讨论如何在 声明通知 中使用注解对象. -
匹配当执行的方法有
@Transactional
注解的任意连接点(只在 Spring AOP 中的方法执行) :@annotation(org.springframework.transaction.annotation.Transactional)
@annotation
也可以以捆绑的形式使用.见后续的章节讨论如何在 声明通知 中使用注解对象. -
匹配有单一的参数并且在运行时传入的参数类型有
@Classified
注解的任意连接点(只在 Spring AOP 中的方法执行) :@args(com.xyz.security.Classified)
@args
也可以以捆绑的形式使用.见后续的章节讨论如何在 声明通知 中使用注解对象. -
匹配在名为
tradeService
的 Spring bean 上的任意连接点(只在Spring AOP中的方法执行) :bean(tradeService)
-
匹配以
*Service
结尾的 Spring bean 上的任意连接点(只在 Spring AOP 中方法执行) :bean(*Service)
编写好的切点
在编译过程中,AspectJ 会尝试和优化匹配性能来处理切点. 检查代码并确定每个连接点是否匹配(静态或动态) 给定切点是一个代价高昂的过程. (动态匹配意味着无法从静态分析中完全确定匹配, 并且将在代码中放置测试,以确定在运行代码时是否存在实际匹配) . 在第一次遇到切点声明时,AspectJ 会将它重写为匹配过程的最佳形式. 这是什么意思? 基本上,切点是在 DNF(析取范式) 中重写的 ,切点的组成部分会被排序,以便先检查那些比较明确的组件. 这意味着开发者不必担心各种切点标识符的性能,并且可以在切点声明中以任何顺序编写.
但是,AspectJ 只能与被它指定的内容协同工作,并且为了获得最佳的匹配性能,开发者应该考虑它们试图实现的目标,并在定义中尽可能缩小匹配的搜索空间. 现有的标识符会自动选择下面三个中的一个 kinded, scoping, 和 contextual:
-
Kinded 选择特定类型的连接点的标识符:
execution
,get
,set
,call
, 和handler
. -
Scoping 选择一组连接点的匹配 (可能是许多种类) :
within
和withincode
-
Contextual 基于上下文匹配 (或可选绑定) 的标识符:
this
,target
, 和@annotation
一个写得很好的切入点应该至少包括前两种类型(kinded 和 scoping) . 同时 contextual 标识符或许会被包括如果希望匹配基于连接点上下文或绑定在通知中使用的上下文. 只是提供 kinded 标识符或只提供 contextual 标识符器也能够工作,但是可能影响处理性能(时间和内存的使用) ,浪费了额外的处理和分析时间或空间. scoping 标识符可以快速匹配并且使用 AspectJ 可以快速排除不会被处理的连接点组, 这也说明编写好的切点表达式是很重要的(因为没有明确指定时,它就会 Loop Lookup 循环匹配) .
5.4.4. 声明通知
通知是与切点表达式相关联的概念,可以在切点匹配的方法之前、之后或之间执行. 切点表达式可以是对命名切点的简单引用,也可以是即时声明的切点表达式.
前置通知
您可以使用 @Before
注解在切面中的通知之前声明:
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
@Aspect
class BeforeExample {
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
fun doAccessCheck() {
// ...
}
}
如果使用内置切点表达式,我们可以重写前面的示例,如下例所示:
@Aspect
public class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
public void doAccessCheck() {
// ...
}
}
@Aspect
class BeforeExample {
@Before("execution(* com.xyz.myapp.dao.*.*(..))")
fun doAccessCheck() {
// ...
}
}
后置返回通知
要想用后置返回通知可以在切面上添加 @AfterReturning
注解:
@Aspect
public class AfterReturningExample {
@AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doAccessCheck() {
// ...
}
}
@Aspect
class AfterReturningExample {
@AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
fun doAccessCheck() {
// ...
}
}
在同一切面中当然可以声明多个通知. 在此只是为了迎合讨论的主题而只涉及单个通知. |
有些时候需要在通知中获取实际的返回值. 可以使用 @AfterReturning
,并指定 returning
字段如下:
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
@Aspect
class AfterReturningExample {
@AfterReturning(
pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
returning = "retVal")
fun doAccessCheck(retVal: Any) {
// ...
}
}
在 returning
属性中使用的名字必须和通知方法中的参数名相关,方法执行返回时,返回值作为相应的参数值传递给 advice 方法. returning
子句还限制只匹配那些返回指定类型的值的方法执行(在本例中为 Object
,它匹配任何返回值对象) .
请注意,当使用 after-returning
的通知时. 不能返回不同的引用.
后置异常通知
当方法执行并抛出异常时后置异常通知会被执行,需要使用 @AfterThrowing
注解来定义. 如以下示例所示:
@Aspect
public class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doRecoveryActions() {
// ...
}
}
@Aspect
class AfterThrowingExample {
@AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
fun doRecoveryActions() {
// ...
}
}
开发者常常希望当给定类型的异常被抛出时执行通知,并且也需要在通知中访问抛出的异常. 使用 throwing
属性来限制匹配(如果需要,使用 Throwable
作为异常类型) ,并将引发的异常绑定到通知参数. 以下示例显示了如何执行此操作:
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
@Aspect
class AfterThrowingExample {
@AfterThrowing(
pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()",
throwing = "ex")
fun doRecoveryActions(ex: DataAccessException) {
// ...
}
}
throwing
属性中使用的名字必须和通知方法中的参数名相关. 当方法执行并抛出异常时,异常将会传递给通知方法作为相关的参数值. 抛出子句还限制与只引发指定类型的异常(在本例中为 DataAccessException
) 的方法执行的匹配.
请注意, |
后置通知(总会执行)
当匹配方法执行之后后置通知(总会执行) 会被执行. 这种情况使用 @After
注解来定义. 后置通知必须被准备来处理正常或异常的返回条件. 通常用于释放资源等等:
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
@Aspect
class AfterFinallyExample {
@After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
fun doReleaseLock() {
// ...
}
}
请注意,AspectJ 中的 |
环绕通知
最后一种通知是 环绕通知,环绕通知围绕方法执行. 可以在方法执行之前和执行之后执行,并且定义何时做什么,甚至是否真正得到执行. 如果需要在方法执行之前和之后以线程安全的方式 (例如启动和停止计时器) 共享状态
确认可使用的通知形式, 要符合最小匹配原则. 例如,如果 before 通知足以满足您的需求,则不要使用 around 通知。 |
环绕通知是通过使用 @Around
注解对方法进行注解来声明的。 这个方法应该声明 Object
作为它的返回类型,以及方法的第一个参数 必须是 ProceedingJoinPoint
类型。 在通知方法的主体内,您必须在 ProceedingJoinPoint
上调用 proceed()
以使底层方法。 不带参数调用 proceed()
将导致调用者的调用底层方法时提供原始参数。 这属于高级用途
proceed
方法也可以通过传递 Object[]
数组的值给原方法作为传入参数, 数组中的值将用作调用时的底层方法。
使用 @Around
注解来定义环绕通知,第一个参数必须是 ProceedingJoinPoint
类型的. 在通知中调用 ProceedingJoinPoint
中的 proceed()
方法来引用执行的方法. proceed
方法也可以被调用传递数组对象- 数组的值将会被当作参数在方法执行时被使用.
proceed
方法也可以传入 Object[]
. 数组中的值在进行时用作方法执行的参数.
在使用 `Object[]` 调用时 `proceed` 的行为与在 AspectJ 编译器编译的环绕通知进行的行为略有不同. 对于使用传统 AspectJ 语言编写的通知, 传递给 `proceed` 的参数数必须与传递给环绕通知的参数数量(不是被连接点处理的参数的数目) 匹配,并且传递的值将 `proceed` 在给定的参数位置取代该值绑定到的实体的连接点的原始值(如果现在无法理解 ,请不要担心) . Spring 处理的方式是简单的并且基于代理的,会生成更好的匹配语义. 现在只需意识到这两种是有这么一点的不同的即可. 为 Spring 编写的 |
环绕通知返回的值将会被调用的方法看到,例如,一个简单的缓存切面可以从缓存中返回一个值(如果有的话) ,如果没有则调用 proceed()
. 请注意,可以在 around 通知的主体内调用一次,多次或根本不调用. 所有这些都是合法的.
如果您将环绕通知方法的返回类型声明为 void ,null 则 proceed() 将忽略任何调用的结果返回给调用者,因此,建议使用环绕通知方法声明返回
Object 的类型。 通知方法通常应该返回从调用 proceed() ,即使底层方法具有 void 返回类型。
但是,通知可以选择返回缓存值、包装值或其他一些值取决于用例。
|
以下示例显示如何使用 around
通知:
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
@Aspect
class AroundExample {
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
fun doBasicProfiling(pjp: ProceedingJoinPoint): Any {
// start stopwatch
val retVal = pjp.proceed()
// stop stopwatch
return retVal
}
}
通知的参数
Spring 提供了全部类型的通知,这意味着需在通知签名中声明所需的参数(正如上面返回和异常的示例) ,而不是一直使用 Object[]
数组. 接着将会看到怎么声明参数以及上下文的值是如何在通知实体中被使用的. 首先,来看看如何编写一般的通知,找出编写通知的法子.
访问当前的 JoinPoint
任何通知方法都可以声明一个类型为 org.aspectj.lang.JoinPoint
的参数作为其第一个参数(注意,需要使用 环绕通知来声明一个类型为 ProceedingJoinPoint
的第一个参数, 它是 JoinPoint
的一个子类. JoinPoint
接口提供很多有用的方法:
-
getArgs()
: 返回方法参数. -
getThis()
: 返回代理对象. -
getTarget()
: 返回目标对象. -
getSignature()
: 返回正在通知的方法的描述. -
toString()
: 打印方法被通知的有用描述.
See the javadoc for more detail.
传递参数给通知
我们已经看到了如何绑定返回的值或异常值(在返回之后和抛出通知之后使用) . 为了在通知代码段中使用参数值,可以使用绑定 args
的形式. 如果在 args
表达式中使用参数名代替类型名称, 则在调用通知时,要将相关的参数值当作参数传递. 例如,假如在 dao 操作时将 Account
对象作为第一个参数传递给通知,并且需要在通知代码段内访问 Account
,可以这样写:
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
fun validateAccount(account: Account) {
// ...
}
切点表达式的 args(account,..)
部分有两个目的. p 它严格匹配了至少带一个参数的执行方法,并且传递给传递的参数是 Account
实例. 第二,它使得实际的 Account
对象通过 account
参数提供给通知.
另一个方法写法就是先定义切点,然后, "provides" Account
对象给匹配的连接点,有了连接点,那么引用连接点作为切点的通知就能获得 Account
对象的值. 这看起来如下:
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private fun accountDataAccessOperation(account: Account) {
}
@Before("accountDataAccessOperation(account)")
fun validateAccount(account: Account) {
// ...
}
有关更多详细信息,请参阅 AspectJ 编程指南.
代理对象( this
),目标对象 ( target
)和注解 ( @within
, @target
, @annotation
, 和 @args
)都可以以类似的方式绑定. 接下来的两个示例显示如何匹配带有 @Auditable
注解的注解方法的执行并获取audit 代码:
首先是 @Auditable
注解的定义:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Auditable(val value: AuditCode)
然后是匹配 @Auditable
方法通知的执行
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
fun audit(auditable: Auditable) {
val code = auditable.value()
// ...
}
通知参数和泛型
Spring AOP 可以处理类声明和方法参数中使用的泛型. 假设如下泛型类型:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
interface Sample<T> {
fun sampleGenericMethod(param: T)
fun sampleGenericCollectionMethod(param: Collection<T>)
}
只需将通知参数输入要拦截方法的参数类型,就可以将方法类型的检测限制为某些参数类型:
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
fun beforeSampleMethod(param: MyType) {
// Advice implementation
}
此方法不适用于泛型集合. 因此,您无法按如下方式定义切点:
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
// Advice implementation
}
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
fun beforeSampleMethod(param: Collection<MyType>) {
// Advice implementation
}
为了使这项工作,我们必须检查集合的每个元素,这是不合理的,因为我们也无法决定如何处理 null
值. 要实现与此类似的操作,您必须将参数输入 Collection<?>
并手动检查元素的类型.
声明参数的名字
参数在通知中的绑定依赖于名字匹配,重点在切点表达式中定义的参数名的方法签名上(通知和切点) . 参数名称不能通过 Java 反射获得,因此 Spring AOP 使用以下策略来确定参数名称:
-
如果用户已明确指定参数名称,则使用指定的参数名称. 通知和切点注解都有一个可选的
argNames
属性,您可以使用该属性指定带注解的方法的参数名称. 这些参数名称在运行时可用. 以下示例显示如何使用argNames
属性:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}
@Before(value = "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames = "bean,auditable")
fun audit(bean: Any, auditable: Auditable) {
val code = auditable.value()
// ... use code and bean
}
如果第一个参数是 JoinPoint
, ProceedingJoinPoint
, 或 JoinPoint.StaticPart
类型,则可以从 argNames
属性的值中省略参数的名称. 例如,如果修改前面的通知以接收连接点对象,则 argNames
属性不需要包含它:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}
@Before(value = "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames = "bean,auditable")
fun audit(jp: JoinPoint, bean: Any, auditable: Auditable) {
val code = auditable.value()
// ... use code, bean, and jp
}
对 JoinPoint
,ProceedingJoinPoint
, 和 JoinPoint.StaticPart
类型的第一个参数的特殊处理方便不收集任何其他连接点上下文的通知. 在这种情况下,可以简单地省略 argNames
属性. 例如,以下建议无需声明 argNames
属性:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
fun audit(jp: JoinPoint) {
// ... use jp
}
-
使用
argNames
属性有点笨拙,所以如果没有指定argNames
属性,Spring AOP会查看该类的调试信息,并尝试从局部变量表中确定参数名称. 只要使用调试信息(-g:vars
) 编译类, 就会出现此信息. 使用此标志进行编译的后果是: (1).您的代码将容易被理解(逆向工程. (2). 类文件的大小将会有些大(通常不是什么事). (3). 对非使用本地变量的优化将不会应用于你的编译器. 换句话说,通过使用此标志构建,您应该不会遇到任何困难.如果即使没有调试信息,AspectJ 编译器( ajc
) 也编译了@AspectJ
方面,则无需添加argNames
属性,因为编译器会保留所需的信息. -
如果代码是在没有必要的调试信息的情况下编译的,那么 Spring AOP 将尝试推断绑定变量与参数的配对(例如,如果在切点表达式中只绑定了一个变量,并且该通知方法只需要一个参数,此时两者匹配是明显的) . 如果给定了可用信息,变量的绑定是不明确的话,则会引发
AmbiguousBindingException
异常. -
如果上述所有策略都失败,则抛出
IllegalArgumentException
异常.
处理参数
前面说过. 将描述如何用在 Spring AOP 和 AspectJ 中一致的参数中编写 proceed
处理函数. 解决方案是确保建议签名按顺序绑定每个方法参数. 以下示例显示了如何执行此操作:
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
@Around("execution(List<Account> find*(..)) && " +
"com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " +
"args(accountHolderNamePattern)")
fun preProcessQueryPattern(pjp: ProceedingJoinPoint,
accountHolderNamePattern: String): Any {
val newPattern = preProcess(accountHolderNamePattern)
return pjp.proceed(arrayOf<Any>(newPattern))
}
在许多情况下,无论如何都要执行此绑定(如前面的示例所示) .
通知的顺序
当多个通知都希望在同一连接点上运行时会发生什么情况? Spring AOP 遵循与 AspectJ 相同的优先级规则来确定通知执行的顺序. 拥有最高优先权的通知会途中先"进入"(因此,给定两条前置通知,优先级最高的通知首先运行) . 从连接点"退出",拥有最高优先级的通知最后才运行(退出) ((因此,如果有两个后置通知,那么拥有最高优先级的将在最后运行(退出) ) .
如果在不同切面定义的两个通知都需要在同一个连接点运行,那么除非开发者指定运行的先后,否则执行的顺序是未定义的. 可以通过指定优先级来控制执行顺序. 这也是 Spring 推荐的方式,通过在切面类实现 org.springframework.core.Ordered
接口或使用 @Order
对其进行注解即可.
如果有两个切面,从 Ordered.getOrder()
(或注解值) 返回较低值的方面具有较高的优先级.
每一个切面的不同通知类型都应作用于连接点,因此 从 Spring Framework 5.2.7 开始,在相同 当在同一切面定义的两条通知都需要在同一个连接点上运行时,排序也是未定义的(因为没有办法通过反射检索Javac编译的类的声明顺序) . 考虑将通知方法与一个通知方法合并,根据每个连接点在每个切面类或将通知切分为切面类,可以在切面级别指定顺序. 当在同一个 |
5.4.5. 引入
引入(作为 AspectJ 中内部类型的声明) 允许切面定义通知的对象实现给定的接口,并代表这些对象提供该接口的实现.
引入使用 @DeclareParents
注解来定义,这个注解用于声明匹配拥有新的父类的类型(因此得名) . 例如, 给定名为 UsageTracked
的接口和名为 DefaultUsageTracked
的接口的实现,以下切面声明服务接口的所有实现者也实现 UsageTracked
接口(例如,通过JMX暴露统计信息) :
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
@Aspect
class UsageTracking {
companion object {
@DeclareParents(value = "com.xzy.myapp.service.*+", defaultImpl = DefaultUsageTracked::class)
lateinit var mixin: UsageTracked
}
@Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
}
}
要实现的接口由注解属性的类型来确定. @DeclareParents
注解的 value
值是 AspectJ 类型模式引过来的. 任何匹配类型的 bean 实现了 UsageTracked
接口。注意上面例子中的前置通知, 服务 bean 可以直接作为 UsageTracked
接口的实现,如果以编程方式访问 bean,您将编写以下内容:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
val usageTracked = context.getBean("myService") as UsageTracked
5.4.6. 切面实例化模型
这是一个高级主题. 如果您刚刚开始使用 AOP,您可以跳过它直到稍后再了解. |
默认情况下,应用程序上下文中的每个切面都有一个实例. AspectJ 将其称为单例实例化模型. 可以使用交替生命周期定义切面. Spring 支持 AspectJ 的 perthis
和 pertarget
实例化模型(目前不支持 percflow
, percflowbelow
, 和 pertypewithin
) .
您可以通过在 @Aspect
注解中指定 perthis
子句来声明相关方面. 请考虑以下示例:
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {
private int someState;
@Before("com.xyz.myapp.CommonPointcuts.businessService()")
public void recordServiceUsage() {
// ...
}
}
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
class MyAspect {
private val someState: Int = 0
@Before(com.xyz.myapp.CommonPointcuts.businessService())
fun recordServiceUsage() {
// ...
}
}
在前面的示例中,perthis
子句的作用是为执行业务服务的每个唯一服务对象创建一个切面实例(每个唯一对象在由切点表达式匹配的连接点处绑定到 this
) . 方法实例是在第一次在服务对象上调用方法时创建的.
当服务对象超出作用域时,该切面也将超出作用域. 在创建切面实例之前,它包含的任意通知都不会执行. 在创建了切面实例后, 其中声明的通知将在匹配的连接点中执行,但仅当服务对象是此切面关联的通知时才会运行. 有关 per
子句的更多信息,请参阅 AspectJ 编程指南.
pertarget
实例化模型的工作方式与 perthis
完全相同,但它为匹配的连接点处的每个唯一目标对象创建一个切面实例.
5.4.7. AOP 例子
现在您已经了解了所有组成部分的工作原理,我们可以将它们放在一起做一些有用的事情.
由于并发问题(例如,死锁失败者) ,业务服务的执行有时会失败. 如果重试该操作,则可能在下次尝试时成功. 对于适合在这种情况下重试的业务服务(不需要返回给用户来解决冲突的幂等操作) . 希望透明地重试该操作,以避免客户端看到 PessimisticLockingFailureException
异常. 这个需求很明显,它跨越了服务层中的多个服务,因此非常适合通过切面来实现.
因为我们想要重试操作,所以我们需要使用环绕通知,以便我们可以多次调用 proceed
. 以下清单显示了基本方面的实现:
@Aspect
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
@Aspect
class ConcurrentOperationExecutor : Ordered {
private val DEFAULT_MAX_RETRIES = 2
private var maxRetries = DEFAULT_MAX_RETRIES
private var order = 1
fun setMaxRetries(maxRetries: Int) {
this.maxRetries = maxRetries
}
override fun getOrder(): Int {
return this.order
}
fun setOrder(order: Int) {
this.order = order
}
@Around("com.xyz.myapp.CommonPointcuts.businessService()")
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
var numAttempts = 0
var lockFailureException: PessimisticLockingFailureException
do {
numAttempts++
try {
return pjp.proceed()
} catch (ex: PessimisticLockingFailureException) {
lockFailureException = ex
}
} while (numAttempts <= this.maxRetries)
throw lockFailureException
}
}
请注意,该方面实现了 Ordered
接口,以便我们可以将切面的优先级设置为高于事务通知(我们每次重试时都需要一个新的事务) . maxRetries
和 order
属性都由 Spring 配置. 主要的操作是在 doConcurrentOperation
的环绕通知中.
请注意,请注意,目前,我们将重试逻辑应用于每个 businessService()
. 尝试执行时,如果失败了,将产生 PessimisticLockingFailureException
异常,但是不用管它,只需再次尝试执行即可,除非已经用尽所有的重试次数.
相应的 Spring 配置如下:
<aop:aspectj-autoproxy/>
<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
为了优化切面以便它只重试幂等操作,我们可以定义以下 Idempotent
注解:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
@Retention(AnnotationRetention.RUNTIME)
annotation class Idempotent// marker annotation
然后使用它来注解服务操作的实现. 对切面的更改只需要重试等幂运算,只需细化切点表达式,以便只匹配 @Idempotent
操作:
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
// ...
}
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
"@annotation(com.xyz.myapp.service.Idempotent)")
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
// ...
}
5.5. 基于 Schema 的 AOP 支持
如果您更喜欢基于 XML 的格式,Spring 还支持使用新的 aop
命名空间标签定义切面.
完全相同的切点表达式和通知类型在使用 @AspectJ
方式时同样得到支持. 因此,在本节中,我们将重点放在新语法上,并将读者引用到上一节(@AspectJ 注解支持) 中的讨论,以了解编写切点表达式和通知参数的绑定.
要使用本节中描述的 aop
命名空间标签,您需要导入 spring-aop
schema,如基于 XML 模式的配置中所述. 有关如何在 aop 命名空间中导入标记,请参阅AOP schema.
在 Spring 配置中,所有 aspect 和 advisor 元素必须放在 <aop:config>
元素中(在应用程序上下文配置中可以有多个 <aop:config>
元素) . <aop:config>
元素可以包含切点,通知者和切面元素(请注意,这些元素必须按此顺序声明) .
<aop:config> 配置样式大量使用了Spring的自动代理 机制.如果已经通过使用 BeanNameAutoProxyCreator 或类似的类使用了显式的自动代理, 则可能会出现问题(如通知还没被编织) .
建议的使用模式是仅使用 <aop:config> 样式或仅使用 AutoProxyCreator 样式,并且永远不要混用它们.
|
5.5.1. 声明切面
如果使用 schema,那么切面只是在 Spring 应用程序上下文中定义为 bean 的常规 Java 对象.在对象的字段和方法中获取状态和行为,并且在 XML 中获取切点和通知信息.
您可以使用 <aop:aspect>
元素声明方面,并使用 ref
属性引用支持 bean,如以下示例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
...
</aop:aspect>
</aop:config>
<bean id="aBean" class="...">
...
</bean>
支持切面的 bean (在这种情况下是 aBean
) 当然可以像任何其他 Spring bean 一样配置和依赖注入.
5.5.2. 声明切点
您可以在 <aop:config>
元素中声明一个命名切点,让切点定义在多个切面和通知者之间共享.
表示服务层中任何业务服务执行的切点可以定义如下:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
</aop:config>
切点表达式本身使用的是相同的 AspectJ 切点表达式语言,如 @Aspect 注解支持 所述. 如果使用基于 schema 的声明样式,则可以引用在切点表达式内的类型(@Aspects
)中定义的命名切点 . 定义上述切入点的另一种方法如下:
<aop:config>
<aop:pointcut id="businessService"
expression="com.xyz.myapp.CommonPointcuts.businessService()"/>
</aop:config>
假设有一个 CommonPointcuts
的切面(如共享通用的切点定义一节所述) .
切面声明切点与声明 top-level 切点非常相似,如下例所示:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
...
</aop:aspect>
</aop:config>
与 @AspectJ
方面的方法相同,使用基于 schema 的定义样式声明的切点可能会收集连接点上下文. 例如,以下切点将 this
对象收集为连接点上下文并将其传递给通知:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
必须通过包含匹配名称的参数来声明接收所收集的连接点上下文的通知,如下所示:
public void monitor(Object service) {
// ...
}
fun monitor(service: Any) {
// ...
}
在组合切点表达式中, &&
在 XML 文档中很难处理,因此您可以分别使用 and
, or
和 not
分别用来代替 &&
, ||
, 和 !
. 例如,以前的切点可以更好地编写如下:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) and this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>
以这种方式定义的切点由其 XML id
引用,不能用作命名切点以形成复合切点. 因此,基于 schema 定义样式中的命名切点比 @AspectJ
样式提供的受到更多的限制.
5.5.3. 声明通知
同样的五种通知类型也支持 @AspectJ
样式,并且它们具有完全相同的语义.
前置通知
前置通知很明显是在匹配方法执行之前被调用, 它通过使用 <aop:aspect>
元素在 <aop:aspect>
中声明,如下例所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
这里 dataAccessOperation
是在最外层的(<aop:config>
)定义的切点 id
. 若要以内联方式定义切点,请将 pointcut-ref
属性替换为 pointcut
属性. 如下所示:
<aop:aspect id="beforeExample" ref="aBean">
<aop:before
pointcut="execution(* com.xyz.myapp.dao.*.*(..))"
method="doAccessCheck"/>
...
</aop:aspect>
正如我们在讨论 @AspectJ
样式时所提到的,使用命名切点可以显着提高代码的可读性.
method
属性定义的 (doAccessCheck
)方法用于通知的代码体内. 这个方法包含切面元素所引用的 bean. 在数据访问操作之前通知会被执行(当然连接点匹配中的切点), 即切面 bean 的 doAccessCheck
方法会被调用.
后置返回通知
在匹配的方法执行正常完成后返回通知运行. 它在 <aop:aspect>
中以与前置通知相同的方式声明. 以下示例显示了如何声明它:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
method="doAccessCheck"/>
...
</aop:aspect>
与 @AspectJ
样式一样,可以在通知代码体内获取返回值. 为此,使用 returning
属性定义参数的名字来传递返回值,如以下示例所示:
<aop:aspect id="afterReturningExample" ref="aBean">
<aop:after-returning
pointcut-ref="dataAccessOperation"
returning="retVal"
method="doAccessCheck"/>
...
</aop:aspect>
doAccessCheck
方法必须声明一个名为 retVal
的参数,此参数的类型约束匹配的方式与 @AfterReturning
所描述的相同. 例如,您可以按如下方式声明方法签名:
public void doAccessCheck(Object retVal) {...
fun doAccessCheck(retVal: Any) {...
后置异常通知
就是匹配的方法运行抛出异常后后置异常通知会运行,它在 <aop:aspect>
中使用 after-throwing
元素声明. 如下例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
method="doRecoveryActions"/>
...
</aop:aspect>
与 @AspectJ
样式一样,可以在通知代码体内获取抛出的异常,使用 throwing
属性定义参数的名字来传递异常. 如以下示例所示:
<aop:aspect id="afterThrowingExample" ref="aBean">
<aop:after-throwing
pointcut-ref="dataAccessOperation"
throwing="dataAccessEx"
method="doRecoveryActions"/>
...
</aop:aspect>
doRecoveryActions
方法必须声明名为 dataAccessEx
的参数. 此参数的类型约束匹配的方式与 @AfterThrowing
所描述的相同. 例如,方法签名可以声明如下:
public void doRecoveryActions(DataAccessException dataAccessEx) {...
fun doRecoveryActions(dataAccessEx: DataAccessException) {...
后置通知(总会执行的)
当方法执行完成并退出后,后置通知会被执行(而且是总会被执行). 你可以使用 after
元素声明. 如以下示例所示:
<aop:aspect id="afterFinallyExample" ref="aBean">
<aop:after
pointcut-ref="dataAccessOperation"
method="doReleaseLock"/>
...
</aop:aspect>
环绕通知
最后一种通知是环绕通知. 环绕通知 "around" 匹配的方法执行运行. 它有机会在方法执行之前和之后进行工作,并确定方法何时、 如何以及甚至是否真正执行. 环绕通知经常用于需要在方法执行前或后在线程安全的情况下共享状态(例如开始和结束时间) .
确认可使用的通知形式, 要符合最小匹配原则. 例如,如果 before 通知足以满足您的需求,则不要使用 around 通知。 |
您可以使用 aop:around
元素声明环绕通知. 通知方法应该以 Object
作为它的返回类型,第一个参数必须是 ProceedingJoinPoint
类型. 在通知代码体中,调用 ProceedingJoinPoint
实现的 proceed()
会使匹配的方法继续执行.
proceed
方法也可以通过传递 Object[]
数组的值给原方法作为传入参数. 有关调用继续使用 Object[]
的说明,请参阅 环绕通知. 以下示例显示如何在 XML 中声明通知:
<aop:aspect id="aroundExample" ref="aBean">
<aop:around
pointcut-ref="businessService"
method="doBasicProfiling"/>
...
</aop:aspect>
doBasicProfiling
通知的运行与 @AspectJ
示例中的完全相同(当然省略了注解) . 如以下示例所示:
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
fun doBasicProfiling(pjp: ProceedingJoinPoint): Any {
// start stopwatch
val retVal = pjp.proceed()
// stop stopwatch
return pjp.proceed()
}
通知参数
基于 schema 的声明样式支持所有类型的通知,其方式与 @AspectJ
支持的描述相同 - 通过按名称匹配切点参数与通知方法参数相匹配. 有关详细信息,请参阅通知参数.
如果希望显式指定通知方法的参数名称(不依赖于前面描述的检测策略) 则使用通知元素的 arg-names
属性来完成这一操作. 其处理方式和通知注解中的 argNames
属性是相同的, 在通知注解中(如声明参数的名字中所述) . 以下示例显示如何在 XML 中指定参数名称:
<aop:before
pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)"
method="audit"
arg-names="auditable"/>
arg-names
属性接受以逗号分隔的参数名称列表.
下面是一个基于 XSD 方式的多调用示例,它说明环绕通知是如何与一些强类型参数共同使用的:
public interface PersonService {
Person getPerson(String personName, int age);
}
public class DefaultPersonService implements PersonService {
public Person getPerson(String name, int age) {
return new Person(name, age);
}
}
interface PersonService {
fun getPerson(personName: String, age: Int): Person
}
class DefaultPersonService : PersonService {
fun getPerson(name: String, age: Int): Person {
return Person(name, age)
}
}
接下来定义切面. 请注意,profile(..)
方法接受许多强类型参数,其中第一个是用于方法调用的连接点. 这个参数用于声明 profile(..)
作为环绕通知来使用,如以下示例所示:
public class SimpleProfiler {
public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable {
StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'");
try {
clock.start(call.toShortString());
return call.proceed();
} finally {
clock.stop();
System.out.println(clock.prettyPrint());
}
}
}
class SimpleProfiler {
fun profile(call: ProceedingJoinPoint, name: String, age: Int): Any {
val clock = StopWatch("Profiling for '$name' and '$age'")
try {
clock.start(call.toShortString())
return call.proceed()
} finally {
clock.stop()
println(clock.prettyPrint())
}
}
}
最后,下面是为特定连接点执行上述建议所需的 XML 配置:
<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">
<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="personService" class="x.y.service.DefaultPersonService"/>
<!-- this is the actual advice itself -->
<bean id="profiler" class="x.y.SimpleProfiler"/>
<aop:config>
<aop:aspect ref="profiler">
<aop:pointcut id="theExecutionOfSomePersonServiceMethod"
expression="execution(* x.y.service.PersonService.getPerson(String,int))
and args(name, age)"/>
<aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod"
method="profile"/>
</aop:aspect>
</aop:config>
</beans>
请考虑以下驱动程序脚本:
public final class Boot {
public static void main(final String[] args) throws Exception {
BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml");
PersonService person = (PersonService) ctx.getBean("personService");
person.getPerson("Pengo", 12);
}
}
fun main() {
val ctx = ClassPathXmlApplicationContext("x/y/plain.xml")
val person = ctx.getBean("personService") as PersonService
person.getPerson("Pengo", 12)
}
使用这样的 Boot 类,我们将在标准输出上获得类似于以下内容的输出:
StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0 ----------------------------------------- ms % Task name ----------------------------------------- 00000 ? execution(getFoo)
通知的顺序
当多个通知需要在同一个连接点(执行方法) 执行时,排序规则如 通知的顺序 中所述. 方面之间的优先级是通过将 order
属性添加到 <aop:aspect>
元素或 @Order
注解添加到支持切面的 bean 或通过让 bean 实现 Ordered
接口来确定的.
与在同一 例如,给定一个环绕通知和一个在同一 根据一般经验,如果发现在同一 |
5.5.4. 引入
引入(作为 AspectJ 中内部类型的声明) 允许切面定义通知的对象实现给定的接口,并代表这些对象提供该接口的实现.
您可以在 aop:aspect
中使用 aop:declare-parents
元素进行引入. 您可以使用 aop:declare-parents
元素声明匹配类型具有父级(因此名称) . 例如,给定名为 UsageTracked
的接口和名为 DefaultUsageTracked
的接口的实现,以下方面声明服务接口的所有实现者也实现 UsageTracked
接口. (例如,为了通过JMX暴露统计信息. )
<aop:aspect id="usageTrackerAspect" ref="usageTracking">
<aop:declare-parents
types-matching="com.xzy.myapp.service.*+"
implement-interface="com.xyz.myapp.service.tracking.UsageTracked"
default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>
<aop:before
pointcut="com.xyz.myapp.CommonPointcuts.businessService()
and this(usageTracked)"
method="recordUsage"/>
</aop:aspect>
然后,支持 usageTracking
bean的类将包含以下方法:
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
}
要实现的接口由 implement-interface
属性确定. types-matching
属性的值是 AspectJ 类型模式. 任何匹配类型的 bean 都将实现 UsageTracked
接口. 请注意,在前面的示例的通知中,服务 bean 可以直接用作 UsageTracked
接口的实现. 要以编程方式访问 bean,您可以编写以下代码:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
val usageTracked = context.getBean("myService") as UsageTracked
5.5.6. 通知者
“advisors” 的概念是在 Spring 1.2 中提出的,能被 AOP 支持. 而在 AspectJ 中没有等价的概念. 通知者就像迷你的切面,包含单一的通知. 通知本身可以通过 bean 来代表,并且必须实现 Spring 中的 Advice Types in Spring 中描述的通知接口之一, 通知者可以利用 AspectJ 的切点表达式
Spring 使用 <aop:advisor>
元素支持通知者概念. 通常会看到它与事务性通知一起使用,它在 Spring 中也有自己的命名空间支持. 以下示例显示了一个通知者:
<aop:config>
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:advisor
pointcut-ref="businessService"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
除了前面示例中使用的 pointcut-ref
属性之外,您还可以使用 pointcut
属性来内联定义切点表达式.
如果想将通知排序,可以定义通知者的优先级. 在通知者上可以使用 order
属性来定义 Ordered
值.
5.5.7. AOP Schema 例子
本节说明如何使用 Schema 支持重写 An AOP Example AOP 例子 中的并发锁定失败重试示例.
由于并发问题(例如,死锁失败者) ,业务服务的执行有时会失败. 如果重试该操作,则可能在下次尝试时成功. 对于适合在这种情况下重试的业务服务(不需要返回给用户来解决冲突的幂等操作) . 希望透明地重试该操作,以避免客户端看到 PessimisticLockingFailureException
异常. 这个需求很明显,它跨越了服务层中的多个服务,因此非常适合通过切面来实现.
因为我们想要重试操作,所以我们需要使用环绕通知,以便我们可以多次调用 proceed
. 以下清单显示了基本方面的实现(使用 Schema 支持的常规 Java 类) :
public class ConcurrentOperationExecutor implements Ordered {
private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
int numAttempts = 0;
PessimisticLockingFailureException lockFailureException;
do {
numAttempts++;
try {
return pjp.proceed();
}
catch(PessimisticLockingFailureException ex) {
lockFailureException = ex;
}
} while(numAttempts <= this.maxRetries);
throw lockFailureException;
}
}
class ConcurrentOperationExecutor : Ordered {
private val DEFAULT_MAX_RETRIES = 2
private var maxRetries = DEFAULT_MAX_RETRIES
private var order = 1
fun setMaxRetries(maxRetries: Int) {
this.maxRetries = maxRetries
}
override fun getOrder(): Int {
return this.order
}
fun setOrder(order: Int) {
this.order = order
}
fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any {
var numAttempts = 0
var lockFailureException: PessimisticLockingFailureException
do {
numAttempts++
try {
return pjp.proceed()
} catch (ex: PessimisticLockingFailureException) {
lockFailureException = ex
}
} while (numAttempts <= this.maxRetries)
throw lockFailureException
}
}
请注意,该方面实现了 Ordered
接口,以便我们可以将切面的优先级设置为高于事务通知(我们每次重试时都需要一个新的事务) . maxRetries
和 order
属性都由 Spring 配置. 主要的操作是在 doConcurrentOperation
的环绕通知中. 请注意,请注意,目前,我们将重试逻辑应用于每个 businessService()
. 尝试执行时,如果失败了,将产生 PessimisticLockingFailureException
异常,但是不用管它,只需再次尝试执行即可,除非已经用尽所有的重试次数.
此类与 @AspectJ 示例中使用的类相同,但删除了注解.
|
相应的Spring配置如下:
<aop:config>
<aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..))"/>
<aop:around
pointcut-ref="idempotentOperation"
method="doConcurrentOperation"/>
</aop:aspect>
</aop:config>
<bean id="concurrentOperationExecutor"
class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
<property name="maxRetries" value="3"/>
<property name="order" value="100"/>
</bean>
请注意,在当时,我们假设所有业务服务都是幂等的. 如果不是这种情况,我们可以通过引入 Idempotent
注解并使用注解来注解服务操作的实现来优化切面,使其重试时是幂等操作,如以下示例所示:
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
// marker annotation
}
@Retention(AnnotationRetention.RUNTIME)
annotation class Idempotent {
// marker annotation
}
对切面的更改只需要重试等幂运算,只需细化切点表达式,以便只匹配 @Idempotent
操作,如下所示:
<aop:pointcut id="idempotentOperation"
expression="execution(* com.xyz.myapp.service.*.*(..)) and
@annotation(com.xyz.myapp.service.Idempotent)"/>
5.6. 选择要使用的 AOP 声明样式
一旦确定某个切面是实现给定需求的最佳方法,您如何决定使用 Spring AOP 或 AspectJ 以及 Aspect语言(代码) 样式, @AspectJ
注解样式还是 Spring XML 样式? 这些决策受到许多因素的影响,包括应用程序要求,开发工具和团队对 AOP 的熟悉程度.
5.6.1. 使用 Spring AOP 还是全面使用 AspectJ?
使用最简单的方法. Spring AOP 比使用完整的 AspectJ 更 简单,因为不需要在开发和构建过程中引入 AspectJ 编译器/ 编织器. 如果只是需要在 Spring bean 上执行通知操作,那么使用 Spring AOP 是正确的选择. 如果需要的通知不是由 Spring 容器管理的对象(通常是域对象) ,那么就需要使用 AspectJ. 如果想使用通知连接点而不是简单的方法执行,也需要使用 AspectJ(例如,字段获取或设置连接点等) ,则还需要使用 AspectJ.
使用 AspectJ 时,您可以选择 AspectJ 语言语法(也称为 "代码样式") 或 @AspectJ
注解样式. 显然,如果没有使用 Java 5+ 版本那么选择已经确定了…使用代码方式. 如果切面在你的设计中扮演重要角色,并且想使用针对Eclipse的(AspectJ 开发工具 (AJDT) ) 插件,那么 AspectJ 语言语法是首选项: 它更清晰和更简单,因为语言是专门用于编写切面的.
如果没有使用 Eclipse,或者只有一些切面在应用程序中不起主要作用,那么可能需要考虑使用 @AspectJ
方式,并在 IDE 中使用常规 Java 编译,并加入切面编织阶段构建的脚本.
5.6.2. 选择 @AspectJ 注解还是 Spring AOP 的 XML 配置?
如果您选择使用 Spring AOP,则可以选择 @AspectJ
或 XML 样式. 需要考虑各种权衡.
XML 样式可能是现有 Spring 用户最熟悉的,并且由真正的 POJO 支持. 当使用 AOP 作为一种工具来配置企业服务时, XML 就是一个很好的选择(可以用以下方法测试: 是否认为切入点表达式是想要独立改变的配置的一部分) . 使用 XML 配置的方式,可以从配置中更清楚地了解系统中存在哪些切面.
XML 样式有两个缺点. 首先,它并没有按实现的要求完全封装到单个地方. DRY 原则是说: 在任何知识系统中,应该有一个单一的、明确的、权威的职责. 使用 XML 的样式时,如果要求的知识是实现拆分的 bean 类的声明,并且是配置在文件的 XML 中. 当使用 @AspectJ
的风格实现单一的模块时,切面的信息是封装的. 其次,XML 的样式在能表达的功能方面比 @AspectJ
风格的有更多的限制,只有 "singleton" 切面的实例化模式得到支持,这在XML声明的切点中是不可能的. 例如,在 @AspectJ
样式中,您可以编写如下内容:
@Pointcut("execution(* get*())")
public void propertyAccess() {}
@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}
@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}
@Pointcut("execution(* get*())")
fun propertyAccess() {}
@Pointcut("execution(org.xyz.Account+ *(..))")
fun operationReturningAnAccount() {}
@Pointcut("propertyAccess() && operationReturningAnAccount()")
fun accountPropertyAccess() {}
在 XML 样式中,您可以声明前两个切入点:
<aop:pointcut id="propertyAccess"
expression="execution(* get*())"/>
<aop:pointcut id="operationReturningAnAccount"
expression="execution(org.xyz.Account+ *(..))"/>
XML 的方法的缺点是,您无法通过组合这些定义来定义 accountPropertyAccess
切点.
@AspectJ
的风格支持更多的实例化模式和丰富的切点组合. 它的优点是将切面确保为单元模块化,@AspectJ
的使用对理解切面也很有优势(也很容易接受) , 无论是通过 Spring AOP 还是 AspectJ 的使用 .
所以如果决定需要 AspectJ 的能力解决额外的要求,然后迁移到一个基于 AspectJ 的方法,是非常简单的. Spring 团队建议使用 @AspectJ
的方式.
5.7. 混合切面类型
在实际应用中,完全有可能混合使用 @AspectJ
的切面方式,用于支持自动代理、schema 定义 <aop:aspect>
,<aop:advisor>
声明通知者甚至在同一配置中定义使用 Spring 1.2 风格的代理和拦截器. 所有这些都是使用相同的底层支持机制实现的,并且可以愉快地共存.
5.8. 代理策略
Spring AOP 使用 JDK 动态代理或 CGLIB 为给定的代理创建代理目标对象. JDK 中内置了 JDK 动态代理,而 CGLIB 是常见的开源类定义库(重新打包为 spring-core
) .
如果要代理的目标对象实现至少一个接口,则使用 JDK 动态代理. 目标类型实现的所有接口都是代理的. 如果目标对象未实现任何接口,则会创建 CGLIB 代理.
如果要强制使用 CGLIB 代理(例如,代理为目标对象定义的每个方法,而不仅仅是那些由其接口实现的方法) ,您可以这样做. 但是,您应该考虑以下问题:
-
final
声明为final
的方法不能使用,因为它们不能被覆盖. -
从 Spring 4.0 开始,代理对象的构造函数不再被调用两次, 因为 CGLIB 代理实例是通过
Objenesis
创建的. 仅当您的 JVM 执行不允许绕过构造函数,您可能会看到两次调用和 来自 Spring 的 AOP 支持的相应调试日志条目.
要强制使用 CGLIB 代理,请将 <aop:config>
元素的 proxy-target-class
属性的值设置为 true
,如下所示:
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>
要在使用 @AspectJ
自动代理支持时强制 CGLIB 代理,请将 <aop:aspectj-autoproxy>
元素的 proxy-target-class
属性设置为 true
,如下所示:
<aop:aspectj-autoproxy proxy-target-class="true"/>
多个 要明确的是,在 |
5.8.1. 理解 AOP 代理
Spring AOP 是基于代理的,在编写自定义切面或使用 Spring 框架提供的任何基于 Spring AOP 的切面前,掌握上一个语句的实际语义是非常重要的.
首先需要考虑的情况如下,假设有一个普通的、非代理的、没有什么特殊的、直接的引用对象. 如下面的代码片段所示:
public class SimplePojo implements Pojo {
public void foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar();
}
public void bar() {
// some logic...
}
}
class SimplePojo : Pojo {
fun foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar()
}
fun bar() {
// some logic...
}
}
如果在对象引用上调用方法,则直接在该对象引用上调用该方法,如下图所示:

public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// this is a direct method call on the 'pojo' reference
pojo.foo();
}
}
fun main() {
val pojo = SimplePojo()
// this is a direct method call on the 'pojo' reference
pojo.foo()
}
当客户端代码是代理的引用时,事情发生了细微的变化. 请考虑以下图表和代码段:

public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
fun main() {
val factory = ProxyFactory(SimplePojo())
factory.addInterface(Pojo::class.java)
factory.addAdvice(RetryAdvice())
val pojo = factory.proxy as Pojo
// this is a method call on the proxy!
pojo.foo()
}
这里要理解的关键是 Main
类的 main(..)
方法中的客户端代码具有对代理的引用. 这意味着对该对象引用的方法将在代理上调用,因此代理将能够委托与该特定方法调用相关的所有拦截器(通知) .
然而,一旦调用终于达到了目标对象(在这个例子中是 SimplePojo
引用) ,任何方法调用都会传递给他,例如 this.bar()
或 this.foo()
, 都会调用这个引用,而不是代理. 这具有重要的意义,这意味着自我调用不会导致与方法调用相关联的通知,从而也不会获得执行的机会.
好的,那要做些什么呢? 最好的方法(这个 “best” , 的,也是迫不得已的) 是重构代码,以便不会发生自我调用. 这确实需要您做一些工作,但这是最好的,最少侵入性的方法. 下一个办法绝对是可怕的,我几乎不愿意指出,正是因为它是如此可怕. 您可以(对我们来说很痛苦) 将类中的逻辑完全绑定到 Spring AOP,如下例所示:
public class SimplePojo implements Pojo {
public void foo() {
// this works, but... gah!
((Pojo) AopContext.currentProxy()).bar();
}
public void bar() {
// some logic...
}
}
class SimplePojo : Pojo {
fun foo() {
// this works, but... gah!
(AopContext.currentProxy() as Pojo).bar()
}
fun bar() {
// some logic...
}
}
这完全将代码与 AOP 相耦合,这使类本身意识到它正在 AOP 上下文中使用,犹如在 AOP 面前耍大刀一般. 当创建代理时,它还需要一些额外的配置. 如以下示例所示:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
factory.setExposeProxy(true);
Pojo pojo = (Pojo) factory.getProxy();
// this is a method call on the proxy!
pojo.foo();
}
}
fun main() {
val factory = ProxyFactory(SimplePojo())
factory.addInterface(Pojo::class.java)
factory.addAdvice(RetryAdvice())
factory.isExposeProxy = true
val pojo = factory.proxy as Pojo
// this is a method call on the proxy!
pojo.foo()
}
最后,必须注意的是 AspectJ 没有这种自我调用问题,因为它不是基于代理的 AOP 框架.
5.9. 编程创建@AspectJ代理
除了在配置中使用 <aop:config>
或 <aop:aspectj-autoproxy>
来声明切面外,还可以使用编程的方式创建代理的通知目标对象. 有关 Spring 的 AOP API 的完整详细信息,请参阅下一章. 在这里,我们的关注点是希望使用@AspectJ方面自动创建代理的能力.
您可以使用 org.springframework.aop.aspectj.annotation.AspectJProxyFactory
类为一个或多个 @AspectJ
切面通知的目标对象创建代理. 此类的基本用法非常简单,如下例所示:
// create a factory that can generate a proxy for the given target object
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);
// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager.class);
// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker);
// now get the proxy object...
MyInterfaceType proxy = factory.getProxy();
// create a factory that can generate a proxy for the given target object
val factory = AspectJProxyFactory(targetObject)
// add an aspect, the class must be an @AspectJ aspect
// you can call this as many times as you need with different aspects
factory.addAspect(SecurityManager::class.java)
// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect
factory.addAspect(usageTracker)
// now get the proxy object...
val proxy = factory.getProxy<Any>()
See the javadoc for more information.
5.10. 在 Spring 应用中使用 AspectJ
到目前为止,我们在本章中介绍的所有内容都是纯粹的 Spring AOP. 将介绍如何使用 AspectJ 编译器/编织器代替 AOP,还介绍了超越 Spring AOP 而单独提供的功能.
Spring 有一个小的 AspectJ 切面库,是一个单独管理的 spring-aspects.jar
包. 如果使用到切面那么需要将它添加到类路径中. 在使用Spring 中的 AspectJ 独立注入域对象 和 在 Spring 中使用的 AspectJ 另外的切面 会讨论这个库的内容以及如何使用.
使用 Spring 的 IoC配置 AspectJ 切面 讨论如何依赖于使用 AspectJ 编译器编织的 AspectJ 切面. 最后, 在 在 Spring 框架中使用AspectJ 的加载时织入 将讨论在 Spring 的应用中使用 AspectJ 涉及的编织时机的讨论.
5.10.1. 使用 Spring 中的 AspectJ 独立注入域对象
Spring 容器实例化和配置会在应用程序上下文中定义 bean. 也可以让 bean 工厂配置预先存在的对象,给定一个包含要应用的配置的 bean 定义名称. spring-aspects.jar
包含了注解驱动的切面, 利用这个功能来允许依赖注入到任意对象. 该支持旨在用于在创建任何容器控制之外的对象. 域对象通常属于这一类,因为它们通常是使用 new
的操作符以编程方式创建的,或由 ORM 工具为数据库查询的结果创建的.
@Configurable
注解标记一个类符合 Spring 驱动配置的条件,在最简单的情况下,您可以纯粹使用它作为标记注解,如下例所示:
@Configurable
public class Account {
// ...
}
@Configurable
class Account {
// ...
}
作为这样一个标识接口, Spring 将会为这个注解类型(在例子中是 Account
) 利用定义 bean 的方式(典型的原型作用域) 配置一个新实例, 这个实例拥有与完全限定类型相同的名字(com.xyz.myapp.domain.Account
).
因为一个 bean 的默认名称是它的类型的完全限定名,这个简便的方式只是省略了它的 id
属性. 如以下示例所示:
<bean class="com.xyz.myapp.domain.Account" scope="prototype">
<property name="fundsTransferService" ref="fundsTransferService"/>
</bean>
如果想要显式指定为原型 bean 使用的名称,可以直接在注解执行此操作,如以下示例所示:
@Configurable("account")
public class Account {
// ...
}
@Configurable("account")
class Account {
// ...
}
Spring 现在查找名为 account
的bean定义,并将其用作配置新 Account
实例的定义.
也可以使用自动装配以避免指定一个特定的专用 bean 定义. Spring 将利用 @Configurable
注解的自动装配属性来自动装配 bean,可以使用 @Configurable(autowire=Autowire.BY_NAME
或者 @Configurable(autowire=Autowire.BY_TYPE)
分别自动装配基于名称和基于类型的 bean.作为替代方法,您可以在其类上或方法级别上使用 @Autowired
或 @Inject
能够使用注解驱动的依赖注入. 有关更多详细信息,请参阅 基于注解的容器配置 基于注解的容器配置.
最后,可以使用 Spring 依赖的名为 dependencyCheck
的特性去检查新建的对象引用以及配置对象(例如, @Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true)
) . 如果将此特性设置为 true
,那么 Spring 将在配置之后确认所有属性(非原始或集合) 已被设置.
当然,使用注解本身没有任何作用. 这是 spring-aspects.jar
包中的 AnnotationBeanConfigurerAspect
注解的存在行为. 实质上, 该切面表达的是,一个带有 @Configurable
注解类型的新对象在初始化返回之后,按照注解的属性使用 Spring 配置创建新的对象.
在这种情况下,初始化是指新实例化的对象(例如, 用 new
运算符实例化的对象) 以及正在经历反序列化(例如,通过 readResolve()) 的 Serializable
对象.
上一段的一个关键短语是 "实质".. 在大多数情况下,精确的语义从一个新对象初始化后返回是适合的. "初始化后"意味着依赖将会在对象被构建完毕后注入 , 这意味着依赖在类构造器当中是不能使用的. 如果想依赖的注入发生在构造器执行之前,而且能够用在构造器之中,那么需要像下面这样声明 Java
Kotlin
您可以在 本附录 中 AspectJ编程指南一书中找到更多有关 AspectJ 的信息 |
这个注解类型必须使用 AspectJ 编织织入才可以工作 , 开发者可以使用构建组件 Ant 或 Maven 来完成这个任务(AspectJ Development
Environment Guide有参考例子) ,或者在装配时织入(请参考 在 Spring 框架中使用AspectJ 的加载时织入) .
AnnotationBeanConfigurerAspect
注解本身需要 Spring 来配置(为了获取一个 bean 工厂引用,被用于配置新的对象) . 如果使用基于 Java 的配置, 那么只需将 @EnableSpringConfigured
注解加入到任意的 @Configuration
类中即可,如下所示:
@Configuration
@EnableSpringConfigured
public class AppConfig {
}
@Configuration
@EnableSpringConfigured
class AppConfig {
}
如果基于 XML 配置,那么只要在 Springcontext
命名空间声明中添加 context:spring-configured
.
您可以按如下方式使用它:
<context:spring-configured/>
在配置切面之前创建 @Configurable
对象的实例将会向调试日志发消息,并且不会对该对象进行配置. 一个例子是在 Spring 配置中的一个 bean,它在 Spring 初始化时创建域对象. 在这种情况下,可以使用 depends-onbean
属性来手动指定 bean 依赖的切面配置. 以下示例显示了如何使用 depends-on
属性:
<bean id="myService"
class="com.xzy.myapp.service.MyService"
depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">
<!-- ... -->
</bean>
不用通过 bean 的切面配置来激活 @Configurable 处理过程,除非真的想在运行中依赖其语义. 特别地,不要在一个已经在容器上注册过的 Spring bean 上去再去使用 @Configurable 注解. 否则,这个 bean 将会被初始化两次,容器一次,切面一次.
|
单元测试 @Configurable
的对象
开启 @Configurable
支持的一个目标就是使单元测试独立于域对象,从而没有碰到诸如硬编码查找一样的困难. 如果 @Configurable
注解没有使用 AspectJ 织入那么它就不会对单元测试造成影响, 这样就可以正常地进行 mock 或 stub 测试.
如果 @Configurable
是使用 AspectJ 织入的,那么依然可以在容器之外正常地进行单元测试,但是如果每次都构建一个 @Configurable
对象都会看到警告消息, 它表示此配置并非 Spring 的配置.
多个应用上下文一起工作
AnnotationBeanConfigurerAspect
类在AspectJ中用来实现 @Configurable
支持的单个切面. 单个切面的作用域与静态成员的作用域是相同的, 也就是说每一个类加载器都会定义这个切面的实例类型. 这意味着,如果使用相同的类加载器层来定义多个应用上下文. 那么必须考虑在哪儿定义 @EnableSpringConfigured
bean以及在哪个路径存放 spring-aspects.jar
包.
考虑一个典型的 Spring Web 应用程序配置,其中有一个共享的父应用上下文,定义公共业务服务和支持它们所需的所有内容,每个Servlet包含一个子应用上下文, 其中包含特定于Servlet的定义. 所有这些上下文共存于相同的类加载器层次,所以 AnnotationBeanConfigurerAspect
能够持有他们之中的一个的引用. 在这种情况下, 建议在共享的(父) 应用上下文上使用 @EnableSpringConfigured
bean 定义,这个定义的服务,
可能想注入到域对象中. 结果是,开发者不能在子上下文(特定的 Servlet) 中使用 @Configurable
去定义域对象的引用bean(也许并不想做些什么) .
在同一个容器部署多个 Web 应用程序时,确保每个 Web 应用程序加载 spring-aspects.jar
类型是在使用自己的加载器引用(例如,通过 WEB-INF/lib
) . 如果 spring-aspects.jar
仅在容器的类路径下(也就是装在父母共享的加载器的引用) ,所有 的 Web 应用程序将共享相同的切面实例,而这可能不是你想要的.
5.10.2. 在Spring中使用的AspectJ额外的切面
除了 @Configurable
切面,spring-aspects.jar
还包含 AspectJ 切面,可以用来驱动 Spring 的事务管理,用于注解带 @Transactional
注解的类型和方法 . 这主要是为那些希望在 Spring 容器之外使用 Spring 框架的事务支持的用户而设计的.
解析 @Transactional
注解的切面是 AnnotationTransactionAspect
. 当使用这个切面时,必须注解这个实现类(和/或在类的方法上) ,不是接口(如果有的话) 的实现类. AspectJ 遵循 Java 的规则,注解的接口不能被继承.
@Transactional
注解的类指定默认的事务语义的各种公共操作的类.
在类的方法上注解 @Transactional
将会覆盖由给定默认事务语义的注解(如果存在) ,任意可见性的方法都可以被注解,包括私有方法. 直接注解非公共方法是获得执行此类方法的事务划分的唯一方法.
从 Spring Framework 4.2 开始, spring-aspects 提供了类似的切面,为标准的 javax.transaction.Transactional 注解提供了完全相同的功能. 查看
JtaAnnotationTransactionAspect 获取更多细节.
|
对于希望使用 Spring 配置和事务管理支持但不希望(或不能) 使用注解的 AspectJ 程序员, spring-aspects.jar
还包含可以扩展以提供自定义切点定义的抽象切面. 有关更多信息,请参阅 AbstractBeanConfigurerAspect
和 AbstractTransactionAspect
切面的源码.
作为示例,以下摘录显示了如何使用与完全限定的类名匹配的原型 bean 定义来编写一个切面 ,用于配置 domain 模型中定义的所有对象实例:
public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {
public DomainObjectConfiguration() {
setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
}
// the creation of a new bean (any object in the domain model)
protected pointcut beanCreation(Object beanInstance) :
initialization(new(..)) &&
CommonPointcuts.inDomainModel() &&
this(beanInstance);
}
5.10.3. 使用 Spring IoC 配置 AspectJ 切面
当在 Spring 应用中使用 AspectJ 的切面时,很自然的希望能够使用 Spring 来配置切面. AspectJ 运行时本身是负责创建和配置切面的, AspectJ 通过 Spring 创建切面取决于 AspectJ 实例化模型的方法(per-xxx
引起的) 的切面使用.
多数的 AspectJ 切面是单例切面. 这些切面的配置非常容易,只需正常地创建一个 bean 定义引用切面的类型,包含 bean 属性 factory-method="aspectOf"
. 这保证了 Spring 获得的是 AspectJ 的实例而不是试图创建实例本身的切面. 下示例显示如何使用 factory-method="aspectOf"
属性:
<bean id="profiler" class="com.xyz.profiler.Profiler"
factory-method="aspectOf"> (1)
<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
1 | 注意 factory-method="aspectOf" 属性 |
非单例切面很难配置,但是这样做也是有可能的,通过创建原型 bean 的定义和从 spring-aspects.jar
中使用 @Configurable
的支持. 这些工作需要在 AspectJ 运行之后在创建之中去配置切面实例才能成功.
如果想要使用 AspectJ 编写一些 @AspectJ
切面(例如,针对领域模型类型使用加载时编织) 以及希望与 Spring AOP 一起使用的其他 @AspectJ
切面,并且这些切面都使用 Spring 进行配置 . 那么需要告诉 Spring AOP @AspectJ
自动代理支持在配置中定义的 @AspectJ
方面的确切子集应该用于自动代理. 可以通过在 <aop:aspectj-autoproxy/>
元素中声明使用一个或多个 <include/>
元素来完成此操作.
每个 <include/>
元素指定一个名称模式,并且只有名称与至少一个模式相匹配的 bean 才会用于 Spring AOP 自动代理配置. 以下示例显示了如何使用 <include/>
元素:
<aop:aspectj-autoproxy>
<aop:include name="thisBean"/>
<aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
不要被 <aop:aspectj-autoproxy/> 元素的名称误导. 使用它会导致创建 Spring AOP 代理. 切面声明的 @AspectJ 方式只是在这里使用,AspectJ 运行时是没有用到的.
|
5.10.4. 在 Spring 框架中使用 AspectJ 的加载时织入
是指 AspectJ 切面在 JVM 加载类文件时被织入到程序的类文件的过程. 本部分的重点是配置和使用 LTW 在 Spring 框架上的具体内容,本节不是 LTW 的简介. 只有 AspectJ 能够详细地讲述 LTW 的特性和配置(与 Spring 完全没有关系) ,可以参看 LTW section of the AspectJ Development Environment Guide.
Spring 框架在 AspectJ 的 LTW 织入的过程中提供了更细粒度的控制,'Vanilla' AspectJ LTW 是一个高效的使用 Java(1.5+) 的代理,它会在 JVM 启动的时候改变一个 VM 参数. 这是一种 JVM 范围的设置,在某些情况下可能会很适合,但是太粗粒度了. Spring 的 LTW 能够为 LTW 提供类加载前的织入,显然这是一个更细粒度的控制,而且它在 'single-JVM-multiple-application'
的环境下更具意义(在典型的应用程序服务器环境中就是这样做的) .
此外,在特定的环境中(查看在某些环境) ,这种方式可以在对应用程序服务器运行脚本不做任何修改的情形下支持 LTW, 但需要添加 -javaagent:path/to/aspectjweaver.jar
(本节稍后将会描述) 或 -javaagent:path/to/org.springframework.instrument-5.3.22.jar
(原名为 spring-agent.jar) . 开发人员只需修改构成应用程序上下文的一个或多个文件,以启用加载时编入,而不是依赖通常负责部署配置的管理文件. 例如启动脚本.
到此为止,推销宣传部分已经结束了,那么让我们首先介绍使用 Spring 的 AspectJ LTW 的快速示例,然后详细介绍示例中介绍的元素. 有关完整示例,请参阅 Petclinic示例应用程序.
第一个例子
假设您是一名应用程序开发人员,负责诊断系统中某些性能问题的原因. 我们无需打开一个分析工具,而是要打开一个简单的剖析切面,让我们能够很快获得了一些性能指标, 这样我们就可以在随后立即使用更细粒度的分析工具.
这里介绍的例子使用 XML 格式的配置,也可以使用 Java配置和 @AspectJ 的方式. 特别是 @EnableLoadTimeWeaving 注解可以起 到替代 <context:load-time-weaver/> (详情见下文 ) .
|
下面是一个用于性能分析的切面,它不需要太花哨,它是一个基于时间的分析器,它使用 @AspectJ
样式的方面声明:
@Aspect
public class ProfilingAspect {
@Around("methodsToBeProfiled()")
public Object profile(ProceedingJoinPoint pjp) throws Throwable {
StopWatch sw = new StopWatch(getClass().getSimpleName());
try {
sw.start(pjp.getSignature().getName());
return pjp.proceed();
} finally {
sw.stop();
System.out.println(sw.prettyPrint());
}
}
@Pointcut("execution(public * foo..*.*(..))")
public void methodsToBeProfiled(){}
}
@Aspect
class ProfilingAspect {
@Around("methodsToBeProfiled()")
fun profile(pjp: ProceedingJoinPoint): Any {
val sw = StopWatch(javaClass.simpleName)
try {
sw.start(pjp.getSignature().getName())
return pjp.proceed()
} finally {
sw.stop()
println(sw.prettyPrint())
}
}
@Pointcut("execution(public * foo..*.*(..))")
fun methodsToBeProfiled() {
}
}
此外还需要创建一个 META-INF/aop.xml
文件,它将通知 AspectJ 将 ProfilingAspect
织入到类中. 这是文件的惯例, 即在 Java 类路径中存在名为 META-INF/aop.xml
的文件(或多个文件) 是标准 AspectJ. 以下示例显示了 aop.xml
文件:
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
<weaver>
<!-- only weave classes in our application-specific packages -->
<include within="foo.*"/>
</weaver>
<aspects>
<!-- weave in just this aspect -->
<aspect name="foo.ProfilingAspect"/>
</aspects>
</aspectj>
现在来配置的 Spring 特定部分. 我们需要配置 LoadTimeWeaver
(稍后解释) . LTW 是从一个或多个 META-INF/aop.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">
<!-- a service object; we will be profiling its methods -->
<bean id="entitlementCalculationService"
class="foo.StubEntitlementCalculationService"/>
<!-- this switches on the load-time weaving -->
<context:load-time-weaver/>
</beans>
现在所有必需的材料( aspect, META-INF/aop.xml
文件, Spring 的配置) 都已到位,我们可以使用 main(..)
方法创建以下驱动程序类,以演示 LTW 的运行情况:
public final class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);
EntitlementCalculationService entitlementCalculationService =
(EntitlementCalculationService) ctx.getBean("entitlementCalculationService");
// the profiling aspect is 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}
fun main() {
val ctx = ClassPathXmlApplicationContext("beans.xml")
val entitlementCalculationService = ctx.getBean("entitlementCalculationService") as EntitlementCalculationService
// the profiling aspect is 'woven' around this method execution
entitlementCalculationService.calculateEntitlement()
}
我们还有最后一件事要做. 本节的介绍确实说可以使用 Spring 在每个 ClassLoader
的基础上有选择地打开 LTW,这是事实. 但是,对于此示例,我们使用 Java 代理(随 Spring 提供) 来打开 LTW. 我们使用以下命令来运行前面显示的 Main
类:
java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main
-javaagent
是一个标志,用于指定和启用 代理程序来检测在 JVM 上运行的程序. Spring Framework 附带了一个代理程序 InstrumentationSavingAgent
, 它包装在 spring-instrument.jar
中,它作为前面示例中 -javaagent
参数的值提供.
Main
的输出将如下所示. (前面已经介绍了 Thread.sleep(..)
声明为 calculateEntitlement()
实现使分析器实际上捕获了比 0 毫秒更多的东西(01234
毫秒不是 AOP 引入的开销) ) 下面的清单显示了输出 我们运行我们的探查器时得到了:
Calculating entitlement StopWatch 'ProfilingAspect': running time (millis) = 1234 ------ ----- ---------------------------- ms % Task name ------ ----- ---------------------------- 01234 100% calculateEntitlement
由于 LTW 是会对 AspectJ 产生影响的,而不是仅仅局限在 Spring 的 beans. 在 Main
程序的轻微变化会产生相同的结果:
public final class Main {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("beans.xml", Main.class);
EntitlementCalculationService entitlementCalculationService =
new StubEntitlementCalculationService();
// the profiling aspect will be 'woven' around this method execution
entitlementCalculationService.calculateEntitlement();
}
}
fun main(args: Array<String>) {
ClassPathXmlApplicationContext("beans.xml")
val entitlementCalculationService = StubEntitlementCalculationService()
// the profiling aspect will be 'woven' around this method execution
entitlementCalculationService.calculateEntitlement()
}
请注意,在前面的程序中,我们如何引导 Spring 容器,然后在 Spring 的上下文之外创建一个新的 StubEntitlementCalculationService
实例. 分析通知依然会被编织.
不可否认,这个例子很简单. 但是在 Spring 中支持 LTW 的基础都介绍到了,而且为什么使用以及怎样使用配置在后面的章节也将解释.
在这个例子中使用的 ProfilingAspect 可能很基础的,但它非常有用. 是一个开发者可以使用在开发过程中使用开发时间切面的例子, 然后很容易地排除来自应用程序被部署到测试或生产中的因素.
|
切面
在 LTW 使用的 aspects 必须是 AspectJ 的切面. 它们可以写在 AspectJ 语言本身也可以在 @AspectJ
方式声明. 这意味着 aspects 在 AspectJ 和 Spring AOP 的切面都有效. 此外,编译切面的类需要包含在类路径中.
'META-INF/aop.xml'
使用 AspectJ LTW 的基础设施是一个或多个 META-INF/aop.xml
配置文件,这是在 Java 类路径中的(直接的或者更通常是一个 JAR 文件) .
LTW 部分 AspectJ 参考文档中详细介绍了此文件的结构和内容. 由于 aop.xml
文件是 100% AspectJ,因此我们不在此进一步描述.
需要的类库(JARS)
至少,您需要以下库来使用 Spring Framework 对 AspectJ LTW 的支持:
-
spring-aop.jar
-
aspectjweaver.jar
如果使用Spring 提供的代理程序启用检测,则还需要:
-
spring-instrument.jar
Spring 的配置
Spring 支持 LTW 的关键部件是 LoadTimeWeaver
接口(位于 org.springframework.instrument.classloading
包) ,而这接口有大部分的实现分布在 Spring 中.
LoadTimeWeaver
负责添加一个或多个 java.lang.instrument.ClassFileTransformers
到运行时的类装载器中. 这为各种有趣的应用程序打开了大门,其中一个恰好是方面的 LTW.
如果您不熟悉运行时类文件转换的概念,请在继续之前查看 java.lang.instrument 包的 javadoc API 文档. 虽然该文档并不全面,但至少可以看到关键接口和类(供您阅读本节时参考) .
|
配置一个特定的 ApplicationContext
LoadTimeWeaver
就像加入一行代码一样容易. (请注意,几乎可以肯定会将 ApplicationContext
作为的Spring容器- 通常一个 BeanFactory
是不够的,因为LTW的支持利用到 BeanFactoryPostProcessors
) .
要启用 Spring Framework 的 LTW 支持,您需要配置 LoadTimeWeaver
,通常使用 @EnableLoadTimeWeaving
注解来完成,如下所示:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
@Configuration
@EnableLoadTimeWeaving
class AppConfig {
}
或者,如果您更喜欢基于 XML 的配置,请使用 <context:load-time-weaver/>
元素. 请注意,元素是在 context
命名空间中定义的. 以下示例显示如何使用 <context:load-time-weaver/>
:
<?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:load-time-weaver/>
</beans>
上面的配置自动为你登记了一些特定的基础 beans,例如 LoadTimeWeaver
和 AspectJWeavingEnabler
. 默认的 LoadTimeWeaver
是 DefaultContextLoadTimeWeaver
类,它试图装饰并自动检测 LoadTimeWeaver
. "自动检测" 的 LoadTimeWeaver
的确切类型取决于您的运行时环境. 下表总结了各种 LoadTimeWeaver
实现:
Runtime Environment | LoadTimeWeaver implementation |
---|---|
Running in Apache Tomcat |
|
Running in GlassFish (limited to EAR deployments) |
|
|
|
Running in IBM’s WebSphere |
|
Running in Oracle’s WebLogic |
|
JVM started with Spring |
|
Fallback, expecting the underlying ClassLoader to follow common conventions
(namely |
|
请注意,该表仅列出使用 DefaultContextLoadTimeWeaver
时自动检测的 LoadTimeWeavers
. 您可以准确指定要使用的 LoadTimeWeaver
实现.
使用 Java 配置指定特定的 LoadTimeWeaver
实现 LoadTimeWeavingConfigurer
接口并覆盖 getLoadTimeWeaver()
方法. 以下示例指定 ReflectiveLoadTimeWeaver
:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {
@Override
public LoadTimeWeaver getLoadTimeWeaver() {
return new ReflectiveLoadTimeWeaver();
}
}
@Configuration
@EnableLoadTimeWeaving
class AppConfig : LoadTimeWeavingConfigurer {
override fun getLoadTimeWeaver(): LoadTimeWeaver {
return ReflectiveLoadTimeWeaver()
}
}
如果使用基于 XML 的配置,则可以将完全限定的类名指定为 <context:load-time-weaver/>
元素上的 weaver-class
属性的值. 同样,以下示例指定了 ReflectiveLoadTimeWeaver
:
<?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:load-time-weaver
weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</beans>
稍后可以使用众所周知的名称 loadTimeWeaver
从 Spring 容器中检索由配置定义和注册的 LoadTimeWeaver
. 请记住, LoadTimeWeaver
只是作为 Spring 的 LTW 基础结构的机制来添加一个或多个 ClassFileTransformer
,执行LTW的实际 ClassFileTransformers
是 ClassPreProcessorAgentAdapter
(来自 org.aspectj.weaver.loadtime
包) .
有关详细信息,请参阅 ClassPreProcessorAgentAdapter
类的类级 javadoc, 因为编织实际如何实现的细节超出了本文档的范围.
剩下要讨论的配置有一个 final
属性: aspectjWeaving
属性(如果使用 XML,则为 aspectj-weaving
) . 此属性控制是否启用 LTW. 它接受三个可能值中的一个,如果该属性不存在,则默认值为 autodetect
. 下表总结了三个可能的值:
Annotation Value | XML Value | Explanation |
---|---|---|
|
|
AspectJ 编织开启,切面在加载时织入. |
|
|
LTW 已关闭. 没有切面加载时织入. |
|
|
如果 Spring LTW 基础结构可以找到至少一个 |
特定环境的配置
最后一部分包含在应用程序服务器 和 Web 容器等环境中使用 Spring LTW 支持时所需的任何其他设置和配置.
Tomcat, JBoss, WebSphere, WebLogic
Tomcat, JBoss/WildFly, IBM WebSphere Application Server 和 Oracle WebLogic Server 提供了一个能够进行本地检测的 ClassLoader
. Spring的原生 LTW 利用这种 ClassLoader
实现来实现 AspectJ 织入. 如前所述,您可以通过激活加载时织入来启用 LTW. 具体来说,您无需修改启动脚本即可添加 -javaagent:path/to/spring-instrument.jar
.
注意在 JBoss 中, 应用程序服务器的扫描需要禁用,防止它加载的类的应用之前实际上已经开始. 快速的解决方案是增加一个叫 WEB-INF/jboss-scanning.xml
的文档并加入以下内容:
<scanning xmlns="urn:jboss:scanning:1.0"/>
通用的 Java 应用
在不支持现有 LoadTimeWeaver
实现或不受现有 LoadTimeWeaver
实现支持的环境中需要类检测时,使用 JDK 代理可能是唯一的解决方案. 对于这种情况, Spring 提供了 InstrumentationLoadTimeWeaver
,它需要 Spring 特有的(但也是非常普通的) VM 代理包 spring-instrument.jar
.
通过常见的 @EnableLoadTimeWeaving
和 <context: load-time-weaver />
设置.
要使用它,必须通过提供以下 JVM 选项来启动带有 Spring 代理的虚拟机:
-javaagent:/path/to/spring-instrument.jar
请注意,这需要修改 JVM 启动脚本,这可能会阻止在应用服务器环境中使用它(具体取决于操作策略) . 就是说,对于每个 JVM 一个应用程序的部署,例如独立 在 Spring Boot 应用程序中,通常无论如何都要控制整个 JVM 设置.
5.11. 更多资源
有关 AspectJ 的更多信息可以在 AspectJ website上找到.
Eclipse AspectJ 由 Adrian Colyer(Addison-Wesley, 2005) 出版 提供了详尽的有关 AspectJ 语言的介绍
AspectJ in Action, 一书的第二版由 Ramnivas Laddad(Manning,2009) 出版,也是强烈推荐的. 这本书的重点是 AspectJ,但也在一定的深度上探讨了普通的 AOP 主题.
6. Spring AOP APIs
前一章介绍了 Spring 使用 @AspectJ
和基于 schema 的切面定义对 AOP 的支持. 在本章中,将讨论较底层的 Spring AOP API 和 AOP 支持. 对于普通应用程序,我们建议将 Spring AOP 与 AspectJ 切入点一起使用,如 前一章所述.
6.1. Spring 中的切点 API
本节描述了 Spring 如何处理切点的关键概念.
6.1.1. 概念
Spring 的切点模式能够让切点独立于通知类型. 针对不同的通知使用相同的切点是可能的.
org.springframework.aop.Pointcut
接口是切点的主要接口,用于特定类和方法的目标通知. 完整的接口如下:
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
将 Pointcut
接口分成两部分,允许重用类和方法匹配部分,以及细粒度的组合操作 (例如与另一个方法匹配器执行 “union”) .
ClassFilter
接口是用来限制切点的一组给定的目标类. 如果 matches()
方法总是返回 true
,那么表示所有的目标类都将匹配. 以下清单显示了 ClassFilter
接口定义:
public interface ClassFilter {
boolean matches(Class clazz);
}
MethodMatcher
接口通常更重要. 完整的接口如下所示:
public interface MethodMatcher {
boolean matches(Method m, Class<?> targetClass);
boolean isRuntime();
boolean matches(Method m, Class<?> targetClass, Object... args);
}
matches(Method, Class)
方法用于测试此切点是否曾经匹配到目标类上的给定方法. 在创建 AOP 代理时可以执行此评估,以避免需要对每个方法调用进行测试. 如果对于给定方法这个双参数 matches
方法返回 true
,并且 MethodMatcher
的 isRuntime()
方法也返回 true
. 则在每次方法调用时都会调用三参数 matches 方法. 这使切点能够在目标通知执行之前,查看传递给方法调用的参数.
大多数 MethodMatcher
实现都是静态的,这意味着它们的 isRuntime()
方法返回 false
. 在这种情况下,永远不会调用三参数 matches
方法.
如果可以,请尝试将切点设为静态的,从而允许 AOP 框架在创建 AOP 代理时缓存对切点评估的结果. |
6.1.2. 切点的操作
Spring 支持对切点的各种操作,特别是并集和交集
并集意味着这个方法只要有一个切点匹配,交集意味着这个方法需要所有的切点都匹配. 并集使用得更广,您可以使用 org.springframework.aop.support.Pointcuts
类中的静态方法或在同一个包中使用 ComposablePointcut
类来组合切 点. 但是,使用 AspectJ 的切点表达式往往是更简单的方式.
6.1.3. AspectJ切点表达式
自 2.0 以来, Spring 使用的最重要的切点类型是 org.springframework.aop.aspectj.AspectJExpressionPointcut
. 这是一个使用 AspectJ 提供的库来解析 AspectJ 切点表达式字符串的切点.
有关支持的 AspectJ 切点语义的讨论, 请参见上一章
6.1.4. 便捷的切点实现
Spring 提供了几个便捷的切点实现,您可以直接使用其中一些. 其他的目的是在特定于应用程序的切点中进行子类化.
静态切点
静态切点是基于方法和目标类的,而且无法考虑该方法的参数. 静态切点在大多数的使用上是充分的、最好的. 在第一次调用一个方法时, Spring 可能只计算一次静态切点,在这之后,无需在使用每个方法调用时都评估切点.
本节的其余部分描述了 Spring 中包含的一些静态切点实现.
正则表达式切点
指定静态切入点的一个显而易见的实现是正则表达式,几个基于 Spring 的 AOP 框架让这成为可能. org.springframework.aop.support.JdkRegexpMethodPointcut
是一个通用的正则表达式切点,它使用 JDK 中的正则表达式支持.
使用 JdkRegexpMethodPointcut
类,可以提供一个匹配的 Strings 列表. 如果其中任意一个都是匹配的,则切点将计算将为 true
(因此,结果实际上是这些切点的并集) .
以下示例显示如何使用 JdkRegexpMethodPointcut
:
<bean id="settersAndAbsquatulatePointcut"
class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
Spring 提供了一个方便使用的类 RegexpMethodPointcutAdvisor
, 它允许引用 Advice
(记住 Advice
可能是一个拦截器、前置通知、异常通知等等) . 而在这个类的后面,Spring 也是使用 JdkRegexpMethodPointcut
类的. 使用 RegexpMethodPointcutAdvisor
来简化织入,用作 bean 封装的切点和通知. 如下例所示:
<bean id="settersAndAbsquatulateAdvisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref bean="beanNameOfAopAllianceInterceptor"/>
</property>
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
您可以将 RegexpMethodPointcutAdvisor
与任何 Advice
类型一起使用.
6.1.5. 切点超类
Spring 提供了相当有用的切点超类,帮助开发者实现自定义切点.
因为静态切点最有用,所以可能会继承 StaticMethodMatcherPointcut
.编写子类. 这需要只实现一个抽象方法 (尽管您可以覆盖其他方法来自定义行为) . 以下示例显示如何子类化 StaticMethodMatcherPointcut
:
class TestStaticPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method m, Class targetClass) {
// return true if custom criteria match
}
}
class TestStaticPointcut : StaticMethodMatcherPointcut() {
override fun matches(method: Method, targetClass: Class<*>): Boolean {
// return true if custom criteria match
}
}
这也是动态切点的超类
6.2. Spring 的通知 API
接下来介绍 Spring AOP 是怎么样处理通知的
6.2.1. 通知的生命周期
每个通知都是 Spring bean.通知实例可以在所有通知对象之间共享,或者对每个通知对象都是唯一的. 这对应于每个类或每个实例的通知.
单类 (Per-class) 通知是最常用的. 它适用于诸如事务通知者之类的一般性通知. 它不依赖于代理对象的状态或添加新状态,它们只是对方法和参数产生作用.
单实例 (Per-instance) 的通知适合于引入,以支持混合使用.在这种情况下,通知将状态添加到代理对象中.
在同一个 AOP 代理中,可以使用混合共享的和单实例的通知.
6.2.2. Advice Types in Spring
Spring 提供了几种通知类型,并且可以扩展以支持任意通知类型. 本节介绍基本概念和标准通知类型.
Spring中的通知类型
在 Spring 中,最基础的通知类型是拦截环绕通知
Spring 使用方法拦截来满足 AOPAlliance
接口的要求. MethodInterceptor
实现环绕通知应该实现以下接口:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
invoke()
方法的参数 MethodInvocation
暴露了将要被触发的方法,目标连接点,AOP 代理,以及方法的参数. invoke()
方法应该返回调用的结果: 连接点的返回值.
以下示例显示了一个简单的 MethodInterceptor
实现:
public class DebugInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("Before: invocation=[" + invocation + "]");
Object rval = invocation.proceed();
System.out.println("Invocation returned");
return rval;
}
}
class DebugInterceptor : MethodInterceptor {
override fun invoke(invocation: MethodInvocation): Any {
println("Before: invocation=[$invocation]")
val rval = invocation.proceed()
println("Invocation returned")
return rval
}
}
请注意对 MethodInvocation
的 proceed()
方法的调用. proceed
从拦截器链上进入连接点. 大多数拦截器调用此方法并返回其返回值. 但是, 与任意的环绕通知一样, MethodInterceptor
可以返回不同的值或引发异常,而不是调用 proceed
方法. 但是,如果没有充分的理由,您不希望这样做.
MethodInterceptor 提供与其他 AOP Alliance 兼容的 AOP 实现. 本节其余部分讨论的其他通知类型实现了常见的 AOP 概念,但这特定于使用 Spring 的方式.
尽管使用最具体的通知类型切面总是有优势的,但如果希望在另一个 AOP 框架中运行该切面面,,则应坚持使用 MethodInterceptor 的通知. 请注意,目前切点不会在框架之间进行交互操作, 并且目前的 AOP Alliance 并没有定义切点接口.
|
前置通知
前置通知是一种简单的通知,它并不需要 MethodInvocation
对象,因为它只会在执行方法前调用.
前置通知的主要优势就是它没有必要去触发 proceed()
方法,因此当拦截器链失败时对它是没有影响的.
以下清单显示了 MethodBeforeAdvice
接口:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object[] args, Object target) throws Throwable;
}
(Spring 的 API 设计允许前置通知使用在域上,尽管通常是适用于字段拦截的,而 Spring 也不可能实现它) .
注意 before
方法的返回类型是 void
的. 前置通知可以在连接点执行之前插入自定义行为,但不能更改返回值. 如果前置通知抛出了异常, 将会中止拦截器链的进一步执行,该异常将会传回给拦截器链. 如果它标记了 unchecked,或者是在触发方法的签名上,那么它将直接传递给客户端. 否则,它由 AOP 代理包装在未经检查的异常中.
以下示例显示了 Spring 中的前置通知,该通知计算所有方法调用:
public class CountingBeforeAdvice implements MethodBeforeAdvice {
private int count;
public void before(Method m, Object[] args, Object target) throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
class CountingBeforeAdvice : MethodBeforeAdvice {
var count: Int = 0
override fun before(m: Method, args: Array<Any>, target: Any?) {
++count
}
}
前置通知可以用在任意的切点上 |
异常通知
异常通知是在连接点返回后触发的,前提是连接点抛出了异常. Spring 提供了类型化的抛出通知. 请注意,这意味着 org.springframework.aop.ThrowsAdvice
接口不包含任何方法. 它只是标识给定对象实现一个或多个类型化异常通知方法的标识接口,这些应该是以下形式:
afterThrowing([Method, args, target], subclassOfThrowable)
这个方法只有最后一个参数是必需的. 方法签名可以有一个或四个参数,具体取决于通知方法是否对方法和参数有影响. 接下来的两个列表显示了作为异常通知示例的类. .
如果抛出 RemoteException
(包括子类) ,则调用以下通知:
public class RemoteThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
}
class RemoteThrowsAdvice : ThrowsAdvice {
fun afterThrowing(ex: RemoteException) {
// Do something with remote exception
}
}
与前面的通知不同,下一个示例声明了四个参数,以便它可以访问被调用的方法,方法参数和目标对象. 如果抛出 ServletException
,则调用以下通知:
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
class ServletThrowsAdviceWithArguments : ThrowsAdvice {
fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
// Do something with all arguments
}
}
最后的示例演示了如何在单个类中使用这两种方法,它能处理 RemoteException
和 ServletException
异常. 任何数量的异常通知方法都可以在单个类中进行组合. 以下清单显示了最后一个示例:
public static class CombinedThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
class CombinedThrowsAdvice : ThrowsAdvice {
fun afterThrowing(ex: RemoteException) {
// Do something with remote exception
}
fun afterThrowing(m: Method, args: Array<Any>, target: Any, ex: ServletException) {
// Do something with all arguments
}
}
如果异常通知方法引发了异常,那么它将会重写原始的异常 (即更改为向用户抛出异常) . 覆盖异常通常是 RuntimeException ,它与任何方法签名兼容. 但是,如果异常通知方法引发了 checked 异常,那么它必须与目标方法的已声明的异常相匹配,因此在某种程度上耦合到特定的目标方法签名. 不要抛出与目标方法签名不兼容的未声明的 checked 异常!
|
异常通知可以被用在任意切点上 |
后置返回通知
Spring 中使用后置返回通知必需实现 org.springframework.aop.AfterReturningAdvice
接口, 如下所示:
public interface AfterReturningAdvice extends Advice {
void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable;
}
后置返回通知可以访问返回值 (不能修改) 、调用的方法、方法参数和目标.
下面例子的后置返回通知会统计所有成功的、不引发异常的方法调用次数:
public class CountingAfterReturningAdvice implements AfterReturningAdvice {
private int count;
public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
class CountingAfterReturningAdvice : AfterReturningAdvice {
var count: Int = 0
private set
override fun afterReturning(returnValue: Any?, m: Method, args: Array<Any>, target: Any?) {
++count
}
}
此通知不会更改执行路径,如果抛出异常,将抛出拦截器链而不是返回值.
后置返回通知能被任何切点使用 |
引入通知
Spring 将引入通知看作是一种特殊的拦截器通知
引入通知需要 IntroductionAdvisor
和 IntroductionInterceptor
,他们都实现了下面的接口:
public interface IntroductionInterceptor extends MethodInterceptor {
boolean implementsInterface(Class intf);
}
从 AOP Alliance MethodInterceptor
接口继承的 invoke()
方法也都必须实现引入. 即如果 invoked
方法是一个引入接口, 引入拦截器将会负责处理这个方法的调用-它无法触发 proceed()
.
引入通知不能与任何切点一起使用,因为它只适用于类级别,而不是方法级别. 开发者只能使用 IntroductionAdvisor
的引入通知,它具有以下方法:
public interface IntroductionAdvisor extends Advisor, IntroductionInfo {
ClassFilter getClassFilter();
void validateInterfaces() throws IllegalArgumentException;
}
public interface IntroductionInfo {
Class<?>[] getInterfaces();
}
在这里如果没有与 MethodMatcher
相关的引入通知类. 也就不会有 Pointcut
. 此时,只有 filtering 类是符合逻辑的.
getInterfaces()
方法返回通知者的引入接口
validateInterfaces()
方法在内部使用,可以查看引入接口是否可以由配置的 IntroductionInterceptor
实现.
考虑 Spring 测试套件中的一个示例,并假设我们要将以下接口引入一个或多个对象:
public interface Lockable {
void lock();
void unlock();
boolean locked();
}
interface Lockable {
fun lock()
fun unlock()
fun locked(): Boolean
}
这个说明是混合型的. 我们希望可以将无论是什么类型的通知对象都转成 Lockable
,这样可以调用它的 lock 和 unlock 方法. 如果调用的是 lock()
方法,希望所有的 setter 方法都抛出 LockedException
异常.
因此,可以添加一个切面,它提供了对象不可变的能力,而不需要对它有任何了解. AOP 的一个很好的例子: a good example of AOP.
首先,我们需要一个可以完成繁重工作的 IntroductionInterceptor
. 在这种情况下,我们扩展了 org.springframework.aop.support.DelegatingIntroductionInterceptor
类更方便. 我们可以直接实现 IntroductionInterceptor
,但使用 DelegatingIntroductionInterceptor
最适合大多数情况.
DelegatingIntroductionInterceptor
设计是为了将引入委托让给引入接口真正的实现类,从而隐藏了拦截器去做这个事. 可以使用构造函数参数将委托设置为任何对象. 默认委托 (当使用无参数构造函数时) 时是 this
的. 因此,在下面的示例中, 委托是 DelegatingIntroductionInterceptor
中的 LockMixin
子类.
给定一个委托 (默认是它本身) , DelegatingIntroductionInterceptor
实例将查找委托(非 IntroductionInterceptor
) 实现的所有接口,并支持对其中任何一个的引入.
子类(如 LockMixin
) 可以调用 suppressInterface(Class intf)
方法来控制不应该暴露的接口. 但是,无论 IntroductionInterceptor
准备支持多少接口,使用 IntroductionAdvisor
都可以控制实际暴露的接口. 引入接口将隐藏目标对同一接口的任何实现.
因此, LockMixin
扩展了 DelegatingIntroductionInterceptor
并实现了 Lockable
本身. 超类自动选择可以支持 Lockable
引入,因此我们不需要指定. 我们可以用这种方式引入任意数量的接口.
请注意使用 locked
实例变量,这有效地将附加状态添加到目标对象中.
以下示例显示了示例 LockMixin
类:
public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {
private boolean locked;
public void lock() {
this.locked = true;
}
public void unlock() {
this.locked = false;
}
public boolean locked() {
return this.locked;
}
public Object invoke(MethodInvocation invocation) throws Throwable {
if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
throw new LockedException();
}
return super.invoke(invocation);
}
}
class LockMixin : DelegatingIntroductionInterceptor(), Lockable {
private var locked: Boolean = false
fun lock() {
this.locked = true
}
fun unlock() {
this.locked = false
}
fun locked(): Boolean {
return this.locked
}
override fun invoke(invocation: MethodInvocation): Any? {
if (locked() && invocation.method.name.indexOf("set") == 0) {
throw LockedException()
}
return super.invoke(invocation)
}
}
通常,您不需要覆盖 invoke()
方法. DelegatingIntroductionInterceptor
实现 (如果引入方法则调用 delegate
方法,否则就对连接点进行操作) 通常就足够了. 在本例中,我们需要添加一个检查: 如果处于锁定模式,则不能调用 setter 方法.
引入通知者是非常简单的,它需要做的所有事情就是持有一个独特的 LockMixin
实例,并指定引入接口 . 在例子中就是 Lockable
. 一个更复杂的示例可能会引用引入拦截器 (被定义为原型) ,在这种情况下,没有与 LockMixin
相关的配置,因此我们使用 new
创建它. 以下示例显示了我们的 LockMixinAdvisor
类:
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
public LockMixinAdvisor() {
super(new LockMixin(), Lockable.class);
}
}
class LockMixinAdvisor : DefaultIntroductionAdvisor(LockMixin(), Lockable::class.java)
我们可以非常简单地应用这个通知者,因为它不需要配置. (但是,没有 IntroductionAdvisor
就不可能使用 IntroductionInterceptor
. ) 与通常的引入一样, 通知者必须是个单实例 (per-instance) ,因为它是有状态的. 需要为每个通知的对象创建每一个不同的 LockMixinAdvisor
实例和 LockMixin
. 通知者也包括通知对象状态的一部分
可以使用 Advised.addAdvisor()
方法或在在XML配置中 (推荐此法) 编写通知者,这与其他任何的通知者一样. 下面讨论的所有代理创建选项, 包括自动代理创建,都正确处理了引入和有状态的mixin.
6.3. Spring 中通知者的 API
在 Spring 中,一个通知者就是一个切面,一个仅包含与单个通知对象关联的切点表达式.
除了引入是一个特殊的例子外,通知者能够用于所有的通知上. org.springframework.aop.support.DefaultPointcutAdvisor
类是最常使用的通知者类. 它可以与 MethodInterceptor
, BeforeAdvice
或 ThrowsAdvice
一起使用.
在同一个 AOP 代理中,可以在 Spring 中混合使用通知者和通知类型. 例如,可以在一个代理配置中同时使用环绕通知、异常通知和前置通知. Spring 自动创建必要的拦截链.
6.4. 使用 ProxyFactoryBean
来创建 AOP 代理
如果你为业务对象使用 Spring IoC 容器 (一个 ApplicationContext
或 BeanFactory
) (同时也应该这么做!) , 那么可能希望用到其中一个 Spring 的 AOP FactoryBean
. (请记住,工厂 bean 引入了一个间接层,让它创建一个不同类型的对象. )
Spring AOP 支持也使用到了工厂 bean |
在 Spring 中创建 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean
. 这将完全控制切点和应用的通知及顺序. 但是,如果不需要这样的控制,可以有更简单的选项.
6.4.1. 基础设置
ProxyFactoryBean
与其他Spring FactoryBean
的实现一样,引入了一个间接层. 如果定义了一个名为 foo
的 ProxyFactoryBean
, 那么引用 foo
的对象不是 ProxyFactoryBean
实例本身,而是由 ProxyFactoryBean
实现的 getObject()
方法创建的对象. 此方法将创建一个用于包装目标对象的 AOP 代理
使用 ProxyFactoryBean
或另一个 IoC 识别类来创建 AOP 代理的最重要的好处之一是,它意味着建议和切点也可以由 IoC 容器管理. 这是一个强大的功能,能够实现其他AOP框架无法实现的方法. 例如,通知本身可以引用应用程序对象 (除了目标,它应该在任何 AOP 框架中可用) ,这得益于依赖注入提供的所有可插入功能.
6.4.2. JavaBean 属性
与 Spring 提供的大多数 FactoryBean
实现一样,ProxyFactoryBean
类本身就是一个 JavaBean. 其属性用于:
-
指定需要代理的目标
-
指定是否使用 CGLIB (稍后介绍,另请参阅基于 JDK 和 CGLIB 的代理) .
一些关键属性继承自 org.springframework.aop.framework.ProxyConfig
(Spring中所有AOP代理工厂的超类) . 这些关键属性包括以下内容:
-
proxyTargetClass
: 如果目标类需要代理,而不是目标类的接口时,则为true
. 如果此属性值设置为true
,则会创建 CGLIB 代理 (但另请参阅基于 JDK 和 CGLIB 的代理) . -
optimize
: 控制是否将积极的优化应用于通过 CGLIB 创建的代理. 除非您完全了解相关 的 AOP 代理如何处理优化,否则不要随意使用此设置. 当前仅用于 CGLIB 代理. 它对 JDK 动态代理无效. -
frozen
: 如果代理配置被frozen
,则不再允许对配置进行更改. 这既可以作为一种轻微的优化,也适用于当不希望调用方在创建代理后能够操作代理 (通过Advised
接口) 的情况. 此属性的默认值为false
,因此如果允许添加其他的通知的话可以更改. -
exposeProxy
: 确定当前代理是否应在ThreadLocal
中暴露,以便目标可以访问它. 如果目标需要获取代理,并且exposeProxy
属性设置为true
. 则目标可以使用AopContext.currentProxy()
方法.
ProxyFactoryBean
特有的其他属性包括以下内容:
-
proxyInterfaces
: 字符串接口名称的数组. 如果未提供此项,将使用目标类的 CGLIB 代理 ( 基于 JDK 和 CGLIB 的代理) . -
interceptorNames
: 要提供的通知者、拦截器或其他通知名称的字符串数组. 在先到先得的服务基础上,Ordering (顺序) 是重要的. 也就是说, 列表中的第一个拦截器将首先拦截调用.这些名称是当前工厂中的 bean 名称,包括来自上级工厂的 bean 名称. 不能在这里提及 bean 的引用,因为这样做会导致
ProxyFactoryBean
忽略通知的单例.可以追加一个带有星号()的拦截器名称. 这将导致应用程序中的所有被
匹配的通知者 bean 的名称都会被匹配上. 您可以在使用 全局通知者中中找到使用此功能的示例.
-
singleton: 工厂强制返回单个对象,无论调用
getObject()
方法多少次. 几个FactoryBean
的实现都提供了这样的方法. 默认值是true
. 如果想使用有状态的通知. 例如,对于有状态的 mixins - 使用原型建议以及单例值false
.
6.4.3. 基于 JDK 和基于 CGLIB 的代理
本节是关于 ProxyFactoryBean
如何为特定目标对象 (即将被代理) 选择创建基于 JDK 或 CGLIB 的代理的权威性文档.
ProxyFactoryBean 关于创建基于 JDK 或 CGLIB 的代理的行为在 Spring 的 1.2.x 和 2.0 版本之间发生了变化. 现在, ProxyFactoryBean 在自动检测接口方面表现出与 TransactionProxyFactoryBean 类相似的语义.
|
如果要代理的目标对象的类 (以下简称为目标类) 未实现任何接口,则创建基于 CGLIB 的代理. 这是最简单的方案,因为 JDK 代理是基于接口的,没有接口意味着甚至不可能进行 JDK 代理. 一个简单的例子是插入目标 bean,并通过 interceptorNames
属性指定拦截器列表.
请注意,即使 ProxyFactoryBean
的 proxyTargetClass
属性被设置为 false
,也会创建 CGLIB 的代理. (显然,这个 false
是没有意义的,最好从 bean 定义中删除,因为它充其量是冗余的,而且是最容易产生混乱) .
如果目标类实现了一个 (或多个) 接口,那么所创建代理的类型取决于 ProxyFactoryBean
的配置.
如果 ProxyFactoryBean
的 proxyTargetClass
属性已设置为 true
,则会创建基于 CGLIB 的代理. 这是有道理的,并且符合最少惊喜的原则. 即使 ProxyFactoryBean
的 proxyInterfaces
属性已设置为一个或多个完全限定的接口名称,proxyTargetClass
属性设置为 true
这一事实也会导致基于 CGLIB 的代理生效.
如果 ProxyFactoryBean
的 proxyInterfaces
属性已设置为一个或多个完全限定的接口名称,则会创建基于 JDK 的代理. 创建的代理实现 proxyInterfaces
属性中指定的所有接口. 如果目标类恰好实现了比 proxyInterfaces
属性中指定的更多的接口,那么这一切都很好,但是这些附加接口将不会由返回的代理实现.
如果 ProxyFactoryBean
的 proxyInterfaces
属性具有没有被设置,而目标类确实实现一个或多个接口,则 ProxyFactoryBean
将自动检测选择,当目标类实际上至少实现一个接口. 将创建 JDK 代理. 实际上代理的接口将是目标类实现的所有接口.
事实上,这与简单地提供了目标类实现到 proxyInterfaces
属性的每个接口的列表相同. 但是,这明显减轻了负担,还避免配置错误.
6.4.4. 代理接口
首先看一下 ProxyFactoryBean
简单的例子,这个例子包含:
-
将被代理的目标 bean,下面示例中的
personTarget
bean定义 -
一个
Advisor
(通知者) 和一个Interceptor
(拦截器) ,用于提供通知. -
指定目标对象(
personTarget
bean)的 AOP 代理 bean 和要代理的接口,以及要应用的通知.
以下清单显示了该示例:
<bean id="personTarget" class="com.mycompany.PersonImpl">
<property name="name" value="Tony"/>
<property name="age" value="51"/>
</bean>
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>
<bean id="person"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<property name="target" ref="personTarget"/>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
注意 interceptorNames
属性是一个 String
列表,放拦截器 bean 的名字或在当前工厂中的通知者. 通知者、拦截器、前置、后置返回和异常通知的对象可以被使用. 通知者是按顺序排列.
您可能想知道为什么列表不包含 bean 引用? 理由是如果 ProxyFactoryBean 的单例属性被设置为 false ,它必须能够返回独立的代理实例. 如果任意的通知者本身是原型的, 那么就需要返回一个独立的实例,所以有必要从工厂获得原型实例. 只保存一个引用是不够的.
|
前面显示的 person
bean定义可以用来代替 Person
实现,如下所示:
Person person = (Person) factory.getBean("person");
val person = factory.getBean("person") as Person;
与普通 Java 对象一样,同一 IoC 上下文中的其他 bean 可以表达对它的强类型依赖. 以下示例显示了如何执行此操作:
<bean id="personUser" class="com.mycompany.PersonUser">
<property name="person"><ref bean="person"/></property>
</bean>
此示例中的 PersonUser
类将暴露类型为 Person
的属性. 就它而言,可以透明地使用 AOP 代理来代替 “real” 的 person 实现. 但是,它的类将是动态代理类. 可以将其转换为 Advised
的接口 (如下所述) :
通过使用匿名内部 bean 可以隐藏目标和代理之前的区别,只有 ProxyFactoryBean
的定义是不同的,包含通知只是考虑到完整性. 以下示例显示如何使用匿名内部 bean:
<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
<property name="someProperty" value="Custom string property value"/>
</bean>
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces" value="com.mycompany.Person"/>
<!-- Use inner bean, not local reference to target -->
<property name="target">
<bean class="com.mycompany.PersonImpl">
<property name="name" value="Tony"/>
<property name="age" value="51"/>
</bean>
</property>
<property name="interceptorNames">
<list>
<value>myAdvisor</value>
<value>debugInterceptor</value>
</list>
</property>
</bean>
这样做的好处是只有一个 Person
类型的对象,如果想阻止应用程序上下文的用户获得对 un-advised 对象的引用,或者需要避免使用 Spring IoC 自动装配的任何含糊不清的情况, 那么这个对象就很有用. ProxyFactoryBean
定义是自包含的,这也是一个好处. 但是,有时能够从工厂获得 un-advised 目标可能是一个优势 (例如,在某些测试场景中) .
6.4.5. 代理类
如果需要代理一个类而不是一个或多个接口,又该怎么办?
考虑上面的例子,没有 Person
接口,需要给一个没有实现任何业务接口的 Person
类提供通知. 在这种情况下,您可以将 Spring 配置为使用 CGLIB 代理而不是动态代理. 简单设置 ProxyFactoryBean
的 proxyTargetClass
属性为 true
.
尽管最佳实践是面向接口编程,不是类. 但在处理遗留代码时, 通知不实现接口的类的能力可能会非常有用 (一般来说,Spring 不是规定性的. 虽然它可以很容易地应用好的实践,但它避免强制使用特定的方法) .
如果你愿意,即使有接口,也可以强制使用 CGLIB 代理.
CGLIB 代理的原理是在运行时生成目标类的子类. Spring 配置这个生成的子类用了委托的方法来调用原始的对象,在通知的编织中,子类被用于实现装饰者模式.
CGLIB 代理通常对于用户应当是透明的,然而还有需考虑一些问题:
-
Final
方法不能被 advised,因为它们不能被覆盖. -
无需添加 CGLIB 到项目的类路径中,从 Spring 3.2 开始,CGLIB 被重新打包并包含在
spring-core
JAR中. 换句话说,基于 CGLIB 的 AOP "开箱即用",JDK 动态代理也是如此.
CGLIB 代理和动态代理之间几乎没有性能差异. 从 Spring 1.0 开始,动态代理略快一些. 但是,这可能会在未来发生变化. 在这种情况下,性能不应该是决定性的考虑因素.
6.4.6. 使用 “Global” (全局)的通知者
通过将星号追加到拦截器名称上,所有与星号前面部分匹配的 bean 名称的通知者都将添加到通知者链中. 如果需要添加一组标准的全局 ( "global") 通知者,这可能会派上用场. 以下示例定义了两个全局的通知者程序:
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="service"/>
<property name="interceptorNames">
<list>
<value>global*</value>
</list>
</property>
</bean>
<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>
6.5. 简明的代理定义
特别是在定义事务代理时,最终可能会定义了许多类似的代理. 使用父级和子级 bean 定义以及内部 bean 定义可以使代理定义变得更简洁和更简明.
首先为代理创建一个父级的、模板的 bean 定义:
<bean id="txProxyTemplate" abstract="true"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
它本身是永远不会被实例化的,因此它实际上可能是不完整的. 然后,每个需要创建的代理都是只是一个子级的 bean 定义,它将代理的目标包装为内部 bean 定义,因为目标永远不会单独使用. 以下示例显示了这样的子 bean:
<bean id="myService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MyServiceImpl">
</bean>
</property>
</bean>
您可以覆盖父模板中的属性. 在以下示例中,事务传播设置如下:
<bean id="mySpecialService" parent="txProxyTemplate">
<property name="target">
<bean class="org.springframework.samples.MySpecialServiceImpl">
</bean>
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
<prop key="store*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
请注意,在上面的例子中,通过使用 abstract
属性显式地将父级的 bean 定义标记为抽象的 (abstract) ,如前所述,这样它就不会被实例化. 应用程序上下文 (但不是简单的 bean 工厂) 将默认提前实例化所有的单例.
因此,重要的是 (至少对于单例 bean) ,如果有一个 (父级) bean 定义,只打算将它用作模板,而这个定义指定一个类,必须确保将抽象 (abstract
) 属性设置为 true
, 否则应用程序上下文将实际尝试提前实例化它.
6.6. 使用 ProxyFactory
编程创建AOP代理
使用 Spring 以编程的方式创建 AOP 代理是很容易的. 这样允许在不依赖于 Spring IoC 的情况下使用 Spring AOP.
目标对象实现的接口将自动代理. 下面的代码显示了使用一个拦截器和一个通知者创建目标对象的代理的过程:
ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();
val factory = ProxyFactory(myBusinessInterfaceImpl)
factory.addAdvice(myMethodInterceptor)
factory.addAdvisor(myAdvisor)
val tb = factory.proxy as MyBusinessInterface
第一步是构建一个类型为 org.springframework.aop.framework.ProxyFactory
的对象. 可以使用目标对象创建此对象. 如前面的示例所示,或者在指定的接口中进行代理而不是构造器.
开发者可以添加通知 (使用拦截器作为一种专用的通知) 和/或通知者,并在 ProxyFactory
的生命周期中进行操作. 如果添加 IntroductionInterceptionAroundAdvisor
,则可以使代理实现其他接口.
ProxyFactory 上还有一些便捷的方法 (从 AdvisedSupport
类继承的) ,允许开发者添加其他通知类型,例如前置和异常通知. AdvisedSupport
是 ProxyFactory
和 ProxyFactoryBean
的超类
将 AOP 代理创建与 IoC 框架集成是多数应用程序的最佳实践,因此强烈建议从 Java 代码中外部配置使用 AOP |
6.7. 处理被通知对象
org.springframework.aop.framework.Advised
接口对它们进行操作. 任何 AOP 代理都可以转换到这个接口,无论它实现了哪个接口. 此接口包括以下方法:
Advisor[] getAdvisors();
void addAdvice(Advice advice) throws AopConfigException;
void addAdvice(int pos, Advice advice) throws AopConfigException;
void addAdvisor(Advisor advisor) throws AopConfigException;
void addAdvisor(int pos, Advisor advisor) throws AopConfigException;
int indexOf(Advisor advisor);
boolean removeAdvisor(Advisor advisor) throws AopConfigException;
void removeAdvisor(int index) throws AopConfigException;
boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;
boolean isFrozen();
fun getAdvisors(): Array<Advisor>
@Throws(AopConfigException::class)
fun addAdvice(advice: Advice)
@Throws(AopConfigException::class)
fun addAdvice(pos: Int, advice: Advice)
@Throws(AopConfigException::class)
fun addAdvisor(advisor: Advisor)
@Throws(AopConfigException::class)
fun addAdvisor(pos: Int, advisor: Advisor)
fun indexOf(advisor: Advisor): Int
@Throws(AopConfigException::class)
fun removeAdvisor(advisor: Advisor): Boolean
@Throws(AopConfigException::class)
fun removeAdvisor(index: Int)
@Throws(AopConfigException::class)
fun replaceAdvisor(a: Advisor, b: Advisor): Boolean
fun isFrozen(): Boolean
getAdvisors()
方法将返回已添加到工厂中的每个 Advisor、拦截器或其他通知类型的通知者. 如果添加了 Advisor
,那么这个索引中的返回的通知者将是添加的对象.
如果添加了拦截器或其他通知类型,那么 Spring 将在通知者中将一个总是返回 true
的切点封装. 因此,如果添加了 MethodInterceptor
,则返回的通知者将是 DefaultPointcutAdvisor
返回来的 MethodInterceptor
和与所有类和方法匹配的切点.
addAdvisor()
方法可用于添加任意的 Advisor
. 通常,持有切点和通知的通知者是通用的 DefaultPointcutAdvisor
类,它可以用于任意通知或切点 (但不能用于引入) .
默认情况下, 即使已经创建了代理,也可以添加或删除通知者或拦截器. 唯一的限制是无法添加或删除引入通知者,因为来自工厂的现有代理将不会展示接口的变化. (开发者可以从工厂获取新的代理,以避免这种问题) .
将 AOP 代理转换为通知接口并检查和操作其 Advisor
的简单示例 :
Advised advised = (Advised) myObject;
Advisor[] advisors = advised.getAdvisors();
int oldAdvisorCount = advisors.length;
System.out.println(oldAdvisorCount + " advisors");
// Add an advice like an interceptor without a pointcut
// Will match all proxied methods
// Can use for interceptors, before, after returning or throws advice
advised.addAdvice(new DebugInterceptor());
// Add selective advice using a pointcut
advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));
assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);
val advised = myObject as Advised
val advisors = advised.advisors
val oldAdvisorCount = advisors.size
println("$oldAdvisorCount advisors")
// Add an advice like an interceptor without a pointcut
// Will match all proxied methods
// Can use for interceptors, before, after returning or throws advice
advised.addAdvice(DebugInterceptor())
// Add selective advice using a pointcut
advised.addAdvisor(DefaultPointcutAdvisor(mySpecialPointcut, myAdvice))
assertEquals("Added two advisors", oldAdvisorCount + 2, advised.advisors.size)
在生产中修改业务对象的通知是否可取(没有双关语) 是值得怀疑的,尽管它是合法的使用案例. 但是,它可能在开发中非常有用 (例如,在测试中) . 有时发现能够以拦截器或其他通知的形式添加测试代码也非常有用, 可以在需要测试的方法调用中获取. (例如,通知可以进入为该方法创建的事务中; 例如,在标记要回滚的事务之前运行 sql 以检查数据库是否已正确更新) . |
根据您创建代理的方式,通常可以设置 frozen
标志. 在这种情况下,通知的 isFrozen()
方法将返回 true
,任何通过添加或删除修改通知的尝试都将导致 AopConfigException
异常. 在某些情况下冻结通知的对象状态的功能很有用 (例如,防止调用代码删除安全拦截器) . 如果已知的运行时通知不需要修改的话,它也可以在 Spring 1.1 中使用以获得最好的优化.
6.8. 使用自动代理功能
到目前为止,上面的章节已经介绍了使用 ProxyFactoryBean
或类似的工厂 bean 显式地创建 AOP 代理.
Spring 还支持使用 “auto-proxy” (自动代理) 的 bean 定义, 允许自动代理选择 bean 定义.这是建立在 Spring 的 Bean 后置处理器基础上的,它允许修改任何 bean 定义作为容器加载.
在这个模式下,可以在 XML bean 定义文件中设置一些特殊的 bean 定义,用来配置基础的自动代理. 这允许开发者只需声明符合自动代理的目标即可,开发者无需使用 ProxyFactoryBean
.
有两种方法可以做到这一点:
-
使用在当前上下文中引用特定 bean 的自动代理创建器
-
自动代理创建的一个特例值得单独考虑: 由源代码级别的元数据属性驱动的自动代理创建.
6.8.1. 自动代理 bean 的定义
本节介绍 org.springframework.aop.framework.autoproxy
包提供的自动代理创建器.
BeanNameAutoProxyCreator
BeanNameAutoProxyCreator
类是一个 BeanPostProcessor
的实现,它会自动为具有匹配文本值或通配符的名称的 bean 创建 AOP 代理. 以下示例显示如何创建 BeanNameAutoProxyCreator
的 bean :
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames" value="jdk*,onlyJdk"/>
<property name="interceptorNames">
<list>
<value>myInterceptor</value>
</list>
</property>
</bean>
与 ProxyFactoryBean
一样,它拥有 interceptorNames
属性而不是持有拦截器列表,以便为原型通知者提供正确的行为. 通知者和任意的通知类型都可命名为 “interceptors”.
与普通的自动代理一样,使用 BeanNameAutoProxyCreator
的主要目的是能将相同的配置同时或共享地应用于多个对象,此时配置是最少的. 将声明性事务应用于多个对象是很普遍的例子.
在上例中,名称匹配的 Bean 定义 (例如 jdkMyBean
和 onlyJdk
) 是带有目标类的、普通的、老式的 bean 定义. AOP 代理由 BeanNameAutoProxyCreator
自动创建. 相同的通知也适用于所有匹配到的 bean. 注意,如果使用通知着 (而不是上述示例中的拦截器) ,那么切点可能随bean的不同用处而变化.
DefaultAdvisorAutoProxyCreator
DefaultAdvisorAutoProxyCreator
是另一个更通用、功能更强大的自动代理创建器. 它会在当前的上下文中自动用于符合条件的通知者,而无需在自动代理通知者的 bean 定义中包含特定的 bean 名称. 它具有 BeanNameAutoProxyCreator
相同的配置,以及避免重复定义的有点.
使用此机制涉及:
-
指定
DefaultAdvisorAutoProxyCreator
bean定义 -
在相同或相关上下文中指定任意数量的通知者. 注意,这里必须是通知者,而不是拦截器或其他通知类型. 这种约束是必需的,因为必须引入对切点的评估, 以检查每个通知是否符合候选 bean 定义的要求.
DefaultAdvisorAutoProxyCreator
将自动评估包含在每个通知者中的切点,以查看它是否适用于每个业务对象 (如示例中的 businessObject1
和 businessObject2
) 的通知 (如果有的话) .
这意味着可以将任意数量的通知者自动用于每个业务对象. 如果任意通知者都没有一个切点与业务对象中的任何方法匹配,那么对象将不会被代理. 当为新的业务对象添加了 bean 定义时,如果需要这些对象都将被自动代理.
一般来说,自动代理具有使调用方或依赖无法获取 un-advised 对象的优点. 在这个 ApplicationContext
调用 getBean("businessObject1")
方法将返回 AOP 代理, 而不是目标业务对象. (前面显示的 "inner bean" 语义也提供了这种好处) .
以下示例创建一个 DefaultAdvisorAutoProxyCreator
bean 以及本节中讨论的其他元素:
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
<property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>
<bean id="customAdvisor" class="com.mycompany.MyAdvisor"/>
<bean id="businessObject1" class="com.mycompany.BusinessObject1">
<!-- Properties omitted -->
</bean>
<bean id="businessObject2" class="com.mycompany.BusinessObject2"/>
如果希望对多个业务对象适用相同的通知,那么 DefaultAdvisorAutoProxyCreator
类会显得非常有用. 一旦基础架构已定义,就可以简单地添加新的业务对象, 而不必再设置特定的代理配置. 还可以很容易地删除其他切面,例如跟踪或性能监视切面 , 这样对配置的更改最小.
DefaultAdvisorAutoProxyCreator
提供对过滤器 (filtering) 的支持 (使用命名约定,以便只评估某些通知者,允许在同一工厂中使用多个不同配置的 AdvisorAutoProxyCreators
) 和排序. 通知者可以实现 org.springframework.core.Ordered
接口,以确保正确的排序,如果需要排序的话.
上面的例子中使用的 TransactionAttributeSourceAdvisor
类具有具有可配置的排序值, 默认的设置是无序的.
6.9. 使用 TargetSource
实现
Spring 提供了 TargetSource
概念,定义在 org.springframework.aop.TargetSource
接口中. 这个接口用于返回目标对象实现的连接点. 每次AOP代理处理方法调用时,都会要求目标实例进行 TargetSource
实现.
使用 Spring AOP 的开发者通常无需直接使用 TargetSource
,一般都是提供了支持池,热部署和用于其他复杂目标的强大手段. 例如,池化的 TargetSource
可以为每个调用返回一个不同的目标实例,并使用一个池来管理实例.
如果未指定 TargetSource
,则使用默认实现来包装本地对象. 每次调用都会返回相同的目标 (正如您所期望的那样) .
将下来介绍 Spring 提供的标准目标源 (target sources) ,以及如何使用.
当使用自定义的 target source,目标通常需要配置成原型而不是单例的 bean 定义. 这允许 Spring 按需时创建新的目标实例 |
6.9.1. Hot-swappable Target Sources
org.springframework.aop.target.HotSwappableTargetSource
的存在是为了允许切换 AOP 代理的目标.
改变目标源的目标会立即有效,HotSwappableTargetSource
是线程安全的.
可以通过 HotSwappableTargetSource
上的 swap()
方法更改目标,如下所示:
HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);
val swapper = beanFactory.getBean("swapper") as HotSwappableTargetSource
val oldTarget = swapper.swap(newTarget)
以下示例显示了所需的 XML 定义:
<bean id="initialTarget" class="mycompany.OldTarget"/>
<bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource">
<constructor-arg ref="initialTarget"/>
</bean>
<bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource" ref="swapper"/>
</bean>
前面的 swap()
方法改变了 swappable bean 的目标. 持有对该 bean 引用的客户端将不会察觉到目标的更改,但会马上开始处理新目标.
虽然这个例子没有添加任何通知 , 也没有必要添加通知来使用 TargetSource
,当然任意的 TargetSource
都可以和任意的通知一起使用.
6.9.2. 创建目标源池
使用池化的目标源为无状态会话 EJB 提供了类似的编程模型,它维护了相同实例池,调用方法将会释放池中的对象.
Spring 池和 SLSB 池有一个关键的区别是: Spring 池可以应用于任意 POJO. 和 Spring 一样,这个服务可以以非侵入的方式应用.
Spring 为 Commons Pool 2.2,提供了开箱即用的支持,它提供了一个相当高效的池化实现. 开发者需要在应用程序的类路径上添加 commons-pool
的 jar 包来启用此功能. 也可以对 org.springframework.aop.target.AbstractPoolingTargetSource
进行子类化来支持任意其它池化的 API.
Commons Pool 1.5+ 的版本也是支持的,但是在 Spring Framework 4.2 已经过时了. |
以下清单显示了一个示例配置:
<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject"
scope="prototype">
... properties omitted
</bean>
<bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPool2TargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
<property name="maxSize" value="25"/>
</bean>
<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetSource" ref="poolTargetSource"/>
<property name="interceptorNames" value="myInterceptor"/>
</bean>
请注意,目标对象 ( 例如示例中的 businessObjectTarget
)必须是原型的. 这允许 PoolingTargetSource
能够实现按需创建目标的新实例,用于扩展池. 请参阅 AbstractPoolingTargetSource
以及用于其属性信息的具体子类. maxSize
是最基本的,并且始终保证存在.
在这种情况下, myInterceptor
是需要在相同的 IoC 上下文中定义的拦截器的名称. 但是,无需指定拦截器来使用池. 如果只希望使用池化功能而不需要通知,那么可以不设置 interceptorNames
属性.
可以对 Spring 进行配置,以便将任意池对象强制转换到 org.springframework.aop.target.PoolingConfig
接口,从而引入暴露的,有关池的配置和当前大小的信息. 此时需要像下面这样定义通知者:
<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="poolTargetSource"/>
<property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>
这个通知者是通过在 AbstractPoolingTargetSource
类上调用一个方便的方法获得的,因此可以调用 MethodInvokingFactoryBean
. 通知者的名字 (在这里是 poolConfigAdvisor
) 必须包含在拦截器名字的列表中,ProxyFactoryBean
暴露了池化的对象.
PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());
val conf = beanFactory.getBean("businessObject") as PoolingConfig
println("Max pool size is " + conf.maxSize)
池化的无状态服务对象一般是没有必要的. 一般这种选择不是默认的,因为大多数无状态的对象本质上是线程安全的,并且如果资源是缓存的话,其实例池化是有问题的. |
使用自动代理可以创建更简单的池,可以设置任何自动代理创建者使用的 TargetSource
.
6.9.3. 原型目标源
设置 “prototype” 目标源与合并 TargetSource
类似. 在这种情况下,每个方法调用都会创建一个新的目标实例. 尽管在现代 JVM 中创建新对象的成本并不高, 但是连接新对象 (满足其 IoC 依赖性) 的成本可能会更高. 因此,如果没有很好的理由,不应该使用这种方法.
为此, 可以修改上面显示的 poolTargetSource
定义,如下所示 (为清晰起见,我们还更改了名称) :
<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
<property name="targetBeanName" ref="businessObjectTarget"/>
</bean>
唯一的属性是目标 bean 的名称. 在 TargetSource
实现中使用继承来确保一致的命名. 与池化目标源一样,目标 bean 必须是原型 bean 定义.
6.9.4. ThreadLocal
的目标源
如果您需要为每个传入请求创建一个对象 (每个线程) ,ThreadLocal
目标源很有用. ThreadLocal
的概念提供了一个 JDK 范围的工具,用于透明地将资源与线程存储在一起. 设置 ThreadLocalTargetSource
几乎与其他类型的目标源设置一样. 如下例所示:
<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
<property name="targetBeanName" value="businessObjectTarget"/>
</bean>
当在多线程和多类加载器环境中错误地使用它们时,ThreadLocal 会带来严重的问题 (可能导致内存泄漏) . 您应该始终考虑将 threadlocal 包装在其他类中,并且永远不要直接使用 ThreadLocal 本身 (除了在包装类中) . 另外,应该始终记得正确设置和取消设置 (后者只需调用 ThreadLocal.set(null) 方法) 线程的本地资源.
在任何情况下都应该写取消设置,如果不取消将会出问题. Spring 的 ThreadLocal 支持此设置并且应当被考虑支持使用 ThreadLocal 而不是手动操作代码.
|
6.10. 定义新的通知类型
Spring AOP 被设计为可扩展的. 尽管拦截器实施机制目前只在内部使用,但除了围绕通知拥有开箱即用的拦截器之外,还可以支持任意的通知类型,例如前置、异常和后置返回的通知.
org.springframework.aop.framework.adapter
包是一个 SPI 包,允许在不改变核心框架的情况下添加新的自定义通知类型. 自定义通知类型的唯一约束是它必须实现 org.aopalliance.aop.Advice
标识接口.
org.springframework.aop.framework.adapter
javadoc 获取更多信息.
7. Null-safety
尽管 Java 不允许使用类型系统来表示 null 安全,但 Spring 框架现在加入了 org.springframework.lang
包,并提供了以下注解,用来声明 API 和字段的 null 特性:
-
@Nullable
: 其中特定参数、返回值或字段可以为null
. -
@NonNull
: 注解指示特定参数,返回值或字段不能为null
(对于@NonNullApi
和@NonNullFields
适用的参数和返回值不需要) -
@NonNullApi
: 在包级别将非null
声明为参数和返回值的默认行为 -
@NonNullFields
: 在包级别将非null
声明为字段的默认行为
Spring 框架用到这些注解,但它们也可以用于任意基于 Spring 的 Java 项目中,用来声明 null 安全的 API 和可选的 null 安全字段. null 特性对于泛型类型参数、varargs 参数和数组元素是不受支持的, 但可能会包含在即将发布的版本中,请参阅 SPR-15942 以获取最新信息. 在 Spring 框架发行版(包括小版本) 之间. 可以将 fine-tuned 声明为 null. 在方法体内判定类型是否为 null 超出了它的能力范围.
像 Reactor 或者 Spring Data 这样的库也用到了 null 安全的 API. |
7.1. 用例
Spring API 除了为 null 提供了显式的声明外,IDE 还可以使用这些注解(如 IDEA 或 Eclipse) 为与 null 安全相关的 Java 开发人员提供有用的警告,用于避免运行时出现 NullPointerException
.
在 Kotlin 项目中也使用到 Spring API 的 null 安全特性,因为 Kotlin 本身支持 null-safety. Kotlin 支持文档提供了更多的详细信息.
7.2. JSR-305 元注解
Spring 注解是被 JSR 305 注解的元注解(一个潜在的但广泛使用的JSR) . JSR 305 元注解允许工具供应商(如 IDEA 或 Kotlin) 以通用方式提供安全支持,而无需为 Spring 注解提供硬编码支持.
为了使用 Spring 的 null 安全 API,其实不需要也不建议在项目类路径中添加 JSR 305 依赖. 而只有在其代码库中使用null安全标识的项目(例如 Spring 基本库) 即可,应该将 com.google.code.findbugs:jsr305:3.0.2
添加到 Gradle 的 compileOnly
或 Maven 的 provided
配置中,就可以避免编译警告了.
8. 数据缓冲区和编解码器
Java NIO 虽然提供了 ByteBuffer
,但许多库在顶层构建自己的字节缓冲区 API,尤其是对于重用缓冲区和/或使用直接缓冲区有利于性能的网络操作. 例如, Netty 具有 ByteBuf
层次结构,
Undertow 使用 XNIO,Jetty 使用带有回调的池化字节缓冲区,等等. spring-core
模块提供了一组抽象来处理各种字节缓冲 API,如下所示:
-
DataBufferFactory 创建抽象数据缓冲区.
-
DataBuffer DataBuffer 表示可以pooled的字节缓冲区.
-
DataBufferUtils 为数据缓冲区提供实用程序方法.
-
Codecs 将数据缓冲流解码或编码为更高级别的对象.
8.1. DataBufferFactory
DataBufferFactory
以两种方式之一创建数据缓冲区:
-
分配新的数据缓冲区,可选择预先指定容量(如果已知) ,即使
DataBuffer
的实现可以按需增长和缩小,这也更有效. -
包装现有的
byte[]
或java.nio.ByteBuffer
,并使用DataBuffer
实现来修饰给定的数据,且不涉及分配.
请注意,WebFlux 应用程序不直接创建 DataBufferFactory
,而是通过 ServerHttpResponse
或客户端的 ClientHttpRequest
访问它. 工厂类型取决于底层客户端或服务器,例如 Reactor Netty 的 NettyDataBufferFactory
,其他的 DefaultDataBufferFactory
.
8.2. DataBuffer
DataBuffer
接口提供与 java.nio.ByteBuffer
类似的操作,但也带来了一些额外的好处,其中一些受 Netty ByteBuf
的启发. 以下是部分好处清单:
-
可以独立的读写,即不需要调用
flip()
来在读写之间交替. -
与
java.lang.StringBuilder
一样,按需扩展容量. -
通过 PooledDataBuffer 缓冲区和引用计数.
-
以
java.nio.ByteBuffer
,InputStream
或OutputStream
的形式查看缓冲区. -
确定给定字节的索引或最后一个索引.
8.3. PooledDataBuffer
正如 Javadoc ByteBuffer 中所解释的,字节缓冲区可以是直接缓冲区,也可以是非直接缓冲区. 直接缓冲区可以驻留在 Java 堆之外,这样就无需复制本地 I/O 操作. 这使得直接缓冲区对于通过套接字接收和发送数据特别有用,但是创建和释放它们也更加昂贵,这导致了池化缓冲区的想法.
PooledDataBuffer
是 DataBuffer
的扩展,它有助于引用计数,这对于字节缓冲池是必不可少的. 它是如何工作的? 当分配 PooledDataBuffer
时,引用计数为 1. 调用 retain()
递增计数,而对 release()
的调用则递减计数. 只要计数大于 0,就保证缓冲区不被释放. 当计数减少到 0 时,可以释放池化缓冲区,这实际上可能意味着缓冲区的保留内存返回到内存池.
请注意,不是直接对 PooledDataBuffer
进行操作,在大多数情况下,最好使用 DataBufferUtils
中的方法, 只有当它是 PooledDataBuffer
的实例时才应用 release
或 retain
到 DataBuffer
.
8.4. DataBufferUtils
DataBufferUtils
提供了许多用于操作数据缓冲区的实用方法:
-
将数据缓冲区流加入单个缓冲区中,可能只有零拷贝,例如 通过复合缓冲区,如果底层字节缓冲区 API 支持.
-
将
InputStream
或 NIOChannel
转化为Flux<DataBuffer>
, 反之亦然, 将Publisher<DataBuffer>
转化为OutputStream
或 NIOChannel
. -
如果缓冲区是
PooledDataBuffer
的实例,则释放或保留DataBuffer
的方法. -
从字节流中跳过或取出,直到特定的字节数.
8.5. Codecs
org.springframework.core.codec
包提供以下策略接口:
-
Encoder
将Publisher<T>
编码为数据缓冲区流. -
Decoder
将Publisher<DataBuffer>
为更高级别的对象流.
spring-core
模块提供 byte[]
, ByteBuffer
, DataBuffer
, Resource
, 和 String
编码器和解码器实现. spring-web
模块增加了 Jackson JSON, Jackson Smile, JAXB2, Protocol Buffers 和其他编码器和解码器. 请参阅 WebFlux 部分中的Codecs.
8.6. 使用 DataBuffer
使用数据缓冲区时,必须特别注意确保缓冲区被释放,因为它们可能被pooled. 我们将使用编解码器来说明它是如何工作的,但概念更普遍适用. 让我们看看内部编解码器必须在内部管理数据缓冲区.
Decoder
是在创建更高级别对象之前读取输入数据缓冲区的最后一个,因此必须按如下方式释放它们:
-
如果
Decoder
只是读取每个输入缓冲区并准备立即释放它,它可以通过DataBufferUtils.release(dataBuffer)
来实现. -
如果
Decoder
使用Flux
或Mono
运算符(如flatMap
,reduce
等) 在内部预取和缓存数据项,或者正在使用诸如filter
,skip
和其他省略项的运算符, 则必须将doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release)
添加到组合链中,以确保在丢弃之前释放这些缓冲区,可能还会导致错误或取消信号. -
如果
Decoder
以任何其他方式保持一个或多个数据缓冲区,则必须确保在完全读取时释放它们,或者在读取和释放高速缓存数据缓冲区之前发生错误或取消信号.
请注意, DataBufferUtils#join
提供了一种安全有效的方法,可将数据缓冲区流聚合到单个数据缓冲区中. 同样,skipUntilByteCount
和 takeUntilByteCount
是解码器使用的其他安全方法.
Encoder
分配其他人必须读取(和释放) 的数据缓冲区. 所以 Encoder
没什么可做的. 但是,如果在使用数据填充缓冲区时发生序列化错误,则 Encoder
必须注意释放数据缓冲区. 例如:
DataBuffer buffer = factory.allocateBuffer();
boolean release = true;
try {
// serialize and populate buffer..
release = false;
}
finally {
if (release) {
DataBufferUtils.release(buffer);
}
}
return buffer;
val buffer = factory.allocateBuffer()
var release = true
try {
// serialize and populate buffer..
release = false
} finally {
if (release) {
DataBufferUtils.release(buffer)
}
}
return buffer
Encoder
的使用者负责释放它接收的数据缓冲区. 在WebFlux应用程序中,Encoder
的输出用于写入 HTTP 服务器响应或客户端 HTTP 请求, 在这种情况下,释放数据缓冲区是代码写入服务器响应或客户端的责任. 请求.
请注意,在 Netty 上运行时,可以使用调试选项来 排除缓冲区泄漏.
9. Logging
从 Spring Framework 5.0 开始, Spring 在 spring-jcl
模块中附带了自己的 Commons Logging 实现.该实现检查 Log4j 2.x 是否存在在类路径中的 API 和 SLF4J 1.7 API, 并使用其中的第一个作为日志记录的实现, 回溯到 Java 平台的核心日志记录工具 (也包括
如果 Log4j 2.x 和 SLF4J 都不可用, 则称为 JUL 或 java.util.logging
.
将 Log4j 2.x 或 Logback (或其他 SLF4J 提供程序) 放在您的类路径中, 无需任何额外操作, 并让框架自动适应您的选择. 有关更多信息, 请参见 Spring Boot Logging Reference Documentation.
Spring 的 Commons Logging 仅用于基础结构的日志记录,核心框架的扩展为目的. 对于应用程序代码中的日志记录需求, 建议直接使用 Log4j 2.x, SLF4J 或 JUL. |
可以通过 org.apache.commons.logging.LogFactory
检索 Log
实现. 如下:
public class MyBean {
private final Log log = LogFactory.getLog(getClass());
// ...
}
class MyBean {
private val log = LogFactory.getLog(javaClass)
// ...
}
10. 附录
10.1. XML Schemas
附录的这一部分列出了与核心容器相关的 XML Schemas.
10.1.1. util
Schema
顾名思义,util
标签处理常见的实用程序配置问题,例如配置集合,引用常量等等. 要使用 util
schema 中的标记,您需要在 Spring XML 配置文件的顶部有以下声明. 下面代码段中的文本引用了正确的 schema,以便您可以使用 util
命名空间中的标记:
<?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:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
<!-- bean definitions here -->
</beans>
使用 <util:constant/>
考虑下面 bean 的定义:
<bean id="..." class="...">
<property name="isolation">
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
</property>
</bean>
上述配置使用 Spring FactoryBean
实现( FieldRetrievingFactoryBean
) ,将 bean 上的隔离属性的值设置为 java.sql.Connection.TRANSACTION_SERIALIZABLE
常量值.这看起来很不错,但它是一个稍微冗长的,并且暴露 Spring 的内部管道给最终用户(这是不必要的) .
以下基于 XML Schema 的版本更简洁,清楚地表达了开发人员的意图("注入此常量值") ,并且它读得更好:
<bean id="..." class="...">
<property name="isolation">
<util:constant static-field="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</property>
</bean>
从字段值设置 Bean 属性或构造函数参数
FieldRetrievingFactoryBean
是一个 FactoryBean
用于检索静态或非静态字段值. 它通常用于检索 public
static
final
常量,然后可用于为另一个 bean 设置属性值或构造函数的参数.
以下示例显示了如何使用 staticField
属性暴露 static
字段:
<bean id="myField"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
<property name="staticField" value="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</bean>
还有一个便捷使用形式,其中 static
字段被指定为 bean 名称,如以下示例所示:
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>
这确实意味着 bean id
不再有任何选择(因此引用它的任何其他 bean 也必须使用这个更长的名称) , 但这种形式定义非常简洁,非常方便用作内部 bean ,因为不必为 bean 引用指定 id
,如下例所示:
<bean id="..." class="...">
<property name="isolation">
<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE"
class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
</property>
</bean>
您还可以访问另一个 bean 的非静态(实例) 字段,如 FieldRetrievingFactoryBean
类的 API 文档中所述
在 Spring 中,将枚举值作为属性或构造函数参数注入到 bean 中非常容易,因为您实际上并不需要做任何事情,也不知道 Spring 内部的任何内容(甚至包括 FieldRetrievingFactoryBean
相似的类) . 以下示例枚举显示了注入枚举值的容易程度:
public enum PersistenceContextType {
TRANSACTION,
EXTENDED
}
enum class PersistenceContextType {
TRANSACTION,
EXTENDED
}
现在考虑下面的 PersistenceContextType
类型的 setter 和相应的 bean 定义:
public class Client {
private PersistenceContextType persistenceContextType;
public void setPersistenceContextType(PersistenceContextType type) {
this.persistenceContextType = type;
}
}
class Client {
lateinit var persistenceContextType: PersistenceContextType
}
<bean class="example.Client">
<property name="persistenceContextType" value="TRANSACTION"/>
</bean>
使用 <util:property-path/>
请考虑以下示例:
<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>
<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<bean id="testBean.age" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
上述配置使用 Spring FactoryBean
实现(PropertyPathFactoryBean
) 创建名为 testBean.age
的 bean(类型为 int
) ,其值等于 testBean
bean 的 age
属性.
现在考虑以下示例,它添加了一个 <util:property-path/>
元素:
<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>
<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<util:property-path id="name" path="testBean.age"/>
<property-path/>
元素的 path
属性的值遵循 beanName.beanProperty
的形式. 在这种情况下,它会获取名为 testBean
的bean的 age
属性. 该 age
属性值是 10
.
使用 <util:property-path/>
设置Bean属性或构造函数参数
PropertyPathFactoryBean
是一个用于计算给定目标对象的属性路径的 FactoryBean
. 目标对象可以直接指定,也可以通过bean名称指定. 然后,您可以在另一个 bean 定义中将此值用作属性值或构造函数参数.
以下示例按名称显示了针对另一个 bean 使用的路径:
<!-- target bean to be referenced by name -->
<bean id="person" class="org.springframework.beans.TestBean" scope="prototype">
<property name="age" value="10"/>
<property name="spouse">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="11"/>
</bean>
</property>
</bean>
<!-- results in 11, which is the value of property 'spouse.age' of bean 'person' -->
<bean id="theAge"
class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
<property name="targetBeanName" value="person"/>
<property name="propertyPath" value="spouse.age"/>
</bean>
在以下示例中,path 被内部 bean 解析:
<!-- results in 12, which is the value of property 'age' of the inner bean -->
<bean id="theAge"
class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
<property name="targetObject">
<bean class="org.springframework.beans.TestBean">
<property name="age" value="12"/>
</bean>
</property>
<property name="propertyPath" value="age"/>
</bean>
这也是一个快捷的形式,其中 bean 名称是属性的路径.
<!-- results in 10, which is the value of property 'age' of bean 'person' -->
<bean id="person.age"
class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
此形式表示 bean 的名称中是没得选择的,对它的任何引用也必须使用相同的 id
,即它的路径. 当然,如果用作内部 bean,则根本不需要引用它.如下所示:
<bean id="..." class="...">
<property name="age">
<bean id="person.age"
class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
</property>
</bean>
结果类型可以在实际定义中具体设置. 对于大多数用例来说,这是不必要的,但对于某些用例来说是可以使用的. 有关此功能的更多信息,请参阅 javadoc.
使用 <util:properties/>
请考虑以下示例:
<!-- creates a java.util.Properties instance with values loaded from the supplied location -->
<bean id="jdbcConfiguration" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="location" value="classpath:com/foo/jdbc-production.properties"/>
</bean>
上述配置使用 Spring FactoryBean
实现(PropertiesFactoryBean
) 来实例化一个 java.util.Properties
实例,其中包含从提供的Resource
位置加载的值.
以下示例使用 util:properties
元素来进行更简洁的表示:
<!-- creates a java.util.Properties instance with values loaded from the supplied location -->
<util:properties id="jdbcConfiguration" location="classpath:com/foo/jdbc-production.properties"/>
使用 <util:list/>
请考虑以下示例:
<!-- creates a java.util.List instance with values loaded from the supplied 'sourceList' -->
<bean id="emails" class="org.springframework.beans.factory.config.ListFactoryBean">
<property name="sourceList">
<list>
<value>pechorin@hero.org</value>
<value>raskolnikov@slums.org</value>
<value>stavrogin@gov.org</value>
<value>porfiry@gov.org</value>
</list>
</property>
</bean>
上述配置使用 Spring FactoryBean
实现(ListFactoryBean
) 创建 java.util.List
实例,并使用从提供的 sourceList
获取的值对其进行初始化.
以下示例使用 <util:list/>
元素进行更简洁的表示:
<!-- creates a java.util.List instance with the supplied values -->
<util:list id="emails">
<value>pechorin@hero.org</value>
<value>raskolnikov@slums.org</value>
<value>stavrogin@gov.org</value>
<value>porfiry@gov.org</value>
</util:list>
您还可以使用 <util:list/>
元素上的 list-class
属性显式控制实例化和填充的 List
的确切类型. 例如,如果我们确实需要实例化 java.util.LinkedList
,我们可以使用以下配置:
<util:list id="emails" list-class="java.util.LinkedList">
<value>jackshaftoe@vagabond.org</value>
<value>eliza@thinkingmanscrumpet.org</value>
<value>vanhoek@pirate.org</value>
<value>d'Arcachon@nemesis.org</value>
</util:list>
如果未提供 list-class
属性,则容器将选择 List
实现.
使用 <util:map/>
请考虑以下示例:
<!-- creates a java.util.Map instance with values loaded from the supplied 'sourceMap' -->
<bean id="emails" class="org.springframework.beans.factory.config.MapFactoryBean">
<property name="sourceMap">
<map>
<entry key="pechorin" value="pechorin@hero.org"/>
<entry key="raskolnikov" value="raskolnikov@slums.org"/>
<entry key="stavrogin" value="stavrogin@gov.org"/>
<entry key="porfiry" value="porfiry@gov.org"/>
</map>
</property>
</bean>
上述配置使用 Spring FactoryBean
实现(MapFactoryBean
) 创建一个 java.util.Map
实例,该实例使用从提供的 'sourceMap'
获取的键值对进行初始化.
以下示例使用 <util:map/>
元素进行更简洁的表示:
<!-- creates a java.util.Map instance with the supplied key-value pairs -->
<util:map id="emails">
<entry key="pechorin" value="pechorin@hero.org"/>
<entry key="raskolnikov" value="raskolnikov@slums.org"/>
<entry key="stavrogin" value="stavrogin@gov.org"/>
<entry key="porfiry" value="porfiry@gov.org"/>
</util:map>
您还可以使用 <util:map/>
元素上的 'map-class'
属性显式控制实例化和填充的 Map
的确切类型. 例如,如果我们真的需要实例化 java.util.TreeMap
,我们可以使用以下配置:
<util:map id="emails" map-class="java.util.TreeMap">
<entry key="pechorin" value="pechorin@hero.org"/>
<entry key="raskolnikov" value="raskolnikov@slums.org"/>
<entry key="stavrogin" value="stavrogin@gov.org"/>
<entry key="porfiry" value="porfiry@gov.org"/>
</util:map>
如果未提供 'map-class'
属性,则容器将选择 Map
实现.
使用 <util:set/>
请考虑以下示例:
<!-- creates a java.util.Set instance with values loaded from the supplied 'sourceSet' -->
<bean id="emails" class="org.springframework.beans.factory.config.SetFactoryBean">
<property name="sourceSet">
<set>
<value>pechorin@hero.org</value>
<value>raskolnikov@slums.org</value>
<value>stavrogin@gov.org</value>
<value>porfiry@gov.org</value>
</set>
</property>
</bean>
上述配置使用 Spring FactoryBean
实现( SetFactoryBean
) 创建一个 java.util.Set
实例,该实例使用从提供的 sourceSet
获取的值进行初始化.
以下示例使用 <util:set/>
元素进行更简洁的表示:
<!-- creates a java.util.Set instance with the supplied values -->
<util:set id="emails">
<value>pechorin@hero.org</value>
<value>raskolnikov@slums.org</value>
<value>stavrogin@gov.org</value>
<value>porfiry@gov.org</value>
</util:set>
您还可以使用 <util:set/>
元素上的 set-class
属性显式控制实例化和填充的 Set
的确切类型. 例如,如果我们确实需要实例化 java.util.TreeSet
,我们可以使用以下配置:
<util:set id="emails" set-class="java.util.TreeSet">
<value>pechorin@hero.org</value>
<value>raskolnikov@slums.org</value>
<value>stavrogin@gov.org</value>
<value>porfiry@gov.org</value>
</util:set>
如果未提供 set-class
属性,则容器将选择 Set
实现.
10.1.2. aop
Schema
aop
标签用于配置 Spring 中的所有 AOP,包括 Spring 自己的基于代理的 AOP 框架和 Spring 与 AspectJ AOP 框架的集成. 这些标签在为面向切面的编程一章中全面介绍.
为了完整性起见,要使用 aop
schema 中的标签,您需要在 Spring XML 配置文件的顶部有以下 xsd:
以下代码段中的文本引用了正确的 schema,以便您可以使用 AOP
命名空间中的标签.
<?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 definitions here -->
</beans>
10.1.3. context
Schema
context
标签处理与管道(plumbing) )有关的 ApplicationContext
配置- 也就是说,通常不是对最终用户很重要的 bean,而是在 Spring 中执行大量 "grunt" 工作的 bean.
例如 BeanfactoryPostProcessors
. 以下代码段引用了正确的 schema,以便您可以使用 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">
<!-- bean definitions here -->
</beans>
使用 <property-placeholder/>
这个元素用于替代 ${…}
的占位符,这些占位符是针对指定的属性文件( Spring 资源位置) 解析的. 此元素是一种便捷机制,可为您设置 PropertySourcesPlaceholderConfigurer
.
如果您需要更多地控制 PropertySourcesPlaceholderConfigurer
,您可以自己明确定义一个.
使用 <annotation-config/>
此元素激活 Spring 基础结构以检测bean类中的注解:
-
Spring 的
@Configuration
模式 -
@Autowired
/@Inject
和@Value
和@Lookup
-
JSR-250 的
@Resource
,@PostConstruct
and@PreDestroy
(if available) -
JAX-WS 的
@WebServiceRef
和 EJB 3 的@EJB
(if available) -
JPA’s
@PersistenceContext
和@PersistenceUnit
(if available) -
Spring 的
@EventListener
或者,您可以选择显式激活这些注解的各个 BeanPostProcessors
.
这个元素没有激活处理Spring的@Transactional 注解. 使用<tx:annotation-driven/> 来激活Spring的@Transactional注解.
enabled 来激活caching 注解
|
使用 <component-scan/>
此元素在 基于注解的容器配置 中进行了详细说明.
使用 <load-time-weaver/>
此元素在AspectJ 的加载时织入进行了详细说明.
使用 <spring-configured/>
此元素在 使用 Spring 中的 AspectJ 独立注入域对象中进行了详细说明.
使用 <mbean-export/>
此元素在 配置基于注解的 MBean 的导出 中进行了详细说明.
10.1.4. Beans Schema
最后,但并非最不重要的是,beans
schema标签.
这些都是相同的标签,已经在 Spring 框架中崭露头角. 此处不显示 bean 架构中各种标签的示例, 因为它们在依赖性和配置的细节(甚至在chapter整个章节) 中有相当全面的介绍.
请注意,您可以向 <bean/>
XML 定义添加零个或多个键值对.
如果有的话,使用这些额外的元数据完成的工作完全取决于您自己的自定义逻辑 (因此,如果您按照 XML Schema Authoring 的附录中所述编写自己的自定义元素自定义元素,通常只能使用它.
以下示例显示了周围 <bean/>
上下文中的 <meta/>
元素(请注意,没有任何逻辑可以解释它,元数据实际上是无用的) .
<?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="foo" class="x.y.Foo">
<meta key="cacheName" value="foo"/> (1)
<property name="name" value="Rick"/>
</bean>
</beans>
1 | 这是示例 meta 元素 |
在上面的示例中,您将假定有一些逻辑将使用 bean 定义,并通过提供的元数据设置一些缓存基础结构.
10.2. XML Schema 创建
从版本 2.0 开始,Spring 就为定义和配置 bean 的基本 Spring XML格式的可扩展性提供了一种机制. 本节介绍如何编写自己的自定义 XML bean 定义解析器并将这些解析器集成到 Spring IoC 容器中.
为了便于使用架构感知的XML编辑器编写配置文件,Spring 的可扩展XML配置机制基于 xml Schema. 如果您对 Spring 当前的 XML 配置扩展不熟悉,则应首先阅读标题为 XML Schemas 的附录.
要创建新的XML配置扩展:
下面是对每个步骤的描述.对于本例,我们将创建一个 XML 扩展(一个自定义 XML 元素) ,它允许我们以一种简单的方式配置 SimpleDateFormat
类型的对象(在 java.text
包中) . 当我们完成后,我们将能够定义类型 SimpleDateFormat
定义如下:
<myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>
(不要担心这个例子过于简单,后面还有很多的案例.第一个简单的案例的目的是完成基本步骤的调用)
10.2.1. 编写 Schema
创建一个用于 Spring 的 IoC 容器的 XML 配置扩展,首先要创建一个 XML Schema 来描述扩展. 对于我们的示例,我们使用以下 schema 来配置 SimpleDateFormat
对象:
<!-- myns.xsd (inside package org/springframework/samples/xml) -->
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.mycompany.example/schema/myns"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
targetNamespace="http://www.mycompany.example/schema/myns"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="http://www.springframework.org/schema/beans"/>
<xsd:element name="dateformat">
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="beans:identifiedType"> (1)
<xsd:attribute name="lenient" type="xsd:boolean"/>
<xsd:attribute name="pattern" type="xsd:string" use="required"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
</xsd:schema>
1 | (强调的行包含可识别的所有标签的扩展库(意味着它们具有 id 属性,将用作容器中的 bean 标识符) . 我们可以使用此属性,因为我们导入了 Spring 提供的 beans 命名空间. |
前面的 schema 允许我们使用 <myns:dateformat/>
元素直接在XML应用程序上下文文件中配置 SimpleDateFormat
对象,如以下示例所示:
<myns:dateformat id="dateFormat"
pattern="yyyy-MM-dd HH:mm"
lenient="true"/>
请注意,在我们创建基础结构类之后,前面的XML代码段与以下XML代码段基本相同:
<bean id="dateFormat" class="java.text.SimpleDateFormat">
<constructor-arg value="yyyy-HH-dd HH:mm"/>
<property name="lenient" value="true"/>
</bean>
前两个片段中的第二个在容器中创建一个 bean(由名称为 SimpleDateFormat
类型的 dateFormat
标识) ,并设置了几个属性.
基于 schema 创建的配置格式可以与带有 schema 感知的 XML 编辑器的 IDE 集成. 使用正确的创建模式,可以让用户再几个配置选择之间进行自由切换(其实说的就是 eclipse 编辑 XML 的多种视图) . |
10.2.2. 编写 NamespaceHandler
除了 schema 之外,我们需要一个 NamespaceHandler
来解析 Spring 在解析配置文件时遇到的这个特定命名空间的所有元素.对于此示例, NamespaceHandler
应该负责解析 myns:dateformat
元素.
NamespaceHandler
接口有三个方法:
-
init()
: 允许初始化NamespaceHandler
,在使用处理程序之前此方法将被 Spring 调用. -
BeanDefinition parse(Element, ParserContext)
: 当 Spring 遇到 top-level 元素(不嵌套在 bean 定义或其他命名空间中)时调用. 此方法可以注册 bean 定义本身和/或返回bean定义. -
BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext)
: 当 Spring 遇到不同命名空间的属性或嵌套元素时调用. 一个或多个 bean 定义的装饰将被使用, (例如) 与Spring支持的作用域一起使用.我们将首先写一个简单的例子,不使用装饰器,之后我们在一个更高级的例子中展示装饰.
尽管完全可以为整个命名空间编写自己的 NamespaceHandler
(从而提供分析命名空间中每个元素的代码) . 但通常情况下,Spring XML 配置文件中的每个顶级 XML 元素都会生成一个 bean 定义(在我们的例子中,
单个 <myns:dateformat/>
元素导致单个 SimpleDateFormat
定义) . Spring 具有许多支持此方案的便捷类.在本例中,我们将使用 NamespaceHandlerSupport
类:
public class MyNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser());
}
}
class MyNamespaceHandler : NamespaceHandlerSupport {
override fun init() {
registerBeanDefinitionParser("dateformat", SimpleDateFormatBeanDefinitionParser())
}
}
您可能会注意到此类中实际上并没有很多解析逻辑.实际上, NamespaceHandlerSupport
类具有内置的委托概念.它支持注册任何数量的 BeanDefinitionParser
实例,当它需要解析其命名空间中的元素时,它会委托给它们.
这种关注的清晰分离使 NamespaceHandler
能够处理对其命名空间中所有自定义元素的解析的编排,同时委托 BeanDefinitionParsers
执行XML解析的繁琐工作.这意味着每个 BeanDefinitionParser
只包含解析单个自定义元素的逻辑,我们可以在下一步中看到.
10.2.3. 使用 BeanDefinitionParser
如果 NamespaceHandler
遇到了已映射到特定bean定义分析器(在本例中为 dateformat
)的类型的 XML 元素,则将使用 BeanDefinitionParser
.换言之, BeanDefinitionParser
负责分析在架构中定义的一个不同的顶级 XML 元素.
在解析器中,我们将可以访问 XML 元素(以及它的子组件) 以便我们能够解析我们的自定义 XML 内容.如下面的示例所示:
public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { (1)
protected Class getBeanClass(Element element) {
return SimpleDateFormat.class; (2)
}
protected void doParse(Element element, BeanDefinitionBuilder bean) {
// this will never be null since the schema explicitly requires that a value be supplied
String pattern = element.getAttribute("pattern");
bean.addConstructorArgValue(pattern);
// this however is an optional property
String lenient = element.getAttribute("lenient");
if (StringUtils.hasText(lenient)) {
bean.addPropertyValue("lenient", Boolean.valueOf(lenient));
}
}
}
1 | 我们使用 Spring 提供的 AbstractSingleBeanDefinitionParser 来处理创建单个 BeanDefinition 的许多基本工作. |
2 | 我们提供 AbstractSingleBeanDefinitionParser 超类,其类型是我们的单个 BeanDefinition 所代表的类型. |
class SimpleDateFormatBeanDefinitionParser : AbstractSingleBeanDefinitionParser() { (1)
override fun getBeanClass(element: Element): Class<*>? { (2)
return SimpleDateFormat::class.java
}
override fun doParse(element: Element, bean: BeanDefinitionBuilder) {
// this will never be null since the schema explicitly requires that a value be supplied
val pattern = element.getAttribute("pattern")
bean.addConstructorArgValue(pattern)
// this however is an optional property
val lenient = element.getAttribute("lenient")
if (StringUtils.hasText(lenient)) {
bean.addPropertyValue("lenient", java.lang.Boolean.valueOf(lenient))
}
}
}
1 | 我们使用 Spring 提供的 AbstractSingleBeanDefinitionParser 来处理创建单个 BeanDefinition 的许多基本工作. |
2 | 我们提供 AbstractSingleBeanDefinitionParser 超类,其类型是我们的单个 BeanDefinition 所代表的类型. |
在这个简单的例子中,这就是我们需要做的一切.我们的单个 BeanDefinition
的创建由 AbstractSingleBeanDefinitionParser
超类处理,bean 定义的唯一标识符的提取和设置也是如此.
10.2.4. 注册处理器和 schema
编码完成.剩下要做的就是让 Spring XML 解析基础架构了解我们的自定义元素. 我们通过在两个专用属性文件中注册我们的自定义 namespaceHandler
和自定义 XSD 文件来实现.这些属性文件都放在应用程序的 META-INF
目录中.例如,可以与JAR文件中的二进制类一起分发.Spring XML 解析基础结构将通过使用这些特殊的属性文件来自动获取新的扩展,其格式将在接下来的两节中详细介绍.
编写 META-INF/spring.handlers
名为 spring.handlers
的属性文件包含 XML Schema URI 到命名空间处理程序类的映射.对于我们的示例,我们需要编写以下内容:
http\://www.mycompany.example/schema/myns=org.springframework.samples.xml.MyNamespaceHandler
(:
字符是 Java 属性格式的有效分隔符,因此 :
URI 中的字符需要使用反斜杠进行转义.)
键值对的第一部分(key)是与自定义命名空间扩展关联的URI,需要与自定义 XSD schema 中指定的 targetNamespace
属性的值完全匹配
编写 'META-INF/spring.schemas'
称为 spring.schemas
的属性文件包含 xml schema 位置(与 xml 文件中的 schema 声明一起使用,将 schema 用作 xsi:schemaLocation
属性的一部分) 到类路径资源的映射.
这个文件需要阻止 Spring 使用绝对的默认的 EntityResolver
及要求网络访问来接收 schema 文件.如果在此属性文件中指定映射,Spring 将在类路径中搜索 schema(在本例中为 org.springframework.samples.xml
包中的 myns.xsd
) . 以下代码段显示了我们需要为自定义schema添加的行:
http\://www.mycompany.example/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd
(请记住: 必须转义 :
字符.)
建议您在类路径上的 NamespaceHandler
和 BeanDefinitionParser
类旁边部署XSD文件(或多个文件) .
10.2.5. 在 Spring XML 配置中使用自定义扩展
使用您自己已经实现的自定义扩展,与使用 Spring 提供的 "自定义" 扩展是没有区别的.在下面的示例中,可以使用 Spring XML 配置文件,以前的步骤开发自定义的 <dateformat/>
元素:
<?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:myns="http://www.mycompany.example/schema/myns"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.mycompany.example/schema/myns http://www.mycompany.com/schema/myns/myns.xsd">
<!-- as a top-level bean -->
<myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/> (1)
<bean id="jobDetailTemplate" abstract="true">
<property name="dateFormat">
<!-- as an inner bean -->
<myns:dateformat pattern="HH:mm MM-dd-yyyy"/>
</property>
</bean>
</beans>
1 | 我们自定义的 bean |
10.2.6. 更详细的例子
本节介绍自定义 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:foo="http://www.foo.example/schema/component"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.foo.example/schema/component http://www.foo.example/schema/component/component.xsd">
<foo:component id="bionic-family" name="Bionic-1">
<foo:component name="Mother-1">
<foo:component name="Karate-1"/>
<foo:component name="Sport-1"/>
</foo:component>
<foo:component name="Rock-1"/>
</foo:component>
</beans>
上述配置实际上嵌套了彼此之间的自定义扩展,由上面的 <foo:component/>
元素实际配置的类是组件类(直接显示在下面).请注意,Component
类如何不暴露 Component
属性的 setter 方法. 这使得使用 setter 注入为 components
类配置bean定义变得困难(或者说是不可能的) . 以下清单显示了 Component
类:
public class Component {
private String name;
private List<Component> components = new ArrayList<Component> ();
// mmm, there is no setter method for the 'components'
public void addComponent(Component component) {
this.components.add(component);
}
public List<Component> getComponents() {
return components;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Component {
var name: String? = null
private val components = ArrayList<Component>()
// mmm, there is no setter method for the 'components'
fun addComponent(component: Component) {
this.components.add(component)
}
fun getComponents(): List<Component> {
return components
}
}
此问题的典型解决方案是创建一个自定义 FactoryBean
,用于暴露 components
属性的 setter 属性.以下清单显示了这样的自定义 FactoryBean
:
public class ComponentFactoryBean implements FactoryBean<Component> {
private Component parent;
private List<Component> children;
public void setParent(Component parent) {
this.parent = parent;
}
public void setChildren(List<Component> children) {
this.children = children;
}
public Component getObject() throws Exception {
if (this.children != null && this.children.size() > 0) {
for (Component child : children) {
this.parent.addComponent(child);
}
}
return this.parent;
}
public Class<Component> getObjectType() {
return Component.class;
}
public boolean isSingleton() {
return true;
}
}
class ComponentFactoryBean : FactoryBean<Component> {
private var parent: Component? = null
private var children: List<Component>? = null
fun setParent(parent: Component) {
this.parent = parent
}
fun setChildren(children: List<Component>) {
this.children = children
}
override fun getObject(): Component? {
if (this.children != null && this.children!!.isNotEmpty()) {
for (child in children!!) {
this.parent!!.addComponent(child)
}
}
return this.parent
}
override fun getObjectType(): Class<Component>? {
return Component::class.java
}
override fun isSingleton(): Boolean {
return true
}
}
这很好用,但它向最终用户暴露了很多 Spring 管道.我们要做的是编写一个隐藏所有 Spring 管道的自定义扩展. 如果我们坚持 前面描述的步骤,我们首先创建 XSD schema 来定义自定义标记的结构,如下面的清单所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.foo.example/schema/component"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.foo.example/schema/component"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:element name="component">
<xsd:complexType>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element ref="component"/>
</xsd:choice>
<xsd:attribute name="id" type="xsd:ID"/>
<xsd:attribute name="name" use="required" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
</xsd:schema>
再次按照前面描述的过程,我们再创建一个自定义 NamespaceHandler
:
public class ComponentNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser());
}
}
class ComponentNamespaceHandler : NamespaceHandlerSupport() {
override fun init() {
registerBeanDefinitionParser("component", ComponentBeanDefinitionParser())
}
}
接下来是自定义 BeanDefinitionParser
.请记住,我们正在创建描述 ComponentFactoryBean
的 BeanDefinition
.以下清单显示了我们的自定义 BeanDefinitionParser
:
public class ComponentBeanDefinitionParser extends AbstractBeanDefinitionParser {
protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
return parseComponentElement(element);
}
private static AbstractBeanDefinition parseComponentElement(Element element) {
BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean.class);
factory.addPropertyValue("parent", parseComponent(element));
List<Element> childElements = DomUtils.getChildElementsByTagName(element, "component");
if (childElements != null && childElements.size() > 0) {
parseChildComponents(childElements, factory);
}
return factory.getBeanDefinition();
}
private static BeanDefinition parseComponent(Element element) {
BeanDefinitionBuilder component = BeanDefinitionBuilder.rootBeanDefinition(Component.class);
component.addPropertyValue("name", element.getAttribute("name"));
return component.getBeanDefinition();
}
private static void parseChildComponents(List<Element> childElements, BeanDefinitionBuilder factory) {
ManagedList<BeanDefinition> children = new ManagedList<BeanDefinition>(childElements.size());
for (Element element : childElements) {
children.add(parseComponentElement(element));
}
factory.addPropertyValue("children", children);
}
}
class ComponentBeanDefinitionParser : AbstractBeanDefinitionParser() {
override fun parseInternal(element: Element, parserContext: ParserContext): AbstractBeanDefinition? {
return parseComponentElement(element)
}
private fun parseComponentElement(element: Element): AbstractBeanDefinition {
val factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean::class.java)
factory.addPropertyValue("parent", parseComponent(element))
val childElements = DomUtils.getChildElementsByTagName(element, "component")
if (childElements != null && childElements.size > 0) {
parseChildComponents(childElements, factory)
}
return factory.getBeanDefinition()
}
private fun parseComponent(element: Element): BeanDefinition {
val component = BeanDefinitionBuilder.rootBeanDefinition(Component::class.java)
component.addPropertyValue("name", element.getAttribute("name"))
return component.beanDefinition
}
private fun parseChildComponents(childElements: List<Element>, factory: BeanDefinitionBuilder) {
val children = ManagedList<BeanDefinition>(childElements.size)
for (element in childElements) {
children.add(parseComponentElement(element))
}
factory.addPropertyValue("children", children)
}
}
最后,需要通过修改 META-INF/spring.handlers
和 META-INF/spring.schemas
文件,在 Spring XML 基础结构中注册各种部件,如下所示:
# in 'META-INF/spring.handlers' http\://www.foo.example/schema/component=com.foo.ComponentNamespaceHandler
# in 'META-INF/spring.schemas' http\://www.foo.example/schema/component/component.xsd=com/foo/component.xsd
自定义 “Normal” 元素的属性
编写自己的自定义分析器和关联的部件并不难,但有时它不是正确的做法. 请考虑您需要将元数据添加到已经存在的 bean 定义的情况. 在这种情况下,你当然不想要去写你自己的整个自定义扩展, 相反,您只想向现有的 bean 定义元素添加一个附加属性.
另一个例子,假设您要为服务对象定义一个 bean (它不知道它) 正在访问群集 JCache,并且您希望确保命名的 JCache 实例在周围的群集.以下清单显示了这样一个定义:
<bean id="checkingAccountService" class="com.foo.DefaultCheckingAccountService"
jcache:cache-name="checking.account">
<!-- other dependencies here... -->
</bean>
然后,我们可以在解析 'jcache:cache-name'
属性时创建另一个 BeanDefinition
.这个 BeanDefinition
将为我们初始化命名的 JCache.
我们还可以修改 'checkingAccountService'
的现有 BeanDefinition
,以便它依赖于这个新的 JCache 初始化 BeanDefinition.以下清单显示了我们的 JCacheInitializer
:
public class JCacheInitializer {
private String name;
public JCacheInitializer(String name) {
this.name = name;
}
public void initialize() {
// lots of JCache API calls to initialize the named cache...
}
}
class JCacheInitializer(private val name: String) {
fun initialize() {
// lots of JCache API calls to initialize the named cache...
}
}
现在我们可以转到自定义扩展.首先,我们需要编写描述自定义属性的 XSD schema,如下所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.foo.example/schema/jcache"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.foo.example/schema/jcache"
elementFormDefault="qualified">
<xsd:attribute name="cache-name" type="xsd:string"/>
</xsd:schema>
接下来,我们需要创建关联的 NamespaceHandler
,如下所示:
public class JCacheNamespaceHandler extends NamespaceHandlerSupport {
public void init() {
super.registerBeanDefinitionDecoratorForAttribute("cache-name",
new JCacheInitializingBeanDefinitionDecorator());
}
}
class JCacheNamespaceHandler : NamespaceHandlerSupport() {
override fun init() {
super.registerBeanDefinitionDecoratorForAttribute("cache-name",
JCacheInitializingBeanDefinitionDecorator())
}
}
接下来,我们需要创建解析器.请注意,在这种情况下,因为我们要解析 XML 属性,所以我们编写 BeanDefinitionDecorator
而不是 BeanDefinitionParser
.以下清单显示了我们的 BeanDefinitionDecorator
:
public class JCacheInitializingBeanDefinitionDecorator implements BeanDefinitionDecorator {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
public BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder holder,
ParserContext ctx) {
String initializerBeanName = registerJCacheInitializer(source, ctx);
createDependencyOnJCacheInitializer(holder, initializerBeanName);
return holder;
}
private void createDependencyOnJCacheInitializer(BeanDefinitionHolder holder,
String initializerBeanName) {
AbstractBeanDefinition definition = ((AbstractBeanDefinition) holder.getBeanDefinition());
String[] dependsOn = definition.getDependsOn();
if (dependsOn == null) {
dependsOn = new String[]{initializerBeanName};
} else {
List dependencies = new ArrayList(Arrays.asList(dependsOn));
dependencies.add(initializerBeanName);
dependsOn = (String[]) dependencies.toArray(EMPTY_STRING_ARRAY);
}
definition.setDependsOn(dependsOn);
}
private String registerJCacheInitializer(Node source, ParserContext ctx) {
String cacheName = ((Attr) source).getValue();
String beanName = cacheName + "-initializer";
if (!ctx.getRegistry().containsBeanDefinition(beanName)) {
BeanDefinitionBuilder initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer.class);
initializer.addConstructorArg(cacheName);
ctx.getRegistry().registerBeanDefinition(beanName, initializer.getBeanDefinition());
}
return beanName;
}
}
class JCacheInitializingBeanDefinitionDecorator : BeanDefinitionDecorator {
override fun decorate(source: Node, holder: BeanDefinitionHolder,
ctx: ParserContext): BeanDefinitionHolder {
val initializerBeanName = registerJCacheInitializer(source, ctx)
createDependencyOnJCacheInitializer(holder, initializerBeanName)
return holder
}
private fun createDependencyOnJCacheInitializer(holder: BeanDefinitionHolder,
initializerBeanName: String) {
val definition = holder.beanDefinition as AbstractBeanDefinition
var dependsOn = definition.dependsOn
dependsOn = if (dependsOn == null) {
arrayOf(initializerBeanName)
} else {
val dependencies = ArrayList(listOf(*dependsOn))
dependencies.add(initializerBeanName)
dependencies.toTypedArray()
}
definition.setDependsOn(*dependsOn)
}
private fun registerJCacheInitializer(source: Node, ctx: ParserContext): String {
val cacheName = (source as Attr).value
val beanName = "$cacheName-initializer"
if (!ctx.registry.containsBeanDefinition(beanName)) {
val initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer::class.java)
initializer.addConstructorArg(cacheName)
ctx.registry.registerBeanDefinition(beanName, initializer.getBeanDefinition())
}
return beanName
}
}
最后,我们需要通过修改 META-INF/spring.handlers
和 META-INF/spring.schemas
文件来注册 Spring XML 基础结构中的各种 artifacts ,如下所示:
# in 'META-INF/spring.handlers' http\://www.foo.example/schema/jcache=com.foo.JCacheNamespaceHandler
# in 'META-INF/spring.schemas' http\://www.foo.example/schema/jcache/jcache.xsd=com/foo/jcache.xsd
10.3. Application Startup Steps
附录的此部分列出了核心容器所使用的现有 StartupSteps
.
有关每个启动步骤的名称和详细信息不是公共合同的一部分, 并且 随时可能更改; 这被视为核心容器的实现细节, 并且将遵循它的行为会改变. |
Name | Description | Tags |
---|---|---|
|
Instantiation of a bean and its dependencies. |
|
|
Initialization of |
|
|
Creation of the |
|
|
Scanning of base packages. |
|
|
Beans post-processing phase. |
|
|
Invocation of the |
|
|
Invocation of the |
|
|
Registration of component classes through |
|
|
Enhancement of configuration classes with CGLIB proxies. |
|
|
Configuration classes parsing phase with the |
|
|
Application context refresh phase. |