Annotations avec Spring

Depuis le JDK 5, en 2004, les annotations permmettent d’attacher des méta-données aux classes, interfaces et membres. Avec Spring Framework, elles permettent de paramétrer les composants métier, et de configurer les composants techniques.

Dans sa version initiale, cette page présentait la transition entre la configuration par XML et par annotations. C’était tout à fait pertinent en 2007, année de rédaction de la première version, et les années suivantes. Depuis, l’utilisation des annotations est devenue classique, on a donc décidé de mettre de coté l’équivalence avec XML.

Toutefois, si cette approche vous intéresse, la page initiale a été transférée, sous le titre Annotations contre XML avec Spring

Introduction

Les annotations ont été une avancée majeure de Java 5[1], surtout depuis leur exploitation dans les composants de Java EE[2] et Spring Framework[3][4].

Les annotations Java permettent de simplifier grandement les fichiers de configuration de Spring, et encore plus avec Spring Boot qui peut réduire la configuration XML à zéro grâce à la priorité de la convention sur la configuration.

Des exemples simples, basés sur une architecture en couches, illustrent l’article.

Architecture Spring en 3 couches

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 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 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.

Exemple de dépendance Spring

Déclaration d’un bean

L’objectif est, ici, de créer un bean de scope prototype de type info.jtips.spring.dao.ProductDAO.

Déclaration d’un bean Spring

Configuration

Pour que Spring Framework charge les beans via les annotations, il faut lui indiquer les paquetages à parcourir.

@Configuration
@ComponentScan("info.jtips.spring")
public class ApplicationConfiguration {
  // ...
}

Classe

Un bean peut être déclaré en annotant une classe avec @Component, ou une des annotations de stéréotype associées.

  • @Controller permet de déclarer un bean Web, c’est à dire de la couche présentation,

  • @Service permet de déclarer un bean de service, c’est à dire de la couche métier,

  • @Repository permet de déclarer un bean DAO[5], c’est à dire de la couche persistance.

Pour ces quatre annotations, le nom du bean peut être passé en paramètre. Par défaut, Spring prend le nom simple de la classe, en mettant l’initiale en minuscule.

L’annotation @Scope permet de préciser la portée du bean ; les valeurs possibles pour cette annotation sont :

  • "singleton",

    • une seule instance est créée et partagée,

    • constante ConfigurableBeanFactory.SCOPE_SINGLETON,

  • "prototype",

    • une instance est créée pour chaque demande (appel explicite de getBean(…​) ou injection),

    • constante ConfigurableBeanFactory.SCOPE_PROTOTYPE

  • "request",

    • uniquement pour les applications Web,

    • une instance est créée à chaque requête HTTP,

    • constante WebApplicationContext.SCOPE_REQUEST,

  • "session",

    • uniquement pour les applications Web,

    • une instance est créée à chaque session HTTP,

    • constante WebApplicationContext.SCOPE_SESSION,

  • "application",

    • uniquement pour les applications Web,

    • une instance est créée à chaque application Web,

    • constante WebApplicationContext.SCOPE_APPLICATION,

Par défaut, un bean est en singleton, ce qui convient à la plupart d’entre eux, surtout en architecture REST.

package info.jtips.spring.web;

@Controller
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ProductDAO {
  //...
}

On utilise l’annotation @Component uniquement pour les beans qui ne peuvent pas être classés dans une des trois couches citées.

Fabrique

Un bean peut être déclaré en annotant une méthode avec @Bean. Cette méthode retourne l’instance du bean.

Une méthode de fabrique peut aussi être annotée avec @Scope, ce qui défini la portée du bean retourné.

  @Bean
  public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    // ...

    return dataSource;
  }

On peut préciser le nom du bean à l’annotation @Bean. Par défaut, Spring prend le nom simple de la méthode.

On utilise cette technique avec des classes qui viennent de librairies tierces, pas forcément conçues pour devenir des beans Spring. Ce sera souvent le cas pour les beans techniques (DataSource, EntityManagerFactory,…​).

Les méthodes de fabrique peuvent être dans des classes de beans, mais sont généralement regroupées dans des classes dédiées, annotées par @Configuration.

Injections

