Spring 的学习和使用

2 装配 Bean

2.1 Spring 配置的可选方案

Spring 容器负责创建应用程序中的 Bean 并通过 DI 来协调这些对象之间的关系.
但是,作为开发人员,你需要告诉 Sprng 要创建哪些 Bean 并且如何将其装配在一起.
当描述 Bean 如何进行装配时,Spring 具有非常大的灵活性,它提供了三种主要的装配机制:

  • 自动化装配
  • 基于 Java 的显式配置
  • 基于 XML 的显式配置

2.2 自动化装配 Bean

Spring 从两个角度来实现自动化装配:

  • 组建扫描(component scanning): Spring 会自动发现应用上下文中所创建的 Bean.
  • 自动装配(autowiring): Spring 自动满足 Bean 之间的依赖.

2.2.1 创建可被发现的 Bean

在类上添加 @Component 注解表明该类会作为组件类,并告知 Spring 要为这个类
创建 Bean.不过组件的扫描默认是不启用的.我们还需要显式配置一下 Spring,从而命令它去寻找带有
@Component 注解的类.Spring 中可以使用 @ComponentScan 注解启用组件扫描.
如果没有其他配置的话, @ComponentScan 默认会扫描与配置类所在包及该包下的所有子包,
寻找带有 @Component 注解的类,并且在 Spring 种为其创建一个 Bean.
如果使用 XML 来启用组件扫描的话,那么可以使用 Spring context 命名空间的
<context:component-scan> 元素.

2.2.2 为组件扫描的 Bean 命名

Spring 应用上下文中所有的 Bean 都会给定一个 ID,如果没有显示指定,Spring
会根据类名为其指定一个 ID(将类名的第一个字母变为小写).
如果想要为这个 Bean 设置不同的 ID,你所要做的就是将期望的 ID 作为值传递给
@Component 注解:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
 @Component("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {
...
}
```
还有另外一种为 Bean 命名的方式,这种方式不使用 @Component 注解,而是使用
Java 依赖注入规范(Java Dependency Injection) 中所提供的 @Named 注解
来为 Bean 设置 ID:
```
package soundsystem;
import javax.inject.Named;

@Named("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {
...
}
```
Spring 支持将 @Named 作为 @Component 注解的替代方案,两者之间有一些细微的差异,
但是在大多数场景中,它们是可以相互替换的.

### 2.2.3 设置组件扫描的基础包
到现在为止,我们没有为 @ComponentScan 注解设置任何属性.这意味着,按照默认规则,
它会以配置类所在的包作为基础包(base package)来扫描组件.但是,如果你想扫描不同的包,
那该怎么办呢?或者,如果你想扫描多个基础包,那又该怎么办呢?

有一个原因会促使我们明确地设置基础包,那就是我们想要将配置类放在单独的包中,
使其与其他应用代码区分开来.如果是这样的话,哪默认的基础包就不能满足要求了.

要满足这样的需求其实也完全没有问题!为了指定不同的基础包,你所需要做的就是在 @ComponentScan
的 value 属性中指明包的名称:
```
@Configuration
@ComponentScan(basePackages="soundsystem")
public class CPPlayerConfig {}
```

如果你想更加清晰地表明你所设置的是基础包,那么你可以通过 basePackages 属性进行设置:
```
@Configuration
@ComponentScan(basePackages="soundsystem")
public class CDPlayerConfig {}
```

可能你已经注意到了 basePackages 属性使用的是复数形式.如果你揣测这是不是意味着可以设置多个基础包,
那么恭喜你猜对了,如果想要这么做的话,只需要将 basePackages 属性设置为要扫描包的一个数组即可:
```
@Configuration
@ComponentScan(basePackages{"soundsystem", "video"})
public class CDPlayerconfig {}
```

在上面的例子中,所设置的基础包是以 String 类型表示的,我认为这是可以的,但这种方法是类型不安全的(not type-safe)
的.如果你重构代码的话,那么所指定的基础包可能就会出现错误了.

除了将包设置为简单的 String 类型之外,@ComponentScan 还提供了另外一种方法,那就是将其指定为包中所
包含的类或接口:
```
@Configuration
@ComponentScan(basePackageClasses={CDPlayer.class, DVDPlayer.class})
public class CDPlayerConfig {}
```

可以看到,basePackages 属性被替换成了 basePackageClasses.同时,我们不是再使用 String 类型的
名称来指定包,为 basePackageClasses 属性所设置的数组中包含了类.这些类所在的包将会作为组件扫描的基础包.

尽管在样例中,为 basePackageClasses 设置的是组件类,但是你可以考虑在包中创建一个用来进行扫描的
空标记接口(marker interface).通过标记接口的方式,你依然能够保持对重构友好的接口引用,但是可以避免
引用任何实际的应用程序代码卡.

在你的应用程序中,如果所有的对象都是独立的,彼此之间没有任何依赖,那么你所需要的可能就是组件扫描而已.
但是,很多对象会依赖其他的对象才能完成任务.这样的话,我们就需要有一种方法能够将组件扫描得到的 bean
和它们的依赖装配在一起.要完成这项任务,我们需要了解一下 Spring 自动装配的另外一方面内容 -- 自动装配.

### 2.2.4 通过为 Bean 添加注解实现自动装配
简单来说,自动装配就是让 Spring 自动满足 Bean 依赖的一种方法,在满足依赖的过程中,会在 Spring 应用上下文中
寻找匹配某个 Bean 需求的其他 Bean.为了声明要进行自动装配,我们可以借助 Spring 的 @Autowired 注解.

比方说,下面的 CDPlayer 类,它的构造上添加了 @Autowired 注解,这表明当 Spring 创建 CDPlayer Bean
的时候,会通过这个构造器来进行实例化并且会传入一个可设置给 CompactDisc 类型的 Bean.

```
# 通过自动装配,将一个 CompactDisc 注入到 CDPlayer 之中

