1.IoC容器
最近发现了一个Spring的中文网,文档翻译的很全,故后续不再继续翻译文档。
耗时约三周,约65000字,终于翻译完啦!!!o( ̄▽ ̄)ブ
文章列表
1.11. 使用JSR 330标准注解
从Spring 3.0开始,Spring提供了对JSR-330标准注解(依赖注入)的支持。以与Spring注解相同的方式扫描这些注解。要使用它们,需要在类路径中有相关的jar文件。
提示
如果使用Maven,则使用在标准的Maven存储库(repo1.maven.org/maven2/java…:
<dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <version>1</version> </dependency>
补充
@Inject
:类似于@Autowired
@Named
:可以用作@Component
,也可以用作@Qualifier
@ManagedBean
:该注解来自JSR-250,可以用作@Component
1.11.1. 使用@Inject
和@Named
进行依赖注入
你可以使用@javax.inject.Inject
代替@Autowired
,如下:
import javax.inject.Inject;
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.findMovies(...);
// ...
}
}
和@Autowired
一样,你可以在字段级、方法级和构造函数参数级使用@Inject
。此外,你可以将注入点声明为Provider
,允许按需访问较短作用域的bean,或者通过调用Provider.get()
延迟访问其他bean。下面的例子是前一个例子的变体:
import javax.inject.Inject;
import javax.inject.Provider;
public class SimpleMovieLister {
private Provider<MovieFinder> movieFinder;
@Inject
public void setMovieFinder(Provider<MovieFinder> movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.get().findMovies(...);
// ...
}
}
如果你想为要注入的依赖项使用限定名,你应该使用@Named
注解,如下例所示:
import javax.inject.Inject;
import javax.inject.Named;
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
与@Autowired
一样,@Inject
也可以与java.util.Optional
或@Nullable
一起使用。这在这里更适用,因为@Inject
没有required
属性。下面两个例子展示了如何使用@Inject
和@Nullable
:
public class SimpleMovieLister {
@Inject
public void setMovieFinder(Optional<MovieFinder> movieFinder) {
// ...
}
}
public class SimpleMovieLister {
@Inject
public void setMovieFinder(@Nullable MovieFinder movieFinder) {
// ...
}
}
1.11.2. @Named
和@ManagedBean
:与@Component
注解等价的标准
你可以使用@javax.inject.Named
或javax.annotation.ManagedBean
代替@Component
,如下示例所示:
import javax.inject.Inject;
import javax.inject.Named;
@Named("movieListener") // 同样可以使用 @ManagedBean("movieListener")
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
在不指定组件名称的情况下使用@Component
是很常见的。@Named
可以以类似的方式使用,如下例所示:
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
当你使用@Named
或@ManagedBean
时,你可以用与使用Spring注解完全相同的方式使用组件扫描,如下面的例子所示:
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
// ...
}
提示
与
@Component
相反,JSR-330@Named
和JSR-250@ManagedBean
注解是不可组合的。您应该使用Spring的构造型模型(stereotype model)来构建自定义组件注解。
1.11.3. JSR-330标准注解的限制
当你使用标准注解时,你应该知道一些重要的特性是不可用的,如下表所示:
Spring | javax.inject.* | javax.inject restrictions / comments |
---|---|---|
@Autowired | @Inject | @Inject 没有required 属性. 可以与Java 8的Optional 一起使用。 |
@Component | @Named / @ManagedBean | JSR-330不提供可组合的模型,只提供一种识别命名组件的方法。 |
@Scope("singleton") | @Singleton | JSR-330的默认作用域类似于Spring的prototype 。但是,为了使其与Spring的一般默认值保持一致,在Spring容器中声明的JSR-330 bean默认情况下是单例的。为了使用单例以外的作用域,你应该使用Spring的@Scope 注解。javax.inject 还提供了@Scope注解。不过,这个工具仅用于创建您自己的注解。 |
@Qualifier | @Qualifier / @Named | javax.inject.Qualifier 只是一个用于构建自定义限定符的元注解。具体的字符串限定符(比如Spring带值的@Qualifier )可以通过javax.inject.Named 来关联。 |
@Value | - | 没有等价 |
@Required | - | 没有等价 |
@Lazy | - | 没有等价 |
ObjectFactory | Provider | javax.inject.Provider 是Spring的ObjectFactory 的直接替代品,只是有一个更短的get() 方法名。它还可以与Spring的@Autowired 或无注解的构造函数和setter方法结合使用。 |
1.12. 基于Java的容器配置(Java-based Container Configuration)
本节介绍如何在Java代码中使用注解来配置Spring容器。它包括以下主题:
- 基本概念:
@Bean
和@Configuration
- 使用
AnnotationConfigApplicationContext
实例化Spring容器 - 使用
@Bean
注解 - 使用
@Configuration
注解 - 组合基于Java的配置
- Bean定义配置文件(Bean Definitions Profiles)
PropertySource
抽象- 使用
@PropertySource
- 语句中的占位符解析
1.12.1. 基本概念:@Bean
和@Configuration
Spring新的Java配置支持中的核心构件是带@Configuration
注解的类和带@Bean
注解的方法。
@Bean
注解用于指示方法实例化、配置和初始化要由Spring IoC容器管理的新对象。对于那些熟悉Spring <beans/>
XML配置,@Bean
注解与<beans/>
元素扮演相同的角色。你可以在任何Spring @Component
中使用带有@Bean
注解的方法。但是,它们最常与@Configuration
bean一起使用。
用@Configuration
注解类表明它的主要目的是作为bean定义的来源。此外,@Configuration
类允许通过调用同一类中的其他@Bean
方法来定义bean间依赖关系。最简单的@Configuration
类如下所示:
@Configuration
public class AppConfig {
@Bean
public MyServiceImpl myService() {
return new MyServiceImpl();
}
}
前面的AppConfig类等价于下面的Spring<beans/>
XML:
<beans>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>
完整@Configuration vs. “轻量”@Bean模式?(Full @Configuration vs. "lite" @Bean mode)
当
@Bean
方法在没有使用@Configuration
注解的类中声明时,它们被称为以**“轻量”模式**处理。在@Component
或甚至在普通的旧类中声明的Bean方法被认为是“轻量”,包含类的主要目的不同,而@Bean
方法在那里有额外的好处。例如,服务组件可以通过每个适用组件类上的附加@Bean
方法向容器公开管理视图。在这种情况下,@Bean
方法是一种通用的工厂方法机制。与完整的
@Configuration
不同,轻量的@Bean
方法不能声明bean之间的依赖关系。相反,它们操作的是包含它们的组件的内部状态,以及它们可能声明的参数(可选)。因此,这样的@Bean
方法不应该调用其他@Bean
方法。每个这样的方法实际上只是特定bean引用的工厂方法,没有任何特殊的运行时语义。这里的积极副作用(positive side-effect)是,在运行时不必应用CGLIB子类,因此在类设计方面没有限制(也就是说,包含的类可能是final
等)。在常见的场景中,
@Bean
方法将在@Configuration
类中声明,以确保始终使用“完整”模式,并且跨方法引用因此被重定向到容器的生命周期管理。这可以防止通过常规Java调用意外地调用相同的@Bean
方法,这有助于减少在“轻量”模式下操作时难以跟踪的细微错误。
下面几节将深入讨论@Bean
和@Configuration
注解。不过,首先我们将介绍使用基于Java的配置创建Spring容器的各种方法。
1.12.2. 使用AnnotationConfigApplicationContext
实例化Spring容器
下面几节介绍Spring的AnnotationConfigApplicationContext
,它是在Spring 3.0中引入的。这个通用的ApplicationContext
实现不仅能够接受@Configuration
类作为输入,还能够接受普通的@Component
类和带有JSR-330元数据注解的类。
当@Configuration
类作为输入提供时,@Configuration
类本身被注册为bean定义,类中声明的所有@Bean
方法也被注册为bean定义。
当提供@Component
和JSR-330类时,它们被注册为bean定义,并且假设在这些类中需要使用像@Autowired
或@Inject
这样的DI元数据。
简单的构造函数
在实例化一个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();
}
如前所述,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();
}
前面的例子假设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(); // 注意这里需要调用refresh()
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
使用scan(String…)
开启组件扫描
要启用组件扫描,你可以这样注解@Configuration类:
@Configuration
@ComponentScan(basePackages = "com.acme") // 启用组件扫描
public class AppConfig {
// ...
}
有经验的Spring用户可能对Spring的
context:
命名空间中的XML声明很熟悉,如下例所示:
<beans> <context:component-scan base-package="com.acme"/> </beans>
在上面的示例中,com.acme
包被扫描以查找任何带有@Component
注解的类,并将这些类注册为容器中的Spring bean定义。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);
}
提示
请记住,
@Configuration
类是用@Component
进行元注解的,因此它们是组件扫描的候选者。在前面的例子中,假设AppConfig
是在com. acme
包中声明的(或其下的任何包),则在调用scan()
期间拾取它。在refresh()
时,它的所有@Bean
方法都被处理并注册为容器中的bean定义。
支持带有AnnotationConfigWebApplicationContext
的Web应用程序
AnnotationConfigApplicationContext
的一个WebApplicationContext
变体可以通过注解AnnotationConfigWebApplicationContext
获得。您可以在配置Spring ContextLoaderListener
servlet监听器、Spring MVC DispatcherServlet
等时使用此实现。下面的web.xml
代码段配置了一个典型的Spring MVC web应用程序(注意使用了contextClass
context-param
和init-param
):
官方的web.xml示例
<web-app>
<!-- 使用`AnnotationConfigWebApplicationContext`配置`ContextLoaderListener` -->
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<!-- 指定`@Configuration`配置类 -->
<!-- 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>
<!-- 使用`AnnotationConfigWebApplicationContext`配置`DispatcherServlet` -->
<!-- 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>
<!-- 指定`@Configuration`配置类 -->
<!-- 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
方法声明:
@Configuration
public class AppConfig {
@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
上面的配置完全等同于下面的Spring XML:
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>
这两个声明都使一个名为transferService
的bean在ApplicationContext
中可用,绑定到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();
}
}
但是,这限制了对指定接口类型(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
public class AppConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
接收生命周期回调
任何用@Bean
注解定义的类都支持常规的生命周期回调,并且可以使用JSR-250中的@PostConstruct
和@PreDestroy
注解。有关更多细节,请参阅JSR-250注解。
它还完全支持常规的Spring生命周期(lifecycle) 回调。如果bean实现了InitializingBean
、DisposableBean
或Lifecycle
,则容器将调用它们各自的方法。
*Aware
接口的标准集合(如 BeanFactoryAware、BeanNameAware、MessageSourceAware、ApplicationContextAware等)也得到了完全支持。
补充
任何
@Bean
定义的bean接收Spring生命周期回调,包括
- JSR-250注解:
@PostConstruct
和@PreDestroy
InitializingBean
,DisposableBean
和Lifecycle
*Aware
:BeanFactoryAware
,BeanNameAware
,MessageSourceAware
,ApplicationContextAware
@Bean
注解支持指定任意的初始化和销毁回调方法,很像Spring XML在<bean>
元素上的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();
}
}
提示
默认情况下,使用具有公共的(public)
close
或shutdown
方法的Java配置定义的bean将自动使用销毁回调调用。如果您有一个公共的close
或shutdown
方法,并且不希望它在容器关闭时被调用,那么您可以在bean定义中添加@Bean(destroyMethod="")
来禁用默认(推断)模式。对于使用JNDI获取的资源,默认情况下您可能希望这样做,因为它的生命周期是在应用程序外部管理的。特别是,要确保始终对
DataSource
执行此操作,因为众所周知,在Java EE应用程序服务器上这是有问题的。下面的例子展示了如何防止一个
DataSource
的自动销毁回调:
@Bean(destroyMethod="") public DataSource dataSource() throws NamingException { return (DataSource) jndiTemplate.lookup("MyDS"); }
此外,对于
@Bean
方法,您通常使用编程JNDI查找,要么使用Spring的JndiTemplate
或JndiLocatorDelegate
助手,要么直接使用JNDIInitialContext
,而不是使用JndiObjectFactoryBean
变体(这将迫使您将返回类型声明为FactoryBean
类型,而不是实际的目标类型,这使得在其他打算引用此处提供的资源的@Bean
方法中使用交叉引用调用变得更加困难)。
在前面提到的例子中的BeanOne
中,在构造过程中直接调用init()
方法同样有效,如下面的例子所示:
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
BeanOne beanOne = new BeanOne();
beanOne.init(); // 直接调用初始化方法
return beanOne;
}
// ...
}
指定bean的作用域
Spring包含@Scope
注解,这样您就可以指定bean的作用域。
使用@Scope
注解
您可以指定使用@Bean
注解定义的bean应该具有特定的作用域。您可以使用Bean作用域(Bean Scopes)部分中指定的任何标准作用域。
默认的作用域是**单例(singleton
)**的,但是你可以用@Scope
注解覆盖它,如下例所示:
@Configuration
public class MyConfiguration {
@Bean
@Scope("prototype")
public Encryptor encryptor() {
// ...
}
}
@Scope
和scope-proxy
Spring提供了一种通过**作用域代理(scoped proxies)处理作用域依赖的方便方法。当使用XML配置时,创建这样一个代理最简单的方法是<aop:scoped-proxy/>
元素。在Java中使用@Scope
注解配置bean提供了与proxyMode
属性相当的支持。默认值是ScopedProxyMode.DEFAULT
,这通常表示不应该创建有作用域的代理**,除非在组件扫描指令级别配置了不同的默认值。您可以指定:
ScopedProxyMode.NO
ScopedProxyMode.TARGET_CLASS
ScopedProxyMode.INTERFACES
如果使用Java将有作用域的代理示例从XML参考文档(参见有作用域代理(scoped proxies))移植到我们的@Bean
,它类似于以下内容:
//暴露一个支持HTTP Session作用域的bean为代理
@Bean
@SessionScope //等价于@Scope(proxyMode=ScopedProxyMode.TARGET_CLASS)
public UserPreferences userPreferences(){
return new UserPreferences();
}
@Bean
public Service userService(){
UserService service=new SimpleUserService();
//引用代理的userPrefences
service.setUserPreference(userPreferences());
return service;
}
定制Bean名称(Customizing Bean Naming)
默认情况下,配置类使用@Bean
方法的名称作为结果bean的名称。但是,可以使用name
属性重写此功能,如下例所示:
@Configuration
public class AppConfig {
@Bean(name = "myThing")
public Thing thing() {
return new Thing();
}
}
Bean别名(Bean Aliasing)
正如命名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...
}
}
Bean描述(Bean Description)
有时,提供bean的更详细的文本描述是有帮助的。当**为了监视目的而公开bean(可能通过JMX)**时,这一点特别有用。
要向@Bean
添加描述,可以使用@Description
注解,如下例所示:
@Configuration
public class AppConfig {
@Bean
@Description("Provides a basic example of a bean")
public Thing thing() {
return new Thing();
}
}
补充
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Description {}
1.12.4. 使用@Configuration
注解
@Configuration
是类级别的注解,表示对象是bean定义的源。@Configuration
类通过@Bean
注解的方法声明bean。对@Configuration
类上的@Bean
方法的调用也可用于定义bean间依赖关系。请参阅 基本概念:@Bean
和@Configuration
(Basic Concepts: @Bean
and @Configuration
)以获得介绍。
注入bean间依赖
当bean彼此依赖时,表达依赖关系就像让一个bean方法调用另一个bean方法一样简单,如下例所示:
@Configuration
public class AppConfig{
@Bean
public BeanOne beanOne(){
return new BeanOne(beanTwo()); // 调用beanTwo()引入BeanTwo依赖
}
@Bean
public BeanTwo beanTwo(){
return new BeanTwo();
}
}
在前面的示例中,beanOne
通过构造函数注入接收到beanTwo
的引用。
提示
这种声明bean间依赖的方法只有在
@Configuration
类中声明@Bean
方法时才有效。你不能通过使用普通的@Component
类来声明bean间依赖。
查找方法注入(Lookup Method Injection)
如前所述,查找方法注入(lookup method injection)是一种高级特性,您应该很少使用。在单例作用域bean依赖于原型(prototype
)作用域bean的情况下,它很有用。使用Java进行这种类型的配置为实现这种模式提供了一种自然的方法。下面的例子展示了如何使用查找方法注入:
public abstract class CommandManager{
//抽象方法,返回一个Command对象
protected abstract Command createCommand();
public Object process(Object commandState){
//调用createCommand()得到一个Command对象
Command command=createCommand();
command.setState(commandState);
return command.execute();
}
}
通过使用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() {
//通过CommandManager的匿名实现,返回一个为`prototype`的Command对象
return new CommandManager() {
protected Command createCommand() {
return asyncCommand();
}
}
}
关于基于Java的配置如何在内部工作的进一步信息
考虑下面的例子,它显示了一个带@Bean
注解的方法被调用两次:
@Configuration
public class AppConfig {
@Bean
public ClientService clientService1() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao()); // 获取ClientDao并设置
return clientService;
}
@Bean
public ClientService clientService2() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao()); // 获取ClientDao并设置
return clientService;
}
@Bean
public ClientDao clientDao() {
return new ClientDaoImpl();
}
}
clientDao()
在clientService1()
和clientService2()
中分别被调用一次。由于该方法创建了一个ClientDaoImpl
的新实例并返回它,因此您通常期望有两个实例(每个服务一个)。这肯定会有问题:在Spring中,实例化的bean默认具有单例(singleton
)作用域。这就是神奇之处:**所有的@Configuration
类都在启动时用CGLIB子类化。**在子类中,子方法在调用父方法并创建新实例之前,首先检查容器中是否存在任何缓存的(限定作用域的)bean。
提示
根据bean的作用域,行为可能会有所不同。我们在这里讨论的是单例。
提示
从Spring 3.2开始,不再需要将CGLIB添加到类路径中,因为CGLIB类已被重新打包在
org.springframework.cglib
下,并直接包含在spring-core
的JAR中。
由于CGLIB在启动时动态添加特性,因此存在一些限制。特别是,配置类不能是
final
的。然而,从4.3开始,配置类上允许使用任何构造函数,包括使用@Autowired
或默认注入的单个非默认构造函数声明。如果您希望避免任何CGLIB强加的限制,请考虑在非
@Configuration
类上声明@Bean
方法(例如,在普通的@Component
类上)。然后,@Bean
方法之间的跨方法调用不会被拦截,因此您必须完全依赖于构造函数或方法级别的依赖注入。
1.12.5. 组合基于Java的配置(Composing Java-based Configurations)
Spring基于Java的配置特性允许您编写注解,这可以降低配置的复杂性。
使用@Import注解
就像Spring XML中<import/>
元素帮助模块化配置一样,@Import
注解允许从另一个配置类加载@Bean
定义,如下面的例子所示:
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(ConfigA.class) // 引入ConfigA配置类
public class ConfigB {
@Bean
public B b() {
return new B();
}
}
现在,在实例化上下文时不需要同时指定ConfigA.class
和ConfigB.class
,只需要显式地提供ConfigB
,如下例所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class); // 仅指定了ConfigB配置类
// Bean A和B都可以访问
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}
这种方法简化了容器实例化,因为只需要处理一个类,而不需要在构造过程中记住可能大量的@Configuration
类。
从Spring Framework 4.2开始,
@Import
也支持对常规组件类的引用,类似于AnnotationConfigApplicationContext.register
方法。如果您想避免组件扫描,通过使用几个配置类作为入口点来显式定义所有组件,这是特别有用的。
在导入的@Bean
定义中注入依赖(Injecting Dependencies on Imported @Bean
Definitions)
前面的例子可以工作,但是过于简单。在大多数实际场景中,bean跨配置类彼此依赖。在使用XML时,这不是问题,因为不涉及编译器,您可以声明ref="someBean"
,并信任Spring在容器初始化期间解决这个问题。当使用@Configuration
类时,Java编译器会对配置模型施加约束,因为对其他bean的引用必须是有效的Java语法。
幸运的是,解决这个问题很简单。正如我们已经讨论过的,@Bean
方法可以有任意数量的参数来描述bean依赖关系。考虑以下更真实的场景,其中有几个@Configuration
类,每个类都依赖于其他类中声明的bean:
@Configuration
public class ServiceConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) { // 依赖AccountRepository
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
@Bean
public AccountRepository accountRepository(DataSource dataSource) { // 依赖DataSource
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
还有另一种方法可以达到同样的结果。请记住,@Configuration
类最终只是容器中的一个bean:这意味着它们可以利用@Autowired
和@Value
注入以及与任何其他bean相同的其他特性。
确保以这种方式注入的依赖项都是最简单的类型。
@Configuration
类是在上下文初始化的早期处理的,强制以这种方式注入依赖项可能会导致意外的早期初始化。只要有可能,就采用基于参数的注入,如前面的示例所示。另外,要特别小心通过
@Bean
定义BeanPostProcessor
和BeanFactoryPostProcessor
。通常应该将这些方法声明为静态@Bean
方法,而不是触发包含它们的配置类的实例化。否则,@Autowired
和@Value
可能无法在配置类本身上工作,因为可以在AutowiredAnnotationBeanPostProcessor
之前将其创建为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
类中的构造函数注入仅在Spring Framework 4.3中得到支持。还要注意,如果目标bean只定义了一个构造函数,则不需要指定@Autowired
。
为了方便导航,完全限定了导入的bean(Fully-qualifying imported beans for ease of navigation)
在前面的场景中,使用@Autowired
工作得很好,并提供了所需的模块化,但是确定装配的bean定义的确切声明位置仍然有些模糊。例如,作为一个查看ServiceConfig
的开发人员,您如何确切地知道@Autowired AccountRepository
bean是在哪里声明的?它在代码中不是显式的,这可能很好。请记住,Spring Tools for Eclipse提供了一些工具,可以呈现显示所有内容如何装配的图形,这可能就是您所需要的。此外,您的Java IDE可以轻松地找到AccountRepository
类型的所有声明和使用,并快速显示返回该类型的@Bean
方法的位置。
如果这种模糊性是不可接受的,并且您希望在IDE中从一个@Configuration
类直接导航到另一个@Configuration
类,请考虑自动装配配置类本身。下面的例子展示了如何这样做:
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
// 通过配置类的@Bean方法导航
return new 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");
}
现在,ServiceConfig
与具体的DefaultRepositoryConfig
是松散耦合的,内置的IDE工具仍然很有用:您可以轻松地获得RepositoryConfig
实现的类型层次结构。通过这种方式,导航@Configuration
类及其依赖关系与导航基于接口的代码的通常过程没有什么不同。
如果您想影响某些bean的启动创建顺序,请考虑将其中一些声明为
@Lazy
(用于在第一次访问时创建,而不是在启动时创建)或将某些其他bean声明为@DependsOn
(确保在当前bean之前创建特定的其他bean,而不是后者的直接依赖所暗示的)。
有条件地包含@Configuration
类或@Bea
n方法(Conditionally Include @Configuration
Classes or @Bean
Methods)
根据一些任意的系统状态,有条件地启用或禁用完整的@Configuration
类,甚至单个的@Bean
方法,通常是很有用的。一个常见的例子是,只有在Spring环境中启用了特定的profile时,才使用@Profile
注解来激活Bean(有关详细信息,请参阅 Bean Definition Profiles)。
@Profile
注解实际上是通过使用更灵活的@Conditional
注解实现的。@Conditional
注解指示了在注册@Bean
之前应该咨询的特定的org.springframework.context.annotation.Condition
实现。
Condition
接口的实现提供了一个返回true
或false
的matches(…)
方法。例如,下面的清单显示了用于@Profile
的实际Condition
实现:
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 读取@Profile注解属性
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
更多细节请参见@Conditional
javadoc。
结合Java和XML配置(Combining Java and XML Configuration)
Spring的@Configuration
类支持并不打算100%完全替代Spring XML。一些工具(如Spring XML名称空间)仍然是配置容器的理想方法。在XML方便或必要的情况下,您可以选择:以“以XML为中心(XML-centric)”的方式实例化容器,例如使用ClassPathXmlApplicationContext
,或者以“以java为中心(Java-centric)”的方式实例化容器,例如使用AnnotationConfigApplicationContext
和@ImportResource
注解来根据需要导入XML。
@Configuration类以XML为中心的使用(XML-centric Use of @Configuration
Classes)
最好是从XML引导Spring容器,并以特别的方式包含@Configuration
类。例如,在使用Spring XML的大型现有代码库中,更容易根据需要创建@Configuration
类,并从现有XML文件中包含它们。在本节后面的部分中,我们将介绍在这种“以XML为中心”的情况下使用@Configuration
类的选项。
声明@Configuration
类作为普通的Spring<bean/>
元素
请记住,@Configuration
类最终是容器中的bean定义。在本系列示例中,我们创建了一个名为AppConfig
的@Configuration
类,并将其作为<bean/>
定义。因为<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());
}
}
下面的例子展示了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);
// ...
}
提示
在
system-test-config.xml
文件中,AppConfig
<bean/>
不声明id
元素。虽然这样做是可以接受的,但这是不必要的,因为没有其他bean引用它,并且不太可能通过名称显式地从容器中获取它。类似地,DataSource
bean只按类型自动装配,因此不严格要求显式beanid
。
使用<context:component-scan/>
拾取@Configuration
类(Using context:component-scan/ to pick up @Configuration
classes)
因为@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>
以类为中心使用XML和@ImportResource
在使用@Configuration
类作为配置容器的主要机制的应用程序中,可能仍然需要至少使用一些XML。在这些场景中,您可以使用@ImportResource
并只定义所需的XML。这样做实现了一种“以java为中心”的方法来配置容器,并将XML保持在最低限度。下面的示例(其中包括一个配置类、一个定义bean的XML文件、一个属性文件和主类)展示了如何使用@ImportResource
注解来实现“以java为中心”的配置,该配置根据需要使用XML:
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
return new DriverManagerDataSource(url, username, password);
}
}
properties-config.xml
<beans>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>
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);
// ...
}
1.13. Environment抽象(Environment Abstraction)
Environment
接口是集成在容器中的抽象,它对应用程序环境的两个关键方面建模:profiles和properties。
一个profile是一个命名的、逻辑的bean定义组,只有在给定的profile处于活动状态时才向容器注册Bean定义。可以将bean分配给配置文件,无论是用XML定义的还是用注解定义的。与profile相关的Environment
对象的作用是确定哪些profile(如果有的话)当前是活动的,以及哪些profile(如果有的话)在默认情况下应该是活动的。
属性在几乎所有应用程序中都扮演着重要的角色,并且可能来自各种来源:属性文件(properties files)、JVM系统属性(JVM system properties)、系统环境变量(system environment variables)、JNDI、servlet上下文参数、专门的(ad-hoc)Properties
对象、Map
对象等等。与属性相关的Environment
对象的作用是为用户提供一个方便的服务接口,用于配置属性源并从中解析属性。
1.13.1. Bean定义Profile(Bean Definition Profiles)
Bean定义profile在核心容器中提供了一种机制,允许在不同的环境中注册不同的Bean。“环境(environment)”这个词对不同的用户有不同的含义,这个特性可以帮助处理许多用例,包括:
- 在开发中使用内存中的数据源,而在QA或生产中从JNDI中查找相同的数据源。
- 只有在将应用程序部署到性能环境中时才注册监视基础设施。
- 为客户A和客户B的部署注册自定义的bean实现。
考虑需要DataSource
的实际应用程序中的第一个用例。在测试环境中,配置可能类似于以下内容:
@Bean
public DataSource dataSource() {
return new 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");
}
问题是如何根据当前环境在使用这两种变体之间进行切换。随着时间的推移,Spring用户已经设计了许多方法来完成这项工作,通常依赖于系统环境变量和**XML <import/>
**的组合。包含${placeholder}
令牌的语句,根据环境变量的值解析到正确的配置文件路径。Bean定义profile是为这个问题提供解决方案的核心容器特性。
如果我们推广前面特定于环境的bean定义示例中显示的用例,我们最终需要在某些上下文中注册某些bean定义,而不是在其他上下文中注册。您可以说,您希望在情况a中注册bean定义的某个profile,而在情况b中注册不同的profile。我们首先更新配置以反映这种需求。
使用@Profile
@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("production")
public class JndiDataConfig {
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
提示
如前所述,对于
@Bean
方法,您通常选择使用编程的JNDI查找,方法是使用Spring的JndiTemplate
/JndiLocatorDelegate
帮助程序,或者使用前面所示的直接JNDIInitialContext
使用方法,但不使用JndiObjectFactoryBean
变体,这将迫使您将返回类型声明为FactoryBean
类型。
profile字符串可以包含一个简单的profile名称(例如,production
)或一个profile表达式。profile表达式允许表达更复杂的profile逻辑(例如,production & us-east
)。profile表达式中支持以下操作符:
!
:profile的逻辑“not”&
:profile的逻辑“and”|
:profile的逻辑“or”
提示
如果不使用括号,就不能混合使用
&
和|
操作符。例如,production & us-east | eu-central
不是一个有效的表达式。它必须表示为production & (us-east | eu-central)
。
您可以使用@Profile
作为元注解(meta-annotation) 来创建自定义组合注解。下面的例子定义了一个自定义的@Production
注解,你可以用它代替@Profile("production")
:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
如果
@Configuration
类被标记为@Profile
,那么所有与该类关联的@Bean
方法和@Import
注解都将被绕过,除非一个或多个指定的profile处于活动状态。如果@Component
或@Configuration
类被标记为@Profile({"p1","p2"})
,则该类不会被注册或处理,除非配置文件'p1'或'p2'已被激活。如果给定的profile以NOT操作符(!
)为前缀,则只有当profile不活动时,才会注册带注解的元素。例如,给定@Profile({"p1","!p2"})
,如果配置文件'p1'处于活动状态或配置文件'p2'未处于活动状态,则将发生注册。
@Profile
也可以在方法级别声明,只包含配置类的一个特定bean(例如,对于特定bean的可选变体),如下例所示:
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("development") // standaloneDataSource方法仅在`development` profile生效
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") // jndiDataSource方法仅在`production` profile生效
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
提示
对于
@Bean
方法上的@Profile
,可能会出现一种特殊的情况:对于具有相同Java方法名的重载@Bean
方法(类似于构造函数重载),需要在所有重载方法上一致地声明@Profile
条件。如果条件不一致,则只有重载方法中第一个声明的条件有关系。因此,@Profile
不能用于选择具有特定参数签名的重载方法。同一bean的所有工厂方法之间的解析遵循Spring在创建时的构造函数解析算法。如果希望定义具有不同profile条件的备选bean,请使用不同的Java方法名,这些方法名通过使用
@Bean
的name
属性指向相同的bean名称,如前面的示例所示。如果参数签名都是相同的(例如,所有的变体都有无参数工厂方法),这是首先在有效的Java类中表示这种安排的唯一方法(因为只能有一个特定名称和参数签名的方法)。
XML Bean定义Profile(XML Bean Definition Profiles)
XML对应物是<beans>
元素的profile
属性。我们前面的示例配置可以重写为两个XML文件,如下所示:
<!-- 指定development profile -->
<beans profile="development"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="...">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<!-- 指定production profile -->
<beans profile="production"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
也可以避免分割,在同一个文件内嵌套<beans/>
元素,如下示例所示:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<!-- other bean definitions -->
<beans profile="development">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
spring-bean.xsd
被限制为只允许文件中最后一个元素。这将有助于提供灵活性,而不会在XML文件中造成混乱。
提示
XML对应物不支持前面描述的profile表达式。但是,可以使用
!
操作符来否定profile。也可以通过嵌套配置文件来应用逻辑上的“and”,如下例所示:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="..."> <!-- other bean definitions --> <beans profile="production"> <beans profile="us-east"> <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> </beans> </beans> </beans>
在前面的示例中,如果
production
和us-east
配置文件都处于活动状态,则公开dataSource
bean。
激活一个Profile(Activating a Profile)
现在我们已经更新了配置,我们仍然需要指示Spring哪个profile是活动的。如果我们现在启动我们的示例应用程序,我们将看到抛出NoSuchBeanDefinitionException
,因为容器找不到名为dataSource
的Spring bean。
激活profile可以通过几种方式完成,但最直接的是通过编程方式针对通过ApplicationContext
提供的Environment
API来完成。下面的例子展示了如何这样做:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
此外,您还可以通过spring.profiles.active
属性声明地激活profile,该属性可以通过系统环境变量、JVM系统属性、web.xml
中的servlet上下文参数来指定,甚至可以作为JNDI中的一个条目来指定(参见 PropertySource
Abstraction)。在集成测试中,活动profile可以通过在spring-test
模块中使用@ActiveProfiles
注解来声明(参见环境profile的上下文配置(context configuration with environment profiles))。
注意,profile不是一个“非此即彼”(“either-or” )的命题。您可以一次激活多个配置文件。通过编程方式,您可以向setActiveProfiles()
方法提供多个profile名称,该方法接受String…
下面的示例激活多个配置文件:
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
声明性地,spring.profiles.active
可以接受以逗号分隔的配置文件名列表,如下例所示:
-Dspring.profiles.active="profile1,profile2"
默认Profile
默认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();
}
}
如果没有活动的profile,则创建dataSource
。您可以将其视为为一个或多个bean提供默认定义的一种方式。如果启用了任何配置文件,则不应用默认配置文件。
您可以通过在环境中使用setDefaultProfiles()
或使用spring.profiles.default
属性来更改默认配置文件的名称。
1.13.2. PropertySource
抽象(PropertySource
Abstraction)
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);
在前面的代码片段中,我们看到了询问Spring是否为当前环境定义了my-property
属性的高级方法。为了回答这个问题,Environment
对象对一组PropertySource
对象执行搜索。PropertySource
是对任何键值对源的简单抽象,Spring的StandardEnvironment
配置了两个PropertySource
对象——一个表示JVM系统属性集(System.getProperties()
),另一个表示系统环境变量集(System.getenv()
)。
提示
这些默认属性源是为
StandardEnvironment
提供的,用于独立应用程序。StandardServletEnvironment
使用额外的默认属性源填充,包括servlet配置、servlet上下文参数和JndiPropertySource
(如果JNDI可用)。
具体地说,当您使用StandardEnvironment
时,如果运行时存在my-property
系统属性或my-property
环境变量,则调用env.containsProperty("my-property")
返回true
。
执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用
envy.getproperty(“my-property”)
期间碰巧在两个地方都设置了my-property
属性,则系统属性值“胜出”并被返回。注意,属性值不是合并的,而是被前面的条目完全覆盖。对于一个通用的
StandardServletEnvironment
,完整的层次结构如下所示,最高优先级的条目位于顶部:
- ServletConfig参数(如果适用——例如,
DispatcherServlet
上下文)- ServletContext参数(
web.xml
上下文参数条目)- JNDI环境变量(
java:comp/env/
entries)- JVM系统属性(-D命令行参数)
- JVM系统环境(操作系统环境变量)
最重要的是,整个机制是可配置的。也许您希望将自定义的属性源集成到此搜索中。为此,实现并实例化您自己的PropertySource
,并将其添加到当前Environment
的PropertySources
集合中。下面的例子展示了如何这样做:
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
在前面的代码中,MyPropertySource
在搜索中以最高优先级被添加。如果它包含my-property
属性,则检测并返回该属性,以支持任何其他PropertySource
中的任何my-property
属性。MutablePropertySources
API公开了许多方法,这些方法允许对属性源集进行精确操作。
1.13.3. 使用@PropertySource
@PropertySource
注解为向Spring Environment
添加PropertySource
提供了一种方便的声明性机制。
给定一个名为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;
}
}
@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;
}
}
假设my.placeholder
存在于已注册的属性源之一中(例如,系统属性或环境变量),占位符被解析为相应的值。如果没有,则使用default/path
作为默认值。如果没有指定默认值并且无法解析属性,则抛出IllegalArgumentException
。
提示
根据Java 8约定,
@PropertySource
注解是可重复的。然而,所有这样的@PropertySource
注解都需要在同一层声明,要么直接在配置类上声明,要么作为同一自定义注解中的元注解声明。不建议混合使用直接注解和元注解,因为直接注解会有效地覆盖元注解。
1.13.4. 语句中的占位符解析(Placeholder Resolution in Statements)
过去,元素中占位符的值只能根据JVM系统属性或环境变量来解析。现在情况已经不同了。因为Environment
抽象集成在整个容器中,所以很容易通过它路由占位符的解析。这意味着您可以以任何喜欢的方式配置解析过程。您可以更改搜索系统属性和环境变量的优先级,或者完全删除它们。您还可以酌情将自己的属性源添加到混合中。
具体地说,只要customer
属性在环境中可用,无论customer
属性在哪里定义,下面的语句都有效:
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>
1.14 注册一个LoadTimeWeaver
Spring使用LoadTimeWeaver
在类加载到Java虚拟机(JVM)时对它们进行动态转换。
要启用加载时编织,你可以将@EnableLoadTimeWeaving
添加到你的@Configuration
类中,如下面的例子所示:
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}
另外,对于XML配置,你可以使用<context:load-time-weaver/>
元素:
<beans>
<context:load-time-weaver/>
</beans>
一旦配置好ApplicationContext
,该ApplicationContext
中的任何bean都可以实现LoadTimeWeaverAware
,从而接收到对加载时编织实例的引用。这在与Spring的JPA支持(Spring’s JPA support)结合使用时特别有用,因为加载时编织可能是JPA类转换所必需的。有关更多细节,请参阅LocalContainerEntityManagerFactoryBean
javadoc。有关AspectJ加载时编织的更多信息,请参阅Spring框架中使用AspectJ进行加载时编织(Load-time Weaving with AspectJ in the Spring Framework)。
1.15. ApplicationContext的附加功能(Additional Capabilities of the ApplicationContext
)
正如本章介绍中所讨论的,org.springframework.beans.factory
包提供了管理和操作bean的基本功能,包括以编程的方式。org.springframework.context
包添加了ApplicationContext
接口,它扩展了BeanFactory
接口,此外还扩展了其他接口,以更面向应用程序框架的风格提供额外的功能。许多人以完全声明式的方式使用ApplicationContext
,甚至不以编程方式创建它,而是依靠诸如ContextLoader
之类的支持类来自动实例化ApplicationContext
,作为Java EE web应用程序正常启动过程的一部分。
为了以更面向框架的风格增强BeanFactory
功能,上下文包还提供了以下功能:
- 通过
MessageSource
接口以i18n样式访问消息。 - 通过
ResourceLoader
接口访问资源,如URL和文件。 - 事件发布,即通过使用
ApplicationEventPublisher
接口实现ApplicationListener
接口的bean。 - 通过
HierarchicalBeanFactory
接口加载多个(分层)上下文,让每个上下文都专注于一个特定的层,例如应用程序的web层。
补充
i18n:是
Internationalization
单词的缩写
1.15.1. 使用MessageSource
进行国际化(Internationalization using 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
。- 上述方法中使用的所有属性也都包装在一个名为
MessageSourceResolvable
类中,您可以与此方法一起使用该类。
当加载ApplicationContext
时,它会自动搜索上下文中定义的MessageSource
bean。bean的名称必须为messageSource
。如果找到了这样的bean,那么对上述方法的所有调用都将委托给消息源。如果没有找到消息源,ApplicationContext
将尝试在父容器查找包含同名bean。如果是,它将使用该bean作为MessageSource
。如果ApplicationContext
找不到任何消息源,则实例化一个空的DelegatingMessageSource
,以便能够接受对上面定义的方法的调用。
Spring提供了三个MessageSource
实现,ResourceBundleMessageSource
, ReloadableResourceBundleMessageSource
和StaticMessageSource
。它们都实现了HierarchicalMessageSource
,以便进行嵌套消息传递。StaticMessageSource
很少使用,但它提供了将消息添加到源的编程方法。下面的例子展示了ResourceBundleMessageSource
:
<beans>
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>format</value>
<value>exceptions</value>
<value>windows</value>
</list>
</property>
</bean>
</beans>
该示例假设在类路径中定义了三个资源包,分别是format
、exceptions
和windows
。任何解析消息的请求都是以通过ResourceBundle
对象解析消息的jdk标准方式处理的。为了本例的目的,假设上述两个资源包文件的内容如下:
# 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);
}
上述程序的输出结果如下:
Alligators rock!
总之,MessageSource
是在一个名为beans.xml
的文件中定义的,该文件存在于类路径的根目录中。messageSource
bean定义通过其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);
}
}
调用execute()
方法的结果输出如下:
The userDao argument is required.
关于国际化(“i18n”),Spring的各种MessageSource
实现遵循与标准JDK ResourceBundle
相同的语言环境解析和回退规则。简而言之,继续前面定义的messageSource
示例,如果希望根据英国(en-GB
)语言环境解析消息,则需要分别创建名为format_en_GB.properties
,exceptions_en_GB.properties
,windows_en_GB.properties
。
通常,区域设置解析由应用程序的周围环境管理。在下面的示例中,(British)消息解析的区域设置是手动指定的:
# 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);
}
运行上述程序得到的输出如下:
Ebagum lad, the 'userDao' argument is required, I say, required.
您还可以使用MessageSourceAware
接口获取对已定义的任何MessageSource
的引用。在实现MessageSourceAware
接口的ApplicationContext
中定义的任何bean在创建和配置bean时都会被注入应用程序上下文的MessageSource
。
提示
因为Spring的
MessageSource
基于Java的ResourceBundle
,所以它不会合并具有相同基名的bundle,而只会使用找到的第一个bundle。具有相同基名的后续消息包将被忽略。
提示
作为
ResourceBundleMessageSource
的替代方案,Spring提供了一个ReloadableResourceBundleMessageSource
类。这个变体支持相同的包文件格式,但比基于标准JDK的ResourceBundleMessageSource
实现更灵活。特别是,它允许从任何Spring资源位置(不仅仅是从类路径)读取文件,并支持bundle属性文件的热重新加载(同时在两者之间有效地缓存它们)。详情请参见ReloadableResourceBundleMessageSource
javadoc。
1.15.2. 标准事件和自定义事件(Standard and Custom Events)
ApplicationContext
中的事件处理是通过ApplicationEvent
类和ApplicationListener
接口提供的。如果将实现ApplicationListener
接口的bean部署到上下文中,则每次将ApplicationEvent
发布到ApplicationContext
时,都会通知该bean。本质上,这是标准的观察者设计模式(Observer design pattern)。
从Spring 4.2开始,事件基础设施得到了显著改进,并提供了基于注解的模型(annotation-based model) 以及发布任意事件的能力(也就是说,不一定是从
ApplicationEvent
扩展的对象)。当这样的对象被发布时,我们将它包装在一个事件中。
下表描述了Spring提供的标准事件:
事件(Event) | 解释 |
---|---|
ContextRefreshedEvent | 在ApplicationContext 被初始化或刷新时发布(例如,通过在ConfigurableApplicationContext 接口上使用refresh() 方法)。这里,“初始化”意味着加载所有bean,检测并激活后置处理器bean,预实例化单例,并准备好使用ApplicationContext 对象。只要上下文没有关闭,只要所选的ApplicationContext 实际上支持这种**“热”刷新**,刷新就可以被触发多次。例如,XmlWebApplicationContext 支持热刷新,但GenericApplicationContext 不支持。 |
ContextStartedEvent | 在ConfigurableApplicationContext 接口上使用start() 方法启动ApplicationContext 时发布。这里,“已启动”意味着所有生命周期bean都接收显式启动信号。通常,该信号用于在显式停止后重新启动bean,但它也可以用于启动未配置为自动启动的组件(例如,在初始化时尚未启动的组件)。 |
ContextStoppedEvent | 在ConfigurableApplicationContext 接口上使用stop() 方法停止ApplicationContext 时发布。这里,“已停止”意味着所有生命周期bean都接收到显式停止信号。停止的上下文可以通过start()调用重新启动。 |
ContextClosedEvent | 通过在ConfigurableApplicationContext 接口上使用close() 方法或通过JVM关闭钩子关闭ApplicationContext 时发布。这里,“关闭”意味着所有的单例bean都将被销毁。一旦上下文关闭,它的生命周期就结束了,不能刷新或重新启动。 |
RequestHandledEvent | 一个特定于web的事件,告诉所有bean一个HTTP请求已经得到了服务。此事件在请求完成后发布。这个事件只适用于使用Spring的DispatcherServlet 的web应用程序。 |
ServletRequestHandledEvent | RequestHandledEvent 的子类,用于添加servlet特定的上下文信息。 |
您还可以创建和发布自己的自定义事件。下面的例子展示了一个扩展Spring的ApplicationEvent
基类的简单类:
public class BlockedListEvent extends ApplicationEvent {
private final String address;
private final String content;
public BlockedListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}
// accessor and other methods...
}
要发布自定义ApplicationEvent
,请调用ApplicationEventPublisher
上的publishEvent()
方法。通常,这是通过创建一个实现ApplicationEventPublisherAware
的类并将其注册为Spring bean来完成的。下面的例子展示了这样一个类:
public class EmailService implements ApplicationEventPublisherAware {
private List<String> blockedList;
private ApplicationEventPublisher publisher;
public void setBlockedList(List<String> blockedList) {
this.blockedList = blockedList;
}
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEmail(String address, String content) {
if (blockedList.contains(address)) {
publisher.publishEvent(new BlockedListEvent(this, address, content));
return;
}
// send email...
}
}
在配置时,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...
}
}
注意,ApplicationListener
一般是用自定义事件的类型参数化的(在前面的例子中是BlockedListEvent
)。这意味着onApplicationEvent()
方法可以保持类型安全,避免任何向下转换的需要。您可以注册任意数量的事件监听器,但请注意,默认情况下,事件监听器以同步方式接收事件。这意味着publishhevent()
方法将阻塞,直到所有监听器完成对事件的处理。这种同步和单线程方法的一个优点是,当监听器接收到事件时,如果事务上下文可用,它将在发布者的事务上下文中操作。如果需要另一种事件发布策略,请参阅javadoc中Spring的ApplicationEventMulticaster
接口和SimpleApplicationEventMulticaster
实现的配置选项。
下面的示例显示了用于注册和配置上面每个类的bean定义:
<bean id="emailService" class="example.EmailService">
<property name="blockedList">
<list>
<value>known.spammer@example.org</value>
<value>known.hacker@example.org</value>
<value>john.doe@example.org</value>
</list>
</property>
</bean>
<bean id="blockedListNotifier" class="example.BlockedListNotifier">
<property name="notificationAddress" value="blockedlist@example.org"/>
</bean>
综上所述,当调用emailService
bean的sendEmail()
方法时,如果有任何应该被阻止的电子邮件消息,则会发布一个BlockedListEvent
类型的自定义事件。blockedListNotifier
bean注册为ApplicationListener
并接收BlockedListEvent
,此时它可以通知适当的各方。
提示
Spring的事件机制是为相同应用程序上下文中的Spring bean之间的简单通信而设计的。然而,对于更复杂的企业集成需求,单独维护的Spring整合(Spring Integration) 项目为构建轻量级的、面向模式的(pattern-oriented)、事件驱动的体系结构提供了完整的支持,这些体系结构构建在众所周知的Spring编程模型之上。
基于注解的事件监听器
您可以使用@EventListener
注解在托管bean的任何方法上注册事件监听器。BlockedListNotifier
可以重写如下:
public class BlockedListNotifier {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
@EventListener
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
方法签名再次声明它要侦听的事件类型,但这一次使用了一个灵活的名称,并且没有实现特定的监听器接口。事件类型也可以通过泛型缩小范围,只要实际事件类型在其实现层次结构中解析泛型参数即可。
如果您的方法应该侦听多个事件,或者您想在没有任何参数的情况下定义它,那么也可以在注解本身上指定事件类型。下面的例子展示了如何这样做:
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
// ...
}
还可以通过使用定义SpEL表达式(SpEL
expression)的注解的条件属性添加额外的运行时过滤,该注解应该与实际调用特定事件的方法相匹配。
下面的例子展示了我们的通知器如何被重写为只有当事件的content
属性等于my-event
时才会被调用:
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
// notify appropriate parties via notificationAddress...
}
每个SpEL
表达式针对一个专用上下文求值。下表列出了上下文可用的项,以便您可以将它们用于条件事件处理:
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
事件(Event) | 根对象(root object) | 实际的ApplicationEvent . | #root.event 或 event |
参数数组(Arguments array) | 根对象(root object) | 用于调用该方法的参数(作为对象数组)。 | #root.args 或 args ; args[0] 用于访问第一个参数,等等. |
参数名称(Argument name) | 评估上下文(evaluation context) | 任何方法参数的名称。如果由于某种原因,名称不可用(例如,因为编译的字节码中没有调试信息),也可以使用#a<#arg> 语法,这里<#arg> 表示参数index(从0开始)。 | #blEvent 或 #a0 (你也可以使用#p0 或 #p<#arg> 作为别名的参数表示法) |
注意#root.event
使您能够访问底层事件,即使您的方法签名实际上引用了已发布的任意对象。
如果你需要发布一个事件作为处理另一个事件的结果,你可以改变方法签名来返回应该发布的事件,如下面的例子所示:
@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
提示
asynchronous listeners不支持此特性。
handleBlockedListEvent()
方法为它处理的每个BlockedListEvent
发布一个新的ListUpdateEvent
。如果需要发布多个事件,则可以返回一个Collection
或事件数组。
异步监听器(Asynchronous Listeners)
如果希望特定的监听器异步处理事件,可以重用常规的@Async支持(regular @Async
support)。下面的例子展示了如何这样做:
@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
// BlockedListEvent is processed in a separate thread
}
在使用异步事件时,请注意以下限制:
- 如果异步事件监听器抛出异常,则不会将其传播给调用者。参见
AsyncUncaughtExceptionHandler
了解更多细节。 - 异步事件监听器方法不能通过返回值来发布后续事件。如果您需要发布另一个事件作为处理的结果,则注入
ApplicationEventPublisher
来手动发布事件。
排序监听器(Ordering Listeners)
如果你需要一个监听器在另一个监听器之前被调用,你可以在方法声明中添加@Order
注解,如下例所示:
@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
通用的事件(Generic Events)
还可以使用泛型进一步定义事件的结构。考虑使用EntityCreatedEvent<T>
其中T
是被创建实体的类型。例如,您可以创建以下监听器定义来仅接收Person
的EntityCreatedEvent
:
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
// ...
}
由于类型擦除,只有当触发的事件解析了事件监听器过滤的泛型参数(即,类似于class PersonCreatedEvent extends EntityCreatedEvent<Person> { … }
)。
在某些情况下,如果所有事件都遵循相同的结构(就像前面示例中的事件一样),这可能会变得非常乏味。在这种情况下,您可以实现ResolvableTypeProvider
来指导框架超越运行时环境提供的内容。下面的事件展示了如何这样做:
public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {
public EntityCreatedEvent(T entity) {
super(entity);
}
@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
}
}
这不仅适用于
ApplicationEvent
,也适用于作为事件发送的任意对象。
1.15.3. 方便地访问低级资源(Convenient Access to Low-level Resources)
为了最佳地使用和理解应用程序上下文,您应该熟悉Spring的资源抽象,如Resources中所述。
应用程序上下文是一个ResourceLoader
,它可以用来加载Resource
对象。Resource
本质上是JDK java.net.URL
类的一个功能更丰富的版本。事实上,Resource
的实现在适当的地方包装了一个java.net.URL
实例。Resource
可以以透明的方式从几乎任何位置获取低级资源,包括从类路径、文件系统位置、任何可以用标准URL描述的位置,以及其他一些变体。如果资源位置字符串是一个没有任何特殊前缀的简单路径,那么这些资源的来源是特定的,并且适合于实际的应用程序上下文类型。
您可以配置部署到应用程序上下文中的bean,以实现特殊的回调接口ResourceLoaderAware
,以便在初始化时自动回调,同时将应用程序上下文本身作为ResourceLoader
传入。您还可以公开Resource
类型的属性,以用于访问静态资源。它们像其他属性一样被注入其中。您可以将这些Resource
属性指定为简单的String
路径,并依赖于部署bean时从这些文本字符串到实际Resource
对象的自动转换。
提供给ApplicationContext
构造函数的位置路径实际上是资源字符串,以简单的形式,根据特定的上下文实现进行适当的处理。例如,ClassPathXmlApplicationContext
将简单的位置路径视为类路径位置。您还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或URL加载定义,而不管实际的上下文类型是什么。
1.15.4. 应用程序启动跟踪(Application Startup Tracking)
ApplicationContext
管理Spring应用程序的生命周期,并围绕组件提供丰富的编程模型。因此,复杂的应用程序可以具有同样复杂的组件图和启动阶段。
使用特定的指标跟踪应用程序启动步骤可以帮助理解在启动阶段时间花在哪里,但它也可以作为一种更好地理解整个上下文生命周期的方法。
AbstractApplicationContext
(和它的子类)被一个ApplicationStartup
工具化,它收集关于不同启动阶段的StartupStep
数据:
- 应用程序上下文生命周期(基包扫描、配置类管理)
- bean生命周期(实例化、智能初始化、后期处理)
- 应用程序事件处理
下面是一个在AnnotationConfigApplicationContext
中插装的例子:
// 创建一个启动步骤并且开始记录
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// 在当前步骤添加标记信息
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// 执行我们正在测试的实际阶段
this.scanner.scan(basePackages);
// 结束当前步骤
scanPackages.end();
应用程序上下文已经通过多个步骤进行了检测。一旦记录下来,这些启动步骤就可以用特定的工具收集、显示和分析。要获得现有启动步骤的完整列表,您可以查看专门的附录部分。
默认的ApplicationStartup
实现是一个无操作的变体,以最小化开销。这意味着默认情况下,在应用程序启动期间不会收集任何指标。Spring框架附带了一个用Java Flight Recorder跟踪启动步骤的实现:FlightRecorderApplicationStartup
。要使用此变体,必须在创建ApplicationContext
后立即将其实例配置到ApplicationContext
。
如果开发人员正在提供他们自己的AbstractApplicationContext
子类,或者如果他们希望收集更精确的数据,他们也可以使用ApplicationStartup
基础结构。
ApplicationStartup
只用于应用程序启动和核心容器;这绝不是Java分析器或度量库(如 Micrometer)的替代品。
要开始收集自定义的StartupStep
,组件可以直接从应用程序上下文中获取ApplicationStartup
实例,让它们的组件实现ApplicationStartupAware
,或者在任何注入点请求ApplicationStartup
类型。
提示
当创建自定义启动步骤时,开发人员不应该使用
"spring.*"
命名空间。这个名称空间保留给Spring内部使用,可能会有变化。
1.15.5. 方便的ApplicationContext实例化Web应用程序(Convenient ApplicationContext Instantiation for Web Applications)
您可以通过使用(例如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>
<!-- 指定ContextLoaderListener用于初始化ApplicationContext -->
<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.15.6. 将Spring ApplicationContext
部署为Java EE RAR文件
可以将Spring ApplicationContext
作为RAR文件部署,将上下文及其所需的所有bean类和库jar封装在Java EE RAR部署单元中。这相当于引导一个能够访问Java EE服务器设施的独立ApplicationContext
(仅托管在Java EE环境中)。RAR部署是部署无头WAR文件场景的一种更自然的替代方案——实际上,没有任何HTTP入口点的WAR文件仅用于在Java EE环境中引导Spring ApplicationContext
。
RAR部署非常适合不需要HTTP入口点,而只包含消息端点和计划作业的应用程序上下文。这种上下文中的bean可以使用应用服务器资源,比如JTA事务管理器和JNDI绑定的JDBC DataSource
实例和JMS ConnectionFactory
实例,还可以向平台的JMX服务器注册——所有这些都是通过Spring的标准事务管理以及JNDI和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.16. BeanFactory
API
BeanFactory
API为Spring的IoC功能提供了底层基础。它的特定约定主要用于与Spring的其他部分和相关的第三方框架集成,它的DefaultListableBeanFactory
实现是高级GenericApplicationContext
容器中的关键委托。
BeanFactory
和相关接口(如BeanFactoryAware
、InitializingBean
、DisposableBean
)是其他框架组件的重要集成点。由于不需要任何注解甚至反射,它们允许容器与其组件之间进行非常有效的交互。应用程序级bean可以使用相同的回调接口,但通常更喜欢通过注解或编程配置进行声明性依赖注入。
请注意,核心BeanFactory
API级别及其DefaultListableBeanFactory
实现没有对要使用的配置格式或任何组件注解做出假设。所有这些风格都是通过扩展(如XmlBeanDefinitionReader
和AutowiredAnnotationBeanPostProcessor
)实现的,并将共享BeanDefinition
对象作为核心元数据表示进行操作。这是使Spring的容器如此灵活和可扩展的本质。
1.16.1. BeanFactory
或ApplicationContext
?
本节将解释BeanFactory
和ApplicationContext
容器级别之间的区别,以及它们对引导的影响。
你应该使用ApplicationContext
,除非你有很好的理由不这样做,使用GenericApplicationContext
和它的子类AnnotationConfigApplicationContext
作为自定义引导的通用实现。这些是Spring核心容器的主要入口点,用于所有常见目的:加载配置文件、触发类路径扫描、以编程方式注册bean定义和带注解的类,以及(从5.0开始)注册功能bean定义。
因为**ApplicationContext
包含了BeanFactory
的所有功能**,所以除了需要完全控制bean处理的场景外,通常建议使用它而不是普通的BeanFactory
。在ApplicationContext
(例如GenericApplicationContext
实现)中,按照约定(即按bean名称或bean类型—特别是后置处理器)检测几种bean,而普通的DefaultListableBeanFactory
不知道任何特殊的bean。
对于许多扩展的容器特性,比如注解处理和AOP代理, BeanPostProcessor
扩展点(BeanPostProcessor
extension point)是必不可少的。如果只使用普通的DefaultListableBeanFactory
,默认情况下不会检测和激活此类后置处理器。这种情况可能令人困惑,因为您的bean配置实际上没有任何问题。相反,在这种情况下,需要通过额外的设置来完全引导容器。
下表列出了BeanFactory
和ApplicationContext
接口和实现提供的特性。
特征 | BeanFactory | ApplicationContext |
---|---|---|
Bean实例化/装配(Bean instantiation/wiring) | Yes | Yes |
整合的生命周期管理(Integrated lifecycle management) | No | Yes |
自动BeanPostProcessor 注册(Automatic BeanPostProcessor registration) | No | Yes |
自动BeanFactoryPostProcessor 注册(Automatic BeanFactoryPostProcessor registration) | No | Yes |
方便的MessageSource 访问(用于国际化)(Convenient MessageSource access (for internationalization)) | No | Yes |
内置ApplicationEvent 发布机制(Built-in ApplicationEvent publication mechanism) | No | Yes |
要显式地用DefaultListableBeanFactory
注册一个bean后置处理器,你需要通过编程方式调用addBeanPostProcessor
,如下面的例子所示:
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 将Bean定义填充到Bean工厂
// 注册任何BeanPostProcessor实例
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());
// 开始使用Bean工厂
要将BeanFactoryPostProcessor
应用到一个普通的DefaultListableBeanFactory
,你需要调用它的postProcessBeanFactory
方法,如下面的例子所示:
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));
// 从Properties文件引入一些属性值
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));
// 现在做替换
cfg.postProcessBeanFactory(factory);
在这两种情况下,显式注册步骤都不方便,这就是为什么在Spring支持的应用程序中,各种ApplicationContext
变体比普通的DefaultListableBeanFactory
更受欢迎,特别是在典型的企业设置中依赖BeanFactoryPostProcessor
和BeanPostProcessor
实例来扩展容器功能时。
提示
一个
AnnotationConfigApplicationContext
已经注册了所有通用的注解后置处理器,并且可以通过配置注解(如@EnableTransactionManagement
)在底层引入额外的处理器。在Spring基于注解的配置模型的抽象层,bean后置处理器的概念变成了仅仅是内部容器细节。