How to dynamically inject a service using a runtime "qualifier" variable?

29,908

Solution 1

You can obtain your bean from the context by name dynamically using a BeanFactory:

@Service
public class Doer {

  @Autowired BeanFactory beans;

  public void doSomething(Case case){
    CaseService service = beans.getBean(case.getCountryCode(), CaseService.class)
    service.doSomething(case);
  }
}

A side note. Using something like country code as bean name looks a bit odd. Add at least some prefix or better consider some other design pattern.

If you still like to have bean per country, I would suggest another approach. Introduce a registry service to get a required service by country code:

@Service
public class CaseServices {

  private final Map<String, CaseService> servicesByCountryCode = new HashMap<>();

  @Autowired
  public CaseServices(List<CaseService> services){
    for (CaseService service: services){
      register(service.getCountryCode(), service);
    }
  }

  public void register(String countryCode, CaseService service) {
    this.servicesByCountryCode.put(countryCode, service);
  }

  public CaseService getCaseService(String countryCode){
    return this.servicesByCountryCode.get(countryCode);
  }
}

Example usage:

@Service
public class DoService {

  @Autowired CaseServices caseServices;

  public void doSomethingWith(Case case){
    CaseService service = caseServices.getCaseService(case.getCountryCode());
    service.modify(case);
  }
}

In this case you have to add String getCountryCode() method to your CaseService interface.

public interface CaseService {
    void modify(Case case);
    String getCountryCode();
}

Alternatively, you can add method CaseService.supports(Case case) to select the service. Or, if you cannot extend the interface, you can call CaseServices.register(String, CaseService) method from some initialiser or a @Configuration class.

UPDATE: Forgot to mention, that Spring already provides a nice Plugin abstraction to reuse boilerplate code for creating PluginRegistry like this.

Example:

public interface CaseService extends Plugin<String>{
    void doSomething(Case case);
}

@Service
@Priority(0)
public class SwissCaseService implements CaseService {

  void doSomething(Case case){
    // Do something with the Swiss case
  }

  boolean supports(String countryCode){
    return countryCode.equals("CH");
  }
}

@Service
@Priority(Ordered.LOWEST_PRECEDENCE)
public class DefaultCaseService implements CaseService {

  void doSomething(Case case){
    // Do something with the case by-default
  }

  boolean supports(String countryCode){
    return true;
  }
}

@Service
public class CaseServices {

  private final PluginRegistry<CaseService<?>, String> registry;

  @Autowired
  public Cases(List<CaseService> services){
    this.registry = OrderAwarePluginRegistry.create(services);
  }

  public CaseService getCaseService(String countryCode){
    return registry.getPluginFor(countryCode);
  }
}

Solution 2

According to this SO answer, using @Qualifier isn't going to help you much: Get bean from ApplicationContext by qualifier

As for an alternative strategy:

  • if you are spring boot, you could use @ConditonalOnProperty or another Conditional.

  • a lookup service, as @aux suggests

  • just name your beans consistently and look them up by name at runtime.

Note that your use case also appears to revolve around the scenario where beans are created on application startup, but the bean chosen needs to be resolved after the applicationContext has finished injecting the beans.

Share:
29,908
maxxyme
Author by

maxxyme

40+ (and counting) year old IT engineer

Updated on August 28, 2020

Comments