package soundsystems;

import.org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CDPlayer implements MediaPlayer {
private CompactDisc cd;

@Autowired
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}

public void play() {
cd.play();
}
}
```
@Autowired 注解不仅能够用在构造器上,还能用在属性的 Setter 方法上,比如说,CDPlayer 有一个 setCompactDisc()
方法,那么可以采用如下的注解形式进行自动装配:
```
@Autowired
public void setCompactDisc(CompactDisc cd) {
this.cd = cd;
}
```

在 Spring 初始化 Bean 之后,它会尽可能的去满足 Bean 的依赖,在本例中,依赖是通过带有 @Autowired 注解
的方法进行声明的,也就是 setCompactDisc() 方法.

实际上, Setter 方法并没有什么特殊之处,@Autowired 注解可以用在类的任何方法上.假设 CDPlayer 类有一个
insertDisc() 方法,那么 @Autowired 能够像在 setCompactDisc() 上那样,发挥完全相同的作用:
```
@Autowired
public void insertDisc(CompactDisc cd) {
this.cd = cd;
}
```

不管是构造器,Setter 方法还是其他的方法,Spring 都会尝试满足方法参数上所声明的依赖.假如有且只有一个 Bean
匹配依赖需求的话,那么这个 Bean 将会被装配进来.

如果没有匹配的 Bean,那么在应用上下文创建的时候,Spring 会抛出一个异常.为避免异常的出现,你可以将 @Autowired
的 required 属性设置为 false:
```
@Autowired(required=false)
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}
```

> Xml配置中的 Bean 要顺序定义.

将 required 属性设置为 false 时,Spring 会尝试执行自动装配,但是如果没有匹配的 Bean 的话,Spring
将会让这个 Bean 处于未装配的状态.但是,把 required 属性设置为 false 时,你需要谨慎对待.如果在你的代码
种没有进行 null 检查的话,这个处于未装配状态的属性有可能会出现 NullPointerException.

如果有多个 Bean 都能满足依赖关系的话,Spring 将会抛出一个异常,表明没有明确指定要选择哪个 Bean 进行自动装配.

@Autowired 注解是 Spring 特有的注解,如果你不愿意在代码中到处使用 Spring 的特定注解来完成自动装配
任务的话,那么你可以考虑将其替换为 @Inject:
```
package soundsystem;

import javax.inject.Inject;
import javax.inject.Named;

