Appearance
Spring 核心:IoC 容器 - Part4
Tip:基于 Spring Core 5.3.30 版本。
1. 基于 Java 的容器配置
这一部分介绍了如何在你的 Java 代码中使用注解来配置 Spring 容器。它包括以下主题:
- 基本概念:
@Bean和@Configuration - 使用
AnnotationConfigApplicationContext实例化 Spring 容器 - 使用
@Bean注解 - 使用
@Configuration注解 - 组合基于 Java 的配置
- Bean 定义配置文件
PropertySource抽象- 使用
@PropertySource - 语句中的占位符解析
1.1. 基本概念:@Bean 和 @Configuration
在 Spring 的 Java 配置支持中,@Configuration 注解的类和 @Bean 注解的方法是核心工件。
@Bean 注解用于指示一个方法实例化、配置并初始化一个新的对象,这个对象将由 Spring IoC 容器管理。对于熟悉 Spring 的 <beans/> XML 配置的人来说,@Bean 注解扮演着与 <bean/> 元素相同的角色。你可以在任何 Spring @Component 中使用 @Bean 注解的方法。然而,它们最常与 @Configuration Bean 一起使用。
使用 @Configuration 注解一个类,表明它的主要目的是作为 Bean 定义的来源。此外,@Configuration 类允许通过在同一个类中调用其他 @Bean 方法来定义 Bean 之间的依赖关系。最简单的 @Configuration 类如下所示:
Java
@Configuration
public class AppConfig {
@Bean
public MyServiceImpl myService() {
return new MyServiceImpl();
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
前面的 AppConfig 类等同于以下的 Spring <beans/> XML:
XML
<beans>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>1
2
3
2
3
Tip:完整的
@ConfigurationVS “精简” 的@Bean模式当
@Bean方法在没有用@Configuration注解的类中声明时,它们被称为以 “精简” 模式处理。在@Component或者普通类中声明的 Bean 方法被认为是 “精简” 的,包含类的主要目的不同,而@Bean方法在那里是一种额外的奖励。例如,服务组件可能通过在每个适用的组件类上额外的@Bean方法,向容器暴露管理视图。在这种情况下,@Bean方法是一种通用的工厂方法机制。与完整的
@Configuration不同,精简的@Bean方法不能声明 Bean 之间的依赖关系。相反,它们对其包含的组件的内部状态进行操作,也可以对它们可能声明的参数进行操作。因此,这样的@Bean方法不应该调用其他@Bean方法。每个这样的方法实际上只是一个特定 Bean 引用的工厂方法,没有任何特殊的运行时语义。这里的积极的副作用是,运行时不必应用 CGLIB 子类化,因此在类设计方面没有任何限制(也就是说,包含类可以是final等)。在常见的场景中,
@Bean方法应该在@Configuration类中声明,确保始终使用 “完整” 模式,并且跨方法引用因此会被重定向到容器的生命周期管理。这防止了同一个@Bean方法被通过常规的 Java 调用意外地调用,这有助于减少在 “精简” 模式下操作时难以追踪的微妙错误。
@Bean 和 @Configuration 注解将在接下来的部分中深入讨论。但首先,我们将介绍使用基于 Java 的配置创建 Spring 容器的各种方式。
1.2. 使用 AnnotationConfigApplicationContext 实例化 Spring 容器
以下部分将详细介绍 Spring 的 AnnotationConfigApplicationContext,该功能在 Spring 3.0 中引入。这种灵活的 ApplicationContext 实现不仅能接受 @Configuration 类作为输入,还能接受普通的 @Component 类和使用 JSR-330 元数据注解的类。
当 @Configuration 类作为输入提供时,@Configuration 类本身被注册为 Bean 定义,类中声明的所有 @Bean 方法也被注册为 Bean 定义。
当 @Component 和 JSR-330 类被提供时,它们被注册为 Bean 定义,并假设在必要的地方在这些类中使用了诸如 @Autowired 或 @Inject 等的 DI 元数据。
1.2.1. 简单构造
就像在实例化 ClassPathXmlApplicationContext 时使用 Spring XML 文件作为输入一样,你也可以在实例化 AnnotationConfigApplicationContext 时使用 @Configuration 类作为输入。这允许你完全不使用 XML 的方式来使用 Spring 容器,如下面的示例所示:
Java
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}1
2
3
4
5
2
3
4
5
如前所述,AnnotationConfigApplicationContext 不仅限于仅与 @Configuration 类一起工作。任何带有 @Component 或 JSR-330 注解的类都可以作为构造函数的输入,如下面的例子所示:
Java
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}1
2
3
4
5
2
3
4
5
以上示例假设 MyServiceImpl、Dependency1 和 Dependency2 使用了 Spring 的依赖注入注解,例如 @Autowired。
1.2.2. 使用 register(Class<?>…) 以编程方式构建容器
你可以通过使用无参数的构造函数来实例化 AnnotationConfigApplicationContext,然后使用 register() 方法进行配置。这种方法在以编程方式构建 AnnotationConfigApplicationContext 时特别有用。以下示例展示了如何操作:
Java
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();
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
1.2.3. 使用 scan(String…) 启用组件扫描
要启用组件扫描,你可以如下注解你的 @Configuration 类:
Java
@Configuration
@ComponentScan(basePackages = "com.acme")
public class AppConfig {
// ...
}1
2
3
4
5
2
3
4
5
Note
有经验的 Spring 用户可能会熟悉 Spring 的
context:命名空间中的 XML 声明等价物,如下例所示:XML<beans> <context:component-scan base-package="com.acme"/> </beans>1
2
3
在前面的示例中,扫描 com.acme 包以寻找任何带有 @Component 注解的类,并将这些类注册为容器中的 Spring Bean 定义。AnnotationConfigApplicationContext 提供了 scan(String... ) 方法,以允许进行相同的组件扫描功能,如下例所示:
Java
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
}1
2
3
4
5
6
2
3
4
5
6
Note:请记住,
@Configuration类是用@Component元注解的,因此它们是组件扫描的候选对象。在前面的示例中,假设AppConfig是在com.acme包(或其下的任何包)中声明的,那么在调用scan()时,它会被选中。在refresh()之后,所有的@Bean方法都会被处理,并在容器中注册为 Bean 定义。
1.2.4. 使用 AnnotationConfigWebApplicationContext 支持 Web 应用程序
AnnotationConfigApplicationContext 的 WebApplicationContext 变体是 AnnotationConfigWebApplicationContext。当配置 Spring 的 ContextLoaderListener 服务器监听器、Spring MVC 的 DispatcherServlet 等时,你可以使用这个实现。以下的 web.xml 片段配置了一个典型的 Spring MVC Web 应用程序(注意 contextClass 上下文参数和初始化参数的使用):
XML
<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>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
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
Tip:对于编程用例,
GenericWebApplicationContext可以作为AnnotationConfigWebApplicationContext的替代品。详情请参见GenericWebApplicationContext的 JavaDoc。
1.3. 使用 @Bean 注解
@Bean 是一个方法级别的注解,直接对应于 XML 中的 <bean/> 元素。该注解支持 <bean/> 提供的一些属性,例如:
你可以在带有 @Configuration 注解或 @Component 注解的类中使用 @Bean 注解。
1.3.1. 声明一个 Bean
要声明一个 Bean,你可以在方法上使用 @Bean 注解。你可以使用这个方法在 ApplicationContext 中注册一个 Bean 定义,该 Bean 的类型由方法的返回值指定。默认情况下,Bean 的名称与方法名称相同。以下示例显示了一个 @Bean 方法声明:
Java
@Configuration
public class AppConfig {
@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
前面的配置与以下的 Spring XML 完全等价:
XML
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>1
2
3
2
3
这两种声明都在 ApplicationContext 中提供了一个名为 transferService 的 Bean,该 Bean 绑定到 TransferServiceImpl 类型的对象实例,如下图所示:
Text
transferService -> com.acme.TransferServiceImpl你也可以使用默认方法来定义 Bean。这允许通过在默认方法上实现带有 Bean 定义的接口来组合 Bean 配置。
Java
public interface BaseConfig {
@Bean
default TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
@Configuration
public class AppConfig implements BaseConfig {
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
你也可以声明你的 @Bean 方法,其返回类型为接口(或基类),如下例所示:
Java
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
然而,这将提前类型预测的可见性限制在指定的接口类型(TransferService)。然后,只有在受影响的单例 Bean 被实例化后,容器才知道完整的类型(TransferServiceImpl)。非延迟加载的单例 Bean 按照它们的声明顺序被实例化,所以你可能会看到不同的类型匹配结果,这取决于另一个组件何时尝试匹配一个未声明的类型(例如 @Autowired TransferServiceImpl,只有在 transferService Bean 被实例化后才能解析)。
Note:如果你始终通过声明的服务接口引用你的类型,那么你的
@Bean返回类型可以安全地加入该设计决策。然而,对于实现了多个接口的组件,或者可能被其实现类型引用的组件,声明尽可能具体的返回类型是更安全的(至少要具体到引用你的 Bean 的注入点要求的具体类型一样)。
1.3.2. Bean 依赖关系
一个用 @Bean 注解的方法可以有任意数量的参数,这些参数描述了构建该 Bean 所需的依赖项。例如,如果我们的 TransferService 需要一个 AccountRepository,我们可以用一个方法参数来实现这个依赖,如下面的示例所示:
Java
@Configuration
public class AppConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
解析机制与基于构造函数的依赖注入几乎完全相同。有关更多详细信息,请参阅相关部分。
1.3.3. 接收生命周期回调
任何用 @Bean 注解定义的类都支持常规的生命周期回调,并且可以使用来自 JSR-250 的 @PostConstruct 和 @PreDestroy 注解。有关更多详细信息,请参阅 JSR-250 注解。
常规的 Spring 生命周期回调也得到了完全支持。如果一个 Bean 实现了 InitializingBean,DisposableBean 或 Lifecycle,那么它们各自的方法将被容器调用。
标准的 *Aware 接口集(如 BeanFactoryAware,BeanNameAware,MessageSourceAware,ApplicationContextAware 等)也得到了完全支持。
@Bean 注解支持指定任意的初始化和销毁回调方法,这与 Spring XML 的 bean 元素上的 init-method 和 destroy-method 属性非常相似,如下面的示例所示:
Java
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();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Warning
默认情况下,使用 Java 配置定义的具有公共
close或shutdown方法的 Bean 会自动注册一个销毁回调。如果你有一个公共的close或shutdown方法,并且你不希望在容器关闭时调用它,你可以在你的 Bean 定义中添加@Bean(destroyMethod="")来禁用默认的inferred(推断)模式。你可能希望对你通过 JNDI 获取的资源默认执行此操作,因为其生命周期是在应用程序之外管理的。特别是,确保始终对
DataSource执行此操作,因为已知在 Java EE 应用服务器上存在问题。以下示例展示了如何防止
DataSource的自动销毁回调:Java@Bean(destroyMethod="") public DataSource dataSource() throws NamingException { return (DataSource) jndiTemplate.lookup("MyDS"); }1
2
3
4此外,对于
@Bean方法,你通常会使用编程式的 JNDI 查找,可以通过使用 Spring 的JndiTemplate或JndiLocatorDelegate辅助工具,或者直接使用 JNDIInitialContext,但不是JndiObjectFactoryBean变体(这将迫使你将返回类型声明为FactoryBean类型,而不是实际的目标类型,这使得在其他打算引用这里提供的资源的@Bean方法中进行交叉引用调用变得更加困难)。
在上述注释中的 BeanOne 示例的情况下,直接在构造过程中调用 init() 方法也是同样有效的,如下面的示例所示:
Java
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
BeanOne beanOne = new BeanOne();
beanOne.init();
return beanOne;
}
// ...
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Note:当你直接在 Java 中工作时,你可以随意操作你的对象,而不总是需要依赖于容器生命周期。
1.3.4. 指定 Bean 作用域
Spring 包含了 @Scope 注解,以便你可以指定 Bean 的作用域。
1.3.4.1. 使用 @Scope 注解
你可以指定用 @Bean 注解定义的 Bean 应该有一个特定的作用域。你可以使用在 Bean 作用域小节中指定的任何标准作用域。
默认的作用域是 singleton,但你可以使用 @Scope 注解来覆盖这个设置,如下面的示例所示:
Java
@Configuration
public class MyConfiguration {
@Bean
@Scope("prototype")
public Encryptor encryptor() {
// ...
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
1.3.4.2. @Scope 和 scoped-proxy
Spring 提供了一种通过作用域代理来处理作用域依赖的便捷方式。在使用 XML 配置时,创建此类代理的最简单方法是 <aop:scoped-proxy/> 元素。使用 @Scope 注解在 Java 中配置你的 Bean 提供了与 proxyMode 属性等效的支持。默认值是 ScopedProxyMode.DEFAULT,这通常表示除非在组件扫描指令级别配置了不同的默认值,否则不应创建作用域代理。你可以指定 ScopedProxyMode.TARGET_CLASS,ScopedProxyMode.INTERFACES 或 ScopedProxyMode.NO。
如果你将 XML 参考文档中的作用域代理示例(参见作用域代理)移植到我们使用 Java @Bean 的示例,它看起来将如下所示:
Java
// 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;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Note:
@SessionScope源码
@SessionScope中将proxyMode声明为ScopedProxyMode.TARGET_CLASS:Java@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Scope(WebApplicationContext.SCOPE_SESSION) public @interface SessionScope { @AliasFor(annotation = Scope.class) ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; }1
2
3
4
5
6
7
8
1.3.5. 自定义 Bean 命名
默认情况下,配置类会使用 @Bean 方法的名称作为生成的 Bean 的名称。然而,这个功能可以被 name 属性覆盖,如下面的示例所示:
Java
@Configuration
public class AppConfig {
@Bean("myThing")
public Thing thing() {
return new Thing();
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
1.3.6. Bean 别名
如在 Bean 命名中所讨论的,有时我们希望给一个 Bean 分配多个名称,这也被称为 Bean 别名。@Bean 注解的 name 属性接受一个字符串数组来实现这个目的。下面的示例展示了如何为一个 Bean 设置多个别名:
Java
@Configuration
public class AppConfig {
@Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
public DataSource dataSource() {
// instantiate, configure and return DataSource bean...
}
}1
2
3
4
5
6
7
2
3
4
5
6
7
1.3.7. Bean 描述
有时,提供关于 Bean 的更详细的文本描述会很有帮助。特别是当 Bean 被暴露(可能通过 JMX)用于监控目的时,这会特别有用。
要给 @Bean 添加描述,你可以使用 @Description 注解,如下面的示例所示:
Java
@Configuration
public class AppConfig {
@Bean
@Description("Provides a basic example of a bean")
public Thing thing() {
return new Thing();
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
1.4. 使用 @Configuration 注解
@Configuration 是一个类级别的注解,表示一个对象是 Bean 定义的来源。@Configuration 类通过 @Bean 注解的方法声明 Bean。在 @Configuration 类上调用 @Bean 方法也可以用来定义 Bean 之间的依赖关系。请参阅基本概念:@Bean 和 @Configuration 小节以获取一般性的介绍。
1.4.1. 注入 Bean 之间的依赖
当 Bean 之间存在依赖关系时,表达这种依赖关系就像一个 Bean 方法调用另一个一样简单,如下面的示例所示:
Java
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
在前面的示例中,beanOne 通过构造函数注入接收到了对 beanTwo 的引用。
Warning:只有当
@Bean方法在@Configuration类中声明时,这种声明 Bean 之间依赖关系的方法才有效。你不能使用普通的@Component类来声明 Bean 之间的依赖关系。
1.4.2. 查找方法注入
如前所述,查找方法注入是一个高级特性,你应该很少使用。在单例作用域的 Bean 依赖于原型作用域的 Bean 的情况下,这个特性很有用。使用 Java 进行这种类型的配置提供了实现这种模式的自然手段。下面的示例展示了如何使用查找方法注入:
Java
public abstract class CommandManager {
public Object process(Map commandState) {
// grab a new instance of the appropriate Command
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
通过使用 Java 配置,你可以创建一个 CommandManager 的子类,其中抽象的 createCommand() 方法被重写,以便它查找一个新的(原型)命令对象。下面的示例展示了如何做到这一点:
Java
@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();
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1.4.3. 关于基于 Java 配置的内部工作方式的更多信息
考虑以下示例,它展示了一个被 @Bean 注解的方法被调用两次,但是只实例化了一个 Bean 对象:
Java
@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();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
clientDao() 在 clientService1() 和 clientService2() 中各被调用了一次。由于这个方法创建了一个新的 ClientDaoImpl 实例并返回它,你通常会期望有两个实例(每个服务一个)。这肯定会有问题:在 Spring 中,默认情况下,实例化的 Bean 的作用域是单例的。这就是魔法发生的地方:所有的 @Configuration 类在启动时都会被 CGLIB 子类化。在子类中,子方法在调用父方法并创建新实例之前,首先检查容器中是否有任何缓存的(作用域的)Bean。
Note 1:你的 Bean 的行为可能会因其作用域的不同而不同。我们在这里讨论的是单例。
Note 2:从 Spring 3.2 开始,你不再需要将 CGLIB 添加到你的类路径中,因为 CGLIB 类已经被重新打包到
org.springframework.cglib下,并直接包含在spring-coreJAR 中。
Note 3
由于 CGLIB 在启动时动态添加功能,因此存在一些限制。特别是,配置类不能是
final的。然而,从 4.3 版本开始,配置类上允许任何构造函数,包括使用@Autowired或声明单个非默认构造函数以进行默认注入。如果你更倾向于避免任何由 CGLIB 引起的限制,可以考虑在非
@Configuration类(例如,普通的@Component类)上声明你的@Bean方法。这样,@Bean方法之间的跨方法调用就不会被拦截,所以你必须完全依赖于在构造函数或方法级别的依赖注入。
1.5. 组合基于 Java 的配置
Spring 的基于 Java 的配置功能让你可以组合注解,这可以减少你的配置的复杂性。
1.5.1. 使用 @Import 注解
就像在 Spring XML 文件中使用 <import/> 元素来帮助模块化配置一样,@Import 注解允许从另一个配置类加载 @Bean 定义,如下面的示例所示:
Java
@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();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
现在,当实例化上下文时,不再需要明确指定 ConfigA.class 和 ConfigB.class,只需要明确提供 ConfigB,如下面的示例所示:
Java
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);
}1
2
3
4
5
6
7
2
3
4
5
6
7
这种方法简化了容器的实例化,因为只需要处理一个类,而不是在构造过程中需要记住可能数量庞大的 @Configuration 类。
Tip:从 Spring Framework 4.2 开始,
@Import也支持引用常规组件类,类似于AnnotationConfigApplicationContext.register方法。如果你想避免组件扫描,通过使用几个配置类作为入口点来明确定义所有的组件,这将特别有用。
1.5.1.1. 对导入的 @Bean 定义注入依赖
前面的示例虽然可行,但过于简单。在大多数实际场景中,Bean 在配置类之间相互依赖。当使用 XML 时,这不是问题,因为没有编译器参与,你可以声明 ref="someBean",并信任 Spring 在容器初始化期间解决它。但是,当使用 @Configuration 类时,Java 编译器对配置模型施加了约束,即对其他 Bean 的引用必须是有效的 Java 语法。
幸运的是,解决这个问题很简单。正如我们已经讨论过的,@Bean 方法可以有任意数量的参数来描述 Bean 的依赖关系。考虑以下更接近实际的场景,有几个 @Configuration 类,每个类都依赖于其他类中声明的 Bean:
Java
@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");
}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
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
还有另一种方式可以达到同样的结果。请记住,@Configuration 类最终只是容器中的另一个 Bean:这意味着它们可以利用 @Autowired 和 @Value 注入以及其他特性,就像任何其他 Bean 一样。
Note
确保你以这种方式注入的依赖关系只是最简单的类型。
@Configuration类在上下文初始化过程中的早期就被处理了,强制以这种方式注入依赖关系可能会导致意外的早期初始化。只要可能,就像前面的示例那样,尽量使用基于参数的注入。另外,通过
@Bean定义BeanPostProcessor和BeanFactoryPostProcessor时要特别小心。这些通常应该被声明为静态的@Bean方法,不触发其包含的配置类的实例化。否则,@Autowired和@Value可能无法在配置类本身上工作,因为有可能在AutowiredAnnotationBeanPostProcessor之前就把它作为一个 Bean 实例创建出来。
以下示例展示了如何将一个 Bean 自动装配到另一个 Bean:
Java
@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");
}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
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
Note:在
@Configuration类中的构造函数注入只从 Spring Framework 4.3 开始支持。另外,如果目标 Bean 只定义了一个构造函数,那么就没有必要指定@Autowired。
完全限定导入的 Bean 以便于导航
在前述场景中,使用 @Autowired 效果很好,提供了所需的模块化,但是确定自动装配的 Bean 定义到底在哪里声明的还是有些模糊。例如,作为一个关注 ServiceConfig 的开发者,你如何确切知道 @Autowired AccountRepository Bean 是在哪里声明的呢?这在代码中并没有明确指出,这可能没什么问题。记住,Eclipse 的 Spring 工具提供了可以渲染显示所有连接方式的图形工具,这可能就是你所需要的。此外,你的 Java IDE 可以轻松找到 AccountRepository 类型的所有声明和使用,并快速向你展示返回该类型的 @Bean 方法的位置。
在这种模糊性不可接受的情况下,如果你希望能够在你的 IDE 中直接从一个 @Configuration 类导航到另一个 @Configuration 类,可以考虑自动装配配置类本身。以下示例展示了如何做到这一点:
Java
@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());
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
在前述情况中,AccountRepository 的定义位置是完全明确的。然而,ServiceConfig 现在与 RepositoryConfig 紧密耦合。这就是需要权衡的地方。这种紧密耦合可以通过使用基于接口或者基于抽象类的 @Configuration 类来在一定程度上缓解。请参考以下示例:
Java
@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 new 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");
}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
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
现在,ServiceConfig 与具体的 DefaultRepositoryConfig 的耦合度较低,而且内置的 IDE 工具仍然很有用:你可以轻松地获取 RepositoryConfig 实现的类型层次结构。这样,导航 @Configuration 类及其依赖就与通常导航基于接口的代码的过程无异。
Note:如果你想影响某些 Bean 的启动创建顺序,可以考虑将其中一些声明为
@Lazy(在首次访问时创建,而不是在启动时创建)或者声明为依赖于(@DependsOn)某些其他 Bean(确保在当前 Bean 之前创建特定的其他 Bean,超越后者直接依赖关系所暗示的)。
1.5.2. 有条件地包含 @Configuration 类或 @Bean 方法
根据一些任意的系统状态,有条件地启用或禁用完整的 @Configuration 类或甚至单个的 @Bean 方法通常是非常有用的。这方面的一个常见示例是使用 @Profile 注解,当在 Spring 环境中已启用了特定的配置文件时,才激活 Bean(详见 Bean 定义配置文件)。
实际上,@Profile 注解是通过使用一个更加灵活的注解 @Conditional 来实现的。@Conditional 注解指示在注册 @Bean 之前应该咨询特定的 org.springframework.context.annotation.Condition 实现。
Condition 接口的实现提供了一个 matches(…) 方法,该方法返回 true 或 false。例如,下方清单显示了用于 @Profile 的实际 Condition 实现:
Java
@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;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
有关更多详细信息,请参阅 @Conditional JavaDoc。
1.5.3. 结合 Java 和 XML 配置
Spring 的 @Configuration 类支持并不旨在完全取代 Spring XML。一些设施,如 Spring XML 命名空间,仍然是配置容器的理想方式。在 XML 方便或必要的情况下,你有一个选择:要么以 “以 XML 为中心” 的方式实例化容器,例如使用 ClassPathXmlApplicationContext,要么以 “以 Java 为中心” 的方式使用 AnnotationConfigApplicationContext 和 @ImportResource 注解按需导入 XML。
1.5.3.1. 以 XML 为中心使用 @Configuration 类
可能更倾向于从 XML 引导 Spring 容器,并以特别的方式包含 @Configuration 类。例如,在一个大型的已存在的使用 Spring XML 的代码库中,根据需要创建 @Configuration 类并从现有的 XML 文件中包含它们会更容易。在本节的后面,我们将介绍在这种 “以 XML 为中心” 的情况下使用 @Configuration 类的选项。
将 @Configuration 类声明为 Spring 的普通 <bean/> 元素
请记住,@Configuration 类最终是容器中的 Bean 定义。在这一系列的示例中,我们创建了一个名为 AppConfig 的 @Configuration 类,并将其作为 <bean/> 定义包含在 system-test-config.xml 中。因为 <context:annotation-config/> 已经开启,所以容器识别了 @Configuration 注解,并正确处理了 AppConfig 中声明的 @Bean 方法。
以下示例展示了一个普通的 Java 配置类:
Java
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
@Bean
public TransferService transferService() {
return new TransferService(accountRepository());
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
以下示例展示了一个样本 system-test-config.xml 文件的一部分:
XML
<beans>
<!-- enable processing of annotations such as @Autowired and @Configuration -->
<context:annotation-config/>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
<bean class="com.acme.AppConfig"/>
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
以下示例展示了一个可能的 jdbc.properties 文件:
Properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=1
2
3
2
3
Java
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}1
2
3
4
5
2
3
4
5
Note:在
system-test-config.xml文件中,AppConfig<bean/>并未声明id元素。虽然这样做是可以接受的,但这是不必要的,因为没有其他 Bean 会引用它,且不太可能通过名称显式地从容器中获取它。同样,DataSourceBean 只是按类型进行自动装配,因此严格来说并不需要显式的 Bean Id。
使用 <context:component-scan/> 来拾取 @Configuration 类
因为 @Configuration 是用 @Component 元注解的,所以带有 @Configuration 注解的类自动成为组件扫描的候选对象。使用与前面示例中描述的相同的场景,我们可以重新定义 system-test-config.xml 来利用组件扫描。请注意,在这种情况下,我们不需要显式声明 <context:annotation-config/>,因为 <context:component-scan/> 启用了相同的功能。
以下示例展示了修改后的 system-test-config.xml 文件:
XML
<beans>
<!-- picks up and registers AppConfig as a bean definition -->
<context:component-scan base-package="com.acme"/>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
1.5.3.2. 以 @Configuration 类为中心,配合 @ImportResource 使用 XML
在 @Configuration 类作为配置容器的主要手段的应用中,可能仍需要使用一些 XML。在这些情况下,你可以利用 @ImportResource,并只定义你所需的 XML。这样,你就实现了一种 “以 Java 为中心” 的配置容器的方式,同时将 XML 限制到最少。以下示例(包括一个配置类、定义 Bean 的 XML 文件、一个属性文件和主类)展示了如何使用 @ImportResource 注解实现 “以 Java 为中心” 并根据需要使用 XML 的配置:
Java
@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);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
properties-config.xml:
XML
<beans>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>1
2
3
2
3
jdbc.properties:
Properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=1
2
3
2
3
Java
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}1
2
3
4
5
2
3
4
5
2. 环境抽象
Environment 接口是一个集成在容器中的抽象,它对应用环境的两个关键方面进行建模:配置文件(profiles)和属性(properties)。
配置文件是一个命名的、逻辑的 Bean 定义组,只有在给定的配置文件激活的情况下才会在容器中注册。无论是在 XML 中定义还是使用注解,Bean 都可以被分配到一个配置文件中。Environment 对象在处理配置文件时的角色在于确定哪些配置文件(如果有的话)当前处于激活状态,以及哪些配置文件(如果有的话)应该默认处于激活状态。
属性在几乎所有的应用中都扮演着重要的角色,可能来自于多种源头:属性文件、JVM 系统属性、系统环境变量、JNDI、Servlet 上下文参数、临时的 Properties 对象、Map 对象等等。Environment 对象在与属性相关的问题上的作用是为用户提供了一个方便的服务接口,用于配置属性源和解析其中的属性。
2.1. Bean 定义配置文件
Bean 定义配置文件在核心容器中提供了一种机制,允许在不同的环境中注册不同的 Bean。“环境” 这个词对于不同的用户可能有不同的含义,此功能可以帮助解决许多用例,包括:
- 在开发环境中使用内存中的数据源,而在 QA 或生产环境中通过 JNDI 查找相应的数据源;
- 只在将应用程序部署到性能环境时注册监控基础设施;
- 为针对客户 A 和客户 B 的部署注册定制的 Bean 实现;
考虑一下在需要 DataSource 的实际应用中的第一个用例。在测试环境中,配置可能类似于以下内容:
Java
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
现在考虑一下这个应用程序如何能够部署到 QA 或生产环境中,假设应用程序的数据源已经在生产应用服务器的 JNDI 目录中注册。我们的 dataSource Bean 现在看起来像下面的列表:
Java
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}1
2
3
4
5
2
3
4
5
问题在于如何根据当前环境切换使用这两种变体。随着时间的推移,Spring 用户已经找到了许多解决这个问题的方法,通常依赖于系统环境变量和包含 ${placeholder} 代号的 XML <import/> 语句的组合,这些代号会根据环境变量的值解析成正确的配置文件路径。Bean 定义配置文件是一种核心容器功能,为这个问题提供了解决方案。
如果我们将前述环境特定 Bean 定义的用例一般化,我们最终需要在某些上下文中注册特定的 Bean 定义,但在其他上下文中不需要。你可以说在情况 A 中,你想要注册特定的 Bean 定义配置文件,在情况 B 中注册不同的配置文件。我们首先更新我们的配置以反映这个需要。
2.1.1. 使用 @Profile
@Profile 注解允许你指示当一个或多个指定的配置文件处于激活状态时,一个组件有资格进行注册。使用我们前面的例子,我们可以按照以下方式重写 dataSource 的配置:
Java
@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
11
12
2
3
4
5
6
7
8
9
10
11
12
Java
@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");
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Note:如前所述,对于
@Bean方法,你通常会选择使用编程式的 JNDI 查找,可以使用 Spring 的JndiTemplate/JndiLocatorDelegate辅助工具,或者像前面显示的那样直接使用 JNDI 的InitialContext,但不是JndiObjectFactoryBean变体,因为这将强制你将返回类型声明为FactoryBean类型。
配置文件字符串可能包含一个简单的配置文件名称(例如,production)或一个配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如,production & us-east)。在配置文件表达式中支持以下运算符:
!:配置文件的逻辑 “非”;&:配置文件的逻辑 “与”;|:配置文件的逻辑 “或”;
Warning:你不能在不使用括号的情况下混合使用
&和|运算符。例如,production & us-east | eu-central不是一个有效的表达式。它必须表示为production & (us-east | eu-central)。
你可以使用 @Profile 作为元注解,以创建自定义的组合注解。下面的例子定义了一个自定义的 @Production 注解,你可以将其作为 @Profile("production") 的替代品使用:
Java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}1
2
3
4
5
2
3
4
5
Warning:如果一个
@Configuration类被标记为@Profile,那么除非一个或多个指定的配置文件处于激活状态,否则该类关联的所有@Bean方法和@Import注解都将被忽略。如果一个@Component或@Configuration类被标记为@Profile({"p1", "p2"}),那么除非配置文件p1或p2已经被激活,否则该类不会被注册或处理。如果给定的配置文件前缀是 NOT 运算符 (!),那么只有当配置文件不处于激活状态时,注解元素才会被注册。例如,给定@Profile({"p1", "!p2"}),如果配置文件p1处于激活状态,或者配置文件p2不处于激活状态,那么就会发生注册。
@Profile 也可以在方法级别声明,以便只包含配置类的一个特定 Bean(例如,某个特定 Bean 的替代变体),如下面的示例所示:
Java
@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");
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Note
对于
@Bean方法上的@Profile,可能会适用一个特殊的场景:在同一 Java 方法名的@Bean方法重载的情况下(类似于构造函数重载),需要在所有重载方法上一致地声明@Profile条件。如果条件不一致,只有在重载方法中第一个声明的条件才有意义。因此,@Profile不能用于选择具有特定参数签名的重载方法而不选择其他方法。对于同一 Bean 的所有工厂方法之间的解析,遵循 Spring 的构造函数解析算法。如果你想定义具有不同配置文件条件的备选 Bean,可以使用不同的 Java 方法名,这些方法名通过使用
@Bean名称属性指向同一 Bean 名称,如前面的示例所示。如果参数签名完全相同(例如,所有变体都有无参数的工厂方法),那么这是在一开始就在有效的 Java 类中表示这种安排的唯一方式(因为在 Java 类中,具有特定名称和参数签名的方法只能有一个)。
2.1.2. XML Bean 定义配置文件
XML 的对应部分是 <beans> 元素的 profile 属性。我们前面的示例配置可以重写为两个 XML 文件,如下所示:
XML
<beans profile="development"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="...">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
XML
<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>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
也可以避免那种分割,将 <beans/> 元素嵌套在同一个文件中,如下面的示例所示:
XML
<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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring-bean.xsd 已经被限制了,只允许在文件中最后出现这样的元素。这应该有助于提供灵活性,同时不会在 XML 文件中产生混乱。
Note
XML 的对应部分不支持前面描述的配置文件表达式。然而,可以使用
!运算符来否定一个配置文件。也可以通过嵌套配置文件来应用逻辑 “与”,如下面的示例所示:XML<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>1
2
3
4
5
6
7
8
9
10
11
12
13
14在上述示例中,如果
production和us-east配置文件都处于激活状态,那么dataSourceBean 将会被暴露。
2.1.3. 激活配置文件
现在我们已经更新了我们的配置,我们仍然需要指导 Spring 哪个配置文件是激活的。如果我们现在启动我们的示例应用程序,我们会看到抛出一个 NoSuchBeanDefinitionException,因为容器找不到名为 dataSource 的 Spring Bean。
激活配置文件可以通过多种方式完成,但最直接的方式是通过 ApplicationContext 可用的 Environment API 来编程实现。下面的示例展示了如何做到这一点:
Java
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();1
2
3
4
2
3
4
此外,你还可以通过 spring.profiles.active 属性声明性地激活配置文件,该属性可以通过系统环境变量、JVM 系统属性、web.xml 中的 Servlet 上下文参数,甚至作为 JNDI 中的一个条目来指定(参见 PropertySource 抽象)。在集成测试中,可以使用 spring-test 模块中的 @ActiveProfiles 注解来声明激活的配置文件(参见带有环境配置文件的上下文配置)。
请注意,配置文件并不是 “非此即彼” 的命题。你可以一次激活多个配置文件。在编程上,你可以向 setActiveProfiles() 方法提供多个配置文件名称,该方法接受 String... 可变参数。下面的示例激活了多个配置文件:
Java
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");在声明上,spring.profiles.active 可以接受一个由逗号分隔的配置文件名称列表,如下面的示例所示:
Text
-Dspring.profiles.active="profile1,profile2"2.1.4. 默认配置文件
默认配置文件代表默认启用的配置文件。请考虑以下示例:
Java
@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();
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
如果没有激活的配置文件,那么 dataSource 将被创建。你可以将这看作为一个或多个 Bean 提供默认定义的一种方式。如果启用了任何配置文件,那么默认配置文件将不适用。
你可以通过在 Environment 上使用 setDefaultProfiles() 或者声明性地使用 spring.profiles.default 属性来更改默认配置文件的名称。
2.2. PropertySource 抽象
Spring 的 Environment 抽象提供了对可配置的属性源层次结构的搜索操作。请考虑以下列表:
Java
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);1
2
3
4
2
3
4
在前面的片段中,我们看到了一种高级的方式来询问 Spring 是否为当前环境定义了 my-property 属性。为了回答这个问题,Environment 对象对一组 PropertySource 对象进行搜索。PropertySource 是对任何键值对源的简单抽象,Spring 的 StandardEnvironment 配置了两个 PropertySource 对象 —— 一个代表 JVM 系统属性集(System.getProperties()),一个代表系统环境变量集(System.getenv())。
Note:这些默认的属性源存在于
StandardEnvironment中,用于独立应用程序。如果 JNDI 可用,StandardServletEnvironment将填充包括 Servlet 配置、Servlet 上下文参数和JndiPropertySource在内的额外默认属性源。
具体来说,当你使用 StandardEnvironment 时,如果在运行时存在 my-property 系统属性或 my-property 环境变量,那么对 env.containsProperty("my-property") 的调用将返回 true。
Tip
执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用
env.getProperty("my-property")时,my-property属性恰好在这两个地方都设置了,那么系统属性值 “胜出” 并被返回。请注意,属性值不是合并的,而是完全被前面的条目覆盖。对于常见的
StandardServletEnvironment,完整的层次结构如下,最高优先级的条目在顶部:
ServletConfig参数(如果适用 —— 例如,在DispatcherServlet上下文的情况下);ServletContext参数(web.xml中的context-param条目);- JNDI 环境变量(
java:comp/env/条目);- JVM 系统属性(
-D命令行参数);- JVM 系统环境(操作系统环境变量);
最重要的是,整个机制是可配置的。也许你有一个自定义的属性源,你想将它集成到这个搜索中。为此,实现并实例化你自己的 PropertySource,并将其添加到当前 Environment 的 PropertySources 集合中。下面的示例展示了如何做到这一点:
Java
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());1
2
3
2
3
在前面的代码中,MyPropertySource 已经被添加到搜索中,并具有最高的优先级。如果它包含一个 my-property 属性,那么这个属性将被检测并返回,优先于任何其他 PropertySource 中的 my-property 属性。MutablePropertySources API 公开了一些方法,允许对属性源集合进行精确操作。
2.3. 使用 @PropertySource
@PropertySource 注解为将 PropertySource 添加到 Spring 的 Environment 提供了一种方便且声明式的机制。
假设有一个名为 app.properties 的文件,其中包含键值对 testbean.name=myTestBean,下面的 @Configuration 类使用 @PropertySource 的方式使得调用 testBean.getName() 返回 myTestBean:
Java
@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;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
在 @PropertySource 资源位置中存在的任何 ${...} 占位符都会根据已经在环境中注册的属性源集合进行解析,如下面的示例所示:
Java
@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;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
假设 my.placeholder 已经存在于已注册的属性源之一中(例如,系统属性或环境变量),那么占位符将被解析为相应的值。如果不存在,则使用 default/path 作为默认值。如果没有指定默认值并且无法解析属性,则会抛出 IllegalArgumentException。
Tip:根据 Java 8 的约定,
@PropertySource注解是可重复的。然而,所有这样的@PropertySource注解需要在同一级别声明,要么直接在配置类上,要么作为同一自定义注解内的元注解。不建议混合直接注解和元注解,因为直接注解会有效地覆盖元注解。
2.4. 语句中的占位符解析
从历史上看,元素中占位符的值只能通过 JVM 系统属性或环境变量来解析。但现在不再是这样。由于 Environment 抽象在整个容器中都已集成,因此很容易通过它(Environment)来路由占位符的解析。这意味着你可以按照你喜欢的方式配置解析过程。你可以改变通过系统属性和环境变量搜索的优先级,或者完全删除它们。你也可以根据需要,将你自己的属性源添加到混合中。
具体来说,只要 customer 属性在 Environment 中可用,无论在哪里定义,以下语句都能正常工作:
XML
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>1
2
3
2
3
3. 注册 LoadTimeWeaver
Spring 使用 LoadTimeWeaver 来动态转换加载到 Java 虚拟机(JVM)中的类。
要启用加载时织入(load-time weaving),你可以在你的一个 @Configuration 类中添加 @EnableLoadTimeWeaving,如下面的示例所示:
Java
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}1
2
3
4
2
3
4
或者,对于 XML 配置,你可以使用 context:load-time-weaver 元素:
XML
<beans>
<context:load-time-weaver/>
</beans>1
2
3
2
3
一旦 ApplicationContext 配置完成,该 ApplicationContext 中的任何 Bean 都可以实现 LoadTimeWeaverAware,从而获得加载时织入实例的引用。这在 Spring 的 JPA 支持中尤其有用,因为 JPA 类转换可能需要加载时织入。更多细节请参阅 LocalContainerEntityManagerFactoryBean 的 JavaDoc。关于 AspectJ 加载时织入的更多信息,请查看 Spring 框架中的使用 AspectJ 进行加载时织入。