SpringでNoUniqueBeanDefinitionExceptionが出たときの解決法
環境
- JDK 17
- Spring Boot 2.7.0
多少バージョンが違っていても、動作は変わらないと思います。
解決したい問題
Foo
インタフェースがあって、その実装クラス(Bean)が2つあります。
package com.example;
public interface Foo {
public void doSomething();
}
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");
}
}
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します。
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
が発生します。
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コンテナが判断できないからです。
@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
に変更すれば、問題は発生しません。
型で指定するほうが分かりやすくなるので、可能な限りこの方法をおすすめします。
@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ページをご覧ください。
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されます。
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 {
}
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 {
}
@Component
@Foo1 // このアノテーションがポイント!
public class FooImpl1 implements Foo {
...
@Component
@Foo2 // このアノテーションがポイント!
public class FooImpl2 implements Foo {
...
@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を指定します。
@Component
@Primary // このアノテーションがポイント!
public class FooImpl1 implements Foo {
...
@Component
// @Primaryを付けない
public class FooImpl2 implements Foo {
...
@Component
public class Bar {
private final Foo foo;
// @Primaryが付いているFooImpl1がDIされる
public Bar(Foo foo) {
this.foo = foo;
}
...
Discussion