24
votes

Here is my question:I have a base interface and two implementation class.

And a Service class has a dependencies on the base interface, the code is like this:

@Component
public interface BaseInterface {}

@Component
public class ClazzImplA implements  BaseInterface{}

@Component
public class ClazzImplB implements  BaseInterface{}

And the configuration is like this :

@Configuration
public class SpringConfig {
    @Bean
    public BaseInterface clazzImplA(){
        return new ClazzImplA();
    }

    @Bean
    public BaseInterface clazzImplB(){
        return new ClazzImplB();
    }
}

The service class has dependencies on the base interface will decide to autowire which Implementation by some business logic.And the code is like this:


@Service
@SpringApplicationConfiguration(SpringConfig.class)
public class AutowiredClazz {
    @Autowired
    private BaseInterface baseInterface;

    private AutowiredClazz(BaseInterface baseInterface){
        this.baseInterface = baseInterface;
    }
}

And the IDEA throws a exception:Could not autowire.There is more than one bean of BaseInterface type.

Although it can be solved by using @Qualifier,but in this situation I can't choose the dependencies class.

@Autowired
@Qualifier("clazzImplA")
private BaseInterface baseInterface;

I tried to read the spring document and it provide a Constructor-based dependency injection but I'm still confused by the problem.

can anyone help me ?

5
It's times like this that I hate Javagdbj
@gdbj facepalm it's about spring, not java You would have the same problem in any other language, that has DI. If you try to wire 2 beans to one field, you will have thisMartinL

5 Answers

29
votes

Spring is confused between the 2 beans you have declared in you configuration class so you can use @Qualifier annotation along with @Autowired to remove the confusion by specifying which exact bean will be wired, apply these modifications on your configuration class

@Configuration
public class SpringConfig {
    @Bean(name="clazzImplA")
    public BaseInterface clazzImplA(){
        return new ClazzImplA();
    }

    @Bean(name="clazzImplB")
    public BaseInterface clazzImplB(){
        return new ClazzImplB();
    }
}

then at @autowired annotation

@Service
@SpringApplicationConfiguration(SpringConfig.class)
public class AutowiredClazz {
    @Autowired
    @Qualifier("the name of the desired bean")
    private BaseInterface baseInterface;

    private AutowiredClazz(BaseInterface baseInterface){
        this.baseInterface = baseInterface;
    }
}
6
votes

This can not be solved by using spring framework only. You mentioned that based on some logic you need a instance of BaseInterface. This use case can be solved using Factory Pattern. Create A Bean which is actually a factory for BaseInterface

@Component
public class BaseInterfaceFactory{

  @Autowired
  @Qualifier("clazzImplA")
  private BaseInterface baseInterfaceA;

  @Autowired
  @Qualifier("clazzImplb")
  private BaseInterface baseInterfaceB;

  public BaseInterface getInstance(parameters which will decides what type of instance you want){
    // your logic to choose Instance A or Instance B
    return baseInterfaceA or baseInterfaceB
  }

}

Configuration (Shamelessly copied from another comment)

@Configuration
public class SpringConfig {
    @Bean(name="clazzImplA")
    public BaseInterface clazzImplA(){
        return new ClazzImplA();
    }

    @Bean(name="clazzImplB")
    public BaseInterface clazzImplB(){
        return new ClazzImplB();
    }
}

Service class

@Service
@SpringApplicationConfiguration(SpringConfig.class)
public class AutowiredClazz {
    @Autowired
    private BaseInterfaceFactory factory;

    public void someMethod(){
       BaseInterface a = factory.getInstance(some parameters);
       // do whatever with instance a
    }
}
1
votes

If you use @Autowired, Spring searches for a bean matching the type of the field you want to autowire. In your case there is more than one bean of the type BaseInterface. That means that Spring can't pick a matching bean unambiguously.

In such a situation you have no other choice to explicitly state the bean Spring should use or resolve the ambiguity.

1
votes

An even cooler solution is to let the implementations themselves contain the logic to determine if they are applicable. You can inject all the available implementations as a collection and iterate over them to find one (or more, if that's what you need) applicable ones:

public interface BaseInterface {
    boolean canHandle(Object parameter);
    Object doTheWork(Object parameter);
}

@Service
public class SomeService {

    private final BaseInterface[] implementations;

    // Spring injects all beans implementing BaseInterface
    public MessageService(BaseInterface... implementations) {
        this.implementations = implementations;
    }

    public Object doSomething(Object parameter) {
        BaseInterface baseInterface = findBaseInterface(parameter);
        return baseInterface.doTheWork(parameter);
    }    

    private BaseInterface findBaseInterface(Object parameter) {
        return Arrays.stream(implementations)
            .findAny(i -> i.canHandle(parameter)
            .orElseThrow(new MyRuntimeException(String.format("Could not find BaseInterface to handle %s", parameter)));
    }
}
0
votes

It has been rightly answered here that in such cases where an interface is implemented by more than one class we have to name each of those beans using @Component(name=$beanName)

I would like to add one more point that in such cases Spring can even autowire such beans in a map:

@Autowired
Map<String,InterfaceName> interfaceMap;
//this will generate beans and index them with beanName as key of map