Configurez Spring en Java

logo-spring-highresDans ce billet, nous verrons comment configurer en Java le contexte Spring d’une application basée sur Spring MVC, Spring Security, Spring Data JPA et Hibernate, et cela sans utiliser la moindre ligne de XML.
Personnellement, je n’ai rien contre la syntaxe XML à laquelle j’étais habitué. D’autant la verbosité de la configuration avait considérablement diminué grâce à l’introduction des namespaces XML et des annotations. Avant d’utiliser la syntaxe Java sur une application d’entreprise, j’étais même sceptique quant aux gains qu’elle pouvait apporter. Aujourd’hui, je comprends mieux son intérêt et pourquoi les projets du portfolio Spring tels Spring Integration 4.0, Spring Web Service 2.2 ou bien Spring Security 3.2 proposent dans leur dernière version un niveau de configuration Java iso-fonctionnel avec leur équivalent XML. Sans compter que le support de la configuration Java leur ouvre la porte d’une intégration plus poussée à Spring Boot, le nouveau fer de lance de Pivotal.

Préambule

Avant de faire partie intégrante du framework Spring, la configuration Java était proposée aux développeurs Spring dans un projet externe : Spring JavaConfig. Depuis la version 3.0 du framework Spring, des fonctionnalités équivalentes ont été introduites au fil des versions. La version 4.0 voit l’aboutissement de ce travail.

Tous les extraits de code suivants sont issus d’une application web disponible sur Github : spring-javaconfig-sample.
Les dépendances maven requises sont déclarées dans le pom.xml. La toute dernière version des frameworks ont été exploitées :

  • Spring Framework 4.0
  • Spring Data JPA 1.6
  • Spring Security 3.2
  • Hibernate 4.3

Afin que cette application puisse être déployée dans un conteneur de Servlet 2.5, l’initialisation de la configuration Spring repose intégralement sur les déclarations réalisées en XML dans le web.xml. A cet effet, lors de la commande mvn clean install , un serveur Jetty 6 compatible servlet 2.5 démarre puis arrête l’application.
Dans un conteneur de Servlet 3.x, la déclaration du DispatcherServlet pourrait être réalisée en Java via la classe AbstractAnnotationConfigDispatcherServletInitializer introduite dans Spring 3.2.

Ce billet n’a pas pour vocation de se substituer au manuel de référence du framework Spring. Il ne vous montrera pas non plus l’équivalent XML de la configuration Java. Son objectif est de vous donner un exemple de configuration afin que vous puissiez monter rapidement une application utilisant la syntaxe Java.

Contextes applicatifs Spring

L’application web donnée en exemple reprend l’architecture type d’une application Spring MVC dans laquelle un WebApplicationContext parent (ou root) est chargé avec le listener ContextLoaderListener et un WebApplicationContext enfant est chargé via la DispatcherServlet.

Le contexte parent met à disposition les beans Spring de la couche service métier et de la couche de persistance (infrastructure comprise). La sécurité est également définie au niveau du contexte parent car les services métiers peuvent être sécurisés (@Secured). Chacune de ces couches étant configurée dans une classe qui lui est dédiée, la classe MainConfig a pour rôle de toutes les référencer :

L’annotation @Configuration joue un rôle central. Les classes où elle est apposée se substituent en effet aux traditionnels fichiers de configuration XML. Nous y retrouvons la déclaration de beans Spring, l’import de fichiers ou de classes de configuration, la détection automatique de beans annotés par analyse de classpath ou bien encore l’activation de fonctionnalités avancées et des annotations associées (@Transactional, @Cacheable, @Scheduled, @Async …)

Remarque : la déclaration de beans Spring peut se faire en dehors d’une classe de @Configuration. C’est ce qu’on appelle le mode lite Beans. Non recommandé, le manuel de référence de Spring explique quels en sont les limitations et les dangers.

La classe MainConfig est également annotée avec l’annotation @Import permettant d’importer d’autres classes de configuration. Via l’annotation @ImportResource, Spring offre la possibilité d’importer des fichiers de configuration XML. Très pratique lorsqu’on dépend de librairies tierces n’offrant qu’une configuration XML.

Pour Spring, les classes annotées avec @Configuration sont des beans Spring :

