Spring进阶 - IoC容器(2)

书接上文

自定义 bean 的行为(Nature)

Spring Framework 提供了许多可用于自定义 bean 行为的接口。依次讲这三种接口:

生命周期回调

想要介入容器的 bean 生命周期管理,可以实现 Spring InitializingBean 和 DisposableBean 接口。前者调用 afterPropertiesSet()初始化,后者调用 destroy() 销毁 ​​bean 。

JSR-250 规范中的 @PostConstruct@PreDestroy 注释通常被认为是在现代 Spring 应用程序中接收生命周期回调的最佳实践。使用这些注释可以使 bean 解耦于这些 Spring 的接口。详见下文。

如果不想使用 JSR-250 规范的注释但仍想解耦,可以使用 init-method 和 destroy-method。

在内部,Spring Framework 使用 BeanPostProcessor 实现来处理它可以找到的任何回调接口并调用对应的方法。如果需要自定义其他 Spring 默认不提供的功能或生命周期行为,可以 BeanPostProcessor 自己实现。

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

接下来讲生命周期回调接口。

初始化回调

org.springframework.beans.factory.InitializingBean 接口允许在容器设置 bean 的所有必要属性后进行初始化工作。InitializingBean 接口规定了一个方法:

1
void afterPropertiesSet() throws Exception;

当然,上文说了,并不推荐使用 InitializingBean 接口,更好的做法是使用 @PostConstruct 方法或者指定 POJO 初始化方法。可以使用 XML 中的 init-method 属性或者 @Bean 注解的 initMethod 指定一个返回 void 且无参的方法作为初始化方法。

销毁回调

对应的 org.springframework.beans.factory.DisposableBean 接口规定了一个销毁前执行的回调:

1
void destroy() throws Exception;

同理,建议 @PreDestroy 和 destroy-method 的 XML 配置或 @Bean 的 destroyMethod 属性。

默认初始化和析构方法

你可以将所有的初始化、销毁的方法使用相同的命名(比如 init、initialize、dispose 等),那么就可以定义默认初始化和析构方法。

举例:

1
2
3
4
5
6
7
<beans default-init-method="init">

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

</beans>

Spring 容器保证在为 bean 提供所有依赖项后立即调用已配置的初始化回调,所以初始化回调会在 AOP 作用之前。首先完全创建 bean,然后配置带有拦截器链的 AOP 代理。所以在 init 方法上使用拦截器可能会导致和预想不一致,因为这样会导致目标 bean 的生命周期与代理或拦截器耦合,代码与原始目标 bean 杂糅的语义就会难以预测。

组合生命周期机制

如果使用了多种生命周期机制,他们的先后顺序如下:

  1. 用注释方法注释 @PostConstruct
  2. InitializingBean 接口定义的 afterPropertiesSet() 回调
  3. 自定义配置的 init() 方法

Destroy 方法以相同的顺序调用:

  1. 用注释方法注释 @PreDestroy
  2. DisposableBean 接口定义的 destroy() 回调
  3. 自定义配置的 destroy() 方法

如果是同一个函数,那么只会执行一次。

启动和关闭回调

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

1
2
3
4
5
6
7
8
public interface Lifecycle {

void start();

void stop();

boolean isRunning();
}

任何 Spring 管理的对象都可以实现 Lifecycle 接口。当 ApplicationContext 接收到启动和停止信号时(例如,对于运行时的停止/重启场景),它将这些调用级联到容器内定义的所有 Lifecycle 实现,这个过程由 LifecycleProcessor 实现:

1
2
3
4
5
6
public interface LifecycleProcessor extends Lifecycle {

void onRefresh();

void onClose();
}

LifecycleProcessor 是 Lifecycle 接口的扩展。它又添加了另外两种方法来响应刷新和关闭的容器。

值得注意的是,常规 org.springframework.context.Lifecycle 接口是显式启动和停止通知的简单合约,并不意味着在上下文刷新时自动启动。要对特定 bean 的自动启动(包括启动阶段)进行细粒度控制,需要实现 org.springframework.context.SmartLifecycle

如果要控制顺序,可以再继承 Phased 接口:

1
2
3
4
public interface Phased {

int getPhase();
}

SmartLifecycle 就继承了这个接口:

1
2
3
4
5
6
public interface SmartLifecycle extends Lifecycle, Phased {

boolean isAutoStartup();

void stop(Runnable callback);
}

Phase 表示相位,启动时从低相位开始执行,停止时低相位最后停止。换句话说,Integer.MIN_VALUE 相位的第一个开始、最后一个停止,而 Integer.MAX_VALUE 最后启动并首先停止。未定义的相位默认为 0。

SmartLifecycle 定义了 stop 回调方法。任何实现在关闭过程完成之后都必须调用其 callbackrun() 方法,这样就可以在必要时启用异步关闭。LifecycleProcessor 接口的默认实现 DefaultLifecycleProcessor 等待每个阶段内对象组的超时值来调用该回调,默认的每阶段超时为 30 秒。也可以通过定义名为 lifecycleProcessor 的 bean 来覆盖默认生命周期处理器实例(不定义就是默认的 DefaultLifecycleProcessor)。如果只想修改超时,则定义以下内容就足够了:

1
2
3
4
<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
<!-- timeout value in milliseconds -->
<property name="timeoutPerShutdownPhase" value="10000"/>
</bean>

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

Spring 的基于 Web 的 ApplicationContext 实现已经可以在相关 Web 应用程序关闭时正常关闭 Spring IoC 容器。本节仅适用于非 Web 应用程序。

如果在非 Web 应用程序环境中使用 Spring 的 IoC 容器(例如,在客户机桌面环境中),请使用 JVM 注册 shutdown hook。这样做可确保正常关闭并在单例 bean 上调用相关的 destroy 方法,以便释放所有资源。我们依然必须正确配置和实现这些 destroy 回调。

要注册 shutdown hook,需要调用接口 registerShutdownHook() 上声明的 ConfigurableApplicationContext 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

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

ApplicationContextAware 和 BeanNameAware

当 ApplicationContext 创建实现 org.springframework.context.ApplicationContextAware 接口的对象实例时,将为该实例提供对该 ApplicationContext 的引用。 ApplicationContextAware 接口的定义如下:

1
2
3
public interface ApplicationContextAware {
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

因此,bean 可以通过 ApplicationContext 接口以编程方式操作创建它们的 ApplicationContext,或者通过将引用转换为此接口的已知子类(例如 ConfigurableApplicationContext,包括其他功能)。一种作用是可以操作其他 bean。有时这种能力很有用。但是通常我们应该避免使用它,因为它会将代码耦合到 Spring 且不遵循 IoC 规范,其中协作者作为属性提供给 bean。 ApplicationContext 还提供对文件资源的访问,发布应用程序事件和对 MessageSource 的访问,详见下文。

从 Spring 2.5 开始,自动装配是另一种获取 ApplicationContext 引用的方法。“传统”构造函数和 byType 自动装配模式(见上文的自动装配协作者)可以分别为构造函数参数或 setter 方法参数提供 ApplicationContext 类型的依赖关系。 为了获得更大的灵活性,包括自动装配字段和多参数方法的能力,我们可以使用基于注释的新自动装配功能。如果带有 @Autowired 注解的字段,构造函数或方法需要 ApplicationContext 类型的参数,那么 ApplicationContext 将会被注入。详见下文的 @Autowired 注解。

BeanNameAware 可以获取到该类实现的 bean 的 name,接口定义如下:

1
2
3
public interface BeanNameAware {
void setBeanName(String name) throws BeansException;
}

会在全体普通 bean 属性定义之后但在初始化回调之前(例如 InitializingBean,afterPropertiesSet 或自定义 init 方法)之前调用回调。

其他 Aware 接口

名称 注入的依赖 解释
ApplicationContextAware ApplicationContext
ApplicationEventPublisherAware 封闭 ApplicationContext 的事件发布者
BeanClassLoaderAware 用于加载 bean 类的类加载器
BeanFactoryAware BeanFactory
BeanNameAware 声明的 bean 的名称
BootstrapContextAware 运行容器的资源适配器 BootstrapContext,仅在 JCA-aware ApplicationContext 的实例中可用
LoadTimeWeaverAware 用于在加载时处理类定义的 weaver
MessageSourceAware 用于解析消息的策略(支持参数化和国际化)
NotificationPublisherAware Spring JMX 通知发布者
ResourceLoaderAware 用于对资源进行低级访问的加载器
ServletConfigAware 运行容器的 ServletConfig,仅在 web-aware Spring ApplicationContext 中可用
ServletContextAware 运行容器的 ServletContext,仅在 web-aware Spring ApplicationContext 中可用

再次提醒,使用这些接口会将代码耦合到 Spring API 且不遵循 IoC 规范。除非是需要以代码方式访问容器的基础架构 bean,否则不建议使用这些方式。

bean 定义的继承

bean 定义可以包含许多配置信息,包括构造函数参数,属性值和特定于容器的信息,例如初始化方法,静态工厂方法名称等。子 bean 定义从父定义继承配置数据。子定义可以覆盖某些值或根据需要添加其他值。使用继承 bean 定义可以节省大量的输入。这是一种模板模型。

在 ApplicationContext 级别上,子 bean 定义使用 ChildBeanDefinition 类表示,大多数时候我们并不用在这个级别上去做定义。以下是一个简单定义:

1
2
3
4
5
6
7
8
9
10
11
12
<bean id="inheritedTestBean" abstract="true"
class="org.springframework.beans.TestBean">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>

<bean id="inheritsWithDifferentClass"
class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBean" init-method="initialize">
<property name="name" value="override"/>
<!-- the age property value of 1 will be inherited from parent -->
</bean>

子 bean 定义从父级继承 scope,构造函数参数值,属性值和方法覆盖并带有添加新值的选项。指定的 scope,初始化方法,销毁方法或静态工厂方法设置都会覆盖相应的父设置。

也有些不会被继承:依赖、自动装配模式、依赖检查、单例、懒加载。

前面的示例通过使用 abstract 属性将父 bean 定义显式标记为 abstract。 如果父定义未指定类,则需要将父 bean 定义显式标记为 abstract,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
<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 的 bean 只做模板 bean 定义。如果尝试强行 getBean 则会报错。容器的 preInstantiateSingletons() 方法会忽略 abstract 的 bean 定义。

注:ApplicationContext 默认情况下预先实例化所有单例。因此,重要的是(至少对于单例 bean),如果你有一个(父)bean 定义,你只打算用作模板,并且这个定义指定了一个类,你必须确保将 abstract 属性设置为 true 否则应用程序上下文将实际(尝试)预先实例化 abstract bean。

容器扩展点

通常,应用程序开发人员不需要继承 ApplicationContext 实现类。 相反,可以通过插入特殊集成接口的实现来扩展 Spring IoC 容器。 接下来的几节将介绍这些集成接口。

使用 BeanPostProcessor 自定义 bean

BeanPostProcessor 接口实现举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package scripting;

import org.springframework.beans.factory.config.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;
}
}

BeanPostProcessor 接口允许用户实现自己的(或覆盖容器的默认)实例化逻辑、依赖关系解析逻辑等。如果要在 Spring 容器完成实例化,配置和初始化 bean 之后实现某些自定义逻辑,则可以插入一个或多个自定义 BeanPostProcessor 实现。多个 BeanPostProcessor 实例可以通过实现 Ordered 接口并设置 order 属性来控制执行顺序。

BeanPostProcessor 实例的范围是每个容器的范围。 仅当我们使用容器层次结构时,这才是相关的。 如果在一个容器中定义 BeanPostProcessor,它只对该容器中的 bean 进行后处理。 换句话说,在一个容器中定义的 bean 不会被另一个容器中定义的 BeanPostProcessor 进行后处理,即使两个容器都是同一层次结构的一部分。

如果要更改的是 bean 定义,我们可以使用 BeanFactoryPostProcessor,详见下文。

org.springframework.beans.factory.config.BeanPostProcessor 接口由两个回调方法组成。当这样的类被注册为容器的后处理器(post-processor)时,对于容器创建的每个 bean 实例,后处理器在容器初始化方法之前从容器中获取回调(比如 InitializingBean.afterPropertiesSet() 或 bean 声明的 init 方法)。后处理器可以对 bean 实例执行任何操作,甚至可以让它忽略回调。bean 后处理器通常用于检查回调接口,或者用来将 bean 包装成代理。一些 Spring AOP 的基础设施类就是用 bean 后处理器实现的,以便提供代理包装逻辑。

ApplicationContext 会自动检测所有实现 BeanPostProcessor 接口的 bean,并将他们注册为后处理器,以便稍后在创建 bean 时调用。后处理器可以用任何 bean 注册方式部署在容器中。

注意,在配置类上使用 @Bean 工厂方法声明 BeanPostProcessor 时,工厂方法的返回类型应该是实现类本身,或者至少是 org.springframework.beans.factory.config.BeanPostProcessor 接口,指示该 bean 的后处理器性质。否则,ApplicationContext 无法在完全创建之前按类型自动检测到它。由于 BeanPostProcessor 需要尽早实例化以便应用于上下文中其他 bean 的初始化,因此这种早期类型检测至关重要。

以编程方式注册 BeanPostProcessor 实例

虽然 BeanPostProcessor 注册的推荐方法是通过 ApplicationContext 自动检测(如前所述),但也可以使用 addBeanPostProcessor 方法以编程方式对 ConfigurableBeanFactory 注册它们。当您需要在注册前评估条件逻辑或甚至跨层次结构中的上下文复制 Bean 后处理器时,这非常有用。 但请注意,以编程方式添加的 BeanPostProcessor 实例不遵循 Ordered 接口。这里,注册的顺序决定了执行的顺序。另请注意,以编程方式注册的 BeanPostProcessor 实例始终在通过自动检测注册的实例之前处理,而不管任何显式排序。

BeanPostProcessor 实例和 AOP 自动代理

BeanPostProcessor 是不能使用 AOP 的。实现 BeanPostProcessor 接口的类是特殊的,容器会对它们进行不同的处理。作为 ApplicationContext 的特殊启动阶段的一部分,它们直接引用的所有 BeanPostProcessor 实例和 bean 都在启动时实例化。接下来,所有 BeanPostProcessor 实例都以排序方式注册,并应用于容器中的所有其他 bean。因为 AOP 自动代理是作为 BeanPostProcessor 本身实现的,所以 BeanPostProcessor 实例和它们直接引用的 bean 都不符合自动代理的条件,因此没有编入方法。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package scripting;

