Quarkus Tip : Séléctionner un bean au runtime

Quarkus Tip : Séléctionner un bean au runtime

Lors du développement d’une application, il est très fréquent d’avoir plusieurs implémentations d’un service, et de devoir sélectionner l’une où l’autre en fonction de l’environnement sur lequel elle est déployée.

Un exemple classique : un service qui appelle une API d’un partenaire externe qu’on veut appeler uniquement en production, et donc mocker sur les environnements de développement et de test / UAT / staging.

Quarkus essaye de déporter au build time, via son plugin Maven ou Gradle, le plus de choses possibles. Entre autre, il découvre les beans CDI au build time, et ne va instancier que ceux nécessaire au runtime.

Pour pouvoir sélectionner un bean, on a la possibilité d’utiliser l’annotation @IfBuildProfile, l’annotation @IfBuildProperty, ou les alternatives CDI. Mais ces solutions vont sélectionner le bean au build time! Celà permet d’avoir un bean différent pour les tests unitaires ou le dev mode mais pas une fois que l’application a été construite. Pour utiliser ce mécanisme pour sélectionner un bean différent sur un environement UAT/staging et sur la prod, il faudrait builder plusieurs packages de l’application.

Sélectionner un bean au runtime reste possible en utilisation une Instance CDI, cela nécessite quelques lignes de code.

Voici comment faire.

Etape 1 : ajouter une propriété dans votre application.properties

service.implementation-name=mock

Etape 2 : annoter vos deux implémentations avec un qualifier CDI, ici on utilisera @Named mais Quarkus propose aussi @Identifier.

@ApplicationScoped
@Named("mock")
public class MockService implements RemoteService {
    // ....
}
@ApplicationScoped
@Named("real")
public class RealService implements RemoteService {
    // ....
}

Etape 3 : dans la classe utilisant votre service, sélectionner le bean au démarrage de l’application en vous basant sur la configuration de l’application.

@Path("/api")
public class MyEndpoint {
    @ConfigProperty(name="service.implementation-name") String implementationName;
    @Inject Instance<RemoteService>remoteServiceInstance;
    
    private RemoteService remoteService;

    @PostConstruct
    void init() {
        remoteService = remoteServiceInstance
            .select(NamedLiteral.of(implementationName)).get();
    }

    // ...
}

MISE A JOUR : Martin Kouba (merci à lui) a proposé une implémentation plus courte et clair, via injection dans le constructeur (ce que, à ma grand honte, je ne suis pas du tout familier). Je vous met donc ci-dessous une étape 3 avec un code simplifié :

@Path("/api")
public class MyEndpoint {   
   private final RemoteService remoteService;

   MyEndpoint(@ConfigProperty(name="service.implementation-name") String implName, 
         Instance<RemoteService> remoteServiceInstance) {
     remoteService = remoteServiceInstance.select(NamedLiteral.of(implName)).get();
   }

    // ...
}

Attention : les définitions (metadata) deux beans MockService et RemoteService seront créées et resterons en mémoire tout au long de la vie de votre application, même si uniquement l’un des deux sera utilisé. Elles ne seront pas supprimées automatiquement par Arc, l’implémentation CDI de Quarkus, via son mécanisme de suppression de bean inutilisé. Les beans quand à eux, s’ils utilisent un scope normal, seront créé de manière lazy.

Conclusion : même si sélectionner un bean au runtime va à l’encontre de la philosophie de Quarkus, c’est bien souvent une nécessité et cela reste possible via quelques lignes de code. Comme c’est quelque chose de classique, je vais évoquer le point avec la communauté Quarkus pour qu’on trouve quelque chose de plus pratique, stay tuned 😉

MISE A JOUR : J’ai depuis évoqué le point avec la communauté Quarkus et une solution plus pratique que l’utilisation de instance.select() a été implémentée grâce aux annotations @LookupIfProperty et @LookupUnlessProperty qui permettent d’ajouter, pour chaque bean, une condition pour leur sélection en fonction d’une propriété de configuration.

On peut donc remplacer les étapes 2 et 3 précédentes par celles-ci :

Etape 2 : annoter vos deux implémentations avec @LookupIfProperty.

@ApplicationScoped
@LookupIfProperty(name = "service.implementation-name", stringValue = "mock")
public class MockService implements RemoteService {
    // ....
}
@ApplicationScoped
@LookupIfProperty(name = "service.implementation-name", stringValue = "real")
public class RealService implements RemoteService {
    // ....
}

Etape 3 : dans la classe utilisant votre service, le bean doit être obtenu au démarrage via la méthode get() de l’instance. La présence de l’annotation @LookupIfProperty sur les deux implémentations fera que seul le bean de type MockService sera créé car la valeur de la propriété service.implementation-name utilisé dans son instance de @LookupIfProperty est mock.

@Path("/api")
public class MyEndpoint {
    @Inject Instance<RemoteService>remoteServiceInstance;
    
    private RemoteService remoteService;

    @PostConstruct
    void init() {
        remoteService = remoteServiceInstance.get();
    }

    // ...
}

3 réflexions sur « Quarkus Tip : Séléctionner un bean au runtime »

  1. You can also use @LookupIfProperty annotation on your bean. Then, to get the bean instance, you just need to call instance.get(). Here is the documentation https://quarkus.io/guides/cdi-reference#declaratively-choose-beans-that-can-be-obtained-by-programmatic-lookup

    Moreover when your bean is annotated with @ApplicationScoped, the bean will be created lazily (when a method of the bean is invoked). So I don’t think your sentence « both MockService and RemoteService beans will be created and remain in memory throughout the life of your application, even if only one of them is used » is correct. I think the @ApplicationScoped bean that is not used will not be created (only the proxy will be created and not the target bean), here is the documentation https://quarkus.io/guides/cdi-reference#lazy_by_default

    1. Hi Olivier,
      The @LookupIfProperty annotation has been created following discussion by this very article 😉
      So, at the time I wrote this article, it didn’t exist. I’ll update the article to reflect this change.

      Regarding the second point, I remember discussing it with one of the CDI expert on the Quarkus teams, as I understand it Instance is handled differently as when you access the Instance objet the CDI container cannot knows which bean you will use (and if you will use the Instance) so the bean will not be removed. But maybe the phrasing is not good as it’s the bean metadata that will not be removed but, as you sais, as beans are created lazilly the overhead is low. I’ll ask on the Quarkus Zulip chat and clarify this sentence if needed.

    2. Hi Olivier,
      I updated the article with both a precision for lazy instanciaton of beans and the new way to select a bean at runtime via @LookupIfProperty. Thanks for pointing this better way of doing it to me.

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.