33

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

Ondra Žižka
  • 43,948
  • 41
  • 217
  • 277
maxxyme
  • 2,164
  • 5
  • 31
  • 48
  • so you are trying to do the equivalent of #{bean.myMethod()} that you can do in xml with @Qualifier right? – crabe Mar 30 '17 at 07:33
  • No really, because the ``bean`` in your example is a Spring-managed bean (like a ``@Service`` or similar), not a POJO-like bean (from the model). – maxxyme Mar 31 '17 at 07:54

2 Answers2

33

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);
  }
}
Genu
  • 827
  • 6
  • 12
aux
  • 1,589
  • 12
  • 20
  • In fact, it's not "country code as a bean name", it's country code as a qualifier value. ;) But in the end, as I'm mentioning the factory pattern as a conclusion above, I started developing something similar. – maxxyme Mar 27 '17 at 08:39
  • 1
    I'm glad you found a solution. Still in my opinion the registry patterns might be preferable for this case, although I understand that your real situation might be different and not so simplified as you described. Good luck+ – aux Mar 27 '17 at 09:45
  • 1
    I think the first example is a bit misleading as `@Autowired BeanFactory beans;` won't work out of the box without further code as it will return `null` – Ghilteras Apr 21 '19 at 01:53
  • I find the registry solution quite elegant, the only question I have is, how is the services ``public CaseServices(List services)`` being autowired? Does Spring framework scanns all available beans that implement the CaseInterface and injects it in the CaseServices? – Ilias Mertzanidis Apr 17 '20 at 13:55
  • Don't have to answer, I already tested it and it worked perfectly. Nice done dude!!! – Ilias Mertzanidis Apr 17 '20 at 15:55
  • @IliasMertzanidis I am new to Spring Boot and I think this solution works perfectly fine for me, but when are we going to set the services list and when and how is that constructor getting called? If possible can you share your code too? – Ajay Kushwaha Oct 16 '22 at 19:37
  • @IliasMertzanidis, this is injected by Spring "autowiring" - this is the core functionality of Spring. You can annotate services as `@Service` and their singleton instances will be provided to the constructor. Injecting lists works as well as in my example. You still can construct these services manually if you like. See docs and tutorials on autowiring and dependency injection, for example: https://www.baeldung.com/spring-autowire – aux Oct 28 '22 at 20:02
  • This looks like a great solution. Is there a way to make it work with Google Guice? I am not able to get the list injected in the constructor. The only work-around seems to have an explicit binding for every implementation of the interface using `MultiBinder`, but this feels a bit odd, as the framework should be able to pick up all the implementations of the interface by itself. – martin_wun Apr 18 '23 at 13:02
4

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.

Community
  • 1
  • 1
ninj
  • 1,529
  • 10
  • 10
  • You'd have to be careful with your factory pattern I think. – ninj Mar 28 '17 at 21:57
  • Your intent on using `myCase.setCountryCode("uk");` isn't threadsafe, unless you use a `ThreadLocal` to store the `CaseService` instance. Potentially you could use an [`ObjectProvider#getObject(Object... args)`](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/factory/ObjectProvider.html#getObject-java.lang.Object...-) to set the ThreadLocal in your setter. – ninj Mar 28 '17 at 22:05
  • I don't set the `CaseService` when calling `setCountryCode()`. This is the initialisation of the object. I edited for clarity. – maxxyme Mar 29 '17 at 09:10