SpringでNoUniqueBeanDefinitionExceptionが出たときの解決法

# 環境
- JDK 17
- Spring Boot 2.7.0

> 多少バージョンが違っていても、動作は変わらないと思います。

# 解決したい問題
`Foo` インタフェースがあって、その実装クラス（Bean）が2つあります。

```java:Foo.java
package com.example;

public interface Foo {
    public void doSomething();
}
```

```java:FooImpl1.java
package com.example.foo1;

import com.example.Foo;
import org.springframework.stereotype.Component;

@Component
public class FooImpl1 implements Foo {
    @Override
    public void doSomething() {
        System.out.println("FooImpl1");
    }
}
```

```java:FooImpl2.java
package com.example.foo2;

import com.example.Foo;
import org.springframework.stereotype.Component;

@Component
public class FooImpl2 implements Foo {
    @Override
    public void doSomething() {
        System.out.println("FooImpl2");
    }
}
```

そして、別の `Bar` クラスに `Foo` 型でDIします。

```java:Bar.java
package com.example.bar;

import com.example.Foo;
import org.springframework.stereotype.Component;

@Component
public class Bar {

    private final Foo foo;

    public Bar(Foo foo) {
        this.foo = foo;
    }

    public void doSomething() {
        foo.doSomething();
    }
}
```

そして、こんな感じで `main()` メソッドを作って実行すると、 `NoUniqueBeanDefinitionException` が発生します。

```java:Application.java
package com.example;

import com.example.bar.Bar;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Application.class);
        Bar bar = context.getBean(Bar.class);
        bar.doSomething();
    }
}
```

```
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.0)

xxxx-xx-xx xx:xx:xx.xxx  INFO 91129 --- [           main] com.example.Application                  : Starting Application using Java 17.0.1 on tadamasatoshinoMacBook-Pro.local with PID 91129 (/Users/xxx/IdeaProjects/spring-same-type-beans/target/classes started by xxx in /Users/xxx/IdeaProjects/spring-same-type-beans)
xxxx-xx-xx xx:xx:xx.xxx  INFO 91129 --- [           main] com.example.Application                  : No active profile set, falling back to 1 default profile: "default"
xxxx-xx-xx xx:xx:xx.xxx  WARN 91129 --- [           main] s.c.a.AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'bar' defined in file [/Users/xxx/IdeaProjects/spring-same-type-beans/target/classes/com/example/bar/Bar.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.example.Foo' available: expected single matching bean but found 2: fooImpl1,fooImpl2
xxxx-xx-xx xx:xx:xx.xxx  INFO 91129 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
xxxx-xx-xx xx:xx:xx.xxx ERROR 91129 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.example.bar.Bar required a single bean, but 2 were found:
	- fooImpl1: defined in file [/Users/xxx/IdeaProjects/spring-same-type-beans/target/classes/com/example/foo1/FooImpl1.class]
	- fooImpl2: defined in file [/Users/xxx/IdeaProjects/spring-same-type-beans/target/classes/com/example/foo2/FooImpl2.class]


Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed


Process finished with exit code 1
```

なぜ例外が起こるかというと、 `Bar` に `Foo` をDIする際に、 `FooImpl1` をDIするのか `FooImpl2` をDIするのか、DIコンテナが判断できないからです。

```java:Bar.java（再掲・一部）
@Component
public class Bar {

    private final Foo foo;

    // FooImpl1をDIするのかFooImpl2をDIするのか判断できない！！！
    public Bar(Foo foo) {
        this.foo = foo;
    }

    ...
```

# 解決方法
大きく4つあります。

## 方法① DIする型を変更する
`Bar` にDIする際に、インタフェースである `Foo` を使っているから問題が発生します。

なので、DIする型を `FooImpl1` や `FooImpl2` に変更すれば、問題は発生しません。

型で指定するほうが分かりやすくなるので、可能な限りこの方法をおすすめします。

```java:Bar.java（再掲・FooImpl1をDIする場合）
@Component
public class Bar {

    private final FooImpl1 foo;

    // DIする型を明確に指定する
    public Bar(FooImpl1 foo) {
        this.foo = foo;
    }

    ...
```

## 方法② `@Qualifier` アノテーションを指定する
型をどうしても `Foo` にしたい場合は、DIしている箇所に `@Qualifier` アノテーションでBean IDを指定します。

次の例では、 `FooImpl1` がDIされます。

> Bean IDとは何かという説明は、[こちらのスライド](https://www.docswell.com/s/MasatoshiTada/K1XMLK-advanced-spring-for-professionals#p13)の13・14ページをご覧ください。

```java:Bar.java（再掲・FooImpl1をDIする場合）
import org.springframework.beans.factory.annotation.Qualifier;

@Component
public class Bar {

    private final Foo foo;

    // DIしたいBeanのBean IDを指定する
    public Bar(@Qualifier("fooImpl1") Foo foo) {
        this.foo = foo;
    }

    ...
```

## 方法③ カスタム `@Qualifier` アノテーションを作成する
個人的には、文字列でBean IDを指定する方法②より、この方法が好みです。

カスタム `@Qualifier` アノテーションを作成して、Bean定義側・DIする側の両方に付加します。

次の例では、 `FooImpl1` がDIされます。

```java:Foo1.java
package com.example;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Qualifier  // このアノテーションがポイント！
public @interface Foo1 {
}
```

```java:Foo2.java
package com.example;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Qualifier  // このアノテーションがポイント！
public @interface Foo2 {
}
```

```java:FooImpl1.java（一部）
@Component
@Foo1  // このアノテーションがポイント！
public class FooImpl1 implements Foo {
    ...
```

```java:FooImpl2.java（一部）
@Component
@Foo2  // このアノテーションがポイント！
public class FooImpl2 implements Foo {
    ...
```

```java:Bar.java（一部）
@Component
public class Bar {

    private final Foo foo;

    // @Foo1を付加すればFooImpl1、@Foo2を付加すればFooImpl2がDIされる
    public Bar(@Foo1 Foo foo) {
        this.foo = foo;
    }

    ...
```

## 方法④ `@Primary` を指定する
`FooImpl1` または `FooImpl2` のどちらかに `@Primary` を付加します。

何も指定せずにDIした場合は、 `@Primary` が付加されているBeanがDIされます。

次の例では、 `FooImpl1` がDIされます。

`FooImpl2` をDIしたい場合は `@Qualifier` でBean IDを指定します。

```java:FooImpl1.java（一部）
@Component
@Primary  // このアノテーションがポイント！
public class FooImpl1 implements Foo {
    ...
```

```java:FooImpl2.java（一部）
@Component
// @Primaryを付けない
public class FooImpl2 implements Foo {
    ...
```

```java:Bar.java（一部）
@Component
public class Bar {

    private final Foo foo;

    // @Primaryが付いているFooImpl1がDIされる
    public Bar(Foo foo) {
        this.foo = foo;
    }

    ...
```