L’injection d’un bean dans un autre bean peut se faire de 3 façons :

  • par constructeur (celle qui est recommandée),

  • par le champ (la plus classique),

  • par setter (la plus ancienne) ou

  • par getter (celle que j’aime le moins).

Exemple d’injection Spring

L’annotation courante est @Autowired, mais nous allons voir que ce n’est pas la seule, et même qu’il n’est plus forcément nécessaire d’utiliser une annotation.

Injection directe

L’injection par annotation peut se faire directement sur un champ. Elle peut se faire même si le champ est privé.

La principale annotation pour ça est @ Autowired. Elle exploite le mécanisme d'autowiring par type, c’est à dire que Spring va chercher un bean dont le type est compatible avec le champ. Si plusieurs beans sont éligibles à l’injection, Spring recherche une correspondance entre le nom du champ et le nom du bean.

 public class ProductDao {

   @Autowired
   private DataSource dataSource;

   // ...
 }

Spring peut aussi exploiter l’annotation @Resource. Cette annotation n’est pas spécifique à Spring. Elle est dans le package javax.annotation qui vient du JDK.

 public class ProductDao {

   @Resource
   private DataSource dataSource;

   // ...
 }

Comment choisir entre @Autowired et @Resource ?

En général, on utilise @Autowired partout, par fainéantise soucis de simplicité. Si on veut faire une distinction, elle doit être essentiellement sémantique.

En effet, @Resource est une annotation standard, du package javax.annotation. 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.

Résolution des conflits

Que ce soit avec l’annotation @Autowired ou @Resource, Spring tente un cablage automatique. Si ça ne fonctionne pas, Spring lève une exception.

  • org.springframework.beans.factory.NoSuchBeanDefinitionException s’il ne trouve pas de bean à injecter,

  • org.springframework.beans.factory.NoUniqueBeanDefinitionException s’il en trouve plusieurs sans pouvoir trancher.

La première solution pour résoudre ce type de conflit est de désigner un bean comme étant prioritaire, avec l’annotation @Primary.

  @Bean @Primary
  public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    // ...

    return dataSource;
  }

La deuxième solution est d'exclure un bean de l’injection. Ça ne peut se faire que sur une méthode de fabrique. Ainsi, le bean ne sera utilisable que dans les autres fabriques de la même classe par appel direct de la méthode.

L’injection d’un bean par appel direct de la méthode est utilisable uniquement dans une classe de configuration, annotée par @Configuration.
  @Bean(autowireCandidate = false)
  public DataSource dataSource() {
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    // ...

    return dataSource;
  }

  @Bean
  public LocalContainerEntityManagerFactoryBean emf() {
    LocalContainerEntityManagerFactoryBean emf
      = new LocalContainerEntityManagerFactoryBean();
    emf.setDataSource(dataSource());

    return emf;
  }

La meilleure solution est en général la qualification de la dépendance, en précisant le nom du bean à injecter.

On a vu qu’avec @ Autowired, Spring cherche d’abord une correspondance de type et résoud un éventuel conflit grâce au nom du champ. Si le nom n’aide pas à la résolution, on peut ajouter une annotation @Qualifier pour choisir explicitement le nom du bean.

 public class ProductDao {

   @Autowired @Qualifier("dataSource")
   private DataSource ds;

   // ...
 }

L’annotation @Resource offre un peu plus de possibilités. Elle peut prendre un argument name, qui injecte un bean nommé explicitement. Avec l’argument mappedName, Spring recherche le composant à injecter dans le registre JNDI.

Toutes ces solutions s’appliquent pour résoudre un conflit entre plusieurs beans actifs en même temps. Elles excluent le cas des beans qui ne sont actifs que pour un profil.

Injection par constructeur

Pour l’injection par constructeur, le bean principal doit avoir un constructeur avec un argument qui correspond au bean à injecter. Dans l’exemple suivant, on injecte le bean “courseDao” dans un autre bean.

public class ProductService {

  private ProductDao dao;

  public ProductService(ProductDao injectedDao) {
    this.dao = injectedDao;
  }

  //...
}

L’injection fonctionne avec le code ci-dessus. En effet, depuis Spring 4.3, l’annotation @Autowired peut être omise pour l’injection par constructeur, à condition que la classe n’en ait qu’un seul.