import org.springframework.beans.factory.config.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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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>
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;

public final class Boot {

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

}

输出:

1
2
Bean'sensenger'创建:org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961

将回调接口或注释与自定义 BeanPostProcessor 实现结合使用是扩展 Spring IoC 容器的常用方法。一个例子是 Spring 的 RequiredAnnotationBeanPostProcessor —— 一个 Spring 提供的 BeanPostProcessor 实现,它保证用 @Required(或者其他自定义的)注释标记的 bean 上的属性一定有值注入。

使用 BeanFactoryPostProcessor 定制配置项元数据

我们看到的下一个扩展点是 org.springframework.beans.factory.config.BeanFactoryPostProcessor。此接口的语义类似于 BeanPostProcessor 的语义,但有一个主要区别:BeanFactoryPostProcessor 对 bean 配置元数据进行操作。也就是说,Spring IoC 容器允许 BeanFactoryPostProcessor 读取配置元数据,并可能在容器实例化除 BeanFactoryPostProcessor 实例之外的任何 bean 之前更改它。

您可以配置多个 BeanFactoryPostProcessor 实例,并且可以通过设置 order 属性来控制这些 BeanFactoryPostProcessor 实例的运行顺序。但是,如果 BeanFactoryPostProcessor 实现 Ordered 接口,则只能设置此属性。如果编写自己的 BeanFactoryPostProcessor,则应考虑实现 Ordered 接口。有关更多详细信息,请参阅 BeanFactoryPostProcessor 和 Ordered 接口的 javadoc。

如果要更改实际的 bean 实例(即,从配置项元数据创建的对象),则需要使用 BeanPostProcessor(前面在使用 BeanPostProcessor 定制 Bean 中进行了描述)。虽然技术上可以在 BeanFactoryPostProcessor 中使用 bean 实例(例如,通过使用 BeanFactory.getBean()),但这样做会导致过早的 bean 实例化,从而违反标准的容器生命周期。这可能会导致负面影响,例如绕过 bean 后期处理。

此外,BeanFactoryPostProcessor 实例的范围是每个容器的范围。仅当您使用容器层次结构时,这才有意义。如果在一个容器中定义 BeanFactoryPostProcessor,则它仅应用于该容器中的 bean 定义。一个容器中的 Bean 定义不会被另一个容器中的 BeanFactoryPostProcessor 实例后处理,即使两个容器都是同一层次结构的一部分。

BeanFactoryPostProcessor 在 ApplicationContext 中声明时自动执行,以便将更改应用于定义容器的配置元数据。Spring 包含许多预定义的 bean 工厂后处理器,例如 PropertyOverrideConfigurer 和 PropertyPlaceholderConfigurer。您还可以使用自定义 BeanFactoryPostProcessor —— 例如,注册自定义属性编辑器。

ApplicationContext 自动检测部署到其中的任何实现 BeanFactoryPostProcessor 接口的 bean。它在适当的时候使用这些 bean 作为 bean factory post-processor。您可以像配置其他 bean 一样配置这些 post-processor bean。

示例:类名替换器 PropertyPlaceholderConfigurer

您可以使用 PropertyPlaceholderConfigurer 将 bean 定义中的属性值分离到外部 Java Properties 文件中。这样做可以使部署应用程序的人员自定义不同环境的属性,例如数据库 URL 和密码,而不会出现修改主 XML 定义文件或容器文件的复杂性或风险。

下面的基于 XML 的配置元数据片段,其中 DataSource 定义了占位符值:

1
2
3
4
5
6
7
8
9
10
11
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<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>

该示例显示了从外部属性文件配置的属性。在运行时,PropertyPlaceholderConfigurer 应用于替换 DataSource 的某些属性的元数据。要替换的值被指定为 ${property-name} 形式的占位符,它遵循 Ant 和 log4j 以及 JSP EL 样式。

实际值来自标准 Java Properties 格式的另一个文件:

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

然后 ${jdbc.username} 字符串在运行时将替换为值’sa’,其他占位符值也是类似。PropertyPlaceholderConfigurer 检查 bean 定义的大多数属性和属性中的占位符。此外,您可以自定义占位符前缀和后缀。

使用 Spring 2.5 中引入的 context 命名空间,您可以使用专用配置元素配置属性占位符。 您可以在 location 属性中以逗号分隔列表的形式提供一个或多个位置,如以下示例所示:

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

PropertyPlaceholderConfigurer 不仅在您指定的属性文件中查找属性。 默认情况下,如果它在指定的属性文件中找不到属性,它还会检查 Java System 属性。 您可以通过使用以下三个受支持的整数值之一设置 configurer 的 systemPropertiesMode 属性来自定义此行为:

  • never (0):从不检查系统属性。
  • fallback(1):如果在指定的属性文件中无法解析,则检查系统属性。这是默认值。
  • override(2):在尝试指定的属性文件之前,首先检查系统属性。这使系统属性可以覆盖任何其他属性源。

有关 PropertyPlaceholderConfigurer 更多信息,请参阅 javadoc。

另,你甚至可以使用 PropertyPlaceholderConfigurer 替换类名称,这在您必须在运行时选择特定实现类时有时很有用。以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<value>classpath:com/something/strategy.properties</value>
</property>
<property name="properties">
<value>custom.strategy.class=com.something.DefaultStrategy</value>
</property>
</bean>

<bean id="serviceStrategy" class="${custom.strategy.class}"/>

如果在运行时无法将类解析为有效类, 在即将创建 bean 时,bean 的解析将失败。对于非懒加载的 bean,这会发生在 ApplicationContext 的 preInstantiateSingletons() 阶段期间。

示例:PropertyOverrideConfigurer

PropertyOverrideConfigurer 是另一个 bean 工厂后处理器,类似于 PropertyPlaceholderConfigurer,但与后者不同,原始定义可以具有默认值,或者根本不具有 bean 属性的值。如果重写的 Properties 文件没有某个 bean 属性的条目,则使用默认的上下文定义。

需要注意的是,bean 定义不会感知到被覆盖,因此无法从 XML 定义文件中立即看出正在使用覆盖配置器。 如果多个 PropertyOverrideConfigurer 实例为同一个 bean 属性定义了不同的值,那么根据覆盖机制,最后一个实例将生效。

属性文件配置行采用以下格式:

1
beanName.property=value

例如:

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

复合属性名称也是支持的,只要路径的每个组件(重写的最终属性除外)都已经非空(可能由构造函数初始化)。在下面的示例中,tom bean 的 fred 属性的 bob 属性的 sammy 属性设置为 123:

1
tom.fred.bob.sammy = 123

使用 Spring 2.5 中引入的 context 命名空间,可以使用专用配置元素配置属性覆盖,如以下示例所示:

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

使用 FactoryBean 自定义实例化逻辑

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

FactoryBean 接口在 Spring IoC 容器实例化逻辑中是可拔插的。如果 bean 的初始化代码相对复杂,更适合以 Java 代码表达,而不是(可能)冗长的 XML,那么我们可以创建自己的 FactoryBean,在该类中编写复杂的初始化,然后将自定义 FactoryBean 插入容器中。

FactoryBean 接口有三个方法:

  • Object getObject():返回此工厂创建的对象的实例。可以共享实例,具体取决于此工厂是返回单例还是原型。

  • boolean isSingleton():如果此 FactoryBean 返回单例,则返回 true,否则返回 false。

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

FactoryBean 概念和接口被用在 Spring Framework 中的许多位置。Spring 提供了 50 多个 FactoryBean 接口的实现。

如果需要向容器询问实际的 FactoryBean 实例本身,那么在调用 ApplicationContext 的 getBean() 方法时需要加上&符号作为 bean 的 id 前缀。比如,对于 id 为 myBean 的 FactoryBean,在容器上调用 getBean("myBean") 将返回 FactoryBean 的产品,调用 getBean(“&myBean”)将返回 FactoryBean 实例本身。

基于注解的容器配置

这里有一个讨论,配置 Spring 的注解是否比 XML 更好?

注解在其声明中提供了大量上下文,从而可以使配置更短更简洁。而 XML 可以在不触及源代码或重新编译它们的情况下连接组件。
有些人更喜欢将注入靠近源,也有人认为有注解的类不再是 POJO,而且配置变得分散且难以控制。

无论选择如何,Spring 都可以兼顾两种风格,甚至可以将它们混合在一起。值得指出的是,通过其 JavaConfig 选项,Spring 允许以非侵入方式使用注释,而无需触及目标组件源代码,并且在工具方面,Spring Tool Suite 支持所有配置样式。

基于注释的配置提供了 XML 设置的替代方案,该配置依赖于字节码元数据来连接组件而不是角括号声明。开发人员不是使用 XML 来描述 bean 连接,而是通过在相关的类,方法或字段声明上使用注释将配置移动到组件类本身。如示例中所述:RequiredAnnotationBeanPostProcessor,将 BeanPostProcessor 与注释结合使用是扩展 Spring IoC 容器的常用方法。例如,Spring 2.0 引入了使用 @Required 注释强制执行所需属性的可能性。 Spring 2.5 使得有可能采用相同的通用方法来驱动 Spring 的依赖注入。从本质上讲,@Autowired 注释提供的功能与自动装配协作者中描述的相同,但具有更细粒度的控制和更广泛的适用性。Spring 2.5 还增加了对 JSR-250 注释的支持,例如 @PostConstruct@PreDestroy。Spring 3.0 增加了对 javax.inject 包中包含的 JSR-330(Java 的依赖注入)注释的支持,例如 @Inject@Named。有关这些注释,详见下文。

注:注解注入在 XML 注入之前执行。因此,对于同事通过这两种方法注入的属性,XML 配置会覆盖注解的配置。

和之前一样,可以将它们注册为单独的 bean 定义,但也可以通过在基于 XML 的 Spring 配置中包含以下标记来隐式注册它们(请注意包含 context 命名空间)

1
2
3
4
5
6
7
8
9
10
11
<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>

(隐式注册的 post-processors 包括 AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, PersistenceAnnotationBeanPostProcessor,和前面提到的 RequiredAnnotationBeanPostProcessor。)

<context:annotation-config/> 仅在定义它的同一应用程序上下文中查找 bean 上的注释。这意味着,如果将 <context:annotation-config/> 放在 DispatcherServlet 的 WebApplicationContext 中,它只检查控制器中有 @Autowired注解的 bean,而不检查您的服务。有关更多信息,请参阅 DispatcherServlet。

@Required

@Required 注释可以用于 bean 属性 setter 方法,如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

// ...
}

这个注解表示被修饰的 bean 属性必须在配置时通过 bean 定义中的显式属性值,或通过 autowiring 装配。 如果被修饰的 bean 属性没有被填充,容器就会抛出异常,以后避免以后抛出 NullPointerException 等。不过最好还是将断言放入 bean 类本身(比如 init 方法内),这样如果在容器外部使用类,这样做也会强制执行那些必需的引用和值。

注:@Required 注释从 Spring Framework 5.1 开始正式弃用,最好使用构造函数注入所需的设置(或者自定义实现 InitializingBean.afterPropertiesSet() 以及 bean 属性的 setter 方法)。

@Autowired

在本节所包含的示例中,可以使用 JSR 330 的@Inject 注释代替 Spring 的@Autowired 注释。

@Autowired 注解可以用于构造函数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {

private final CustomerPreferenceDao customerPreferenceDao;

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

// ...
}

从 Spring Framework 4.3 开始,如果目标 bean 只定义了一个构造函数入口,则不再需要在这样的构造函数上使用@Autowired 注释。但是,如果有多个构造器可用,则必须注释至少一个构造器以告诉容器使用哪一个。

@Autowired 注解也可用于“传统” setter 方法:

1
2
3
4
5
6
7
8
9
10
11
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

// ...
}

甚至是具有任意名称和多个参数的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MovieRecommender {

private MovieCatalog movieCatalog;

private CustomerPreferenceDao customerPreferenceDao;

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

// ...
}

也可以应用于字段,甚至可以将它与构造函数混合使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MovieRecommender {

private final CustomerPreferenceDao customerPreferenceDao;

@Autowired
private MovieCatalog movieCatalog;

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

// ...
}

注:确保目标组件(例如,MovieCatalog 或 CustomerPreferenceDao)始终按照用于@Autowired 注释注入点的类型声明。否则,由于在运行时未找到类型匹配,注入可能会失败。

对于通过类路径扫描找到的 XML 定义的 bean 或组件类,容器通常预先知道具体类型。但是,对于 @Bean 工厂方法,您需要确保声明的返回类型足够准确。对于实现多个接口的组件或可能由其实现类型引用的组件,请考虑在工厂方法上声明最具体的返回类型(至少与引用 bean 的注入点所需的特定类型一致)。

甚至可以用于数组,ApplicationContext 会提供符合类型的所有 bean:

1
2
3
4
5
6
7
public class MovieRecommender {

@Autowired
private MovieCatalog[] movieCatalogs;

// ...
}

乃至 Collection 类型:

1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {

private Set<MovieCatalog> movieCatalogs;

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

// ...
}

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

您可以在目标类级别和 @Bean 方法上声明 @Order 注解,可能是通过单个 bean 定义(在多个定义使用相同 bean 类的情况下)。@Order 值可能影响注入时的优先级,但要注意这并不会影响启动的顺序,这是由依赖关系和 @DependsOn 声明正交决定的。

请注意,标准的 javax.annotation.Priority 注释在 @Bean 级别不可用,因为它无法在方法上声明。它的语义可以通过 @Order 值或是指定 @Primary 在同类型的某一个 bean 上实现。

即使是 Map,只要 key 类型是 String,也可以被自动装配。Map 值包含所有期望类型的 bean,并且键包含相应的 bean 名称,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
public class MovieRecommender {

private Map<String, MovieCatalog> movieCatalogs;

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

// ...
}

默认情况下,当给定注入点没有匹配的候选 bean 时,自动装配会失败。对于声明的数组,集合或映射,至少需要一个匹配元素。

默认情况下 @Autowired 注解的方法和字段是必须的依赖项。我们也可以通过修改 required 参数来改为非必要,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

// ...

}

并且,如果依赖项(可能是多个参数依赖项中的某一个)无法注入,那么这个被注解的方法都完全不会被调用(比如上文的 setMovieFinder)。