@Named
public class CDPlayer {
...

@Inject
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}
}
```

@Inject 注解来源于 Java 依赖注入规范,该规范同时还为我们定义了 @Named 注解.在自动装配中,Spring 同时支持
@Inject 和 @Autowired.尽管 @Inject 和 @Autowired 之间有着一些细微的差别,但是在大多数场景下,它们都是
可以互相替换的.

### 2.2.5 验证自动装配
现在,我们已经在 CDPlayer 的构造器中添加了 @Autowired 注解,Spring 将把一个可分配给 CompactDisc 类型的
Bean 自动注入进来.

## 2.3 通过 Java 代码装配 Bean
尽管在很多场景下通过组件扫描和自动装配实现 Spring 的自动化配置是更为推荐的方式,但有时候自动化配置的方案行不通,
因此需要明确配置 Spring.比如说,你想要将第三方库中的组件装配到你的应用中,在这种情况下,是没有办法在它的类添加
@Component 和 @Autowired 注解的,因此就不能使用自动化装配的方案了.

在这种情况下,必须要采用显式装配的方式.在进行显式装配的时候,又两种可选方案: Java 和 XML.相比 XML 配置方式,
JavaConfig 是更好的方案,因为它更为强大,类型安全并且对重构友好.因为它就是 Java 代码,就像应用程序中其他 Java
代码一样.

同时,JavaConfig 与其他 Java 代码又有所区别.在概念上,它与应用程序中的业务逻辑和领域代码是不同的.尽管它与其他
的组件一样都使用相同的语言进行表述,但 JavaConfig 是配置代码.这意味着它不应该包含任何业务逻辑,JavaConfig
也不应该侵入到业务逻辑代码之中.尽管不是必须的,但通常会将 JavaConfig 放到单独的包中,使它与其他的应用程序逻辑
分开来,这样对于它的意图就不会产生困惑了.

### 2.3.1 创建配置类
```
package soundsystems;
import org.springframeword.context.annotation.Configuration;

@Configuration
public class CDPlayerConfig {

}

创建 JavaConfig 类的关键在于为其添加 @Configuration 注解,@Configuration 注解表明这个类是一个配置类.

2.3.2 声明简单的 Bean

要在 JavaConfig 中声明 Bean,我们需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加 @Bean
注解.比方说,下面的代码声明了 CompactDisc Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Bean 
public CompactDisc sgtPeppers() {
return new SgtPeppers();
}
```
@Bean 注解会告诉 Spring 这个方法将会返回一个对象,该对象要注册为 Spring 应用上下文中的 Bean.方法体中
包含了最终产生 Bean 实例的逻辑.

默认情况下, Bean 的 ID 与带有 @Bean 注解的方法名是一样的.在本例中, Bean 的名字将会是 sgtPeppers.
如果你想为其设置成一个不同的名字的话,那么可以重命名该方法,也可以通过 name 属性指定一个不同的名字:
```
@Bean(name="lonelyHeartsClubBand")
public CompactDisc sgtPeppers() {
return new SgtPeppers();
}

不管你采用什么方法来为 Bean 命名,Bean 声明都是非常简单的.方法体返回了一个新的 SgtPeppers 实例.这里是使用
Java 来jinxing描述的,因此我们可以发挥 Java 提供的所有功能,只要最终生成一个 CompactDisc 实例即可.

2.3.3 借助 JavaConfig 实现注入

前面所声明的 CompactDisc Bean 是非常简单的,它自身没有其他的依赖.但现在,我们需要声明 CDPlayer Bean,
它依赖与 CompactDisc.在 JavaConfig 中,要如何将它们装配到一起呢?

在 JavaConfig 中装配 Bean 的最简单方式就是创建 Bean 的方法.例如,下面就是一种声明 CDPlayer 的可行方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Bean
public CDPlayer cdPlayer() {
return new CDPlayer(sgtPeppers());
}
```
cdPlayer() 方法像 sgtPeppers() 方法一样,同样使用了 @Bean 注解,这表明这个方法会创建一个 Bean 实例并将其
注册到 Spring 应用上下文中.所创建的 Bean ID 为 cdPlayer,与方法的名字相同.

cdPlayer() 的方法体与 sgtPeppers() 稍微有些区别.在这里并没有使用默认的构造器构建实例,而是调用了需要传入
CompactDisc 对象的构造器来创建 CDPlayer 实例.

看起来,CompactDisc 是通过调用 sgtPeppers() 得到的,但情况并非完全如此,因为 sgtPeppers() 方法上
添加了 @Bean 注解,Spring 将会拦截所有对它的调用,并确保直接返回该方法所创建的 Bean,而不是每次都对其
进行实际的调用.

还有一种理解起来更为简单的方式:
```
@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc) {
return new CDPlayer(compactDisc);
}

在这里,cdPlayer() 方法请求一个 CompactDisc 作为参数.当 Spring 调用 cdPlayer() 创建 CDPlayer bean
的时候,它会自动装配一个 CompactDisc 到配置中.然后,方法体就可以按照合适的方式来使用它.借助这种技术,cdPlayer()
方法也能够将 CompactDisc 注入到 CDPlayer 的构造器中,而且不用明确引用 Compact:Disc 的 @Bean 方法.

通过这种方式引用其他的 Bean 通常是最佳的选择,因为它不会要求将 compactDisc 声明到同一个配置类之中.在这里
甚至没有要求 CompactDisc 必须要在 JavaConfig 中声明,实际上它可以通过组件扫描功能自动发现或者通过 XML 来
进行配置..不管 CompactDisc 是采用什么方式创建出来的, Spring 都会将其注入到配置方法中.

2.4 通过 XML 装配 Bean

2.5 导入和混合配置

高级装配

本章内容:

  • Spring profile
  • 条件化的 Bean 声明
  • 自动装配与歧义性
  • Bean 的作用域
  • Spring 表达式语言

3.1 环境与 Profile

3.1.1 配置 Profile Bean

Spring 为环境相关的 Bean 所提供的解决方案其实与构建时的方案没有太大差别.当然,在这个过程中需要根据环境决定该
创建哪个 Bean 和不创建哪个 Bean.不过 Spring 并不是在构建的时候做出这样的决策,而是等到运行时再来确定.这样的
结果就是同一个部署单元(可能会是 WAR 文件)能够适用于所有的环境,没有必要进行重新构建.

在 Java 配置中,可以使用 @Profile 注解指定某一个 Bean 或者方法属于哪一个 profile,例如,在配置类中,
嵌入数据库的 DataSource 可能会配置成如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@Profile("dev")
public class DevelopmentProfileConfig {
@Bean(destroyMethod="shutdown")
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder() {}
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
}

它会告诉 Spring 这个类或者方法只有在 dev profile 激活时才会创建.如果 dev profile 没有激活的话,那么带有
@Bean 注解的方法都会被忽略掉.

在 Spring3.1 中,只能在类级别上使用 @Profile 注解.不过,从 Spring3.2 开始,你也可以在方法级别上使用 @Profile
注解,与 @Bean 注解一同使用.这样的话,就能将这两个 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 DataSourceConfig {
@Bean(destroyMethod="shutdown")
@Profile("dev")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder() {}
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}

@Bean
@Profile("prod")
public DataSource jndiDataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource)jndiObjectFactoryBean.getObject();
}
}

这里有个问题需要注意,尽管每个 DataSource Bean 都被声明在一个 profile 中,并且只有当规定的 profile
激活时,相应的 Bean 才会被创建,但是可能会有其他的 Bean 并没有声明在一个给定的 profile 范围内.没有指定
profile 的 Bean 始终都会被创建,与激活哪个 profile 没有关系.

3.1.2 在 XML 中配置 profile

3.1.3 激活 profile

Spring 在确定哪个 profile 处于激活状态时,需要依赖两个独立的属性:

  • spring.profiles.active
  • spring.profiles.default
    如果设置了 spring.profiles.active 属性的话,那么它的值就会用来确定哪个 profile 是激活的.但如果没有
    设置 spring.profiles.active 属性的话,那 Spring 将会查找 spring.profiles.default 的值.如果都
    没有设置的话,那就没有激活的 profile,因此只会创建那些没有定义在 profile 中的 Bean.

3.2 条件化的 Bean

3.3 处理自动装配的歧义性

在第二章中,我们已经看到如何使用自动装配让 Spring 完全负责将 bean 引用注入到构造参数
和属性中.自动装配能够提供很大的帮助,因为它会减少应用程序组件时所需要的显示配置的数量.

不过,仅有一个 bean 匹配所需的结果时,自动装配才是有效的.如果不仅有一个 bean 能够匹配
结果的话,这种歧义性会阻碍 Spring 自动装配属性/构造器参数或方法参数.

以 @Component 注解为例,当多个类都实现了同一个类,并且都使用了 @Component 注解,在自动
装配时,Spring 无法做出选择,此时会抛出 NoUniqueBeanDefinitionExeption.

Spring 提供了多种可选方案来解决歧义性问题.你可以将可选 bean 中的某一个设为首选(primary)
的 bean,或者使用限定符(qualifier)来帮助 Spring 将可选的 bean 的范围缩小到只有一个 bean.

