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

maxxyme picture maxxyme · Mar 20, 2017 · Viewed 23.2k times · Source

I can't find a simple way to inject a component/service given a runtime value.

I started reading @ Spring's doc: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html#beans-autowired-annotation-qualifiers but I can't find there how to variabilize the values passed to the @Qualifier annotation.

Let's say I've got a model entity with such interface:

public interface Case {

    String getCountryCode();
    void setCountryCode(String countryCode);

}

In my client code, I would do something like:

@Inject
DoService does;

(...)

Case myCase = new CaseImpl(); // ...or whatever
myCase.setCountryCode("uk");

does.whateverWith(myCase);

... with my service being:

@Service
public class DoService {

    @Inject
    // FIXME what kind of #$@& symbol can I use here?
    // Seems like SpEL is sadly invalid here :(
    @Qualifier("${caze.countryCode}")
    private CaseService caseService;

    public void whateverWith(Case caze) {
        caseService.modify(caze);
    }

}

I expect the caseService to be the UKCaseService (see related code below).

public interface CaseService {

    void modify(Case caze);

}

@Service
@Qualifier("uk")
public class UKCaseService implements CaseService {

}

@Service
@Qualifier("us")
public class USCaseService implements CaseService {

}

So how do I "fix" all of this in the most simple / elegant / efficient way by using either/all Spring feature(s), so essentially NO .properties, NO XML, only annotations. However I already suspect something is wrong in my DoService because Spring would need to know the "case" before injecting the caseService... but how to achieve this without the client code knowing about the caseService?! I can't figure this out...

I already read several issues here on SO, but most of the times either they don't really have the same needs and/or config as I have, or the posted answers aren't enough satisfying to me (look like they're essentially workarounds or (old) usage of (old) Spring features).

How does Spring autowire by name when more than one matching bean is found? => only refers to component-like classes

Dynamically defining which bean to autowire in Spring (using qualifiers) => really interesting but the most elaborated answer (4 votes) is... almost 3 1/2 years-old?! (July 2013)

Spring 3 - Dynamic Autowiring at runtime based on another object attribute => quite similar problem here, but the answer really look like a workaround rather a real design pattern (like factory)? and I don't like implementing all the code into the ServiceImpl as it's done...

Spring @Autowiring, how to use an object factory to choose implementation? => 2nd answer seems interestingly but its author does not expand, so altough I know (a bit) about Java Config & stuff, I'm not really sure what he's talking about...

How to inject different services at runtime based on a property with Spring without XML => interesting discussion, esp. the answer, but the user has properties set, which I don't have.

Also read this: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html#expressions-bean-references => I can't find expanded examples about the use of "@" in expressions. Does someone know about this?

Edit: Found other related-to-similar issues, no one got a proper answer: How to use @Autowired to dynamically inject implementation like a factory pattern Spring Qualifier and property placeholder Spring: Using @Qualifier with Property Placeholder How to do conditional auto-wiring in Spring? Dynamic injection in Spring SpEL in @Qualifier refer to same bean How to use SpEL to inject result of method call in Spring?

Factory Pattern might be a solution? How to use @Autowired to dynamically inject implementation like a factory pattern

Answer

aux picture aux · Mar 23, 2017

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