注入构造函数和工厂方法参数是一种特殊情况,因为 @Autowired 上的 required 标志具有一些不同的含义,因为 Spring 的构造函数解析算法可能要处理多个构造函数。默认情况下构造函数和工厂方法参数需要保证有效,但在单构造函数场景中有一些特殊规则,例如,如果多元素注入点(数组,集合,Map)没有匹配的 bean 可用,则解析为空(empty)的实例。这样所有的依赖关系都可以在同一个多参的构造函数中声明,比如可以声明为一个没有 @Autowired 注解的公共构造函数。

每个类只能标记一个带 required 注解的构造函数,但可以标注多个非 required 注解的构造函数。在这种情况下,每个构造函数都是可选的,Spring 会使用满足依赖性的“最贪心”(具有最多参数的构造函数)的构造函数。构造函数解析算法与具有重载构造函数的非注释类相同,只是将候选者缩小到带注释的构造函数。

建议使用 @Autowiredrequired 属性而不是 setter 方法的 @Required 注释。required 属性表示该属性不是自动装配所必需的。如果无法自动装配,则会忽略该属性。另一方面,@Required 更强大,因为它要求容器必须通过任意容器支持的方式设置属性。如果未定义任何值,则会引发相应的异常。

另外,Java 8 提供了 java.util.Optional 特性来解决这个问题:

1
2
3
4
5
6
7
public class SimpleMovieLister {

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

从 Spring Framework 5.0 开始,还可以使用 @Nullable 注释(任何包中的任何类型的注释 - 比如 JSR-305 中的javax.annotation.Nullable ):

1
2
3
4
5
6
7
public class SimpleMovieLister {

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

您还可以将 @Autowired 用于众所周知的可解析依赖项的接口:BeanFactory,ApplicationContext,Environment,ResourceLoader,ApplicationEventPublisher 和 MessageSource。 这些接口及其扩展接口(如 ConfigurableApplicationContext 或 ResourcePatternResolver)将自动解析,无需特殊设置。 以下示例自动装配 ApplicationContext 对象:

1
2
3
4
5
6
7
8
9
10
public class MovieRecommender {

@Autowired
private ApplicationContext context;

public MovieRecommender() {
}

// ...
}

@Autowired@Inject@Value@Resource 注解由 BeanPostProcessor 实现处理。 这意味着我们不能在自己的 BeanPostProcessor 或 BeanFactoryPostProcessor 类型中应用这些注释。必须使用 XML 或 Spring @Bean 方法显式地“连接”这些类型。

@Primary

由于按类型自动装配可能会导致多个候选项,因此通常需要对选择过程进行更多控制。实现这一目标的一种方法是使用 Spring 的 @Primary 注释。@Primary 表示当多个 bean 可以自动装配到单值依赖项时,应该优先选择特定的 bean。如果候选项中只存在一个 primary 的 bean,那么它就是自动装配的值。

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class MovieConfiguration {

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

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

// ...
}
1
2
3
4
5
6
7
public class MovieRecommender {

@Autowired
private MovieCatalog movieCatalog;

// ...
}

MovieRecommender 将会自动装配 firstMovieCatalog

限定符微调自动装配

@Primary 可以确定一个主要候选者。当您需要更准确控制选择过程时,可以使用 Spring 的 @Qualifier 注释。您可以将限定符值与特定参数相关联,缩小类型匹配集,以便为每个参数选择特定的 bean。有个简单的例子:

1
2
3
4
5
6
7
8
public class MovieRecommender {

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

// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

// ...
}

作为降级,bean 名称被视为默认 qualifier 的值。因此,您可以使用 bean 的 id 得到相同的匹配结果。但是,虽然您可以使用此约定来按名称引用特定 bean,限定符值在类型匹配集中只是具有缩小的语义,它们在语义上不表示对唯一 bean id 的引用。良好的限定符值是 main 或 EMEA 或 persistent,类似这些,不同于自动生成的 bean id。

限定符也适用于类型化集合,比如前面的 Set<MovieCatalog>。在这种情况下,根据声明的限定符,所有匹配的 bean 都作为集合注入。这意味着限定符不必是唯一的,它们只是一个过滤条件。例如,您可以使用相同的限定符值 action 定义多个 MovieCatalog bean,所有这些 bean 都注入到使用 @Qualifier("action") 注解的 Set<MovieCatalog> 中。

在类型匹配候选项中,根据目标 bean 名称选择限定符值,在注入点不需要 @Qualifier 注释。如果没有其他解析指示符(例如限定符或主要标记),则对于非唯一依赖性情况,Spring 会将注入点名称(即字段名称或参数名称)与目标 bean 名称进行匹配,然后选择同名的候选人,如果有的话。

也就是说,如果您打算按名称表达注释驱动的注入,请不要主要使用 @Autowired,即使它能够在类型匹配候选项中通过 bean 名称进行选择。相反,使用 JSR-250 @Resource 注释,该注释在语义上定义为通过其唯一名称标识特定目标组件,声明的类型与匹配过程无关。 @Autowired 具有相当不同的语义:在按类型选择候选 bean 之后,仅在那些类型选择的候选项中考虑指定的字符串限定符值(例如,将 account 限定符与标记有相同限定符标签的 bean 匹配)。

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

从 4.3 开始,@Autowired 还会考虑自引用注入(即,引用回到当前注入的 bean)。请注意,自我注入是一种后备。对其他组件的常规依赖性始终具有优先权。从这个意义上说,自我引用并不参与常规的候选人选择,因此从不是主要的。相反,它们总是最低优先级。在实践中,您应该仅使用自引用作为最后的手段(例如,通过 bean 的事务代理调用同一实例上的其他方法)。考虑在这种情况下将受影响的方法分解为单独的委托 bean。或者,您可以使用 @Resource,它可以通过其唯一名称获取代理回到当前 bean。

@Autowired 适用于字段,构造函数和多参数方法,允许在参数级别缩小限定符注释。相比之下,@Resource 仅支持字段和具有单个参数的 bean 属性 setter 方法。因此,如果注射目标是构造函数或多参数方法,则应该使用限定符。

我们可以创建自己的自定义限定符注释,需要定义注释并在定义中提供 @Qualifier 注释,如以下示例所示:

1
2
3
4
5
6
7
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {

String value();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MovieRecommender {

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

private MovieCatalog comedyCatalog;

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

// ...
}

接下来,您可以提供候选 bean 定义的信息。 您可以将 <qualifier/> 标记添加为 <bean/> 标记的子元素,然后指定与自定义限定符注释匹配的类型和值。 类型与注释的完全限定类名匹配。或者,为方便起见,如果不存在冲突名称的风险,您可以使用短类名称。以下示例演示了这两种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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>

也可以使用注解:

1
2
3
4
5
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}

这个后文会细讲。

在某些情况下,使用没有值的注释可能就足够了。当注释用于更通用的目的并且可以应用于多种不同类型的依赖项时,这可能很有用。例如,您可以提供可在没有 Internet 连接时搜索的脱机目录:

1
2
3
4
5
6
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {

}
1
2
3
4
5
6
7
8
9
public class MovieRecommender {

@Autowired
@Offline
private MovieCatalog offlineCatalog;

// ...

}

您还可以定义除简单值属性之外或代替简单值属性接受命名属性的自定义限定符注释。如果随后在要自动装配的字段或参数上指定了多个属性值,则 bean 定义必须匹配所有此类属性值才能被视为自动装配候选:

1
2
3
4
5
6
7
8
9
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {

String genre();

Format format();
}
1
2
3
public enum Format {
VHS, DVD, BLURAY
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;

// ...
}

泛型微调自动装配

除了@Qualifier注释之外,您还可以使用 Java 泛型类型作为隐式的限定形式。例如,假设您具有以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class MyConfiguration {

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

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

假设前面的 bean 实现了一个通用接口(即 Store<String>Store<Integer>),你可以 @Autowire Store 接口,并将泛型用作限定符,如下例所示:

1
2
3
4
5
@Autowired
private Store<String> s1; // <String> qualifier, injects the stringStore bean

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

通用限定符也适用于自动装配列表,Map 实例和数组。以下示例自动装配通用 List:

1
2
3
4
// 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;

使用 CustomAutowireConfigurer

CustomAutowireConfigurer 是一个 BeanFactoryPostProcessor,它允许注册自己的自定义限定符注释类型,即使它们没有使用 Spring 的 @Qualifier 注释进行注释。 以下示例显示如何使用 CustomAutowireConfigurer:

1
2
3
4
5
6
7
8
<bean id="customAutowireConfigurer"
class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
<property name="customQualifierTypes">
<set>
<value>example.CustomQualifier</value>
</set>
</property>
</bean>

AutowireCandidateResolver 通过以下方式确定 autowire 候选者:

  • 每个 bean 定义的 autowire-candidate 值

  • <beans/>元素上可用的任何 default-autowire 候选模式

  • 存在 @Qualifier 注释以及使用 CustomAutowireConfigurer 注册的任何自定义注释

当多个 bean 有资格作为 autowire 候选者时,“primary”的确定如下:当候选者中刚好只有一个 bean 定义的 primary 属性设置为 true,就选择它。

使用 @Resource 注入

Spring 还通过对字段或 bean 属性 setter 方法使用 JSR-250 @Resource 注释(javax.annotation.Resource)来支持注入。这是 Java EE 中的常见模式:例如,在 JSF 管理的 bean 和 JAX-WS 端点中。Spring 也支持 Spring 管理对象的模式。

@Resource 采用名称属性。默认情况下,Spring 将该值解释为要注入的 bean 名称。换句话说,它遵循按名称语义,如以下示例所示:

1
2
3
4
5
6
7
8
9
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

如果未明确指定名称,则默认名称是从字段名称或 setter 方法派生的。如果是字段,则采用字段名称。在 setter 方法的情况下,它采用 bean 属性名称。下面的例子将把 movieFinder bean 注入其 setter 方法:

1
2
3
4
5
6
7
8
9
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

注释中的 name 由 ApplicationContext 解析为 bean 名称,由 CommonAnnotationBeanPostProcessor 拿到该名称。如果显式配置 Spring 的 SimpleJndiBeanFactory,则可以通过 JNDI 解析名称。但是,我们建议您依赖于默认行为并使用 Spring 的 JNDI 查找功能来保证间接级别。

在某些特殊情况下 @Resource 不指定明确的名称,与 @Autowired 类似,@Resource 会不看名称,使用主要类型匹配,这些情况包括以下类:BeanFactory, ApplicationContext,ResourceLoader,ApplicationEventPublisher 和 MessageSource 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MovieRecommender {

@Resource
private CustomerPreferenceDao customerPreferenceDao;

@Resource
private ApplicationContext context;

public MovieRecommender() {
}

// ...
}

使用 @PostConstruct 和 @PreDestroy

CommonAnnotationBeanPostProcessor 不仅识别 @Resource 注释,还识别 JSR-250 生命周期注释:javax.annotation.PostConstructjavax.annotation.PreDestroy。在 Spring 2.5 中引入,对这些注释的支持提供了初始化回调销毁回调中描述的生命周期回调机制的替代方法。假如 CommonAnnotationBeanPostProcessor 在 ApplicationContext 中注册,那么带有这些注释的方法会在相应的 Spring 生命周期接口方法或显式声明的回调方法的同时被调用。在以下示例中,缓存在初始化时预填充并在销毁时清除:

1
2
3
4
5
6
7
8
9
10
11
12
public class CachingMovieLister {

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

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

有关组合各种生命周期机制的效果的详细信息,参见上文组合生命周期机制

@Resource 一样,@PostConstruct@PreDestroy 注释类型是 JDK 6 到 8 的标准 Java 库的一部分。但是,整个 javax.annotation 包与 JDK 9 中的核心 Java 模块分离,最终在 JDK 11 中删除。如果需要,现在需要通过 Maven Central 获取 javax.annotation-api 组件,并添加到应用程序的类路径中。

类路径扫描和托管组件

本章中的大多数示例都使用 XML 来指定在 Spring 容器中生成每个 BeanDefinition 的配置。 上一节(基于注释的容器配置)演示了如何通过源级注释提供大量配置元数据。但是,即使在这些示例中,基本 bean 定义也在 XML 文件中显式定义,而注释仅驱动依赖项注入。本节介绍通过扫描类路径隐式检测候选组件的选项。候选组件是与筛选条件匹配的类,并且具有向容器注册的相应 bean 定义。这消除了使用 XML 执行 bean 注册的需要。相反,您可以使用注释(比如@Component),AspectJ 类型表达式或您自己的自定义筛选条件来选择哪些类具有向容器注册的 bean 定义。

从 Spring 3.0 开始,Spring JavaConfig 项目提供的许多功能是核心 Spring Framework 的一部分。这使您可以使用 Java 而不是使用传统的 XML 文件来定义 bean。看看的@Configuration,@Bean, @Import,和@DependsOn 注释有关如何使用这些新功能的例子。

@Component 和更多的 Stereotype 注释

@Repository 注解是实现存储库的角色或构造型(也称为数据访问对象或 DAO)的任何类的标记。 该标记的用途包括自动翻译异常,详见“异常翻译”。

Spring 提供了进一步的构造型注释:@Component@Service@Controller@Component 是任何 Spring 托管组件的通用构造型。 @Repository@Service@Controller@Component 的特化,用于更具体的用例(分别在持久层,服务层和表示层中)。因此,您可以使用 @Component 来注释组件类,但是通过使用 @Repository@Service@Controller 来注释组件类,您的类更适合于通过工具进行处理或与方面相关联。例如,这些构造型注释成为切入点的理想目标。 @Repository@Service@Controller 在 Spring 框架的将来版本中也可以带有其他语义。因此,如果在服务层使用 @Component@Service 之间进行选择,则 @Service 显然是更好的选择。同样,如前所述,@Repository 已被支持作为持久层中自动异常转换的标记。

使用元注释和组合注释

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

1
2
3
4
5
6
7
8
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {

// ....
}

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

您还可以组合元注释来创建“组合注释”。 例如,Spring MVC 中的 @RestController 批注由 @Controller@ResponseBody 组成。

此外,组合注释可以选择从元注释中重新声明属性,以允许自定义。当您只希望公开元注释属性的子集时,此功能特别有用。例如,Spring 的 @SessionScope注释将作用域名称硬编码为会话,但仍允许自定义 proxyMode。以下清单显示了 SessionScope 批注的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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;

}

使用时:

1
2
3
4
5
@Service
@SessionScope
public class SessionScopedService {
// ...
}

您还可以覆盖 proxyMode 的值,如以下示例所示:

1
2
3
4
5
@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
// ...
}

自动检测类并注册 Bean 定义

Spring 可以自动检测构造型类,并向 ApplicationContext 注册相应的 BeanDefinition 实例。 例如,以下两个类别有资格进行这种自动检测:

1
2
3
4
5
6
7
8
9
10
@Service
public class SimpleMovieLister {

private MovieFinder movieFinder;

@Autowired
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
1
2
3
4
@Repository
public class JpaMovieFinder implements MovieFinder {
// implementation elided for clarity
}

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

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
...
}

为简便起见,前面的示例可能使用了 注释的 value 属性(即 @ComponentScan("org.example") )。

扫描类路径包需要在类路径中存在相应的目录条目。使用 Ant 构建 JAR 时,请确保未激活 JAR 任务的仅文件开关。此外,在某些环境中,可能不会基于安全策略公开类路径目录,例如,在 JDK 1.7.0_45 及更高版本上的独立应用程序(这需要在清单中设置“受信任的库”-请参阅 https://stackoverflow.com/Questions/19394570/java-jre-7u45-breaks-classloader-getresources)。
在 JDK 9 的模块路径(Jigsaw)上,Spring 的类路径扫描通常可以按预期进行。但是,请确保将组件类导出到 module-info 描述符中。如果您期望 Spring 调用您的类的非公共成员,请确保它们是“打开的”(也就是说,它们使用 opens 声明而不是描述符中的 exports 声明 module-info)。

此外,当您使用 component-scan 元素时,AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 都隐式包括在内。这意味着将自动检测这两个组件并将它们连接在一起,而这一切都不需要 XML 中提供的任何 bean 配置元数据。

您可以通过将注释配置属性包括为 false 来禁用 AutowiredAnnotationBeanPostProcessor 和 CommonAnnotationBeanPostProcessor 的注册。

使用过滤器自定义扫描

默认情况下,只有使用 @Component@Repository@Service@Controller 进行注释的类或使用 @Component 进行注释的自定义注释是可以被检测到的候选组件。但是,您可以通过应用自定义过滤器来修改和扩展此行为。将它们添加为 @ComponentScan 批注的 includeFilters 或 excludeFilters 参数(或作为 component-scan 元素的 include-filter 或 exclude-filter 子元素)。每个过滤器元素都需要 type 和 expression 属性。 下表描述了过滤选项:

过滤器类型 范例表达 描述
注释(默认) org.example.SomeAnnotation 在目标组件的类型级别上存在的注释。
分配 org.example.SomeClass 目标组件可分配给(扩展或实现)的类(或接口)。
AspectJ org.example..*Service+ 目标组件要匹配的 AspectJ 类型表达式。
正则表达式 org.example.Default.* 要与目标组件类名称匹配的正则表达式。
自定义 org.example.MyTypeFilter org.springframework.core.type.TypeFilter 接口的自定义实现。

以下示例显示了忽略所有 @Repository 注释并改为使用“存根”存储库的配置:

1
2
3
4
5
6
7
@Configuration
@ComponentScan(basePackages = "org.example",
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
excludeFilters = @Filter(Repository.class))
public class AppConfig {
...
}

您还可以通过在注释上设置 useDefaultFilters = false 或通过将 use-default-filters=“false” 作为 <component-scan/> 元素的属性来禁用默认过滤器。这将禁用对@Component@Repository@Service@Controller@Configuration 注释的类的自动检测。

在组件中定义 Bean 元数据

Spring 组件还可以将 bean 定义元数据贡献给容器。您可以 @Bean 使用与在带 @Configuration 注释的类中定义 Bean 元数据相同的注释来执行此操作。以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class FactoryMethodComponent {

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

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

上一类是 Spring 组件,在其 doWork() 方法中具有特定于应用程序的代码。 但是,它也提供了一个具有工厂方法的 bean 定义,该工厂方法引用了方法 publicInstance()@Bean 批注标识工厂方法和其他 bean 定义属性,例如通过 @Qualifier 批注的限定符。可以指定其他的方法级别注释比如 @Scope@Lazy 和自定义限定符注释。

除了用于组件初始化的角色外,您还可以将 @Lazy 批注放置在标有 @Autowired@Inject 的注入点上。在这种情况下,它导致了惰性解析代理的注入。

如前所述,除了支持自动装配的字段和方法,还支持自动装配@Bean 方法。 以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Component
public class FactoryMethodComponent {

private static int i;

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

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

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

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

该示例将 String 方法参数国家/地区自动连线到另一个名为 privateInstance 的 bean 上 age 属性的值。Spring Expression Language 元素通过符号 #{<expression>} 定义属性的值。对于 @Value 批注,表达式解析程序已预先配置为在解析表达式文本时查找 bean 名称。

从 Spring Framework 4.3 开始,您还可以声明类型为 InjectionPoint 的工厂方法参数(或更具体的子类:DependencyDescriptor),以访问触发当前 bean 创建的请求注入点。请注意,这仅适用于实际创建的 Bean 实例,不适用于注入现有实例。因此,此功能对原型范围的 bean 最有意义。对于其他作用域,factory 方法仅在给定作用域中看到触发创建新 bean 实例的注入点(例如,触发创建惰性单例 bean 的依赖项)。 在这种情况下,可以将提供的注入点元数据与语义一起使用。 以下示例显示了如何使用 InjectionPoint:

1
2
3
4
5
6
7
8
@Component
public class FactoryMethodComponent {

@Bean @Scope("prototype")
public TestBean prototypeInstance(InjectionPoint injectionPoint) {
return new TestBean("prototypeInstance for " + injectionPoint.getMember());
}
}

常规 Spring 组件中的 @Bean 方法的处理方式与 Spring @Configuration 类中的 @Bean 方法不同。区别在于,@Component 类没有使用 CGLIB 来拦截方法和字段的调用。 CGLIB 代理是一种调用 @Configuration 类中 @Bean 方法中的方法或字段的方法,用于创建 Bean 元数据引用以协作对象。此类方法不是使用常规 Java 语义调用的,而是通过容器进行的,以提供通常的生命周期管理和 Spring Bean 的代理,即使通过 @Bean 方法的编程调用引用其他 Bean 时也是如此。相反,在普通 @Component 类内的 @Bean 方法中调用方法或字段具有标准 Java 语义,而无需特殊的 CGLIB 处理或其他约束。

您可以将 @Bean 方法声明为静态方法,从而允许在不将其包含配置类创建为实例的情况下调用它们。在定义后处理器 Bean(例如 BeanFactoryPostProcessor 或 BeanPostProcessor 类型)时,这特别有意义,因为此类 Bean 在容器生命周期的早期进行了初始化,并且应避免在那时触发配置的其他部分。

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

@Bean 方法的 Java 语言可见性不会对 Spring 容器中的最终 bean 定义产生直接影响。您可以在非 @Configuration 类中自由声明自己的工厂方法,也可以在任何地方声明静态方法。但是,@Configuration 类中的常规 @Bean 方法必须是可重写的——即,不得将它们声明为 private 或 final。

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

最后,一个类可以为同一个 bean 保留多个 @Bean 方法,这取决于在运行时可用的依赖关系,从而可以使用多个工厂方法。这与在其他配置方案中选择“最贪婪”的构造函数或工厂方法的算法相同:在构造时选择具有最大可满足依赖关系数量的变量,类似于容器在多个 @Autowired 构造函数之间进行选择的方式。

命名自动检测的组件

在扫描过程中自动检测到组件时,其 bean 名称由该扫描程序已知的 BeanNameGenerator 策略生成。 默认情况下,任何包含名称值的 Spring 构造型注释(@Component@Repository@Service@Controller)都会将该名称提供给相应的 bean 定义。

如果这样的注释不包含名称,value 或者不包含任何其他检测到的组件(例如,通过自定义过滤器发现的组件),则缺省 bean 名称生成器将返回未大写的非限定类名称。例如,如果检测到以下组件类,则名称为 myMovieLister 和 movieFinderImpl:

1
2
3
4
@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
1
2
3
4
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}

如果您不想依赖默认的 Bean 命名策略,则可以提供自定义 Bean 命名策略。首先,实现 BeanNameGenerator 接口,并确保包括默认的无参构造函数。然后,在配置扫描器时提供完全限定的类名,如以下示例注释和 Bean 定义所示:

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
...
}
1
2
3
4
<beans>
<context:component-scan base-package="org.example"
name-generator="org.example.MyNameGenerator" />
</beans>

作为一般规则,每当其他组件可能对其进行显式引用时,请考虑使用注释指定名称。另一方面,只要容器负责接线,自动生成的名称就足够了。

提供自动检测组件的范围

通常,与 Spring 管理的组件一样,自动检测到的组件的默认且最常见的作用域是单例。 但是,有时您需要使用@Scope 批注指定的其他范围。 您可以在注释中提供范围的名称,如以下示例所示:

1
2
3
4
5
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}

@Scope 注释仅在具体的 bean 类(对于带注释的组件)或工厂方法(对于 @Bean 方法)上进行内省。 与 XML bean 定义相反,没有 bean 定义继承的概念,并且在类级别的继承层次结构与元数据目的无关。

有关特定于 Web 的范围的详细信息,例如 Spring 上下文中的“request”或“session”,请参见 Request,Session,Application 和 WebSocket Scope。与这些作用域的预构建批注一样,您也可以使用 Spring 的元注释方法来组成自己的作用域注释:例如,用@Scope(“prototype”),进行元注释的自定义注释,也可能会声明自定义作用域代理模式。

要提供用于范围解析的自定义策略,而不是依赖于基于注释的方法,可以实现该 ScopeMetadataResolver 接口。确保包括默认的无参数构造函数。然后,可以在配置扫描程序时提供完全限定的类名,如以下注释和 Bean 定义示例所示:

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
...
}
1
2
3
<beans>
<context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>

使用某些非单作用域时,可能有必要为作用域对象生成代理。在范围 Bean 中将推理描述为依赖项。为此,在 component-scan 元素上可以使用 scoped-proxy 属性。三个可能的值是:no,interfaces,和 targetClass。例如,以下配置生成标准的 JDK 动态代理:

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
...
}
1
2
3
<beans>
<context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>