Cette spécificité infère aux beans de configuration la possibilité d’utiliser l’injection de dépendance via les annotations @Autowired et @Inject. Leur cycle de vie permet également d’utiliser les annotations @PostConstruct et @PreDestroy.
La classe MainConfig exploite cette possibilité pour injecter l’Environment modélisant l’environnement d’exécution de l’application. Cette interface permet notamment d’accéder aux profils Spring activés. Lors de l’initialisation de la configuration spécifiée par MainConfig, la méthode initApp() annotée avec @PostConstruct génère une trace listant les profils actifs.

Le contexte enfant est quant à lui définie au travers d’une seule classe WebMvcConfig que nous détaillerons par la suite. Cette hiérarchie de contexte permet aux beans de type @Service déclarés dans le contexte parent d’être visibles par les beans de type @Controller, l’inverse n’étant pas vrai.

Tester la configuration Spring

De par le fait qu’elle est vérifiée à la compilation, la configuration codée en Java permet d’éviter de facto les erreurs que l’on retrouvait couramment en XML : fautes de frappe, déplacement de classes, JAR non présent dans le classpath …
Ce premier garde-fou n’empêche pas d’autres erreurs. Par exemple, l’annotation @EnableCaching génère une exception lorsqu’aucun bean de type CacheManager n’a pas été déclaré :

Pour se prémunir de genre d’exceptions découvertes au démarrage de votre application, le module Spring Test propose tout un jeu d’annotations : @WebAppConfiguration, @ContextConfiguration, @ActiveProfiles ou bien encore @ContextHierarchy. Introduite avec la version 3.2.2 de Spring, cette dernière permet de reproduire une hiérarchie de contextes Spring et de tester ainsi le chargement de contextes en conditions réelles.
La classe SpringConfigTest donne un exemple de test d’intégration de configuration Spring :

DataSource

La couche de persistance reposant sur JPA, une connexion à une base de données relationnelle est nécessaire. Selon le contexte dans lequel s’exécute l’application, cette DataSource peut être récupérée de 2 manières :

  1. Par lookup JNDI lorsque l’application est déployée dans un serveur d’application JavaEE ou un conteneur web gérant ses propres ressources.
  2. Initialisée au démarrage de l’application par création d’une base de données en mémoire. Utile pour les tests d’intégration ou pour faciliter le déploiement de l’application.

Déclaré dans le bean de configuration DataSourceConfig, le bean dataSource sera par la suite injecté dans le bean de configuration InfrastructureConfig.

L’instanciation, la configuration et l’initialisation d’un bean Spring est réalisé dans une méthode annotée par @Bean. Par défaut, le nom du bean Spring est déduit du nom de sa méthode. Voici un exemple de déclaration de beans :

On retrouve une méthode de déclaration de DataSource pour chacun des 2 contextes présentés ci-dessus. Chaque méthode et annotée avec un @Profile Spring différent : javaee et test.
Le profile javaee est activé dans le web.xml :

Le profile test est quant à lui activé par l’annotation @ActiveProfiles(« test ») apposée sur la classe de test SpringConfigTest.
La méthode dataSource() utilise la fabrique de beans JndiObjectFactoryBean pour récupérer la DataSource à partir de son nom JNDI.
Le bean Environment est de nouveau utilisé. Cette fois-ci, pour récupérer la valeur de la propriété « jdbc.jndiDataSource » définie dans le fichier datasource.properties chargée à l’aide de l’annotation @PropertySource.

A noter que le type de retour de cette méthode est un JndiObjectFactoryBean. Pour retourner directement un DataSource, il aurait été nécessaire de se substituer au conteneur Spring en invoquant la méthode afterPropertiesSet() de la fabrique de beans :

En effet, appelée pendant la phase d’initialisation du bean, afterPropertiesSet() effectue le lookup JNDI. Sans cet appel, la dataSource serait null :

 Couche de persistance

Déjà amorcée par la déclaration de la source de données, la mise en œuvre de la couche de persistance est complétée par 2 autres beans de configuration :

  1. InfrastructureConfig : infrastructure JPA composée d’une fabrique du gestionnaire d’entités JPA et du gestionnaire de transactions associé
  2. RepositoryConfig : activation de Spring Data JPA et détection des beans de type Repository (DAOs)

