Injection de dépendance et type-safe binding avec Spring

Une des fonctionnalités les plus importantes de CDI (Contexts and Dependency Injection) est l’injection de dépendance avec types (type-safe binding). Cette fonctionnalité est offerte par CDI via l’annotation @Qualifier de la JSR 330. Dans cet article, nous verrons comment obtenir avec Spring IOC un comportement similaire d’injection de dépendance grâce l’utilisation de l’annotation @Qualifier de Spring.

L’annotation @org.springframework.beans.factory.annotation.Qualifier a été introduite dans Spring 2.5, version publiée en 2007, ce qui en fait une fonctionnalité plutôt ancienne. Cette fonctionnalité permet de lever l’ambiguïté sur plusieurs références d’un bean lorsque Spring n’est pas capable de le déterminer. Cette annotation est le plus souvent utilisée en déterminant la référence du bean à travers son nom. L’idée est de montrer une technique permettant de récupérer la bonne référence à travers un typage fort.

Injection avec Spring

Avant d’aborder l’injection par typage fort, nous allons discuter dans cette première partie des problématiques qui vont nous conduire à utiliser cette fonctionnalité.

Injection classique

Il s’agit du cas le plus simple et le plus classique rencontré dans les projets utilisant Spring, un seul bean d’un certain type et un point d’injection avec ce type. Le code ci-dessous utilise l’injection par type en utilisant l’annotation @Autowired.

@Component
public class PaymentServiceImpl implements PaymentService {
}

public class PaymentResource {
    @Autowired
    private PaymentService paymentService;
}

Il y a un algorithme de type-safe résolution au niveau du point d’injection qui vérifie qu’un seul bean de ce type peut être injecté à chaque fois.

Injection avec plusieurs implémentations

Cette fois-ci, notre classe client contient un point d’injection correspondant à plusieurs beans dans le conteneur. Quel est le comportement de celui-ci dans ce cas-là ? Le conteneur cherche un bean qui satisfait le contrat.

  • S’il en trouve exactement un, il injecte une instance de ce bean.
  • Sinon, il détecte un conflit et lance une erreur.
@Component
public class MasterCardServiceImpl implements PaymentService {
}

@Component
public class VisaPaymentService implements PaymentService {
}

public class PaymentResource {
    // ambiguïté sur le point d'injection
    @Autowired 
    private PaymentService paymentService;
}

Dans notre exemple ci-dessus, le conteneur lance une erreur car il y a une ambiguïté sur le point d’injection. Il n’est pas capable de déterminer la dépendance à injecter. Il existe plusieurs façons d’indiquer au conteneur la bonne dépendance.

Résolution avec le nom du bean

Tout bean est référencé avec un nom unique (identifiant) dans le conteneur Spring. Lorsque deux beans ont le même nom, vous aurez une exception au démarrage.

Si aucun nom est explicitement fourni dans la configuration du bean en utilisant @Component, Spring utilise la convention suivante : le nom non qualifié de la classe, en CamelCase, en commençant par une minuscule. Il s’agit de la stratégie par défaut pour nommer un bean. Cette stratégie est modifiable dans la configuration du conteneur pour un besoin précis.

Le bean VisaPaymentService est automatiquement nommé « visaPaymentService » par le conteneur. L’annotation @Qualifier permet d’indiquer au conteneur le nom du bean à injecter.

@Component
public class VisaPaymentService implements PaymentService {
}

public class PaymentResource {
    @Autowired
    @Qualifier("visaPaymentService")
    private PaymentService paymentService;
}

Nous avons la possibilité de nommer explicitement un bean avec l’attribut value de @Component. On peut également le spécifier avec les annotations @Service, @Repository, @Controller, @RestController, etc.

@Component("visa")
public class VisaPaymentService implements PaymentService {
}

public class PaymentResource {
    @Autowired
    @Qualifier("visa")
    private PaymentService paymentService;
}

Cette solution de résolution n’est pas satisfaisante à cause de l’utilisation des chaînes de caractères pour identifier un bean. Elle est beaucoup plus sensible aux erreurs de frappes et aux modifications de code.