提供带有注释的限定符元数据

在@Qualifier 注释中讨论与预选赛微调基于注解的自动连接。该部分中的示例演示了如何使用@Qualifier 注释和自定义限定符注释在解析自动装配候选时提供细粒度的控制。由于这些示例基于 XML Bean 定义,因此通过使用 XML 中的元素的 qualifier 或 meta 子元素,在候选 Bean 定义上提供了限定符元数据 bean。当依靠类路径扫描来自动检测组件时,可以在候选类上为限定符元数据提供类型级别的注释。下面的三个示例演示了此技术:

1
2
3
4
5
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
1
2
3
4
5
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
1
2
3
4
5
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
// ...
}

与大多数基于注释的替代方法一样,请记住,注释元数据绑定到类定义本身,而 XML 的使用允许相同类型的多个 bean 提供其限定符元数据的变体,因为该元数据是按-instance 而不是按类。

生成候选组件的索引

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

您现有的 @ComponentScan<context:component-scan> 指令必须保持原样,以请求上下文扫描某些软件包中的候选对象。当 ApplicationContext 检测到这样的索引时,它将自动使用它而不是扫描类路径。

要生成索引,请向每个包含组件的模块添加附加依赖关系,这些组件是组件扫描指令的目标。以下示例显示了如何使用 Maven 进行操作:

1
2
3
4
5
6
7
8
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<version>5.1.8.RELEASE</version>
<optional>true</optional>
</dependency>
</dependencies>

对于 Gradle 4.5 和更早版本,应在 compileOnly 配置中声明依赖项,如以下示例所示:

1
2
3
dependencies {
compileOnly "org.springframework:spring-context-indexer:5.1.8.RELEASE"
}

对于 Gradle 4.6 及更高版本,应在 annotationProcessor 配置中声明依赖项,如以下示例所示:

1
2
3
dependencies {
annotationProcessor "org.springframework:spring-context-indexer:5.1.8.RELEASE"
}

该过程将生成一个 META-INF/spring.components 包含在 jar 文件中的文件。

在 IDE 中使用此模式时,spring-context-indexer 必须将其注册为注释处理器,以确保在更新候选组件时索引是最新的。

META-INF/spring.components在类路径上找到 a 时,索引将自动启用。如果某个索引对于某些库(或用例)部分可用,但无法为整个应用程序构建,则可以通过将设置 spring.index.ignoretrue,来回退到常规的类路径安排(好像根本没有索引)属性或 spring.properties 类路径根目录下的文件中。

使用 JSR 330 标准注释

从 Spring 3.0 开始,Spring 提供对 JSR-330 标准注释(依赖注入)的支持。这些注释的扫描方式与 Spring 注释的扫描方式相同。要使用它们,您需要在类路径中有相关的 jar。