L’annotation @EnableTransactionManagement portée par le bean InfrastructureConfig permet d’activer l’utilisation des annotations @Transactional chargées de délimiter les transactions base de données :

Récupérée par JNDI ou instanciée en mémoire, la DataSource est injectée sur le même modèle que n’importe quel bean Spring.
Viennent ensuite la déclaration des beans transactionManager et transactionTemplate :

Mettons de côté la méthode entityManagerFactory() sur laquelle nous reviendrons plus loin. Cette configuration montre comment mettre en relations 2 beans Spring : le bean transactionTemplate utilise en effet le bean transactionManager. En XML, cette mise en relation est habituellement réalisée à l’aide de la balise ref et de l’identifiant du bean. En Java, l’injection d’un bean dans un autre se fait en utilisant la méthode de déclaration du bean à injecter, en l’occurrence ici transactionManager(). Ce qui ressemble à un appel de méthode est trompeur. En effet, Spring interprète ce type d’appel afin de gérer le cycle de vie des Beans. Par exemple, lorsqu’un bean est de portée singleton, la méthode de création du bean n’est invoquée qu’une seule et unique fois même si le bean est injecté dans plusieurs beans. Techniquement, Spring instrumente les classes annotées avec @Configuration au démarrage du contexte applicatif.

Le bean entityManagerFactory est déclaré de la façon suivante :

Il utilise le bean dataSource injecté plus haut dans la classe de configuration. Derrière la méthode jpaVendorAdaper() se cache un autre bean. Quant à additionalProperties(), il s’agit d’un vrai appel de méthode. De la même manière que pour la DataSource, l’appel aux méthodes afterPropertiesSet() et getObject() sont nécessaires pour retourner un bean de type EntityManagerFactory.

Une autre possibilité aurait consisté à renvoyer un LocalContainerEntityManagerFactoryBean puis à utiliser l’annotation @Autowired sur la méthode transactionManager. Charge à Spring de demander à la fabrique de beans de lui retourner un EntityManagerFactory :

L’injection du bean dataSource dans le LocalContainerEntityManagerFactoryBean aurait pu être réalisée d’une autre manière. En effet, en tant que beans Spring, les beans de configuration peuvent être injectés dans d’autres bean de configuration. Ainsi, le bean DataSourceConfig aurait pu être injecté dans InfrastructureConfig. Il devient alors possible d’appeler  dataSourceConfig.dataSource()  pour récupérer la dataSource :

Bien que facilitant la navigation dans la configuration Spring, cette technique induit un couplage fort entre les 2 beans de configuration.
Une fois l’infrastructure Hibernate / JPA mis en place, le bean de configuration RepositoryConfig initie la couche d’accès aux données. Par commodité, son implémentation est réalisée à l’aide du projet Spring Data JPA. L’annotation @EnableJpaRepositories active ce dernier : toutes les interfaces contenues dans un package donné et étendant l’interface Repository de Spring Data sont détectées puis interprétées. Par défaut, les requêtes JPA sont générées à partir du nom des méthodes exposées dans ces interfaces. Les implémentations personnalisées des Repository sont recherchées à l’aide du suffixe Impl.

La couche de persistance est sans doute celle nécessitant le plus de configuration. A côté, la couche services métiers est particulièrement simple.

Couche services

L’objectif premier de ServiceConfig consiste à détecter tous les beans Spring annotés par l’annotation @Service. L’annotation @ComponentScan joue ce rôle en analysant récursivement le package Java dédié aux services métiers, dans notre exemple le package com.javaetmoi.sample.service :

C’est dans cette couche que je vous propose d’activer des fonctionnalités avancées de Spring :

Comme indiqué plus haut, l’annotation @EnableCaching nécessite un CacheManager. La définition d’un bean cacheManager et d’un defaultCache sont donnés ici à titre indicatif :

Afin de pouvoir personnaliser le pool de threads utilisé lors de traitements asynchrone, l’annotation @EnableAsync encourage à définir son propre pool en implémentant l’interface AsyncConfigurer et sa méthode public Executor getAsyncExecutor() :

Avant de passer à la configuration de la couche présentation, terminons l’initialisation du contexte racine par la mise en place de la sécurité.

Sécurité

