# 3.3.2　限定自动装配的 bean

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

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

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

```java
@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 组合使用，如下所示：

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

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

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

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

```java
@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，会发生什么情况呢：

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

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

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

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

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

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

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

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

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

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

{% code title="Cold.java" %}

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

{% endcode %}

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

{% code title="Creamy.java" %}

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

{% endcode %}

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

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

{% code title="IceCream.java" %}

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

{% endcode %}

类似地，Popsicle 类可以添加 @Cold 和 @Fruity 注解：

{% code title="Popsicle.java" %}

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

{% endcode %}

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

```java
@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。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://potoyang.gitbook.io/spring-in-action-v4/di-3-zhang-gao-ji-zhuang-pei/untitled-3/untitled-1.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