如果使用 Maven,javax.inject 则可以在标准 Maven 存储库(https://repo1.maven.org/maven2/javax/inject/javax.inject/1/)中找到该工件。您可以将以下依赖项添加到文件 pom.xml 中:

1
2
3
4
5
6
> <dependency>
> <groupId>javax.inject</groupId>
> <artifactId>javax.inject</artifactId>
> <version>1</version>
> </dependency>
>

与 @Inject 和的依赖注入 @Named

除了 @Autowired,您可以使用 @javax.inject.Inject 以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 。以下示例提供了先前示例的变体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 批注,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

1
2
3
4
5
6
7
public class SimpleMovieLister {

@Inject
public void setMovieFinder(Optional<MovieFinder> movieFinder) {
...
}
}
1
2
3
4
5
6
7
public class SimpleMovieLister {

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

@Named 和 @ManagedBean:@Component 注释的标准等效项

代替@Component,您可以使用@javax.inject.Named 或 javax.annotation.ManagedBean,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.inject.Inject;
import javax.inject.Named;

@Named("movieListener") // @ManagedBean("movieListener") could be used as well
public class SimpleMovieLister {

private MovieFinder movieFinder;

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

// ...
}

@Component 不指定组件名称的情况下使用非常常见。@Named 可以类似的方式使用,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 注释完全相同的方式来使用组件扫描,如以下示例所示:

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
...
}

与相比@Component,JSR-330 @Named 和 JSR-250 ManagedBean 注释是不可组合的。您应该使用 Spring 的构造型模型来构建自定义组件注释。

JSR-330 标准注释的局限性

当使用标准注释时,您应该知道某些重要功能不可用,如下表所示:

表 6. Spring 组件模型元素与 JSR-330 变体

Spring javax.inject.* javax.inject 限制/注释
@Autowired @Inject @Inject 没有“必填”属性。可以与 Java 8 一起使用 Optional。
@Component @Named / @ManagedBean JSR-330 不提供可组合的模型,仅提供一种识别命名组件的方法。
@Scope(“singleton”) @Singleton JSR-330 的默认范围类似于 Spring 的 prototype。但是,为了使其与 Spring 的默认默认值保持一致,默认情况下,在 Spring 容器中声明的 JSR-330 bean 是 a singleton。为了使用之外的范围 singleton,您应该使用 Spring 的@Scope 注释。javax.inject 还提供了 @Scope 批注。不过,此仅用于创建自己的注释。
@Qualifier @Qualifier / @Named javax.inject.Qualifier 只是用于构建自定义限定符的元注释。具体的 String 限定词(例如@Qualifier 带有值的 Spring 的限定词)可以通过关联 javax.inject.Named。
@Value - 没有等效
@Required - 没有等效
@Lazy - 没有等效
ObjectFactory Provider javax.inject.Provider 是 Spring 的直接替代方法 ObjectFactory,只是 get()方法名称较短。它也可以与 Spring @Autowired 或非注释构造函数和 setter 方法结合使用。

基于 Java 的容器配置

本节介绍如何在 Java 代码中使用注释来配置 Spring 容器。它包括以下主题:

  • 基本概念:@Bean@Configuration
  • 使用实例化 Spring 容器 AnnotationConfigApplicationContext
  • 使用@Bean 注释
  • 使用@Configuration 注释
  • 组成基于 Java 的配置
  • Bean 定义配置文件
  • PropertySource 抽象化
  • 运用 @PropertySource
  • 声明中的占位符解析

基本概念:@Bean@Configuration

Spring 的新 Java 配置支持中的主要构件是 @Configuration 注释的类和 @Bean 注释的方法。

@Bean批注用于指示方法实例化,配置和初始化要由 Spring IoC 容器管理的新对象。 对于那些熟悉 Spring 的 <beans/> XML 配置的人来说,@Bean 注释与 <bean/>元素具有相同的作用。 您可以将 @Bean 批注方法与任何 Spring @Component 一起使用。 但是,它们最常与 @Configuration bean 一起使用。

用注释类 @Configuration 表示其主要目的是作为 Bean 定义的来源。此外,@Configuration 类允许通过调用 @Bean 同一类中的其他方法来定义 Bean 之间的依赖关系。最简单的 @Configuration 类如下:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

@Bean
public MyService myService() {
return new MyServiceImpl();
}
}

上一 AppConfig 类等效于以下 Spring <beans/> XML:

1
2
3
<beans>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>

完整的 @Configuration 与“精简” @Bean 模式?

如果在未使用 @Configuration 注释的类中声明 @Bean 方法,则将它们称为以“精简”模式进行处理。在 @Component 或是甚至在简单的旧类中声明的 Bean 方法被认为是“精简版”,其中包含类的主要目的不同,而 @Bean 方法在那里具有某种优势。例如,服务组件可以通过每个适用组件类上的其他 @Bean 方法将管理视图公开给容器。在这种情况下,@Bean 方法是一种通用的工厂方法机制。

与完整的 @Configuration 不同,精简 @Bean 方法无法声明 Bean 之间的依赖关系。取而代之的是,它们在其包含组件的内部状态上进行操作,并且还可以根据其可能声明的参数进行操作。因此,此类 @Bean 方法不应调用其他 @Bean 方法。实际上,每个此类方法仅是用于特定 bean 引用的工厂方法,而没有任何特殊的运行时语义。这里的积极副作用是,不必在运行时应用 CGLIB 子类,因此在类设计方面没有任何限制(即,包含类可能是 final 修饰之类的)。

在常见情况下,@Bean 方法将在 @Configuration 类中声明,以确保始终使用“完全”模式,因此跨方法引用将重定向到容器的生命周期管理。这样可以防止通过常规 Java 调用意外地调用同一 @Bean 方法,从而有助于减少在“精简”模式下运行时难以追查的细微错误。

以下各节将详细讨论 @Bean@Configuration 批注。 但是,首先,我们介绍了使用基于 Java 的配置来创建 spring 容器的各种方法。

使用实例化 Spring 容器 AnnotationConfigApplicationContext

以下各节介绍了 Spring 3.0 中引入的 Spring 的 AnnotationConfigApplicationContext。这种通用的 ApplicationContext 实现不仅能够接受 @Configuration 类作为输入,而且还可以接受普通的 @Component 类和带有 JSR-330 元数据注释的类。

当提供 @Configuration 类作为输入时,@Configuration 类本身将注册为 Bean 定义,并且该类中所有已声明的 @Bean 方法也将注册为 Bean 定义。

提供 @Component 和 JSR-330 类时,它们将注册为 bean 定义,并且假定在必要时在这些类中使用了诸如 @Autowired@Inject 之类的 DI 元数据。

简单实现

与实例化 ClassPathXmlApplicationContext 时将 Spring XML 文件用作输入的方式几乎相同,您可以在实例化 AnnotationConfigApplicationContext 时将 @Configuration 类用作输入。如下面的示例所示,这允许完全不使用 XML 来使用 Spring 容器:

1
2
3
4
5
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 带注释的类作为输入提供给构造函数,如以下示例所示:

1
2
3
4
5
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 时,此方法特别有用。以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class, OtherConfig.class);
ctx.register(AdditionalConfig.class);
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}

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

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "com.acme")
public class AppConfig {
...
}

等价于

1
2
3
4
5
6
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
}

值得注意的是,@Configuration 类带有 @Component 元注解,因此是组件扫描的候选对象。在前面的示例中,假定 AppConfigcom.acme 包(或下面的任何包)中声明,那么在调用 scan() 时,@Configuration 类被检测到并注册,但不会注册其中的 @Bean。直到调用 refresh() 方法,其所有 @Bean 方法才都被处理并注册为容器内的 Bean 定义。

支持 Web 应用程序的 AnnotationConfigWebApplicationContext

AnnotationConfigWebApplicationContext = WebApplicationContext + AnnotationConfigApplicationContext,这个实现可以配置 Spring 的 ContextLoaderListener servlet 侦听器, Spring MVC 的 DispatcherServlet 等。以下 web.xml 代码片段配置了典型的 Spring MVC Web 应用程序 (注意 contextClass context-param and init-param 的使用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<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>

使用 @Bean 注解

@Bean 是方法级别的注解,类似 XML 中的 <bean/> 元素。同时支持提供 <bean/> 的属性,例如:init-method、destroy-method、autowiring。

你可以使用在有 @Configuration@Component 注解的类内使用 @Bean 注释。

声明一个 bean

要声明一个 bean,可以对方法进行 @Bean 注解。您可以使用此方法在 ApplicationContext 指定为该方法的返回值的类型内注册 Bean 定义。默认情况下,bean 名称与方法名称相同。以下示例显示了@Bean 方法声明:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}

前面的配置与下面的 Spring XML 完全等效:

1
2
3
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

这两个声明都使一个名为 transferService 的 bean 在 ApplicationContext 中可用,并绑定到类型为 TransferServiceImpl 的对象实例,如以下文本镜像所示:

1
transferService -> com.acme.TransferServiceImpl

您还可以使用接口(或基类)返回类型声明@Bean 方法,如以下示例所示:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}

但是,这将高级类型预测的可见性限制为指定的接口类型(TransferService)。然后,使用仅一次使容器知道的完整类型(TransferServiceImpl),就可以实例化受影响的单例 bean。非惰性单例 bean 根据其声明顺序实例化,因此您可能会看到不同的类型匹配结果,具体取决于另一个组件何时尝试按非声明类型进行匹配(例如@Autowired TransferServiceImpl,仅 transferService 在实例化 bean 后才解析)。

如果您通过声明的服务接口一致地引用类型,则@Bean 返回类型可以安全地加入该设计决策。但是,对于实现多个接口的组件或由其实现类型潜在引用的组件,声明可能的最具体的返回类型(至少与引用您的 bean 的注入点所要求的具体类型一样)更为安全。

Bean 依赖

带@Bean 注释的方法可以具有任意数量的参数,这些参数描述构建该 bean 所需的依赖关系。例如,如果我们 TransferService 需要一个 AccountRepository,我们可以使用方法参数来实现该依赖关系,如以下示例所示:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}

解析机制与基于构造函数的依赖注入几乎相同。

接收生命周期回调

用@Bean 注释定义的任何类都支持常规的生命周期回调,并且可以使用 JSR-250 中的@PostConstruct 和@PreDestroy 注释。有关更多详细信息,请参见 JSR-250 注释。

常规 Spring 生命周期回调也得到完全支持。如果 bean 实现 InitializingBean,DisposableBean 或 Lifecycle,则容器将调用它们各自的方法。

也完全支持标准*Aware 接口集(例如 BeanFactoryAware, BeanNameAware, MessageSourceAware, ApplicationContextAware 等)。

该@Bean 注释支持指定任意初始化和销毁回调方法,就像 Spring XML 中的 init-method 和 destroy-method 属性的 bean 元素,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class BeanOne {

public void init() {
// initialization logic
}
}

public class BeanTwo {

public void cleanup() {
// destruction logic
}
}

@Configuration
public class AppConfig {

@Bean(initMethod = "init")
public BeanOne beanOne() {
return new BeanOne();
}

@Bean(destroyMethod = "cleanup")
public BeanTwo beanTwo() {
return new BeanTwo();
}
}

默认情况下,使用 Java config 定义的具有公开关闭或停止方法的 bean 将自动加入销毁回调。如果你有一个公开的关闭或停止方法,但是你不希望在容器关闭时被调用,只需将@Bean(destroyMethod =””)添加到你的 bean 定义中即可禁用默认(推测)模式。 默认情况下,您可能希望通过 JNDI 获取资源,因为它的生命周期在应用程序之外进行管理。特别地,请确保始终为 DataSource 执行此操作,因为它已知在 Java EE 应用程序服务器上有问题。

默认情况下,您可能要对通过 JNDI 获取的资源执行此操作,因为其生命周期是在应用程序外部进行管理的。特别是,请确保始终对进行操作 DataSource,因为这在 Java EE 应用程序服务器上是有问题的。

以下示例显示了如何防止对的自动销毁回调 DataSource:

1
2
3
4
@Bean(destroyMethod="")
public DataSource dataSource() throws NamingException {
return (DataSource) jndiTemplate.lookup("MyDS");
}

另外,通过@Bean 方法,通常会选择使用编程来进行 JNDI 查找:要么使用 Spring 的 JndiTemplate/JndiLocatorDelegate 帮助类,要么直接使用 JNDI InitialContext,但不能使用 JndiObjectFactoryBean 变体来强制将返回类型声明为 FactoryBean 类型以代替目标的实际类型,它将使得在其他@Bean 方法中更难用于交叉引用调用这些在此引用提供资源的方法。
当然上面的 BeanOne 例子中,在构造期间直接调用 init()方法同样有效:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class AppConfig {

@Bean
public BeanOne beanOne() {
BeanOne beanOne = new BeanOne();
beanOne.init();
return beanOne;
}

// ...
}

当您直接在 Java 中工作时,您可以对对象执行任何您喜欢的操作,并不总是需要依赖容器生命周期。

指定 bean 的作用域

使用@Scope 注解

你可以指定@Bean 注解定义的 bean 应具有的特定作用域。你可以使用 Bean 作用域章节中的任何标准作用域。

默认的作用域是单例,但是你可以用@Scope 注解重写作用域。

1
2
3
4
5
6
7
8
9
10
@Configuration
public class MyConfiguration {

@Bean
@Scope("prototype")
public Encryptor encryptor() {
// ...
}

}
@Scope 和 scope 代理

Spring 提供了一个通过范围代理来处理范围依赖的便捷方法。使用 XML 配置创建此类代理的最简单方法是元素。使用@Scope 注解配置 Java 中的 bean 提供了与 proxyMode 属性相似的支持。默认是没有代理(ScopedProxyMode.NO),但您可以指定 ScopedProxyMode.TARGET_CLASS 或 ScopedProxyMode.INTERFACES。

如果你使用 Java 将 XML 参考文档(请参阅上述链接)到范围的@Bean 中移植范围限定的代理示例,则它将如下所示
如果你将 XML 参考文档的 scoped 代理示例转化为 Java @Bean,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// an HTTP Session-scoped bean exposed as a proxy
@Bean
@SessionScope
public UserPreferences userPreferences() {
return new UserPreferences();
}

@Bean
public Service userService() {
UserService service = new SimpleUserService();
// a reference to the proxied userPreferences bean
service.setUserPreferences(userPreferences());
return service;
}

自定义 Bean 命名

默认情况下,配置类使用@Bean 方法的名称作为结果 bean 的名称。但是,可以使用 name 属性覆盖此功能,如以下示例所示:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

@Bean(name = "myThing")
public Thing thing() {
return new Thing();
}
}

Bean 别名

如前文命名 bean 中所讨论的,有时希望为单个 Bean 提供多个名称,否则会导致 Bean 混淆。 为此 name,@Bean 注释的属性接受 String 数组。以下示例显示了如何为 bean 设置多个别名:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

@Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
public DataSource dataSource() {
// instantiate, configure and return DataSource bean...
}
}

Bean 描述

有时,提供有关 bean 的更详细的文本描述会很有帮助。当出于监视目的而暴露(可能通过 JMX)bean 时,这特别有用。

要将说明添加到@Bean,可以使用 @Description 批注,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
@Configuration
public class AppConfig {

@Bean
@Description("Provides a basic example of a bean")
public Thing thing() {
return new Thing();
}

}
1
2


使用 @Configuration 注解

@Configuration 是类级别的注释,指示对象是 Bean 定义的源。 @Configuration 类通过公共 @Bean 注释方法声明 bean。 对 @Configuration 类的 @Bean 方法的调用也可以用于定义 Bean 之间的依赖关系。 有关一般性介绍,请参见[基本概念:@Bean@Configuration](<#基本概念:@Bean\ 和\ @Configuration>)。

注入 bean 间的依赖关系

当 bean 相互依赖时,表示依赖关系就像让一个 bean 方法调用另一个依赖一样简单,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class AppConfig {

@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}

@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}

