Spring学习笔记(处理自动装配的歧义性)

《Spring in Action(4th Edition)》学习随笔,备查。

问题:

假设使用@Autowired注解标注了setDessert()方法:

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

其中Dessert是一个接口,有三个实现类:

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

因为这三个实现均使用了@Component注解,在组件扫描的时候,能够发现它们并将其创建为Spring应用上下文里面的bean,但是当Spring试图自动装配setDessert()中的Dessert参数时,由于没有唯一的、无歧义的可选值。Spring会抛出NoUniqueBeanDefinitionException。

 

解决方案:

1. 标示首选的bean

可以通过@Primary注解与@Component或@Bean注解配合使用或者在XML配置bean时指定primary属性标示首选的bean。

代码如下:

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

或者

@Bean
@Primary
public Dessert iceCream() {
    return new IceCream();
}

或者

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

但是当同时标示两个及以上的首选bean时,spring就无法正常工作了,此时需要使用下一种方法:

 

2. 限定自动装配的bean

spring提供使用限定符在所有可选的bean上缩小选择范围。@Qualifier注解是使用限定符的主要方式,它可以与@Autowired和@Inject协同使用。

最简单的例子(使用默认限定符):

@Autowired
@Qualifier("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的名称是紧耦合的。对类名称的任意改动都会导致限定符失效。这时我们需要:

(1)创建自定义的限定符

在这里所需要做的就是在bean声明上添加@Qualifier注解。例 如,它可以与@Component组合使用,如下所示:

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

在这种情况下,cold限定符分配给了IceCreambean。因为它没有耦合类名,因此你可以随意重构IceCream的类名,而不必担心会破坏自动 装配。在注入的地方,只要引用cold限定符就可以了:

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

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

@Bean
@Qualifier("cold")
public Dessert IceCream {
    return new IceCream;
}

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

 

由于Java不允许在同一个条目上重复出现相同类型的多个注解,所以当出现另外一个用@Qualifier("cold")注解的bean时(例如:Popsicle,代码省略),不能再用一个其他的@Qualifier来区分这两个bean,这时需要我们创建自定义限定符的注解:

@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 {}

应用的时候,可以为IceCream添加@Cold和@Creamy注解,为Popsicle添加@Cold和@Fruity注解:

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

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

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

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

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

posted @ 2018-11-05 15:00  天南星2018  阅读(196)  评论(1编辑  收藏  举报