La DI avec Spring dans un environnement SE peut se faire sans XML

Spring Après avoir gouté aux joies de la DI (Dependancy Injection) de Spring via annotations, je n’en peux plus me passer.
Seulement, je n’ai fait celà que dans un contexte Web, et plus précisément avec JSF.
Dans un tel environnement, on est en mode managé, c’est à dire qu’on crée rien à la main, et le tout est configurable, ce qui permet à Spring de s’intégrer parfaitement dans l’equation, en offrant d’une manière totalement transparente la DI.

Par contre, dans un environnement standard (SE), les choses ne sont pas aussi simples …

En effet, dans un tel environnement, c’est à la charge du développeur de créer ses classes. Or, si on veut faire bénéficier ces classes de DI, il faut que Spring le fasse.
Plus précisément, le point qui pose problème ici est le point final qui consomme les dépendances.
C’est à dire, que si on a les 3 classes suivantes:

  • model.dao.TestDao: représente un DAO fictif
  • model.service.TestService: représente un service fictif, et ayant une dépendance vers TestDao
  • Et enfin, une classe client.TestConsumer qui a une dépendance vers TestService

Et voici leur code:

model.dao.TestDao:

package model.dao; 
@Repository 
public class TestDao { 
  //Une méthode fictive qui ne fait que retourner 10 ... impressionnant ! 
  public int getValue(){ 
    return 10; 
  } 
} 

model.service.TestService:

package model.service; 
@Service 
public class TestService { 
  @Resource 
  private TestDao testDao; 
  //Une méthode fictive qui ne fait que retourner le double 
  //  de ce que retourne le DAO ... impressionnant ! 
  public int getValue() { 
    return 2*testDao.getValue(); 
  } 
} 

client.TestConsumer:

package client; 
@Component 
public class TestConsumer { 
  @Resource 
  private TestService testService; 
  //Une méthode fictive qui ne fait que retourner ce que retourne le service 
  public int getValue() { 
    return testService.getValue(); 
  } 
}

Rien de spécial à noter, seulement ques ces classes sont annotés avec @Repository, @Service et @Component pour les déclarer comme mangés par Spring, et que les dépendances sont déclarés via l’annotation @Resource (la classe TestService a besoin d’un TestDao, et Testconsumer a besoin d’un TestService).

Et voici le fichier de configuration applicationContext.xml:

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:context="http://www.springframework.org/schema/context" 
  xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd 
    http://www.springframework.org/schema/context  
    http://www.springframework.org/schema/context/spring-context.xsd"> 
    
  <context:component-scan base-package="model,client" /> 
  
</beans>

On déclare juste à Spring que les classes des packages model et client (ainsi que leurs fils) doivent être scannés pour vois si elles sont annotés avec un des stéréotypes (@Repository, @Service, @Controller, @Componenet), et si elle le sont, elle seront ajoutés en tant que Spring Beans.

Pour que la DI marche correctement, il faut obligatoirement que Spring ait la main sur la classe finale que l’utilisateur va manipuler, c’est à dire TestConsumer.

La solution directe à ce problème serait de créer à la main une instance d’ApplicationContext, et de récupérer depuis elle une instance de TestConsumer, ce qui donne:

public class Test { 
 
  public static void main(String[] args) { 
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext( 
        "applicationContext.xml", Test.class); 
    TestConsumer testConsumer = (TestConsumer) applicationContext 
        .getBean("testConsumer"); 
    System.out.println(testConsumer.getValue()); 
  } 
}

Ce qui afficherait 20 à la console. Génial !
Seulement, c’est pénible à mourir à écrire … imaginer une application Swing avec des centaines de classes (et de dépendances) avec du code pareil … Pas beau, pas beau du tout.

C’est pour celà que j’ai consacré dun peu de temps à jouer avec l’API et la documentation de Spring pour voir comment faire pour rendre la chose moins pénible.
Et je viens de pondre un truc qui pourrait en effet régler ça ! Merci l’API super pensé et ouvert de Spring.

Voici le code de la bête:

package spring.di.in.se.can.be.easy; 
 
import org.springframework.context.ApplicationContext; 
import org.springframework.context.support.ClassPathXmlApplicationContext; 
 
public class Injector { 
  private static ApplicationContext applicationContext; 
 
  public static void init(String... configLocations) { 
    applicationContext = new ClassPathXmlApplicationContext(configLocations); 
  } 
 
  public static void init(Class clazz, String... configLocations) { 
    applicationContext = new ClassPathXmlApplicationContext( 
        configLocations, clazz); 
  } 
 
  public static void configure(Object object) { 
    applicationContext.getAutowireCapableBeanFactory().autowireBean(object); 
  } 
}

Super simple comme vous le notez: juste deux initialiseurs (pas constructeurs, vu qu’on est dans le domaine statique là) et une méthode configure qui prend un objet comme paramètre et qui est composé d’une seule ligne de code.