3.3.1 标示首选的 Bean

在声明 bean 的时候,通过将其中一个可选的 bean 设置为首选(primary)bean 能够避免自动装配
时的歧义性.当遇到歧义性的时候,Spring 将会使用首选的 bean,而不是其他可选的 bean.实际上,
你所声明就是”最喜欢”的 bean.

假设冰激凌就是你最喜欢的甜点.在 Spring 中,可以通过 @Primary 来表达最喜欢的方案.
@Primary 能够与 @Component 组合用在组件扫描的 bean 上,也可以与 @Bean 组合用在
Java 配置的 bean 声明中.比如,下面的代码展现了如何将 @Component 注解的 IceCream
bean 声明为首选的 bean:

1
2
3
4
5
6
7
8
9
10
11
@Component
@Primary
public class IceCream implements Dessert {...}
```
或者,如果你通过 Java 配置显式地声明 IceCream,那么 @Bean 方法应该如下所示:
```
@Bean
@Primary
public Dessert iceCream() {
return new IceCream();
}

如果你使用 XML 配置 bean 的话,同样可以实现这样的功能. 元素有一个 primary 属性
用来指定首选的 bean:

1
2
3
<bean id="iceCream"
class=""
primary="true" />

不管你采用什么方式来标示首选 bean,效果都是一样的,都是告诉 Spring 在遇到
歧义性的时候要选择首选的 bean.

就像 Spring 无法从多个可选的 bean 中做出选择一样,它也无法从多个首选的 bean 中
做出选择.显示,如果不止一个 bean 被设置成了首选 bean,那实际上也就是没有首选 bean 了.

3.3.2 限定自动装配的 bean

设置首选 bean 的局限性在于 @Primary 无法将可选方案的范围限定到唯一一个无歧义性的选项中.
它只能标示一个优先的可选方案.当首选 bean 的数量超过一个时,我们并没有其他的进一步缩小可选范围.

与之相反,Spring 的限定符能够在所有可选的 bean 上进行缩小范围的操作,最终能够达到只有一个 bean
满足所规定的限制条件.如果将所有的限定符都用上后依然存在歧义性,那么你可以继续使用更多的限定符来缩小选择范围.

@Qualifier 注解是使用限定符的主要方式.它可以与 @Autowired@Inject 协同使用,在注入的时候指定想
要注入进去的是哪个 bean.例如,我们想要确保要将 IceCream 注入到 setDessert() 之中:

1
2
3
4
5
@Autowired
@Qulifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}

这是使用限定符的最简单的例子.为 @Qualifier 注解所设置的参数就是想要注入的 bean 的 ID.所有使用 @Component
注解声明的类都会创建为 bean,并且 bean 的 ID 为首字母变为小写的类名.因此,@Qualifier(“iceCream”) 指向的是
组件扫描时所创建的 bean,并且这个 bean 是 IceCream 类的实例.

实际上,还有一点需要补充一下.更准确地讲,@Qualifier(“iceCream”) 所引用的 bean 要具有 String 类型的 “iceCream”
作为限定符.如果没有指定其他的限定符的话,所有的 bean 都会给定一个默认的限定符,这个限定符与 bean 的 ID 相同.因此,
框架会将具有 “iceCream” 限定符的 bean 注入到 setDessert() 方法中.这恰巧就是 ID 为 iceCream 的 bean,他是
IceCream 类在组件扫描的时候创建的.

基于默认的 bean ID 作为限定符是非常简单,但这有可能会引入一些问题.如果你重构了 IceCream 类,将其重命名为 Gelato 的话,
那此时会发生什么情况呢?如果这样的话,bean 的 ID 和默认的限定符会变为 gelato,这就无法匹配 setDessert() 方法中的限定符.
自动装配会失败.

这里的问题在于 setDessert() 方法上所指定的限定符与要注入的 bean 的名称是紧耦合的,对类名的任意改动都会导致限定符失效.

创建自定义的限定符

我们可以为 bean 设置自己的限定符,而不是依赖于将 bean ID 作为限定符.在这里所需要做的就是在 bean 声明上添加 @Qulifier
注解.例如,它可以与 @Component 组合使用,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Qualifier("cold")
public class IceCream implements Dessert {...}
```
在这种情况下,cold 限定符分配给了 IceCream.因为它没有耦合类名,因此你可以随意重构 IceCream 的类名,而不必担心会破坏自动装配.
在注入的地方,只要引用 cold 限定符就可以了:
```
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}

