3.3.2 限定自动装配的 bean

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

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

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

@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 的名称是紧耦合的。对类名称的任意改动都会导致限定符失效。

创建自定义的限定符

我们可以为 bean 设置自己的限定符,而不是依赖于将 bean ID 作为限定符。在这里所需要做的就是在 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”。

使用自定义的限定符注解

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

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

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

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

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

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

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

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

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

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

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

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

同样,你可以创建一个新的 @Creamy 注解来代替 @Qualifier("creamy"):

Creamy.java
@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 注解,如下所示:

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

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

Popsicle.java
@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 类型来指定限定符,自定义的注解也更为类型安全。

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

在本节和前面的节中,我们讨论了几种通过自定义注解扩展 Spring 的方式。为了创建自定义的条件化注解,我们创建一个新的注解并在这个注解上添加了 @Conditional。为了创建自定义的限定符注解,我们创建一个新的注解并在这个注解上添加了 @Qualifier。这种技术可以用到很多的 Spring 注解中,从而能够将它们组合在一起形成特定目标的自定义注解。

现在我们来看一下如何在不同的作用域中声明 bean。

Last updated