Maintenat, ce qui compte est de montrer comment l’utiliser.
Pour revenir à l’exemple précédent, on va pouvoir se débarasser du code superflu de l’initialisation de l’applicationContext ainsi que du cast.
Voici ce que ça donne:

public class Test { 
 
  public static void main(String[] args) { 
    Injector.init(Test.class, "applicationContext.xml"); 
    TestConsumer cl = new TestConsumer(); 
    System.out.println(cl.getValue()); 
  } 
}

Vous remarquerez que l’on peut maitnenat créer à la main (avec new) les classe consommatrices de dépendances (TestConsumer).
Il faut juste initialiser Injector au tout début de la méthode main avec le chemin de fichier xml de configuration de Spring.

Cependant, il faut aussi modifier la classe qui consomme les dépendance (Testconsumer), comme ceci:

public class TestConsumer { 
  @Resource 
  private TestService testService; 
 
  public TestConsumer() { 
    Injector.configure(this); 
  } 
 
  public int getValue() { 
    return testService.getValue(); 
  } 
}

Ce qui a changé est:

  • Je n’ai plus à la déclarer en tant que composant Spring (plus d’annotation @Component au niveau de la classe) mais je garde les déclarations de dépendances via @Resource.
  • Dans le constructeur, j’invoque la méthode configure de la classe Injector en lui passant this (instance courante) comme paramètre.

Et c’est tout ! Le tour est joué.
Ca induit un lien de ses classes avec la classe Injector, et ça impose de ne pas oublier d’appeler Injector.init(this) dans chaque constructeur, mais c’est beaucoup plus léger et agréable que de passer par l’ApplicationContext de Spring.

En espérant que vous trouverez ceci utile ;)

(Suite à la remarque d’evenisse, ceci ne fonctionne qu’avec Spring 2.5.2).

Advertisements

3 Responses to La DI avec Spring dans un environnement SE peut se faire sans XML

  1. clincks says:

    Bonjour,

    j’ai lu avec intérêt cet article.
    Une chose me gène cependant…

    Dans l’exemple que vous donnez… on utilise des références vers des classes et non plus vers des interfaces comme j’ai pris l’habitude de faire.

    –> L’injection de dépendance avait comme énorme avantage de injecter n’importe quelle classe pour peu qu’elle respecte l’interface.

    Très utile pour faire des test notamment.
    On fait un grand pas en arrière en mettant en dur la classe finale.

    Qu’en pensez-vous?

    Merci

    clincks

    • jawher says:

      Bonjour et merci pour votre commentaire.

      Le point que vous soulevez est valide. Cependant, le fait que je sois passé par des classes directement et non pas par des interfaces n’est pas une limitation de la technique montrée dans ce billet, mais plutôt un raccourci que j’avais pris, histoire de garder les exemples simples et courts.

      Dans un projet réel et/ou plus complexe, je n’aurais pas fait dépendre directement mes classes clientes aux classes d’implémentation, et j’aurais introduit des interfaces entre elles.

  2. clincks says:

    jawher :
    Bonjour et merci pour votre commentaire.
    Le point que vous soulevez est valide. Cependant, le fait que je sois passé par des classes directement et non pas par des interfaces n’est pas une limitation de la technique montrée dans ce billet, mais plutôt un raccourci que j’avais pris, histoire de garder les exemples simples et courts.
    Dans un projet réel et/ou plus complexe, je n’aurais pas fait dépendre directement mes classes clientes aux classes d’implémentation, et j’aurais introduit des interfaces entre elles.

    Bonjour,

    Pourriez-vous alors m’expliquer comment cela fonctionne si dans le projet on a plusieurs classes qui implémente la même interface?
    –> Votre technique me plait… mais je ne vois pas comment résoudre ce problème.

    J’ai déjà réfléchi au problème… j’avais pensé un moment ajouter une annotation me permettant de spécifier un “contexte” en paramètre. Cela me permettrait de sélectionner une implémentation en jouant sur un paramètre de l’annotation.
    Mais je trouve cela pas toute à fait élégant… et surtout intrusif.

    Note concernant votre exemple de code…
    Il serait sans doute possible (j’ai pas essayé) de ne pas du tout changer l’implémentation de la classe TestConsumer en héritant de celle-ci:

    01 public class TestConsumerExt extend TestConsumer {
    02
    03 public TestConsumerExt() {
    04 Injector.configure(this);
    05 }

    Cela allourdi un peu le code… d’un autre côté… on n’est pas intrusif.
    Une autre amélioration serait sans doute de remplacer le code Injector.configure(this); par une annotation qui vous serait propre.
    A mettre au niveau de la déclaration de la classe elle même.
    –> Avantage… si un autre développeur ajoute un constructeur… il risque d’oublier dans ce constructeur l’appel à l’Injecteur.

    Mais bon… là on rentre dans des pratiques dont cet articles ne voulait sans doute pas couvrir.

    Merci pour le concept en tout cas… je vais surement m’en servir si vous me montrer comment définir l’implémentation qui doit être utilisée lors du tissage.

    Bonne journée.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: