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 | @Component("lonelyHeartsClub") |
创建 JavaConfig 类的关键在于为其添加 @Configuration 注解,@Configuration 注解表明这个类是一个配置类.
2.3.2 声明简单的 Bean
要在 JavaConfig 中声明 Bean,我们需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加 @Bean
注解.比方说,下面的代码声明了 CompactDisc Bean:
1 | @Bean |
不管你采用什么方法来为 Bean 命名,Bean 声明都是非常简单的.方法体返回了一个新的 SgtPeppers 实例.这里是使用
Java 来jinxing描述的,因此我们可以发挥 Java 提供的所有功能,只要最终生成一个 CompactDisc 实例即可.
2.3.3 借助 JavaConfig 实现注入
前面所声明的 CompactDisc Bean 是非常简单的,它自身没有其他的依赖.但现在,我们需要声明 CDPlayer Bean,
它依赖与 CompactDisc.在 JavaConfig 中,要如何将它们装配到一起呢?
在 JavaConfig 中装配 Bean 的最简单方式就是创建 Bean 的方法.例如,下面就是一种声明 CDPlayer 的可行方案:
1 | @Bean |
在这里,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 | @Configuration |
它会告诉 Spring 这个类或者方法只有在 dev profile 激活时才会创建.如果 dev profile 没有激活的话,那么带有
@Bean 注解的方法都会被忽略掉.
在 Spring3.1 中,只能在类级别上使用 @Profile 注解.不过,从 Spring3.2 开始,你也可以在方法级别上使用 @Profile
注解,与 @Bean 注解一同使用.这样的话,就能将这两个 Bean 的声明放到同一个配置类之中:
1 | @Configuration |
这里有个问题需要注意,尽管每个 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 | @Component |
如果你使用 XML 配置 bean 的话,同样可以实现这样的功能.
用来指定首选的 bean:
1 | <bean id="iceCream" |
不管你采用什么方式来标示首选 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 | @Autowired |
这是使用限定符的最简单的例子.为 @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 | @Component |
值得一提的是,当通过 Java 显示定义 bean 的时候,@Qualifier 也可以与 @Bean 注解一起使用:
1 | @Bean |
当使用自定义的 @Qualifier 值时,最佳实践是为 bean 选择特征性或描述性的术语,而不是使用随意的名字.在本例中,我将 IceCream
bean 描述为 “cold” bean.在注入的时候,可以将这个需求理解为”给我一个凉的甜点”,这其实就是描述的 IceCream.类似地,我可以将
Cake 描述为”soft”,将 Cookie 描述为 “crispy”.
使用自定义的限定符注解
面向特征的限定符要比基于 bean ID 的限定符要好一些.但是,如果多个 bean 都具备相同特征的话,这种做法也会出现问题.例如,
如果引入了这个新的 Dessert bean,会发生什么情况呢:
1 | @Component |
现在我们有了两个 “cold” 限定符的甜点.在自动装配 Dessert bean 的时候,我们再次遇到了歧义性的问题,需要使用更多的限定符符
来将可选范围限定到只有一个 bean.
可能想到的解决方案就是在注入点和 bean 定义的地方同时在添加另外一个 @Qualifier 注解.IceCream 类大致就会如下所示:
1 | @Component |
Popsicle 类同样也可能再添加另外一个 @Qualifier 注解:
1 | @Component |
在注入点中,我们可能会使用这样的方式来将范围缩小到 IceCream:
1 | @Autowired |
这里只有一个小问题:Java 不允许在同一个条目上重复出现相同类型的多个注解.如果你试图这样做的话,编译器会提示错误.在这里,
使用 @Qualifier 注解并没有办法(至少没有直接的办法)将自动装配的可选 bean 缩小范围至仅有一个可选的 bean.
但是,我们可以创建自定义的限定符注解,借助这样的注解来表达 bean 所希望限定的特性.这里所需要做的就是创建一个注解,
它本身要使用 @Qualifier 注解来标注.这样我们将不再使用 @Qualifier(“cold”),而是使用自定义的 @Cold 注解,
该注解的定义如下所示:
1 | @Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) |
同样,你可以创建一个新的 @Creamy 注解来代替
1 | @Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) |
当你不想用 @Qualifier 注解的时候,可以类似地创建 @Soft, @Crispy 和 @Fruity.通过在定义时添加 @Qualifier
注解,它们就具有了 @Qualifier 注解的特性.它们本身实际上就成为了限定符注解.
现在,我们可以重新看一下 IceCream,并为其添加 @Cold 和 @Creamy 注解,如下所示:
1 | @Component |
类似地,Popsicle 类可以添加 @Cold 和 @Fruity 注解:
1 | @Component |
最终,在注入点,我们使用必要的限定符进行任意组合,从而将可选范围缩小到只有一个 bean 满嘴需求.为了得到 IceCream bean,
setDessert() 方法可以这样使用注解:
1 | @Autowired |
通过声明自定义的限定符注解,我们可以同时使用多个限定符,不会亏再有 Java 编译器的限制或错误.
与此同时,相对于使用原始的 @Qualifier 并借助 String 类型来指定限定符,自定义的注解也更为类型安全.
让我们近距离观察一下 setDessert() 方法以及它的注解,这里并没有在任何地方明确指定要将 IceCream 自动装配到该方法中.
相反,我们使用所需 bean 的特性来进行指定,即 @Cold 和 @Creamy.因此,setDessert() 方法依然能够与特定的 Dessert
实现保持解藕.任意满足这些特征的 bean 都是可以的.在当前选择 Dessert 实现时,恰好如此,IceCream 是唯一能够与之匹配的 bean.