Dans tous les cas, on peut mettre une annotation @Autowired sur le constructeur, et @Qualifier sur les paramètres.

 public class ProductDao {

   @Autowired
   public ProductDao(@Qualifier("dataSource") DataSource ds) {
     //...
   }

   // ...
 }

Transactions

La gestion des transactions dans Spring permet de gérer et de propager les transactions entre les méthodes des beans déclarées comme transactionnelles. 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.

Gestionnaire de transactions

Il faut déclarer un gestionnaire de transactions (PlatformTransactionManager) adapté à notre mode de persistance

Dans notre exemple, il s’agit de JDBC via une datasource.

  @Bean
  public PlatformTransactionManager transactionManager() {
    return new DataSourceTransactionManager(dataSource());
  }

Si on avait utilisé JPA, on aurait eu ceci :

  @Bean
  public PlatformTransactionManager transactionManager() {
    return new JpaTransactionManager(emf().getObject());
  }

Configuration

Pour que les annotations transactionnelles soient prisent en compte, il faut activer en la gestion sur une classe de configuration.

@Configuration
@EnableTransactionManagement
public class ApplicationConfiguration {

  @Bean
  public PlatformTransactionManager transactionManager() {
    return ...;
  }

  // ...
}

Classe

L’activation du mode transactionnel pour une classe est simple puisqu’il suffit 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 ProductService, 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 de lecture que la transaction est readOnly.

package info.jtips.spring.service;

@Service
@Transactional
public class ProductService {

  @Transactional(readOnly=true)
  public List<Product> getAll() {
    return dao.findAll();
  }

  //...
}

Pour la classe ProductDAO, on déclare que la classe est transactionnelle, en précisant le mode de propagation (MANDATORY).

package info.jtips.spring.dao;

@Repository
@Transactional(propagation=Propagation.MANDATORY)
public class ProductDao {

  @Transactional(readOnly=true)
  public List<Product> findAll() {
    //...
  }

  //...
}

JSR-330

Sous le nom barbare de JSR-330 se cache un ensemble d’annotations standards dédiées aux mécanismes d’injection. Elles sont intégrées dans Java EE, utilisée avec CDI, mais peuvent aussi être utilisées de façon autonome, avec des frameworks comme Google Guice ou Spring Framework.

@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("productDao")
public class ProductDAO {
  //...
}

En cas d’omission du nom, la valeur par défaut sera le nom simple de la classe, avec une initiale en minuscule.

@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.

@Named
public class ProductService {

  @Inject
  private ProductDAO 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.

public class ProductService {

  @Inject @Named("productDao")
  private ProductDAO dao;

  // ...
}

Intérêt

On pourrait trouver intéressant d’utiliser un jeu d’annotations standards. Et bien NON !

L’intérêt est faible parce que la spécification du standard est famélique. Seule la syntaxe y est définie, sans aucune sémantique.

Le résultat est qu’entre CDI et Spring, ces annotations ne s’utilisent pas de la même façon. Un standard avec de telles variations n’a que peu d’intérêt.

Conclusion

Voici la liste des annotations vues dans cette page :

  • Configuration

    • @Configuration

    • @EnableTransactionManagement

  • Déclaration d’un bean

    • @Component, @Repository, @Service et @Controller

    • @Bean

    • @Named

  • Injection

    • @Autowired, @Qualifier

    • @Resource

    • @Inject

  • Transaction

    • @Transactional

Comme souvent, Spring Framework propose plusieurs solutions pour la même chose. J’ai pris l’habitude d’en exploiter le minimum viable. En l’occurence, j’utilise @Component et ses stéréotypes pour la déclaration des beans métier, @Bean pour la déclaration des beans techniques et @Autowired pour l’injection. @Qualifier n’est généralement utile que pour des cas particuliers, et plus spécifiquement pour des beans techniques.

Dans cette page, on n’a vu que les annotations principales, pour la déclaration des beans et leur injection, ainsi que pour les transactions. D’autres familles d’annotations doivent être approfondies :

Pour toute cette page, on a utilisé Spring Framework 5, sans l’apport de Spring Boot. Le code des exemples est disponible sur le compte GitLab de JTips.


1. JDK 5 est sorti en septembre 2005
2. Java EE 5 est sorti en 2007
3. Spring Framework 2.5 est sorti en 2007
4. Spring Framework 3.0 est sorti en 2009
5. Data Access Object