Configuration de Spring v3: Trois modes

L'objet de ce billet est de décrire les trois modes de configuration de Spring, version 3+.
La première manière de configurer Spring est centrée sur XML.
La seconde, depuis la version 2, est centrée sur les annotations. Chose est sûre, elle exige de la rigueur.
Enfin, la troisième détaillée ci-dessous, est centrée sur du code java avec les annotations @Configuration et @Bean.

Une petite démo, sans aucune configuration xml, permet de voir comment réaliser la configuration centrée-java de Spring (java-centric or based java configuration ).
Le débat dans la communauté sur la meilleure approche est toujours aussi vif.
Il me semble qu'avec ce nouveau mode, le développeur de Spring est gagnant car il aura plus de choix, de contrôle et de liberté.

La nouvelle approche centrée-java offre des avantages :

  • La configuration de Spring est isolée dans une ou plusieurs classes spécifiques annotées et non mélangées avec celles de métier,
  • Le compilateur veille à l'unicité des id des beans (il est recommandé de ne pas utiliser l'attribut name de l'annotation @Bean).
  • La possibilité de mixer les 3 modes de configuration et le tout peut être géré par le code java.

Passons à la pratique

Cette première démonstration est assez complète réalisée sous eclipse (windows) et java6.

Commencer par créer sous eclipse ou STS (Springsource Tool Suite) votre projet java standard.
Puis configurer le build path du projet avec les 8 librairies comme suit:

spring-beans-xxx.jar
spring-core-xxx.jar
spring-context-xxx.jar
spring-asm-xxx.jar
spring-expression-xxx.jar
cglib-nodep-2.2.jar
commons-logging-1.1.1.jar
JUnit-4.8.jar

Où xxx est la version par exemple, 3.1.1.RELEASE.

Sinon, vous pouvez créer un projet maven avec les dépendances suivantes:


<!--  Spring -->
<dependency>
	<groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
	<version>3.1.1.RELEASE</version>
</dependency>


