LoginSignup
4
1

More than 1 year has passed since last update.

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

Posted at

環境

  • 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;
    }

    ...
4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1