Annotations avec Spring

ou comment développer une application avec Spring à l'aide d'annotations ?


Les annotations ont été une avancée majeure de Java 5, surtout depuis leur exploitation dans les composants de JavaEE 5, tels que les EJB 3 et JSF 1.2. Cette technique a été intégrée dans Spring Framework 2.5, à condition d'utiliser une JVM 5 ou ultérieure ; elle a été enrichie avec des annotations standard en Spring 3.

Les annotations Java permettent de simplifier grandement les fichiers de configuration de Spring et de séparer plus nettement les beans techniques (DataSource, TransactionManager,...), qui restent configurés en fichier XML, des beans fonctionnels pour lesquels les annotations apportent des simplifications.

Dans cet article, nous parcourons les principales annotations de Spring et nous comparons leur usage par rapport à la technique de configuration traditionnelle. Des exemples simples, basés sur une architecture en couches, illustrent l'article.

Spring3Couches.png

Introduction

L'exemple qui illustre cet article est composé d'une couche de service et d'une couche de persistance. La couche de service est constituée d'une interface et d'une classe, sur laquelle nous grefferons une gestion des transactions et une gestion des traces par AOP. La couche de persistance est composée d'une interface et d'une classe qui accède à une base de données par une datasource. Nous ne verrons pas le détail des classes, mais nous concentrerons sur leur configuration.

SpringExemple.png


Déclaration d'un bean

L'objectif est, ici, de créer un bean de scope prototype de type fr.sewatech.university.dao.CoursDaoDS.

SpringBean.png

Sans annotation

Dans une configuration traditionnelle, ce bean doit être déclaré dans le fichier de configuration (applicationContext.xml pour notre exemple).

 <beans ...>
   <bean id="courseDao"
         class="fr.sewatech.university.dao.CourseDaoDS" scope="prototype" />
 </beans>

Configuration

Pour que Spring Framework charge les beans via les annotations, il faut lui indiquer, dans son fichier de configuration, les paquetages à parcourir ; Spring parcourra tous les sous-packages de celui qui est indiqué. On notera le préfixe "context", pour lequel il faut déclarer le schéma (xsd).

 <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-2.5.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
   <context:component-scan base-package="fr.sewatech"/>
 </beans>

Classe

L'annotation @Service permet de déclarer un bean de service, c'est à dire de la couche métier, son nom étant passé en paramètre. Cette valeur est d'ailleurs le seul paramètre de cette annotation. L'annotation @Scope permet de préciser la portée du bean ; les valeurs possibles pour cette annotation sont "singleton" et "prototype", pour tous les types d'applications, ainsi que "request" et "session" pour les applications Web.

L'annotation @Repository s'utilise de la même façon, pour les bean DAO (Data Access Object) de la couche de persistance.

 // CourseDaoDS.java
 package fr.sewatech.university.dao;
 
 import org.springframework.stereotype.Service;
 
 @Repository("courseDao")
 @Scope("prototype")
 public class CourseDaoDS implements CourseDao {
   //...
 }

Le bean déclaré ici est exactement le même que celui du fichier XML ci-dessus : son id est courseDao et sa portée est prototype.

Conflit entre une annotation et une configuration

Si le même bean (même id) est déclaré à la fois via une annotation et un fichier XML de contexte, c'est la configuration du fichier XML qui est prise en compte, quel que soit l'ordre de déclaration dans le fichier XML.

Ainsi, dans l'exemple ci-dessous, le bean courseDao est un singleton, bien que l'annotation @Scope indique "prototype".

 <beans ...>
   <context:component-scan base-package="fr.sewatech" />
   
   <bean id="courseDao"
       	class="fr.sewatech.university.dao.CourseDaoDS" 
       	scope="singleton" />
 </beans>


Cette priorité donnée à la configuration facilite les tests unitaires, en particulier lorsque des beans sont déclarés en scope "session" ou "request".

Injections