<!-- CGLIB (required for processing @Configuration classes -->
 <dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib-nodep</artifactId>
  <version>2.1_3</version>
</dependency>

La démo présente un client test Junit4 qui appelle la méthode userInfos() de la seule classe service.
Or, la méthode userInfos() à son tour appelle la méthode getUserInfos() de la seule classe dao UserDao puis enrichit la réponse en ajoutant la date.

La démo est détaillée en 7 étapes. Seule l'étape 5 présente un intérêt majeur car le reste est du code java normal.
Le projet de démo ne contient aucune configuration xml.

Step1- Écrire le code de l'interface IUserDao:

	
package fr.netapsys.blog.dao;
public interface IUserDao {
	String getUserInfos();
}

Step2- Écrire une implémentation de IUserDao:

package fr.netapsys.blog.dao;
public class UserDao implements IUserDao {
  public String getUserInfos() {
     //simulation acces bd
     return "<dao/>";
  }
}

NOTE:
Aucune annotation de Spring n'est utilisée dans cette classe ni dans les classes d'implémentation à venir.

Step3- Écrire l'interface IUserService:

package fr.netapsys.blog.services;
public interface IUserService {
  String  userInfos();
}

Step4- Écrire UserService une implémentation de IUserService:

   
package fr.netapsys.blog.services;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import fr.netapsys.blog.dao.IUserDao;

public class UserService implements IUserService{
  private IUserDao dao;
  public UserService() {}
  public UserService(IUserDao dao) {
    this.dao = dao;
  }
  public String userInfos(){
    final  Date date=Calendar.getInstance().getTime();
    return dao.getUserInfos() + " date:"+
            SimpleDateFormat.getInstance().format(date) ;
  }
 public void setDao(IUserDao dao) {
	this.dao = dao;
 }
}

NOTE:Idem, aucune annotation de Spring n'est utilisée.

Step5- Écrire une classe java de configuration de Spring nommée AppConfig.java:

package fr.netapsys.blog.cfg.spring3;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import fr.netapsys.blog.dao.IUserDao;
import fr.netapsys.blog.dao.UserDao;
import fr.netapsys.blog.services.IUserService;
import fr.netapsys.blog.services.UserService;

@Configuration
public class AppConfig {
  @Bean 
  public IUserService userService() {
    return new UserService( dao());
  }
  @Bean
  public IUserDao dao(){
     return new UserDao();
  }
}

NB:
Cette classe utilise les interfaces et les implémentations ce qui est normal car c'est elle
qui permet de configurer Spring et son IoC/DI (Inversion of Control / Dependency Injection).

Step6- Écrire une classe JUnit :

package fr.netapsys.blog.tests;
import junit.framework.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import fr.netapsys.blog.cfg.spring3.AppConfig;
import fr.netapsys.blog.services.IUserService;

public class TestBlogDemoCfgCCentricClazz {
  private static ApplicationContext ctx;
  @BeforeClass
  public static void avantTests() {
   // create the spring container using AppConfig.class
    ctx = new AnnotationConfigApplicationContext(AppConfig.class);
  }
  @Test
  public void testCfgClazz() {
    IUserService service=ctx.getBean("userService",IUserService.class);
    Assert.assertTrue(service.userInfos().contains("<dao/>"));
  }
}

Quelques eplications:
L'attribut ctx est initialisé avec l'implémentation AnnotationConfigApplicationContext qui prend en argument la ou les classes annotées avec @Configuration.
Les méthodes, annotées avec @Bean, de cette(ces) classe(s) définissent justement les arguments à passer à la méthode getBean dont la signature est:

 T getBean(String, Class<T>) 

D'autres précisions sont données ci-dessous.

Step7- Exécution du test unitaire:

Sous eclipse, lancer le test avec un clic-droit sur la classe JUnit->Run As->JUnit Test Case, la barre verte s'affiche car service.userInfos contient la chaîne <dao/>.

CONCLUSION:
A l'étape 5, nous avons vu l'annotation @Configuration sur la classe AppConfig.
C'est ce qui fait que cette classe est spécifique.
Elle est spécifique car la présence des annotations @Configuration et @Bean font qu'elle est destinée à configurer Spring.
Néanmoins AppConfig est un bean comme les autres dans Spring. Et en tant que bean elle profite de tous les avantages de Spring
Les méthodes annotées avec @Bean permettent d'indiquer à Spring comment construire les instances.
Par exemple, le morceau de code :

 @Bean 
 public IUserService userService() {
    return new UserService( dao());
 }

présente une méthode nommée userService, c'est aussi l'id du bean dans le conteneur Spring.
Cette méthode fait appel au constructeur de UserService prenant un argument IUserDao.
Justement la méthode dao() annotée également avec @Bean renvoie le bean dao.
Ainsi, au lancement, Spring (pré)configure les instances userService et dao (exactement les noms des méthodes annotées avec @Bean).
C'est la raison pour laquelle la ligne suivante (Cf. test JUnit de l'étape6) :

 IUserService service = ctx.getBean("userService",IUserService.class);

emploie la méthode T getBean(String, Class<T>) du factory ApplicationContext (plus précisément BeanFactory).
Celle-ci permet de s'en passer du cast puisqu'il suffit de fournir les deux arguments de getBean: l'id du bean et son type de classe.
Dans notre cas : "userService" et IUserService.class
Le mode de configuration centrée-java autorise de mieux configurer/contrôler le conteneur dans des situations particulières où un certain ordre est utile.
Et aussi les exceptions levées liées à l'autowiring (cas byType) lorsqu'il existe plusieurs implémentations pour une même interface sont de facto résolues.

Prochainement, d'autres présentations sur les thèmes suivants:
Un projet où les trois modes de configuration cohabitent et sont bien gérés par l'approche centrée-java (java-centric).
Le langage SpEL de Spring (Spring Expression Langage).
La nouvelle annotation @Profile.

Un commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

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