仅当 @Bean@Configuration 类中声明该方法时,此声明 bean 间依赖关系的方法才有效。您不能使用普通 @Component 类声明 bean 间的依赖关系。

查找方法注入

如前所述,查找方法注入是一项高级功能,您应该很少使用。在单例作用域的 bean 对原型作用域的 bean 有依赖性的情况下,这很有用。将 Java 用于这种类型的配置为实现这种模式提供了自然的方法。以下示例显示如何使用查找方法注入:

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}

// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}

通过使用 Java 配置,可以创建一个覆盖 CommandManager 抽象 createCommand() 方法的子类,该方法将以某种方式查找新的(原型)命令对象。以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
AsyncCommand command = new AsyncCommand();
// inject dependencies here as required
return command;
}

@Bean
public CommandManager commandManager() {
// return new anonymous implementation of CommandManager with createCommand()
// overridden to return a new prototype Command object
return new CommandManager() {
protected Command createCommand() {
return asyncCommand();
}
}
}

有关基于 Java 的配置在内部如何工作的更多信息

考虑以下示例,该示例显示了一个带 @Bean 注释的方法被调用两次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class AppConfig {

@Bean
public ClientService clientService1() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}

@Bean
public ClientService clientService2() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}

@Bean
public ClientDao clientDao() {
return new ClientDaoImpl();
}
}

clientDao()clientService1() 中被调用一次,并在 clientService2() 中被调用一次。由于此方法会创建一个 ClientDaoImpl 的新实例并返回它,因此通常希望有两个实例(每个服务一个)。那肯定是有问题的:在 Spring 中,实例化的 bean 默认情况下具有单例作用域。这就是神奇之处所在:所有 @Configuration 类在启动时都使用 CGLIB 进行了子类化。在子类中,子方法在调用父方法并创建新实例之前,首先检查容器中是否有任何缓存(作用域)的 bean。

根据 bean 的作用域,行为可能有所不同。我们在这里只谈论单例。

从 Spring 3.2 开始,不再需要将 CGLIB 添加到您的类路径中,因为 CGLIB 类已经被重新打包 org.springframework.cglib 并直接包含在 spring-core JAR 中。

由于 CGLIB 在启动时会动态添加功能,因此存在一些限制。特别是,配置类不能是 final。但是,从 4.3 版本开始,配置类上允许使用任何构造函数,包括 @Autowired 对默认注入使用或单个非默认构造函数声明。

如果您希望避免 CGLIB 施加的限制,请考虑 @Bean 在非 @Configuration 类上声明您的方法(例如,在普通 @Component 类上声明)。那么 @Bean 就不会截获方法之间的跨方法调用,因此您必须专门依赖那里的构造函数或方法级别的依赖项注入。

组成基于 Java 的配置

Spring 的基于 Java 的配置功能使您可以编写批注,这可以降低配置的复杂性。

使用 @Import 注释

就像 <import/> 在 Spring XML 文件中使用该元素来帮助模块化配置一样,@Import 注释允许 @Bean 从另一个配置类加载定义,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class ConfigA {

@Bean
public A a() {
return new A();
}
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

@Bean
public B b() {
return new B();
}
}

现在,无需同时指定两者 ConfigA.classConfigB.class 实例化上下文,只需 ConfigB 显式提供,如以下示例所示

1
2
3
4
5
6
7
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

// now both beans A and B will be available...
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}

这种方法简化了容器的实例化,因为只需要处理一个类,而无需在构造过程中记住大量潜在的 @Configuration 类。

从 Spring Framework 4.2 开始,@Import 还支持对常规组件类的引用,类似于 AnnotationConfigApplicationContext.register 方法。如果要通过使用一些配置类作为入口点来显式定义所有组件,从而避免组件扫描,则此功能特别有用。

注入对导入@Bean 定义的依赖

前面的示例有效,但过于简单。在大多数实际情况下,Bean 在配置类之间相互依赖。使用 XML 时,这不是问题,因为不涉及编译器,并且您可以声明 ref="someBean" 并信任 Spring 在容器初始化期间对其进行处理。使用 @Configuration 类时,Java 编译器会在配置模型上施加约束,因为对其他 bean 的引用必须是有效的 Java 语法。

幸运的是,解决这个问题很简单。正如我们已经讨论的那样,一个 @Bean 方法可以具有任意数量的描述 Bean 依赖关系的参数。考虑以下具有多个 @Configuration 类的更真实的场景,每个类都取决于其他类中声明的 bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Configuration
public class ServiceConfig {

@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}

@Configuration
public class RepositoryConfig {

@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

@Bean
public DataSource dataSource() {
// return new DataSource
}
}

public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}

还有另一种方法可以达到相同的结果。 请记住,@Configuration 类最终仅是容器中的另一个 bean:这意味着它们可以利用 @Autowired@Value 注入以及与任何其他 bean 相同的其他功能。

确保以这种方式注入的依赖项只是最简单的一种。 @Configuration 类是在上下文初始化期间非常早地处理的,并且强制以这种方式注入依赖项可能导致意外的早期初始化。 如上例所示,尽可能使用基于参数的注入。

另外,通过 @Bean 使用 BeanPostProcessorBeanFactoryPostProcessor 定义时要特别小心。 通常应将这些声明为静态 @Bean 方法,从而不触发其包含的配置类的实例化。 否则,@Autowired@Value 可能不适用于配置类本身,因为该 bean 实例的创建可能比 AutowiredAnnotationBeanPostProcessor 要早。

以下示例说明如何将一个 bean 自动连接到另一个 bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Configuration
public class ServiceConfig {

@Autowired
private AccountRepository accountRepository;

@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRepository);
}
}

@Configuration
public class RepositoryConfig {

private final DataSource dataSource;

public RepositoryConfig(DataSource dataSource) {
this.dataSource = dataSource;
}

@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

@Bean
public DataSource dataSource() {
// return new DataSource
}
}

public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}

仅从 Spring Framework 4.3 开始支持 @Configuration 类中的构造方法注入。还要注意,如果目标 bean 仅定义了一个构造函数,那么无需指定 @Autowired

便于浏览的完全合格的导入 bean

在前面的场景中,使用 @Autowired 效果很好,并提供了所需的模块化,但是要确切确定声明自动装配的 Bean 定义的位置仍然有些模棱两可。例如,当开发人员查看时 ServiceConfig,您如何确切知道该 @Autowired AccountRepository bean 的声明位置?它在代码中不是明确的,这可能很好。请记住, Spring Tools for Eclipse 提供了可以渲染图形的工具,这些图形显示了所有接线的方式,这可能就是您所需要的。另外,您的 Java IDE 可以轻松找到该 AccountRepository 类型的所有声明和使用,并快速向您显示@Bean 返回该类型的方法的位置。

如果这种歧义是不可接受的,并且您希望从 IDE 内部直接从一个 @Configuration 类导航到另一个类,请考虑自动装配配置类本身。以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class ServiceConfig {

@Autowired
private RepositoryConfig repositoryConfig;

@Bean
public TransferService transferService() {
// navigate 'through' the config class to the @Bean method!
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}

在上述情况下,在哪里定义了 AccountRepository 是完全显式的。但是,ServiceConfig 现在与 RepositoryConfig 紧密耦合。那是权衡。通过使用基于接口或基于抽象类的 @Configuration 类,可以在某种程度上缓解这种紧密耦合。考虑以下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@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(用于首次访问而不是在启动时创建)或声明为 @DependsOn 某些其他 Bean(确保在当前 Bean 之前创建特定的其他 Bean),后者的直接依赖意味着什么)。

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

根据某些系统状态,有条件地启用或禁用完整的 @Configuration 类甚至单个 @Bean 方法通常很有用。一个常见的示例是仅在 Spring 环境中启用了特定配置文件后,才使用@Profile 批注来激活 Bean(有关详细信息,请参见后文 Bean 定义配置文件)。

@Profile 批注实际上是通过使用更灵活的称为 @Conditional 的批注来实现的。 @Conditional 批注指示在注册 @Bean 之前应参考的特定 org.springframework.context.annotation.Condition 实现。

Condition 接口的实现提供了一个 matches(…) 方法,该方法返回 truefalse。例如,以下清单显示了用于 @Profile 的实际 Condition 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// Read the @Profile annotation attributes
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
return true;
}
}
return false;
}
return true;
}

有关 @Conditional 更多详细信息,请参见 javadoc

结合 Java 和 XML 配置

Spring 的 @Configuration 类支持并非旨在 100% 完全替代 Spring XML。 某些工具(例如 Spring XML 名称空间)仍然是配置容器的理想方法。 在使用 XML 方便或有必要的情况下,您可以选择:使用“以 XML 为中心”的方式实例化容器,比如 ClassPathXmlApplicationContext ,或使用“以 Java 中心”的方式实例化容器,也就是使用 AnnotationConfigApplicationContext@ImportResource 批注来根据需要导入 XML。

以 XML 为中心的 @Configuration 类使用

最好从 XML 引导 Spring 容器并以 ad-hoc 方式包含 @Configuration 类。 例如,在使用 Spring XML 的大型现有代码库中,根据需要创建 @Configuration 类并从现有 XML 文件中将它们包含在内会变得更加容易。 在本节的后面,我们将介绍在这种“以 XML 为中心”的情况下使用 @Configuration 类的选项。

@Configuration 类声明为纯 Spring <bean/> 元素

请记住,@Configuration 类最终是容器中的 bean 定义。在本系列示例中,我们创建一个名为 AppConfig@Configuration 类, 并将其包含在system-test-config.xml 中作为 <bean/> 定义。因为 <context:annotation-config/> 已打开,所以容器会识别 @Configuration 注释并正确处理 AppConfig 中声明的 @Bean 方法。

以下示例显示了 Java 中的普通配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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 文件的一部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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 文件:

1
2
3
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
1
2
3
4
5
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 进行接线,因此也并不严格要求 id 定义。

使用 <context:component-scan/>拾取 @Configuration

因为 @Configuration@Component 进行元注释,所以 @Configuration 注释的类自动成为组件扫描的候选对象。使用与先前示例中描述的场景相同的场景,我们可以重新定义 system-test-config.xml 以利用组件扫描的优势。请注意,在这种情况下,我们无需显式声明 <context:annotation-config/>,因为 <context:component-scan/> 可启用相同的功能。

以下示例显示了修改后的 system-test-config.xml 文件:

1
2
3
4
5
6
7
8
9
10
11
<beans>
<!-- picks up and registers AppConfig as a bean definition -->
<context:component-scan base-package="com.acme"/>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
@Configuration 以类为中心的 XML 与 @ImportResource

@Configuration 类是配置容器的主要机制的应用程序中,仍然有必要至少使用一些 XML。在这些情况下,您可以使用 @ImportResource 并仅定义所需的 XML。这样做实现了一种“以 Java 为中心”的方法来配置容器,并将 XML 保持在最低限度。以下示例(包括配置类,定义 Bean 的 XML 文件,属性文件和主类)显示了如何使用 @ImportResource 批注来实现按需使用 XML 的以 Java 为中心的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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:

1
2
3
<beans>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>

jdbc.properties:

1
2
3
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
1
2
3
4
5
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}

环境抽象

Environment 接口是集成在容器中的抽象,它对应用程序环境的两个关键方面进行建模:概要文件(profiles)和属性(properties)。

profile 是仅在给定概要文件处于活动状态时才向容器注册的 Bean 定义的命名逻辑组。可以将 Bean 通过 XML 定义还或注释方式分配给 profile。与 profile 相关的 Environment 对象的作用是确定当前哪些 profile(如果有)处于活动状态,以及默认情况下哪些 profile(如果有)应处于活动状态。

properties 在几乎所有应用程序中都起着重要作用,并且可能源自多种来源:属性文件,JVM 系统属性,系统环境变量,JNDI,Servlet 上下文参数,ad-hoc Properties 对象,Map 对象等。Environment 对象与 properties 相关的作用是为用户提供方便的服务接口,用于配置属性源并从中解析属性。

Bean 定义配置文件(profiles)

Bean 定义配置文件在核心容器中提供了一种机制,该机制允许在不同环境中注册不同的 Bean。“环境”一词对不同的用户可能具有不同的含义,并且此功能可以帮助解决许多用例,包括:

  • 在开发中针对内存中的数据源进行工作,而不是在进行 QA 或生产时从 JNDI 查找相同的数据源。
  • 仅在将应用程序部署到性能环境中时注册监视相关的基础设施。
  • 为客户 A 和客户 B 部署注册 bean 的自定义实现。

考虑实际应用中需要使用的第一个用例 DataSource。在测试环境中,配置可能类似于以下内容:

1
2
3
4
5
6
7
8
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}

现在,假设该应用程序的数据源已在生产应用程序服务器的 JNDI 目录中注册,请考虑如何将该应用程序部署到 QA 或生产环境中。 现在,我们的 dataSource bean 看起来像下面的清单:

1
2
3
4
5
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

问题是如何根据当前环境在使用这两种变体之间进行切换。 随着时间的流逝,Spring 用户已经设计出多种方法来完成此任务,通常依赖于系统环境变量和包含 ${placeholder} 令牌的 XML <import/>语句的组合,这些语句根据值解析为正确的配置文件路径环境变量。 Bean 定义配置文件是一个核心容器功能,可提供此问题的解决方案。

如果我们概括前面特定于环境的 Bean 定义示例中所示的用例,那么最终需要在某些上下文中而不是在其他上下文中注册某些 Bean 定义。 您可能会说您要在情况 A 中注册一个特定的 bean 定义配置文件,在情况 B 中注册一个不同的配置文件。我们首先更新配置以反映这种需求。

使用 @Profile

@Profile 批注可让您指示一个或多个指定的配置文件处于活动状态时有资格注册的组件。 使用前面的示例,我们可以如下重写 dataSource 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
@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();
}
}
1
2
3
4
5
6
7
8
9
10
@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 帮助器或前面展示的直接 JNDI InitialContext 用法,而不是 JndiObjectFactoryBean 变体,这将迫使您将返回类型声明为 FactoryBean 类型。

配置文件字符串可以包含简单的配置文件名称(例如 production)或配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如 production & us-east)。概要文件表达式中支持以下运算符:

  • !: 非
  • &: 与
  • |: 或

您不能在不使用括号的情况下混合使用 &| 运算符。例如,production & us-east | eu-central 不是有效的表达式。它必须表示为 production & (us-east | eu-central)