L'injection d'un bean dans un autre bean peut se faire de 3 façons : par setter (la plus classique), par constructeur ou par getter (celle que j'aime le moins).

SpringInjection.png

Sans annotation

Pour l'injection par setter, le bean principal doit avoir un attribut avec la méthode set qui correspond. Dans notre exemple ci-dessous, on injecte une datasource dans le bean "courseDao".

 // CourseDaoDS.java
 public class CourseDaoDS implements CourseDao {
   private DataSource dataSource;
   
   public void setDataSource(DataSource dataSource) {
     this.dataSource = dataSource;
   }
 }

L'injection se paramètre avec le bean, via une propriété, en faisant référence à un bean existant (déclaré dans un autre portion du fichier ou dans un autre fichier).

 <beans ...>
   <bean id="courseDao"
         class="fr.sewatech.university.dao.CourseDaoDS" scope="prototype">
     <property name="dataSource" ref="dataSource" />
   </bean>
 </beans>

Pour l'injection par constructeur, le bean principal doit avoir un constructeur avec un argument qui correspond à l'attribut à initialiser. Dans l'exemple suivant, on injecte le bean "courseDao" dans un autre bean, "courseService".

 // CourseServiceImpl.java
 public class CourseServiceImpl implements CourseService {
   private CourseDao dao;
   
   public CourseServiceImpl(CourseDao realDao) {
     this.dao = realDao;
   }
   //...
 }

La configuration pour ce nouveau bean, avec l'injection est la suivante :

 <beans ...>
 <bean id="courseService"
       class="fr.sewatech.university.service.CourseServiceImpl">
   <constructor-arg ref="courseDao" />
 </bean>

Configuration

L'utilisation des annotations pour l'injection est indépendante des annotations de déclaration de bean. Ces annotations fonctionnent avec des beans déclarés en fichier XML ou déclarés par annotations.

 <beans ...>
   <context:annotation-config/>
 </beans>

Classe

L'injection par annotation peut se faire directement sur un champ ou sur un setter. Dans les deux cas, elle peut se faire même si le champ ou la méthode est private. Deux annotations peuvent être utilisées : @Resource, qui injecte explicitement un bean par son nom, ou @Autowire.

L'annotation @Resource prend des arguments facultatifs. Sans argument, Spring injectera le bean qui correspond à la logique d'autowiring par nom. C'est à dire qu'il recherchera un bean qui porte le même nom que le champ ou la propriété. Si aucun bean ne correspond, Spring recherchera le bean à injecter en autowiring par type. Elle peut aussi prendre un argument name, qui injecte un bean nommé explicitement, ce qui est généralement conseillée. Lorsqu'on utilise l'argument mappedName, Spring recherche le composant à injecter dans le registre JNDI.

 // CourseDaoDS.java
 public class CourseDaoDS implements CourseDao {
   @Resource(name="dataSource")
   private DataSource dataSource;
   
   // ...
 }

L'annotation @Autowire exploite le mécanisme d'autowiring par type. Il est possible de précisant, de façon facultative, le nom du bean à injecter, avec l'annotation @Qualifier. Si ce nom n'est pas précisé le mécanisme automatique va chercher le bean unique qui correspond au type attendu et si plusieurs beans sont compatibles, une exception est levée.

 // CourseDaoDS.java
 public class CourseDaoDS implements CourseDao {
   @Autowired @Qualifier("dataSource")
   private DataSource dataSource;
   
   // ...
 }

Pour l'injection par constructeur, les mêmes annotations @Autowire et @Qualifier sont utilisées respectivement sur le constructeur et sur ses arguments :

 // CourseServiceImpl.java
 public class CourseServiceImpl implements CourseService {
   private CourseDao dao;
   
   @Autowired
   public CourseServiceImpl(@Qualifier("courseDao") CourseDao dao) {
     this.dao = dao;
   }
   
   // ...
 }

Comment choisir entre @Autowired et @Resource ? La différence n'est pas que technique, elle est aussi sémantique. En effet, @Resource est une annotation standard, du package javax.annotation, disponible dans JavaEE 5 ou JavaSE 6. Son rôle est défini dans les spécifications pour l'injection de ressources, c'est-à-dire de composants gérés par le conteneur. On peut mettre dans cette catégorie les datasources et autres composants techniques. Par opposition, les composants métier devraient être injectés par @Autowired.

@Resource n'est disponible qu'avec Java6

Transactions

La gestion des transactions dans Spring permet de gérer et de propager les transactions entre les méthodes des beans déclarés comme transactionnel. Le mécanisme et le vocabulaire sont très proches de ceux des EJB. Dans l'exemple, nous souhaitons déclarer un mode de propagation "REQUIRED" pour le bean de service et "MANDATORY" pour le bean de DAO.

Sans annotation

La configuration des transaction dans le fichier XML s'appuie sur le namespace tx (à déclarer), et (depuis Spring 2.0) sur des mécanismes orientés aspects (AOP) avec le namespace aop (à déclarer aussi).

Il faut déclarer un gestionnaire de transactions (PlatformTransactionManager) adapté à notre mode de persistance (JDBC via une datasource). Puis on déclare un conseil transactionnel (<tx:advice>) qui spécifie la logique de propagation, en fonction de patterns de nommage des méthodes. Enfin, on déclare les classes transactionnelles par des configurations AOP (<aop:config>) qui associent des classes référencées par un pattern de nommage (package + nom de la classe) à l'advice.

 <beans ...>
   <bean id="txManager"
         class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
     <property name="dataSource" ref="dataSource" />
   </bean>
   
   <tx:advice id="serviceTxAdvice" transaction-manager="txManager">
     <tx:attributes>
       <tx:method name="find*" propagation="REQUIRED" read-only="true" />
       <tx:method name="*" propagation="REQUIRED" />
     </tx:attributes>
   </tx:advice>
   <tx:advice id="daoTxAdvice" transaction-manager="txManager">
     <tx:attributes>
       <tx:method name="find*" propagation="REQUIRED" read-only="true" />
       <tx:method name="*" propagation="MANDATORY" />
     </tx:attributes>
   </tx:advice>
   
   <aop:config>
     <aop:pointcut id="serviceMethods"
         expression="execution(* fr.sewatech.university.service.*Service.*(..))" />
     <aop:advisor advice-ref="serviceTxAdvice" pointcut-ref="serviceMethods" />
   </aop:config>
   <aop:config>
     <aop:pointcut id="daoMethods"
         expression="execution(* fr.sewatech.university.dao.*Dao*.*(..))" />
     <aop:advisor advice-ref="daoTxAdvice" pointcut-ref="daoMethods" />
   </aop:config>
 </beans>

Configuration

L'utilisation des annotations pour les transactions est indépendante des annotations précédentes. Elles existent d'ailleurs depuis Spring 2.0, alors que les annotations présentées jusqu'ici n'existent que depuis Spring 2.5.

Da la configation classique ne reste que le gestionnaire de transactions (PlatformTransactionManager), auquel on adjoint la déclaration de pilotage des transactions par les annotations (<tx:annotation-driven>).

 <beans ...>
   <bean id="txManager"
         class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
     <property name="dataSource" ref="dataSource" />
   </bean>
   
   <tx:annotation-driven transaction-manager="txManager"/>
 </beans>

On notera que le préfixe aop n'est plus nécessaire dans cette nouvelle configuration.

Classe

L'activation du mode transactionnel pour les classes devient beaucoup plus simple puisqu'au lieu de jongler avec des expressions régulières, il suffit maintenant d'ajouter l'annotation @Transactional, avec éventuellement quelques attributs supplémentaires. Cette annotation peut être utilisée au niveau de la classe et/ou des méthodes, pour affiner le paramétrage.

Pour la classe CourseServiceImpl, on déclare que la classe est transactionnelle, avec le mode de propagation par défaut (REQUIRED), puis on précise pour chaque méthode find que la transaction est readOnly.

 // CourseServiceImpl.java
 package fr.sewatech.university.service;
 
 @Service("courseService")
 @Transactional
 public class CourseServiceImpl implements CourseService {
   //...
   @Transactional(readOnly=true)
   public List<CourseData> findAll() throws ServiceException {
     //...
   }
   //...
 }


Pour la classe CourseDaoDS, on déclare que la classe est transactionnelle, en précisant le mode de propagation (MANDATORY), puis on précise un mode de propoagation différent pour chaque méthode find.

 package fr.sewatech.university.dao;
 
 @Repository("courseDao")
 @Scope("singleton")
 @Transactional(propagation=Propagation.MANDATORY)
 public class CourseDaoDS implements CourseDao {
   //...
   @Transactional(propagation=Propagation.REQUIRED, readOnly=true)
   public List<CourseData> findAll() throws DaoException {
     //...
   }
   //...
 }

AOP

Les annotations AOP sont aussi disponible depuis Spring 2.0.

Sans annotation

Intégrer une interception AOP consiste à créer un bean de conseil qui contient le code à exécuter, puis à déclarer par un configuration AOP (<aop:config>), les classes et méthodes concernées.

 <beans ...>
   <bean id="traceAdvice"
         class="fr.sewatech.university.utils.SysoutInterceptor" />
   <aop:config>
     <aop:aspect id="log" ref="traceAdvice">
       <aop:pointcut id="logPointcut"
                     expression="execution(* fr.sewatech.university.service.*Service.*(..))" />
       <aop:around pointcut-ref="logPointcut" method="doLog" />
     </aop:aspect>
   </aop:config>
 </beans>

Le bean d'interception doit implémenter une méthode avec un argument de type ProceedingJoinPoint.

 // Fichier SysoutInterceptor.java
 package fr.sewatech.university.utils;
 
 import org.aspectj.lang.ProceedingJoinPoint;
 
 public class SysoutInterceptor {
 	public Object doLog(ProceedingJoinPoint point) throws Throwable {
     String methodName = point.getTarget().getClass().getSimpleName() + "." + point.getSignature().getName();
     System.out.println("==> Début méthode : " + methodName);
     Object obj = point.proceed();
     System.out.println("<== Fin méthode : " + methodName);
     
     return obj;
   }
 }

Il est bien évident que l'implémentation de cette classe ne respecte pas les bonnes pratiques de développement en utilisant System.out. On se contentera de cette version pour l'exemple...

Avec annotation

Pour réaliser la même chose avec annotations, on active le moteur AOP et on déclare le bean d'advice. Aucune configuration d'AOP supplémentaire n'est nécessaire.

 <beans ...>
   <aop:aspect-autoproxy/>
   <bean id="traceAdvice"
         class="fr.sewatech.university.utils.SysoutInterceptor" />
 </beans>

La classe d'advice est implémentée avec des annotations :

 // Fichier SysoutInterceptor.java
 package fr.sewatech.university.utils;
 
 @Aspect
 public class SysoutInterceptor {
   @Around("fr.sewatech.university.utils.SysoutAspect.logPointcut()")
   public Object doLog(ProceedingJoinPoint point) throws Throwable {
     // ...
 	}
 }

Une interface de plus est nécessaire pour définir le pointcut. Cette interface définit l'aspect ; elle peut éventuellement être remplace par une classe qui ne contient pas de code.

 package fr.sewatech.university.utils;
 
 @Aspect
 public interface SysoutAspect {
   @Pointcut("execution(* fr.sewatech.university.service.*Service.*(..))")
   void logPointcut();
 }

Cas particuliers

Spring, Hibernate et les annotations

Dans le cadre de l'intégration d'Hibernate avec Spring, il peut être intéressant de faire hériter nos beans DAO de la classe org.springframework.orm.hibernate3.support.HibernateDaoSupport. Dans ce cas, il faut impérativement injecter la sessionFactory dans la propriété homonyme. Comme pour toute injection, on peut se poser la question du mode d'injection : setter ou constructeur ?

Dans notre cas la solution du setter est rapidement éléminée car l'attribut sessionFactory ou son setter devrait être défini dans notre classe de DAO ; comme ils sont définis dans la sur-classe, en final pour le setter, cette technique est impossible.

La solution de l'injection par constructeur s'impose donc.

 @Repository("courseDao")
 @Transactional
 public class CourseDaoHibernate extends HibernateDaoSupport implements CourseDao {
   @Autowired
   public CourseDaoHibernate(@Qualifier("sessionFactory") SessionFactory sessionFactory) {
     super.setSessionFactory(sessionFactory);
   }
   // ...
 }

JSR-330

Sous le nom barbare de JSR-330, ou tout simplement réservé aux initiés, se cache un ensemble d'annotations standards dédiées aux mécanismes d'injection. Elles sont intégrées dans JavaEE 6, mais peuvent aussi être utilisées de façon autonome, avec des frameworks comme Google Guice ou Spring 3.0.

@Named

L'annotation @Named sert à déclarer un bean ; elle se place sur la classe en remplacement de @Component, @Service ou @Repository. Cette annotation peut recevoir une valeur facultative qui permet de qualifier le bean en précisant son nom.

@Named("courseDao")
public class CourseDaoDS implements CourseDao {
  //...
}

En cas d'omission du nom, la valeur par défaut sera le nom simple de la classe, avec une initiale en minuscule. Ici, ce serait courseDaoDS.

@Inject

L'annotation @Inject sert à injecter un bean ; elle se place sur un champs, un constructeur ou une méthode de type setter, en remplacement de @Autowired. Comme cette dernière, @Inject fait de la résolution de bean par type.

// CourseServiceImpl.java
public class CourseServiceImpl implements CourseService {
  @Inject
  private CourseDao dao;
  
  // ...
}

Si la résolution par type est impossible, parce que plusieurs beans implémentent la même interface, l'injection peut être qualifiée par le nom.

// CourseServiceImpl.java
public class CourseServiceImpl implements CourseService {
  @Inject @Named("coursDaoJpa")
  private CourseDao dao;
  
  // ...
}