web-dev-qa-db-fra.com

Comment injecter dynamiquement un service à l'aide d'une variable "qualificative" d'exécution?

Je ne peux pas trouver un moyen simple d'injecter un composant/service étant donné une valeur d'exécution.

J'ai commencé à lire le document de @ Spring: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html#beans-autowired-annotation-qualifiers mais Je ne peux pas trouver là comment variabiliser les valeurs passées à l'annotation @Qualifier.

Disons que j'ai une entité modèle avec une telle interface:

public interface Case {

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

}

Dans mon code client, je ferais quelque chose comme:

@Inject
DoService does;

(...)

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

does.whateverWith(myCase);

... avec mon service étant:

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

}

Je m'attends à ce que le caseService soit le UKCaseService (voir le code associé ci-dessous).

public interface CaseService {

    void modify(Case caze);

}

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

}

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

}

Alors, comment puis-je "corriger" tout cela de la manière la plus simple/élégante/efficace en utilisant l'une ou l'autre des fonctionnalités de Spring, donc essentiellement NO .properties, NO XML, uniquement des annotations. Cependant, je soupçonne déjà que quelque chose ne va pas dans mon DoService parce que Spring aurait besoin de connaître le "cas" avant d'injecter le caseService ... mais comment y parvenir sans que le code client ne connaisse le caseService?! Je ne peux pas comprendre ça ...

J'ai déjà lu plusieurs problèmes ici sur SO, mais la plupart du temps, ils n'ont pas vraiment les mêmes besoins et/ou configuration que moi, ou les réponses publiées ne me satisfont pas assez (on dirait qu'elles sont essentiellement contournements ou (ancienne) utilisation des (anciennes) fonctionnalités Spring).

Comment Spring effectue-t-il le câblage automatique par nom lorsque plus d'un bean correspondant est trouvé? => fait uniquement référence aux classes de type composant

Définition dynamique du bean à câbler automatiquement au printemps (en utilisant des qualificatifs) => vraiment intéressant mais la réponse la plus élaborée (4 votes) est ... presque 3 1/2 ans?! (Juillet 2013)

Spring 3 - Dynamic Autowiring à l'exécution basé sur un autre attribut d'objet => problème assez similaire ici, mais la réponse ressemble vraiment à une solution de contournement plutôt qu'à un vrai modèle de conception (comme l'usine)? et je n'aime pas implémenter tout le code dans ServiceImpl car c'est fait ...

Spring @Autowiring, comment utiliser une fabrique d'objets pour choisir l'implémentation? => La 2e réponse semble intéressante mais son auteur ne se développe pas, donc je connais (un peu) environ Java Config & stuff, je ne sais pas trop de quoi il parle ...

Comment injecter différents services à l'exécution en fonction d'une propriété avec Spring sans XML => discussion intéressante, en particulier. la réponse, mais l'utilisateur a des propriétés définies, que je n'ai pas.

Lisez également ceci: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html#expressions-bean-references => Je ne peux pas trouver des exemples étendus sur l'utilisation de "@" dans les expressions. Quelqu'un le sait-il?

Edit: Trouvé d'autres problèmes liés à des problèmes similaires, personne n'a obtenu une bonne réponse: Comment utiliser @Autowired pour injecter dynamiquement l'implémentation comme un modèle d'usineSpring Qualifier et propriété placeholderSpring: Utilisation de @Qualifier avec l'espace réservé de propriétéComment faire un câblage automatique conditionnel dans Spring?Injection dynamique dans SpringSpEL dans @ Le qualificatif fait référence au même beanComment utiliser SpEL pour injecter le résultat de l'appel de méthode au printemps?

Pattern Factory pourrait être une solution? Comment utiliser @Autowired pour injecter dynamiquement l'implémentation comme un modèle d'usine

18
maxxyme

Vous pouvez obtenir votre bean à partir du contexte par son nom de manière dynamique en utilisant un BeanFactory :

@Service
public class Doer {

  @Autowired BeanFactory beans;

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

Une note latérale. Utiliser quelque chose comme le code pays comme nom de bean semble un peu étrange. Ajoutez au moins un préfixe ou mieux envisagez un autre modèle de conception.

Si vous aimez toujours avoir des haricots par pays, je suggérerais une autre approche. Introduisez un service de registre pour obtenir un service requis par code de pays:

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

Exemple d'utilisation:

@Service
public class DoService {

  @Autowired CaseServices caseServices;

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

Dans ce cas, vous devez ajouter la méthode String getCountryCode() à votre interface CaseService.

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

Alternativement, vous pouvez ajouter la méthode CaseService.supports(Case case) pour sélectionner le service. Ou, si vous ne pouvez pas étendre l'interface, vous pouvez appeler la méthode CaseServices.register(String, CaseService) à partir d'un initialiseur ou d'une classe @Configuration.

MISE À JOUR: J'ai oublié de mentionner que Spring fournit déjà une abstraction Nice Plugin pour réutiliser le code passe-partout pour créer PluginRegistry comme ça.

Exemple:

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

Selon cette réponse SO, en utilisant @Qualifier ne va pas vous aider beaucoup: Récupérer le bean d'ApplicationContext par qualificatif

Quant à une stratégie alternative:

  • si vous êtes Spring Boot, vous pouvez utiliser @ConditonalOnProperty ou un autre Conditional.

  • un service de recherche, comme @aux suggère

  • nommez simplement vos beans de manière cohérente et recherchez-les par leur nom lors de l'exécution.

Notez que votre cas d'utilisation semble également tourner autour du scénario où les beans sont créés au démarrage de l'application, mais le bean choisi doit être résolu après l'applicationContext a fini d'injecter les haricots.

4
ninj