Reposant sur projet Spring Security, la configuration de la sécurité applicative est centralisée dans le fichier SecurityConfig. L’annotation @EnableWebMvcSecuritys’utilise de pair avec la classe abstraite WebSecurityConfigurerAdapter. Cette dernière permet d’activer la configuration par défaut de Spring Security. La redéfinition de certaines méthodes permet de personnaliser la configuration via les classes AuthenticationManagerBuilder , HttpSecurity et WebSecurity.

Publié sur le blog de Spring, le billet Spring Security Java Config Preview: Web Security explique comment utiliser la syntaxe Java.
Dans notre exemple, la déclaration du filtre springSecurityFilterChain dans le web.xml permet de rester compatible avec les conteneurs de Servlet 2.5.

L’un des apports du fichier SecurityConfig réside dans la mise à disposition du bean authenticatedUserDetails. Implémentant l’interface UserDetails de Spring Security, ce bean permet d’accéder aux données utilisateurs (ex : login, nom, habilitations) de l’utilisateur authentifié. De portée session (spécifiée avec l’annotation @Scope), ce bean peut être injecté dans des beans de scopes différents, par exemple dans les contrôleurs ou les services métiers de portée singleton. Très pratique pour les logs et/ou les données de traçabilité.

Remarque : le système d’authentification sur lequel est branchée votre application peut renvoyer des données supplémentaires. Il est alors fréquent d’avoir à spécialiser la classe User implémentant UserDetails. Si vous ne définissez pas d’interface sur votre classe MyUser, l’utilisation du ScopedProxyMode.TARGET_CLASS est requise par Spring pour proxyfier votre classe.

Couche présentation

La configuration Java de Spring MVC ressemble à celle de Spring Security. L’annotation @EnableWebMvc s’utilise conjointement à la classe abstraite WebMvcConfigurerAdapter. L’application bénéficie d’une configuration par défaut qu’il est possible de personnaliser par redéfinition de méthodes. La classe WebMvcConfig joue ce rôle :

En complément de la redéfinition de méthode, il est possible de configurer Spring MVC en paramétrant les beans créés automatiquement par l’annotation @EnableWebMvc et la classe DelegatingWebMvcConfiguration qu’elle référence.
C’est précisément le cas du bean RequestMappingHandlerAdapter qui est paramétré dans init() appelée au chargement du contexte applicatif Spring.
Pour les nouvelles applications Spring MVC, il est recommandé par Spring de forcer à true le flag ignoreDefaultModelOnRedirect. Lors de redirection, l’utilisation de l’interface RedirectAttributes devient alors nécessaire pour spécifier quelle donnée doit être passée à la RedirectView.

La configuration donnée en exemple ne donne qu’un aperçu de l’éventail des possibilités de configuration offertes par Spring MVC : choix de la technologie de rendu (ici JSP), accès aux ressources statiques, définition du cache, internationalisation, déclaration des formateurs globaux …

Conclusion

En guise de conclusion, voici un tableau récapitulant les avantages et les inconvénients qu’a un développeur Java à passer sur une syntaxe full Java :

Avantages Inconvénients
  • Type safe : syntaxe vérifiée à la compilation
  • Complétion dans l’IDE sans plugin
  • Import Java mettant en évidence les dépendances non présentes
  • Refactoring facilité de la configuration
  • Ajout de code possible lors de la déclaration de beans (ex : logs)
  • Accélère le chargement du contexte applicatif
  • Traite la configuration comme du code
  • Navigation dans la conf facilitée
  • Mixité possible avec la configuration XML
  • Plus besoin de connaitre les namespaces XML
  • Nouvelle syntaxe à appréhender
  • Subtilités à maîtriser : afterPropertiesSet(), appels de méthodes annotées avec @Bean qui n’en sont pas

Références :

  1. Manuel de référence Spring Framework 4 (Pivotal)
  2. Spring Java Based Configuration (Tutorials Point)
  3. Spring Data, une autre façon d’accéder aux données (So@t)
  4. Spring Security Java Config Preview: Web Security (Pivotal)
  5. Spring Java Config 101 (DZone)
  6. Spring Cache (Zenika)

Une réflexion au sujet de « Configurez Spring en Java »

Laisser un commentaire

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