📘

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

2024/08/13に公開

環境

  • JDK 17
  • Spring Boot 2.7.0

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

解決したい問題

Foo インタフェースがあって、その実装クラス(Bean)が2つあります。

Foo.java
package com.example;

public interface Foo {
    public void doSomething();
}
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");
    }
}
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します。

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 が発生します。

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

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

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する型を FooImpl1FooImpl2 に変更すれば、問題は発生しません。

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

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とは何かという説明は、こちらのスライドの13・14ページをご覧ください。

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されます。

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 {
}
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 {
}
FooImpl1.java(一部)
@Component
@Foo1  // このアノテーションがポイント!
public class FooImpl1 implements Foo {
    ...
FooImpl2.java(一部)
@Component
@Foo2  // このアノテーションがポイント!
public class FooImpl2 implements Foo {
    ...
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を指定します。

FooImpl1.java(一部)
@Component
@Primary  // このアノテーションがポイント!
public class FooImpl1 implements Foo {
    ...
FooImpl2.java(一部)
@Component
// @Primaryを付けない
public class FooImpl2 implements Foo {
    ...
Bar.java(一部)
@Component
public class Bar {

    private final Foo foo;

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

    ...

Discussion