您可以将其@Profile用作元注释,以创建自定义的组合注释。以下示例定义了一个自定义 @Production 批注,您可以将其用作替代品 @Profile("production")

1
2
3
4
5
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

如果用 @Configuration 标记了一个类,则除非一个或多个指定的配置文件处于活动状态,否则将忽略与该类关联的 @Profile 所有 @Bean 方法和 @Import 注释。如果一个 @Component@Configuration 类标记有 @Profile({"p1", "p2"}),则除非已激活配置文件“p1”或“p2”,否则该类不会注册或处理。如果给定的配置文件以 非 运算符(!)为前缀,则仅在该配置文件不活动时才注册带注释的元素。例如,给定@Profile({"p1", "!p2"}),如果配置文件“p1”处于活动状态或配置文件“p2”未处于活动状态,则会进行注册。

@Profile 也可以在方法级别声明为仅包括配置类的一个特定 Bean(例如,特定 Bean 的替代变体),如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class AppConfig {

@Bean("dataSource")
@Profile("development")
public DataSource standaloneDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}

@Bean("dataSource")
@Profile("production")
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}

@Bean 方法上使用 @Profile 时,可能会出现特殊情况:对于具有 @Bean 相同 Java 方法名称的重载方法(类似于构造函数重载),@Profile 需要在所有重载方法上一致声明条件。如果条件不一致,则仅重载方法中第一个声明的条件有效。因此,@Profile 不能用于选择具有特定参数签名的重载方法。在创建时,相同 bean 的所有工厂方法之间的解析都遵循 Spring 的构造函数解析算法。

如果要使用不同的概要文件条件定义备用 bean,@Bean name 属性需要使用不同的 Java 方法名称来指向相同的 bean 名称,如前面的示例所示。如果参数签名都相同(例如,所有变体都具有无参工厂方法),则这是首先在有效 Java 类中表示这种排列的唯一方法(因为只能有一个特定名称和参数签名的方法)。

XML Bean 定义配置文件

XML 对应项是元素的 profile 属性 <beans> 。我们前面的示例配置可以用两个 XML 文件重写,如下所示:

1
2
3
4
5
6
7
8
9
10
11
<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>
1
2
3
4
5
6
7
8
<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/>在同一文件中拆分和嵌套元素,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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 对应项不支持前面描述的配置文件表达式。但是,可以通过使用!运算符来取消配置文件。也可以通过嵌套配置文件来应用逻辑“与”,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> <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>
>

在前面的示例中,如果 productionus-east profiles 都处于活动状态,则将显示 dataSource bean。

激活 profile

现在我们已经更新了配置,我们仍然需要指示 Spring 哪个配置文件处于活动状态。如果我们现在启动示例应用程序,则会看到 NoSuchBeanDefinitionException 抛出的错误,因为容器找不到名为 dataSource 的 Spring bean。

可以通过多种方式来激活配置文件,但最直接的方法是针对可通过 ApplicationContext 获得的 Environment API 以编程方式进行配置。以下示例显示了如何执行此操作:

1
2
3
4
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

另外,您还可以通过 spring.profiles.active 属性声明性地激活配置文件,可以通过系统环境变量,JVM 系统属性,web.xml 中的 servlet 上下文参数 或甚至作为 JNDI 中的条目来指定配置文件 (请参见 [PropertySource 抽象化](<#PropertySource\ 抽象化>))。在集成测试中,可以通过使用 spring-test 模块中的 @ActiveProfiles 注释来声明活动配置文件 (请参阅环境配置文件的上下文配置)。

请注意,profile 不是“非此即彼”的。您可以一次激活多个配置文件。通过编程,您可以为 setActiveProfiles() 方法提供多个配置文件名称,该方法接受 String…​ 变长参数。以下示例激活多个配置文件:

1
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

以声明方式,spring.profiles.active 可以接受以逗号分隔的配置文件名称列表,如以下示例所示:

1
-Dspring.profiles.active="profile1,profile2"

默认 profile

1
2
3
4
5
6
7
8
9
10
11
12
@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();
}
}

如果没有配置文件处于活动状态,则将创建上面的dataSource。您可以看到这是为一个或多个 bean 提供默认定义的一种方法。如果启用了任何配置文件,则默认配置文件将不适用。

您可以通过Environment上的setDefaultProfiles(),或者声明 spring.profiles.default 属性,来更改默认的配置文件的名称,。

PropertySource 抽象

Spring 的 Environment 抽象提供了对属性源可配置层次结构的搜索操作。考虑以下清单:

1
2
3
4
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。有关详细信息,请参见 javadoc。

具体来说,当您使用StandardEnvironment时,env.containsProperty("my-property") 如果运行时存在 my-property 系统属性或 my-property 环境变量,则对的调用将返回 true 。

执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果my-property在调用期间在两个地方同时设置了 env.getProperty("my-property") 该属性,则系统属性值将“获胜”并返回。请注意,属性值不会合并,而是会被前面的条目完全覆盖。

对于常规 StandardServletEnvironment,完整层次结构如下,最高优先级条目位于顶部:

  1. ServletConfig 参数(如果适用,例如在 DispatcherServlet 上下文的情况下)
  2. ServletContext 参数(web.xmlcontext-param 条目)
  3. JNDI 环境变量(java:comp/env/ 条目)
  4. JVM 系统属性(-D 命令行参数)
  5. JVM 系统环境(操作系统环境变量)

最重要的是,整个机制是可配置的。也许您有一个要集成到此搜索中的自定义属性源。为此,请实现并实例化自己的 PropertySource 实例并将其添加到 EnvironmentPropertySourcescurrent 集合中。以下示例显示了如何执行此操作:

1
2
3
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

在前面的代码中,MyPropertySource 已在搜索中添加了最高优先级。如果它包含一个my-property 属性,则检测并返回该属性,超越其他 PropertySource 中的my-property 属性 。MutablePropertySources API 公开了许多方法,这些方法允许对属性源集进行精确操作。

使用 @PropertySource

@PropertySource 注解提供了一种添加PropertySource 到 Spring 的Environment 的便利和声明性的机制。

给定一个名为 app.properties 的文件,包含键-值对 testbean.name=myTestBean,下面的 @Configuration 类使用 @PropertySource,其方式是对 testBean.getName() 的调用返回 myTestBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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 资源位置中存在的任何 ${…} 占位符都是根据已经针对该环境注册的一组属性源来解析的,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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 批注都需要在同一级别上声明,可以直接在配置类上声明,也可以在同一自定义批注中声明为元批注。不建议将直接注释和元注释混合使用,因为直接注释会覆盖元注释。

声明中的占位符解析

从历史上看,元素中占位符的值只能根据 JVM 系统属性或环境变量来解析。这已不再是这种情况。由于 Environment 抽象是在整个容器中集成的,因此很容易通过它路由占位符的解析。这意味着您可以按照自己喜欢的任何方式配置解析过程。您可以更改搜索系统属性和环境变量的优先级,也可以完全删除它们。您还可以根据需要将自己的属性源添加到混合中。

具体而言,无论该 customer 属性在何处定义,只要该属性在 Environment 可用,以下语句均有效:

1
2
3
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>

注册一个 LoadTimeWeaver

Spring 使用 LoadTimeWeaver 在将类加载到 Java 虚拟机(JVM)中时对其进行动态转换。

要启用加载时编织(load-time weaving),可以将 @EnableLoadTimeWeaving 添加到您的 @Configuration 类之一,如以下示例所示:

1
2
3
4
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

另外,对于 XML 配置,可以使用 context:load-time-weaver 元素:

1
2
3
<beans>
<context:load-time-weaver/>
</beans>

ApplicationContext 配置后,该 ApplicationContext 中的任何 bean 都可以实现 LoadTimeWeaverAware,从而接收对加载时 weaver 实例的引用。 与 Spring 的 JPA 支持结合使用时,该功能特别有用,因为在 JPA 类转换中可能需要进行加载时编织。 有关更多详细信息,请查阅 LocalContainerEntityManagerFactoryBean javadoc。 有关 AspectJ 加载时编织的更多信息,请参见 Spring 框架中的 AspectJ 加载时编织。

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

使用 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。Bean 的 name 必须是 messageSource。如果找到了这样的 bean,则对先前方法的所有调用都将委派给消息源。 如果找不到消息源,则 ApplicationContext 尝试查找包含同名 bean 的父级。如果找到了,它将使用该 bean 作为 MessageSource。如果 ApplicationContext 找不到任何消息源,则将实例化一个空的 DelegatingMessageSource,以便能够接受对上述方法的调用。

Spring 提供了两个 MessageSource 实现,即 ResourceBundleMessageSourceStaticMessageSource。两者都实现 HierarchicalMessageSource 以便进行嵌套消息传递。 StaticMessageSource 很少使用,但是提供了将消息添加到源中的编程方式。下面的示例显示 ResourceBundleMessageSource

1
2
3
4
5
6
7
8
9
10
11
12
<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 对象解析消息的方式来处理。就本示例而言,假定上述两个资源束文件的内容如下:

1
2
# in format.properties
message=Alligators rock!
1
2
# in exceptions.properties
argument.required=The {0} argument is required.

下一个示例显示了执行 MessageSource 功能的程序。请记住,所有 ApplicationContext 实现也是 MessageSource 实现,因此可以转换为 MessageSource 接口。

1
2
3
4
5
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);
}

以上程序的结果输出如下:

1
Alligators rock!

总而言之,MessageSource 是在名为 beans.xml 的文件中定义的,该文件位于类路径的根目录下。 messageSource bean 定义通过其 basenames 属性引用了许多资源包。列表中传递给 basenames 属性的三个文件在类路径的根目录下以文件形式存在,分别称为 format.propertiesexceptions.propertieswindows.properties

下一个示例显示了传递给消息查找的参数。这些参数将转换为 String 对象,并插入到查找消息中的占位符中。

1
2
3
4
5
6
7
8
9
10
11
12
13
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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() 方法调用的结果输出如下:

1
The userDao argument is required.

关于国际化(“i18n”),Spring 的各种 MessageSource 实现遵循与标准 JDK ResourceBundle 相同的语言环境解析和后备规则。简而言之,继续前面定义的示例 messageSource,如果要针对英国(en-GB)语言环境解析消息,则可以分别创建名为 format_en_GB.propertiesexceptions_en_GB.propertieswindows_en_GB.properties 的文件。

通常,语言环境解析由应用程序的周围环境管理。在以下示例中,手动指定了针对其解析(英国)消息的语言环境:

1
2
# in exceptions_en_GB.properties
argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.
1
2
3
4
5
6
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);
}

运行上述程序的结果输出如下:

1
Ebagum lad, the 'userDao' argument is required, I say, required.

您还可以使用 MessageSourceAware 接口获取对已定义的任何 MessageSource 的引用。 创建和配置 bean 时,在 ApplicationContext 中实现 MessageSourceAware 接口的所有 bean 都会与应用程序上下文的 MessageSource 一起注入。

作为 ResourceBundleMessageSource 的替代,Spring 提供了 ReloadableResourceBundleMessageSource 类。 此变体支持相同的包文件格式,但比基于标准 JDK 的 ResourceBundleMessageSource 实现更灵活。特别是,它允许从任何 Spring 资源位置(不仅从类路径)读取文件,并且支持捆绑属性文件的热重载(同时在它们之间有效地进行缓存)。有关详细信息,请参见 ReloadableResourceBundleMessageSource javadoc

标准和自定义事件

通过 ApplicationEvent 类和 ApplicationListener 接口提供 ApplicationContext 中的事件处理。 如果将实现 ApplicationListener 接口的 bean 部署到上下文中,则每次将 ApplicationEvent 发布到 ApplicationContext 时,都会通知该 bean。 本质上,这是标准的 Observer 设计模式。

从 Spring 4.2 开始,事件基础结构得到了显着改进,并提供了基于注释的模型以及发布任意事件(即不一定从 ApplicationEvent 扩展的对象)的功能。发布此类对象后,我们会为您包装一个事件。

下表描述了 Spring 提供的标准事件:

事件 说明
ContextRefreshedEvent 在初始化或刷新 ApplicationContext 时发布(例如,通过使用 ConfigurableApplicationContext 接口上的 refresh() 方法)。 在这里,“已初始化”是指所有 Bean 均已加载,检测到并激活了后处理器 Bean,已预先实例化单例并且可以使用 ApplicationContext 对象。 只要尚未关闭上下文,只要选定的 ApplicationContext 实际上支持这种“热”刷新,就可以多次触发刷新。例如,XmlWebApplicationContext 支持热刷新,但 GenericApplicationContext 不支持。
ContextStartedEvent ConfigurableApplicationContext 接口上使用 start() 方法启动 ApplicationContext 时发布。在这里,“已启动”表示所有 Lifecycle bean 都收到一个明确的启动信号。通常,此信号用于在显式停止后重新启动 Bean,但也可以用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。
ContextStoppedEvent 通过使用 ConfigurableApplicationContext 接口上的 stop() 方法停止 ApplicationContext 时发布。 在这里,“已停止”表示所有 Lifecycle bean 都收到一个明确的停止信号。 停止的上下文可以通过 start() 调用重新启动。
ContextClosedEvent 通过使用 ConfigurableApplicationContext 接口上的 close() 方法或通过 JVM 关闭钩子关闭 ApplicationContext 时发布。 在这里,“已关闭”意味着所有单例 bean 将被销毁。 关闭上下文后,它将达到使用寿命,无法刷新或重新启动。
RequestHandledEvent 一个特定于 Web 的事件,告诉所有 Bean HTTP 请求已得到服务。请求完成后,将发布此事件。此事件仅适用于使用 Spring 的 DispatcherServlet 的 Web 应用程序。
ServletRequestHandledEvent RequestHandledEvent 的子类,用于添加特定于 Servlet 的上下文信息。

您还可以创建和发布自己的自定义事件。以下示例显示了一个简单的类,该类扩展了 Spring 的 ApplicationEvent 基类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BlackListEvent extends ApplicationEvent {

private final String address;
private final String content;

public BlackListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}

// accessor and other methods...
}

要发布自定义的 ApplicationEvent,请在 publishEvent() 上调用方法 ApplicationEventPublisher。通常,这是通过创建一个实现 ApplicationEventPublisherAware 并注册为 Spring bean 的类来完成的。以下示例显示了此类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class EmailService implements ApplicationEventPublisherAware {

private List<String> blackList;
private ApplicationEventPublisher publisher;

public void setBlackList(List<String> blackList) {
this.blackList = blackList;
}

public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}

