Spring Bean implementing multiple interfaces
Asked Answered
S

2

6

I have a bean which implements two interfaces. The barebones code is as follows:

interface InterfaceA {
  ...
}

interface InterfaceB {
  ...
}

public class ClassC implements InterfaceA, InterfaceB {
  ...
}

In my AppConfig I am specifying the following:

@Bean(name = "InterfaceA")
public InterfaceA interfaceA() {
    return new ClassC();
}

@Bean(name = "InterfaceB")
public InterfaceB interfaceB() {
    return new ClassC();
}

And I use it so:

public class MyClass {

    @Inject
    private final InterfaceA a;

    public MyClass(@Named("InterfaceA") InterfaceA a) {
        this.a = a;
    }
     ...
}

However, Spring complains that:

No qualifying bean of type [com.example.InterfaceA] is defined: expected single matching bean but found 2: InterfaceA, InterfaceB

Similar question was asked and answered for EJB here but I could not find anything for Spring beans. Anybody know the reason?

The workaround is to introduce a new interface which extends both InterfaceA and InterfaceB and then let ClassC implement that. However, I am loath to change my design because of framework constraints.

Spondee answered 20/7, 2015 at 11:5 Comment(1)
Why... Just create a single instance of ClassC... The return type of the method doesn't matter here, as the bean will be both InterfaceA and InterfaceB. You don't need an additional interface...Chaucerian
F
4

Spring is right ... When you write

@Bean(name = "InterfaceA")
public InterfaceA interfaceA() {
    return new ClassC();
}

@Bean(name = "InterfaceB")
public InterfaceB interfaceB() {
    return new ClassC();
}

Spring creates to ClassC objects, one named InterfaceA, the other InterfaceB, both implementing InterfaceA and InterfaceB.

Then when you write :

@Inject
private final InterfaceA a;

you ask Spring to find a bean implementing InterfaceA, but as said above there are 2 so the error.

You could either create only one object of type ClassC, or use @Qualifier or @Named annotations :

@Inject
@Named("InterfaceA")
private final InterfaceA a;

That way, you explicitely ask Spring to find the bean named InterfaceA, and hopefuly it is now unique.

Fiedler answered 20/7, 2015 at 11:34 Comment(3)
Thanks. Adding @Named to the field fixed it. I will accept your answer. However, I did annotate the constructor parameter with @Named. Why was that not sufficient? Didn't that tell Spring to use the correct bean?Spondee
@Spondee : If you bean creation uses the constructor with the @Named annotation, it should work ... provided you remove the @Inject on a attribute. If not, as soon as the MyClass object is constructed, Spring honours the @Inject annotation and tries to set a with a InterfaceA bean and finds 2 of them.Fiedler
Oh, I see. I was setting @Named for the constructor parameter and @Inject on the field. I will have to remove @Inject then. Thanks.Spondee
T
6

Thank you for your excellent question.

In my case, I created an interface that extends both A and B:

public interface InterfaceC extends InterfaceA, InterfaceB {}

... and the common implementation implements the unified interface:

public class ClassC implements InterfaceC {
  //...
}

This unified interface allows then to create a single bean:

@Bean
public InterfaceC implementationForAandB() {
    return new ClassC();
}

The Spring framework is then able to inject or autowire the common implementation to dependencies expressed in terms of the primary interfaces:

public class MyClass {

    @Inject
    private final InterfaceA a;

    @Inject
    private final InterfaceB b;

    public MyClass(InterfaceA a, InterfaceB b) {
        //...
    }

}

Teakettle answered 25/2, 2021 at 13:30 Comment(1)
This solution is not adequate because the InterfaceC violates the ISP, i.e. you now have a single interface mixing the concerns of two different domains. The OP tries to respect the ISP by having one interface for each domain. The fact that both interfaces are implemented by the same class should remain an implementation detail. But the introduction of the InterfaceC spoils everything.Ischium
F
4

Spring is right ... When you write

@Bean(name = "InterfaceA")
public InterfaceA interfaceA() {
    return new ClassC();
}

@Bean(name = "InterfaceB")
public InterfaceB interfaceB() {
    return new ClassC();
}

Spring creates to ClassC objects, one named InterfaceA, the other InterfaceB, both implementing InterfaceA and InterfaceB.

Then when you write :

@Inject
private final InterfaceA a;

you ask Spring to find a bean implementing InterfaceA, but as said above there are 2 so the error.

You could either create only one object of type ClassC, or use @Qualifier or @Named annotations :

@Inject
@Named("InterfaceA")
private final InterfaceA a;

That way, you explicitely ask Spring to find the bean named InterfaceA, and hopefuly it is now unique.

Fiedler answered 20/7, 2015 at 11:34 Comment(3)
Thanks. Adding @Named to the field fixed it. I will accept your answer. However, I did annotate the constructor parameter with @Named. Why was that not sufficient? Didn't that tell Spring to use the correct bean?Spondee
@Spondee : If you bean creation uses the constructor with the @Named annotation, it should work ... provided you remove the @Inject on a attribute. If not, as soon as the MyClass object is constructed, Spring honours the @Inject annotation and tries to set a with a InterfaceA bean and finds 2 of them.Fiedler
Oh, I see. I was setting @Named for the constructor parameter and @Inject on the field. I will have to remove @Inject then. Thanks.Spondee

© 2022 - 2024 — McMap. All rights reserved.