Injection avec l’annotation @Qualifier

Pour garder le typage fort, Spring permet de définir de nouveaux types Java avec l’annotation @Qualifier. Pour identifier un bean injecté sans spécifier son nom, nous allons définir des annotations spécifiques en utilisant @Qualifier.

Notre exemple définit deux implémentations de l’interface PaymentService, une pour les paiements par carte MasterCard et une autre pour les paiements par carte Visa. Nous définissons les annotations @MasterCard et @Visa pour chaque implémentation.

@Target({FIELD, TYPE, METHOD, PARAMETER})
@Retention(RUNTIME)
@Qualifier
public @interface MasterCard {
}

@Target({FIELD, TYPE, METHOD, PARAMETER})
@Retention(RUNTIME)
@Qualifier
public @interface Visa {
}

Les implémentations de PaymentService sont annotées avec ces deux annotations @Qualifier. MasterCardPaymentService est annotée avec @MasterCard et VisaPaymentService est annotée avec @Visa. Ces deux annotations sont utilisées par Spring pour identifier les implémentations de notre interface, la résolution de nos beans se fait maintenant par typage-fort.

@Component
@MasterCard
public class MasterCardPaymentService implements PaymentService {
}

@Component
@Visa
public class VisaPaymentService implements PaymentService {
}

Il suffit d’utiliser l’annotation @Autowired et votre propre annotation @Qualifier au niveau du point d’injection pour injecter la dépendance.

public class PaymentResource {
   	@Autowired
   	@Visa
   	private PaymentService visa;

   	@Autowired 
   	@MasterCard
   	private PaymentService masterCard;
}

Comme vous pouvez le voir, la résolution sur la dépendance à injecter est faite sans utiliser de chaînes de caractères. On dispose ici du typage fort grâce à l’utilisation des annotations via @Qualifier, d’où le terme de « type-safe binding ».

Spring permet également de combiner plusieurs annotations @Qualifier pour un même bean. Il est obligatoire de déclarer toutes les annotations au niveau de notre point d’injection.

@Target({FIELD, TYPE, METHOD, PARAMETER})
@Retention(RUNTIME)
@Qualifier
public @interface CreditCard {
}

@Component
@CreditCard @Visa
public class VisaPaymentService implements PaymentService {
}

public class PaymentResource {
   	@Autowired 
  	@CreditCard @Visa
   	private PaymentService visa;
}

Support de la JSR 330

Spring supporte la JSR 330 depuis sa version 3.0. Il s’agit d’une spécification Java EE proposant une API légère pour l’injection de dépendances et constituée de 6 annotations.

Ci-dessous est présenté l’adaptation de notre exemple précédent en utilisant les annotations de cette spécification. L’annotation @Qualifier devient @javax.inject.Qualifier, @Component devient @Named et @Autowired devient @Inject. En dehors de l’adaptation de ces annotations, le reste du code reste identique à l’exemple précédent.

@Target({FIELD, TYPE, METHOD, PARAMETER})
@Retention(RUNTIME)
@javax.inject.Qualifier
public @interface MasterCard {
}

@Target({FIELD, TYPE, METHOD, PARAMETER})
@Retention(RUNTIME)	
@javax.inject.Qualifier
public @interface Visa {
}

@Named
@MasterCard
public class MasterCardPaymentService implements PaymentService {
}

@Named
@Visa
public class VisaPaymentService implements PaymentService {
}

public class PaymentResource {
   	@Inject 
  	@MasterCard
   	private PaymentService masterCard;

   	@Inject 
  	@Visa
   	private PaymentService visa;
}

Conclusion

Cette fonctionnalité nous permet d’utiliser des annotations à la place des chaînes de caractères pour injecter nos dépendances. C’est une solution plus sûre en terme de typage. Elle apporte plus de sémantiques dans l’application. On ne se fie pas seulement à des chaînes de caractères, ce qui réduit les erreurs de frappes. Le but est d’avoir un couplage faible entre nos classes tout en conservant un typage fort.

Les exemples de code sont disponibles à l’adresse https://github.com/juliensadaoui/ippevent2015-youngblood