值得一提的是,当通过 Java 显示定义 bean 的时候,@Qualifier 也可以与 @Bean 注解一起使用:

1
2
3
4
5
@Bean
@Qualifier("cold")
public Dessert iceCream() {
return new IceCream();
}

当使用自定义的 @Qualifier 值时,最佳实践是为 bean 选择特征性或描述性的术语,而不是使用随意的名字.在本例中,我将 IceCream
bean 描述为 “cold” bean.在注入的时候,可以将这个需求理解为”给我一个凉的甜点”,这其实就是描述的 IceCream.类似地,我可以将
Cake 描述为”soft”,将 Cookie 描述为 “crispy”.

使用自定义的限定符注解

面向特征的限定符要比基于 bean ID 的限定符要好一些.但是,如果多个 bean 都具备相同特征的话,这种做法也会出现问题.例如,
如果引入了这个新的 Dessert bean,会发生什么情况呢:

1
2
3
@Component
@Qualifier
public class Popsicle implements Dessert {...}

现在我们有了两个 “cold” 限定符的甜点.在自动装配 Dessert bean 的时候,我们再次遇到了歧义性的问题,需要使用更多的限定符符
来将可选范围限定到只有一个 bean.

可能想到的解决方案就是在注入点和 bean 定义的地方同时在添加另外一个 @Qualifier 注解.IceCream 类大致就会如下所示:

1
2
3
4
@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert {...}

Popsicle 类同样也可能再添加另外一个 @Qualifier 注解:

1
2
3
4
@Component
@Qualifier("cold")
@Qualifier("fruity")
public class Popsicle implements Dessert {...}

在注入点中,我们可能会使用这样的方式来将范围缩小到 IceCream:

1
2
3
4
5
6
@Autowired
@Qualifier("cold")
@Qualifier("creamy")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}

这里只有一个小问题:Java 不允许在同一个条目上重复出现相同类型的多个注解.如果你试图这样做的话,编译器会提示错误.在这里,
使用 @Qualifier 注解并没有办法(至少没有直接的办法)将自动装配的可选 bean 缩小范围至仅有一个可选的 bean.

但是,我们可以创建自定义的限定符注解,借助这样的注解来表达 bean 所希望限定的特性.这里所需要做的就是创建一个注解,
它本身要使用 @Qualifier 注解来标注.这样我们将不再使用 @Qualifier(“cold”),而是使用自定义的 @Cold 注解,
该注解的定义如下所示:

1
2
3
4
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {}

同样,你可以创建一个新的 @Creamy 注解来代替

1
2
3
4
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy {}

当你不想用 @Qualifier 注解的时候,可以类似地创建 @Soft, @Crispy 和 @Fruity.通过在定义时添加 @Qualifier
注解,它们就具有了 @Qualifier 注解的特性.它们本身实际上就成为了限定符注解.

现在,我们可以重新看一下 IceCream,并为其添加 @Cold 和 @Creamy 注解,如下所示:

1
2
3
4
@Component
@Cold
@Creamy
public class IceCream implements Dessert {...}

类似地,Popsicle 类可以添加 @Cold 和 @Fruity 注解:

1
2
3
4
@Component
@Cold
@Fruity
public class Popsicle implements Dessert {...}

最终,在注入点,我们使用必要的限定符进行任意组合,从而将可选范围缩小到只有一个 bean 满嘴需求.为了得到 IceCream bean,
setDessert() 方法可以这样使用注解:

1
2
3
4
5
6
@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}

通过声明自定义的限定符注解,我们可以同时使用多个限定符,不会亏再有 Java 编译器的限制或错误.
与此同时,相对于使用原始的 @Qualifier 并借助 String 类型来指定限定符,自定义的注解也更为类型安全.

让我们近距离观察一下 setDessert() 方法以及它的注解,这里并没有在任何地方明确指定要将 IceCream 自动装配到该方法中.
相反,我们使用所需 bean 的特性来进行指定,即 @Cold 和 @Creamy.因此,setDessert() 方法依然能够与特定的 Dessert
实现保持解藕.任意满足这些特征的 bean 都是可以的.在当前选择 Dessert 实现时,恰好如此,IceCream 是唯一能够与之匹配的 bean.

3.4 Bean 的作用域

分享到:
Disqus 加载中...

如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理