Spring in Action 4 读书笔记之解决自动装配 bean 的二义性问题

字数1,201 大约花费5分钟

目录

  1. 1. 使用 @Primary 标签指定首选 bean
  2. 2. 使用 @Qualifier 标签装配指定 id 的 bean

Spring in Action(Spring 实战)的第三章第三节(3.3 Addressing ambiguity in autowiring)讲述了如何解决自动装配 bean 的二义性问题。

Spring 的自动注入机制极大地减少装配 bean 的工作量,但在默认情况下,这个机制仅在只有一个 bean 满足条件时才工作。当有多个 bean 满足条件时,这种二义性使 Spring 无法确定使用哪一个 bean,也就无法完成自动装配,并且会抛出 NoUniqueBeanDefinitionException 异常。

比如下面的代码就存在二义性:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}

@Component
public class Cake implements Dessert { ... }

@Component
public class Cookies implements Dessert { ... }

@Component
public class IceCream implements Dessert { ... }

因为 Cake,Cookies,IceCream 都实现了 Dessert 接口,Dessert 声明的 bean 存在二义性,所以 setDessert 方法在自动注入 Dessert 类型的组件时,不知道应该注入哪种具体类型的 component, 将会抛出 NoUniqueBeanDefinitionException 异常:

1
2
3
4
nested exception is
org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type [com.desserteater.Dessert] is defined:
expected single matching bean but found 3: cake,cookies,iceCream

当然,上面的例子只是为了解释什么时候会产生二义性,在实际项目中,很少会遇到这种情况。如果在某些特殊场景遇到了,也可以使用 Spring 提供的方案很好的解决,具体方案如下:

使用 @Primary 标签指定首选 bean

Spring 可以对 bean 进行自动扫描,也可以显式定义要装配的 bean,这两种情况下都可以使用 @Primary 标签进行标注。

自动扫描:

1
2
3
@Component
@Primary
public class IceCream implements Dessert { ... }

使用 java 显式定义要装配的 bean:

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

使用 XML 显示定义 bean:

1
2
3
<bean id="iceCream"
class="com.desserteater.IceCream"
primary="true" />

当有多个 bean 都加了 @Primary 标签时,这种方法其实并没有解决二义性问题,因为 spring 仍然会把所有加了这个 @Primary 标签的 bean 作为候选 bean,所以,更好的方式是使用 @Qualifier 标签来给每个 bean 定义 id。

使用 @Qualifier 标签装配指定 id 的 bean

实际上 @Qualifier 标签有两种用法。一种是在注入 bean 时,和 @Autowired 或者 @Inject 合用,指定要装配哪个 id 的 bean,比如:

1
2
3
4
5
6
// 代码清单一 
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}

表示要注入 id 为 iceCream 的 bean。

另一种用法是,在要被装配的 bean 上,使用 @Qualifier,可以为这个 bean 定义 id(以替代根据类名自动生成的 id)。比如:

1
2
3
4
// 代码清单二 
@Component
@Qualifier("cold")
public class IceCream implements Dessert { ... }

定义 IceCream 类型的 bean 的 id 为 cold,作者建议使用 cold 这样描述性的词汇替代类名自动声明的 id,防止 bean 的类名进行重命名造成注入 bean 的地方也要跟着改。比如代码清单二中 IceCream 重命名为 Gelato 时,代码清单一中使用 @Qualifier(“iceCream”) 就找不到 id 为 iceCream,就会注入失败了。当然,在不存在二义性的情况下,不需要使用 @Qualifier 标签来注入 bean,也不用特意为类指定 id。

因为使用描述性的词汇定义 bean,所以很可能遇到重名的情况,比如:

1
2
3
4
// 代码清单三 
@Component
@Qualifier("cold")
public class Popsicle implements Dessert { ... }

这样 Popsicle 和 IceCream 的 beanId 都是 cold,又生成二义性了,此时可以通过使用多个 @Qualifier 的方式进行定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 代码清单四 
@Component
@Qualifier("cold")
@Qualifier("creamy")
public class IceCream implements Dessert { ... }


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

@Autowired
@Qualifier("cold")
@Qualifier("creamy")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}

由于 Spring 的 @Qualifier 标签没有用 @Repeatable 标注,因此不具备 @Repeatable 的属性,所以像上面这样重复使用 @Qualifier 是无效的,清单四的代码要改为:

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
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD,
ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold { }


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

@Component
@Cold
@Creamy
public class IceCream implements Dessert { ... }

@Component
@Cold
@Fruity
public class Popsicle implements Dessert { ... }

@Autowired
@Cold
@Creamy
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}

新增了 @Cold,@Creamy 等标签替代 @Qualifier。类比于 html,这样用法是把 id 当作 class 使用,好处是更加灵活,但代码看起来也更复杂了一些。其实,一般情况下,使用一个 @Qualifier 标签就够了,用不到这种多重定义。

谈谈 IT 的文章均为原创或翻译(翻译会注明外文来源),转载请以链接形式标明本文地址: http://tantanit.com/springinaction4-du-shu-bi-ji-zhi-jie-jue-zi-dong-zhuang-pei-shi-de-er-yi-xing/

谈谈IT

欢迎关注官方微信公众号获取最新原创文章