public void sendEmail(String address, String content) {
if (blackList.contains(address)) {
publisher.publishEvent(new BlackListEvent(this, address, content));
return;
}
// send email...
}
}

在配置时,Spring 容器检测到 EmailService 实现了 ApplicationEventPublisherAware 并自动调用 setApplicationEventPublisher()。实际上,传入的参数是 Spring 容器本身。您正在通过其 ApplicationEventPublisher 接口与应用程序上下文进行交互。

要接收自定义 ApplicationEvent,可以创建一个实现 ApplicationListener 的类并将其注册为 Spring Bean。 以下示例显示了此类:

1
2
3
4
5
6
7
8
9
10
11
12
public class BlackListNotifier implements ApplicationListener<BlackListEvent> {

private String notificationAddress;

public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}

public void onApplicationEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
}

请注意,ApplicationListener 通常使用您的自定义事件的类型(上一示例中的 BlackListEvent)进行参数化。这意味着 onApplicationEvent() 方法可以保持类型安全,从而避免了任何向下转换的需求。您可以根据需要注册任意数量的事件侦听器,但是请注意,默认情况下,事件侦听器会同步接收事件。这意味着 publishEvent() 方法将阻塞,直到所有侦听器都已完成对事件的处理为止。这种同步和单线程方法的一个优点是,当侦听器接收到事件时,如果有可用的事务上下文,它将在发布者的事务上下文内部进行操作。如果有必要采用其他发布事件的策略,请参阅 Spring 的 ApplicationEventMulticaster 接口的 javadoc 和配置选项的 SimpleApplicationEventMulticaster 实现

以下示例显示了用于注册和配置上述每个类的 Bean 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
<bean id="emailService" class="example.EmailService">
<property name="blackList">
<list>
<value>known.spammer@example.org</value>
<value>known.hacker@example.org</value>
<value>john.doe@example.org</value>
</list>
</property>
</bean>

<bean id="blackListNotifier" class="example.BlackListNotifier">
<property name="notificationAddress" value="blacklist@example.org"/>
</bean>

总而言之,当调用 emailService bean 的 sendEmail() 方法时,如果有任何电子邮件应列入黑名单,则将发布 BlackListEvent 类型的自定义事件。 blackListNotifier bean 被注册为 ApplicationListener 并接收 BlackListEvent,这时它可以通知适当的参与者。

Spring 的事件机制旨在在同一应用程序上下文内在 Spring bean 之间进行简单的通信。 但是,对于更复杂的企业集成需求,单独维护的 Spring Integration 项目为基于著名的 Spring 编程模型构建轻量级,面向模式,事件驱动的架构提供了完整的支持。

基于注释的事件侦听器

从 Spring 4.2 开始,您可以使用 @EventListener 注释在托管 Bean 的任何公共方法上注册事件侦听器。BlackListNotifier 可改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BlackListNotifier {

private String notificationAddress;

public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}

@EventListener
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
}

方法签名再次声明其侦听的事件类型,但是这次使用灵活的名称,并且没有实现特定的侦听器接口。只要实际事件类型在其实现层次结构中解析您的通用参数,也可以通过通用类型来缩小事件类型。

如果您的方法应该侦听多个事件,或者您要完全不使用任何参数来定义它,则事件类型也可以在注释本身上指定。以下示例显示了如何执行此操作:

1
2
3
4
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
// ...
}

也可以通过使用定义 SpEL 表达式的注释的 condition 属性来添加其他运行时过滤,该注释应匹配以针对特定事件实际调用该方法。

以下示例显示了仅当事件的 content 属性等于 my-event 时,才可以重写我们的通知程序以进行调用:

1
2
3
4
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlackListEvent(BlackListEvent blEvent) {
// notify appropriate parties via notificationAddress...
}

每个 SpEL 表达式都会根据专用上下文进行求值。 下表列出了可用于上下文的项目,以便您可以将它们用于条件事件处理:

名称 位置 描述
事件 根对象 实际的 ApplicationEvent #root.eventevent
参数数组 根对象 用于调用方法的参数(作为对象数组)。 #root.argsargs; args[0]访问第一个参数,等等。
参数名称 求值上下文 任何方法参数的名称。如果由于某种原因这些名称不可用(例如,由于在编译的字节码中没有调试信息),则也可以使用#a<#arg>语法(其中<#arg>代表参数索引(从 0 开始)。 #blEvent#a0(您也可以使用 #p0#p<#arg> 参数符号作为别名

请注意,即使您的方法签名实际上引用了已发布的任意对象,root.event 也使您可以访问基础事件。

如果由于处理一个事件而需要发布另一个事件,则可以更改方法签名以返回应发布的事件,如以下示例所示:

1
2
3
4
5
@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}

异步侦听器不支持此功能。

此新方法为上述方法处理的每个 BlackListEvent 发布一个新的 ListUpdateEvent。如果您需要发布多个事件,则可以返回事件的 Collection

异步侦听器

如果希望特定的侦听器异步处理事件,则可以重用常规的 @Async 支持。 以下示例显示了如何执行此操作:

1
2
3
4
5
@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
// BlackListEvent is processed in a separate thread
}

使用异步事件时,请注意以下限制:

如果异步事件侦听器引发 Exception,则不会将其传播到调用方。有关更多详细信息,请参见 AsyncUncaughtExceptionHandler

异步事件侦听器方法无法通过返回值来发布后续事件。如果您需要发布另一个事件作为处理的结果,请注入 ApplicationEventPublisher 以手动发布事件。

侦听器排序

如果需要先调用一个侦听器,则可以将 @Order 注释添加到方法声明中,如以下示例所示:

1
2
3
4
5
@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}

一般事件

您还可以使用泛型来进一步定义事件的结构。考虑使用 EntityCreatedEvent<T>,其中 T 是已创建的实际实体的类型。例如,您可以创建以下侦听器定义以仅接收 PersonEntityCreatedEvent

1
2
3
4
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
// ...
}

由于类型擦除,只有在触发的事件解析了事件侦听器所依据的通用参数(即 class PersonCreatedEvent extends EntityCreatedEvent<Person> {…})时,此方法才起作用。

在某些情况下,如果所有事件都遵循相同的结构,这可能会变得很乏味(就像前面示例中的事件一样)。 在这种情况下,您可以实现 ResolvableTypeProvider 来指导框架超出运行时环境提供的范围。以下事件显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
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,而且适用于您作为事件发送的任何任意对象。

方便地访问低级资源

为了获得最佳用法和对应用程序上下文的理解,您应该熟悉 Spring 的 Resource 抽象,如参考资料所述。

应用程序上下文是 ResourceLoader,可用于加载 Resource 对象。Resource 本质上是 JDK java.net.URL 类的功能更丰富的版本。实际上,资源的实现在适当的地方包装了 java.net.URL 的实例。资源可以以透明的方式从几乎任何位置获取低级资源,包括从类路径、文件系统位置、可使用标准 URL 描述的任何位置以及一些其他变体。如果资源位置字符串是没有任何特殊前缀的简单路径,则这些资源的来源是特定的,并且适合于实际的应用程序上下文类型。

您可以配置部署到应用程序上下文中的 bean,以实现特殊的回调接口 ResourceLoaderAware,以便在初始化时自动调用,并将应用程序上下文本身作为 ResourceLoader 传入。您还可以公开 Resource 类型的属性,以用于访问静态资源。它们像其他任何属性一样注入其中。您可以将那些 Resource 属性指定为简单的 String 路径,并在部署 bean 时依靠从这些文本字符串到实际 Resource 对象的自动转换。

提供给 ApplicationContext 构造函数的一个或多个位置路径实际上是资源字符串,并且根据特定的上下文实现以简单的形式对其进行适当处理。例如,ClassPathXmlApplicationContext 将简单的位置路径视为类路径位置。您也可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或 URL 中加载定义,而不管实际的上下文类型如何。

Web 应用程序的便捷 ApplicationContext 实例化

您可以使用例如 ContextLoader 声明性地创建 ApplicationContext 实例。 当然,您还可以使用 ApplicationContext 实现之一以编程方式创建 ApplicationContext 实例。

您可以使用 ApplicationContext 来注册一个 ContextLoaderListener,如以下示例所示:

1
2
3
4
5
6
7
8
<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 的任何子目录中的文件)。

将 Spring ApplicationContext 部署为 Java EE RAR 文件

可以将 Spring ApplicationContext 部署为 RAR 文件,并将上下文及其所有必需的 bean 类和库 JAR 封装在 Java EE RAR 部署单元中。这等效于引导独立的 ApplicationContext(仅托管在 Java EE 环境中)能够访问 Java EE 服务器功能。 RAR 部署是部署无头 WAR 文件的方案的一种更自然的选择——实际上,这种 WAR 文件没有任何 HTTP 入口点,仅用于在 Java EE 环境中引导 Spring ApplicationContext

对于不需要 HTTP 入口点而仅由消息端点和计划的作业组成的应用程序上下文,RAR 部署是理想的选择。在这样的上下文中,Bean 可以使用应用程序服务器资源,例如 JTA 事务管理器和 JNDI 绑定的 JDBC DataSource 实例以及 JMS ConnectionFactory 实例,并且还可以在平台的 JMX 服务器上注册-通过 Spring 的标准事务管理以及 JNDI 和 JMX 支持工具。应用程序组件还可以通过 Spring 的 TaskExecutor 抽象与应用程序服务器的 JCA WorkManager 进行交互。

有关 RAR 部署中涉及的配置详细信息,请参见 SpringContextResourceAdapter 类的 javadoc

对于将 Spring ApplicationContext 作为 Java EE RAR 文件的简单部署:

  1. 将所有应用程序类打包到 RAR 文件(这是具有不同文件扩展名的标准 JAR 文件)中。将所有必需的库 JAR 添加到 RAR 归档文件的根目录中。添加一个 META-INF/ra.xml 部署描述符(如 javadoc 中的 SpringContextResourceAdapter 所示)和相应的 Spring XML bean 定义文件(通常为 META-INF/applicationContext.xml)。

  2. 将生成的 RAR 文件拖放到应用程序服务器的部署目录中。

此类 RAR 部署单元通常是独立的。它们不会将组件暴露给外界,甚至不会暴露给同一应用程序的其他模块。与基于 RAR 的 ApplicationContext 的交互通常是通过与其他模块共享的 JMS 目标进行的。例如,基于 RAR 的 ApplicationContext 还可以安排一些作业或对文件系统(或类似文件)中的新文件做出反应。如果需要允许来自外部的同步访问,则可以(例如)导出 RMI 端点,该端点可以由同一台计算机上的其他应用程序模块使用。

BeanFactory

BeanFactory API 为 Spring 的 IoC 功能提供了基础。它的特定合同主要用于与 Spring 的其他部分以及相关的第三方框架集成,并且它的 DefaultListableBeanFactory 实现是更高级别的 GenericApplicationContext 容器中的关键委托。

BeanFactory 和相关接口(例如 BeanFactoryAwareInitializingBeanDisposableBean)是其他框架组件的重要集成点。通过不需要任何注释,甚至不需要反射,它们可以在容器及其组件之间进行非常有效的交互。应用程序级 Bean 可以使用相同的回调接口,但通常更喜欢通过注释或通过编程配置进行声明式依赖注入。

请注意,核心 BeanFactory API 级别及其 DefaultListableBeanFactory 实现不对配置格式或要使用的任何组件注释进行假设。所有这些风味都是通过扩展(例如 XmlBeanDefinitionReaderAutowiredAnnotationBeanPostProcessor)引入的,并以核心元数据表示形式对共享 BeanDefinition 对象进行操作。这就是使 Spring 的容器如此灵活和可扩展的本质。

BeanFactory 还是 ApplicationContext

本节说明 BeanFactoryApplicationContext 容器级别之间的区别以及对引导的影响。

除非有充分的理由,否则应使用 ApplicationContext,除非将 GenericApplicationContext 及其子类 AnnotationConfigApplicationContext 作为自定义引导的常见实现,否则应使用 ApplicationContext。这些是用于所有常见目的的 Spring 核心容器的主要入口点:加载配置文件,触发类路径扫描,以编程方式注册 Bean 定义和带注释的类,以及(从 5.0 版本开始)注册功能性 Bean 定义。

因为 ApplicationContext 包含 BeanFactory 的所有功能,所以通常建议在普通 BeanFactory 上使用,除非需要完全控制 Bean 处理的方案。在 ApplicationContext(例如 GenericApplicationContext 实现)中,按照约定(即,按 Bean 名称或 Bean 类型(尤其是后处理器))检测到几种 Bean,而普通的 DefaultListableBeanFactory 不知道任何特殊的 Bean。

对于许多扩展的容器功能,例如注释处理和 AOP 代理,BeanPostProcessor 扩展点是必不可少的。如果仅使用普通的 DefaultListableBeanFactory,则默认情况下不会检测到此类后处理器并将其激活。这种情况可能会造成混淆,因为您的 bean 配置实际上并没有错。而是在这种情况下,需要通过其他设置完全引导容器。

下表列出了 BeanFactoryApplicationContext 接口和实现所提供的功能。

特征 BeanFactory ApplicationContext
Bean 实例化/接线
集成生命周期管理 没有
自动 BeanPostProcessor 注册 没有
自动 BeanFactoryPostProcessor 注册 没有
方便的 MessageSource 访问(用于国际化) 没有
内置 ApplicationEvent 发布机制 没有

要向 DefaultListableBeanFactory 显式注册 Bean 后处理器,需要以编程方式调用 addBeanPostProcessor,如以下示例所示:

1
2
3
4
5
6
7
8
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

要将 BeanFactoryPostProcessor 应用于普通的 DefaultListableBeanFactory,您需要调用其 postProcessBeanFactory 方法,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
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);

在这两种情况下,显式的注册步骤都是不方便的,这就是为什么在 Spring 支持的应用程序中,各种 ApplicationContext 变量比普通的 DefaultListableBeanFactory 更为可取的原因,尤其是在典型企业设置中依赖 BeanFactoryPostProcessorBeanPostProcessor 实例来扩展容器功能时。

AnnotationConfigApplicationContext 已注册了所有常见的注释后处理器,并且可以通过配置注释(例如 @EnableTransactionManagement)在幕后引入其他处理器。 在 Spring 基于注释的配置模型的抽象级别上,bean 后处理器的概念仅是内部容器详细信息。