Affichage des articles dont le libellé est spring. Afficher tous les articles
Affichage des articles dont le libellé est spring. Afficher tous les articles

lundi 17 février 2014

Traitements asynchrones avec Spring @Async

Dans une application Web, l'utilisateur ne doit pas attendre inutilement. Si une requête HTTP déclenche un traitement long et que le résultat de ce traitement n’a aucun impact sur la réponse à renvoyer, pourquoi attendre qu'il se termine ?

Un traitement long ne devrait jamais être appelé de manière synchrone. Cependant, le rendre asynchrone demande un travail plus ou moins important. Heureusement, Spring vient à la rescousse avec une annotation, @Async, qui fait tout le boulot.

Traitements asynchrones


L'exemple ci-dessus parle d'une requête Web, mais n'importe quel processus peut tirer parti de l'asynchronisme. Web ou non, il y a deux cas de figure:
  • Le premier, c’est celui évoqué ci-dessus : un processus (peut-être une requête Web) entraîne un traitement dont l’issue lui importe peu. On peut imaginer un log d’accès à des fins de statistiques. Ce n’est pas nécessairement un long traitement, mais le processus parent peut suivre son cours, indépendamment de son résultat (le log). Ce besoin peut être implémenté de différentes manières, mais le rendre asynchrone, c’est-à-dire créer un thread séparé du processus parent et y faire s’exécuter le traitement, est une solution.
  • Le deuxième cas de figure est que le résultat d'un processus dépend du résultat d’un long traitement. Il est intéressant de lancer ce dernier de manière asynchrone car, pendant qu’il s’exécute dans son thread, le processus principal peut suivre son cours en parallèle. Ce système est plus difficile à mettre en place et implique typiquement l’utilisation d’un Future, un objet qui, comme son nom l’indique, contiendra une réponse dans le futur.

Spring @Async


Avec Spring, il suffit de placer l'annotation @Async sur la méthode englobant le traitement long pour le rendre asynchrone. Bon, c’est vrai, il faut un peu de configuration aussi.

Pour commencer, Spring aura besoin d’un TaskExecutor qui se chargera de l’exécution asynchrone de la méthode. Spring propose plusieurs implémentations de cette interface, que je ne vais pas examiner ici. Il suffit de savoir que, dans la plupart des cas, sa création sera très simple.

Dans le fichier xml de configuration de Spring, il suffit d’indiquer:
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:task="http://www.springframework.org/schema/task"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd" 
    default-autowire="byName">

    <task:executor id="executor" pool-size="10"/>
    <task:annotation-driven executor="executor" />

    <!--A suivre…-->
Cette configuration définit un bean nommé "executor", instance de la classe ThreadPoolTaskExecutor (un pool de 10 threads dans ce cas).

Il faut ensuite activer la définition de l'asynchronisme via annotation, en précisant le TaskExecutor à utiliser, dans la ligne <task:annotation-driven />.

A noter que cette dernière entrée permet aussi de configurer des tâches planifiées (des Schedulers, avec l’annotation @Scheduled). Il n’en sera pas question ici.

Ecriture de services asynchrones


Pour continuer notre exploration, je vais utiliser une méthode de longue : le calcul d’une factorielle utilisant des BigInteger.

Quelques remarques sur ce choix et sur son implémentation :

  • Pour le moment, sur une machine standard, ce traitement est "long". Je ne sais pas si ce sera toujours le cas dans quelques années.
  • Le calcul de la factorielle sert généralement de démonstration à la récursivité. Néanmoins, l’implémentation récursive est un peu particulière dans le cas présent. J’y reviendrai plus tard, mais dans l’immédiat, le traitement sera non récursif.
  • Le bean contenant le calcul de la factorielle implémentera une interface. C’est effectivement une bonne pratique de masquer l’implémentation, d’autant plus que Spring doit créer un proxy pour gérer l'asynchronisme, un proxy implémentant l'interface et interceptant les appels vers le véritable bean. Ceci étant dit, la création du proxy fonctionne également sans interface… tant que la classe n’est pas "final".
  • Enfin, pour cette démonstration, j’ai choisi un traitement qui retourne une valeur. Cela implique donc l’utilisation d’un Future. Comment faire si aucun retour n’est attendu ? Renvoyer void tout simplement.

L’interface est simple :
public interface AsyncBean {
     Future<BigInteger> asyncFact(BigInteger n);
}
A noter que la méthode renvoie un Future<BigInteger>. La réponse sera un BigInteger, mais dans le futur…

Comment gère-t-on un Future ? Plusieurs méthodes de Future<T> sont utiles :

  • isDone renvoie true si le résultat est disponible;
  • get() permet de récupérer la valeur (le résultat) du Future. Cette fonction est synchrone : elle attend que le résultat soit disponible. Il est possible d’y ajouter un timeout. Dans ce cas, si la réponse n’est pas disponible dans le délai imparti, une exception est lancée;
  • cancel() permet d’annuler la tâche;
  • isCancelled permet de vérifier si la tâche a été annulée.

Une implémentation asynchrone (et non récursive) de la méthode est la suivante :
@Component
public class AsyncBeanImpl implements AsyncBean {
 
     @Async
     public Future<BigInteger> asyncFact(final BigInteger n) {
          BigInteger accu = BigInteger.ONE;
          BigInteger counter = BigInteger.ONE;
          while(counter.compareTo(n) != 1){
               accu = accu.multiply(counter);
               counter = counter.add(BigInteger.ONE);
          }
          return new AsyncResult<BigInteger>(accu);
     }
}
C’est grâce à l’annotation @Async que l’appel de la méthode sera asynchrone. Quant à l’annotation @Component, elle fera de notre classe un bean Spring.

La valeur de retour est un AsyncResult, une implémentation Spring de Future. Elle est remplie avec le résultat du calcul de la factoriel.

On peut tester l’implémentation avec la classe de test (TestNg) suivante :
@ContextConfiguration(locations="classpath:async/test-async-spring.xml")
public class TestAsyncExecution extends AbstractTestNGSpringContextTests{
     @Autowired
     private AsyncBean asyncBean;

     @Test
     public void testCalculationIsRealyAsynchronous(){
          //La valeur attendue, soit 1000!, il fallait bien un BigInteger
          BigInteger response = new BigInteger("402387260077093773543702433923003985719374864210714632543799910429938512398629020592044208486969404800479988610197196058631666872994808558901323829669944590997424504087073759918823627727188732519779505950995276120874975462497043601418278094646496291056393887437886487337119181045825783647849977012476632889835955735432513185323958463075557409114262417474349347553428646576611667797396668820291207379143853719588249808126867838374559731746136085379534524221586593201928090878297308431392844403281231558611036976801357304216168747609675871348312025478589320767169132448426236131412508780208000261683151027341827977704784635868170164365024153691398281264810213092761244896359928705114964975419909342221566832572080821333186116811553615836546984046708975602900950537616475847728421889679646244945160765353408198901385442487984959953319101723355556602139450399736280750137837615307127761926849034352625200015888535147331611702103968175921510907788019393178114194545257223865541461062892187960223838971476088506276862967146674697562911234082439208160153780889893964518263243671616762179168909779911903754031274622289988005195444414282012187361745992642956581746628302955570299024324153181617210465832036786906117260158783520751516284225540265170483304226143974286933061690897968482590125458327168226458066526769958652682272807075781391858178889652208164348344825993266043367660176999612831860788386150279465955131156552036093988180612138558600301435694527224206344631797460594682573103790084024432438465657245014402821885252470935190620929023136493273497565513958720559654228749774011413346962715422845862377387538230483865688976461927383814900140767310446640259899490222221765904339901886018566526485061799702356193897017860040811889729918311021171229845901641921068884387121855646124960798722908519296819372388642614839657382291123125024186649353143970137428531926649875337218940694281434118520158014123344828015051399694290153483077644569099073152433278288269864602789864321139083506217095002597389863554277196742822248757586765752344220207573630569498825087968928162753848863396909959826280956121450994871701244516461260379029309120889086942028510640182154399457156805941872748998094254742173582401063677404595741785160829230135358081840096996372524230560855903700624271243416909004153690105933983835777939410970027753472000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
          Future<BigInteger> r = asyncBean.asyncFact(BigInteger.valueOf(1000L));
          assertFalse(r.isDone(),"Pas encore calculé");
          BigInteger result = null;
          try{
               result = r.get();
          } catch(Exception e){
               fail("Exception durant l’exécution", e);
          }
          assertNotNull(result,"Le résultat ne peut être null ");
          assertTrue(r.isDone(),"Maintenant, la tâche est terminée ");
          assertEquals(result, response,"Le résultat doit évidemment être correct");
     }
}
Le fichier test-async-spring.xml est le suivant :
<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"
    xmlns:task="http://www.springframework.org/schema/task"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
    http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd" 
    default-autowire="byName">

    <context:component-scan base-package="*" />

    <!-- Crée un ThreadPoolTaskExecutor -->
    <task:executor id="executor" pool-size="10"/>

    <task:annotation-driven executor="executor" />

</beans>
L’implémentation peut paraître curieuse car l’instanciation du Future ne sera faite qu’après le calcul. Pourtant, dans notre test, nous recevons bien un Future "tout de suite", alors que notre méthode n’a pas encore terminé son calcul et donc créé son Future avec un résultat.

En fait, Spring utilise l’AOP pour décorer notre classe avec un AsyncExecutionInterceptor, lequel crée un Callable et le soumet au TaskExecutor (qui gère les threads et les exécutions). Cette soumission renvoie un Future qui ne sera initialisé que lors du retour de notre méthode en prenant la valeur de notre Future.

Il est assez simple de vérifier (en mode debug) que le Future reçu par le test, lors de l’appel asyncBean.asyncFact(BigInteger.valueOf(1000L)) n’est pas le même que celui renvoyé par notre implémentation : le premier est une java.util.concurrent.FutureTask (renvoyée par l'AsyncExecutionInterceptor), alors que le deuxième, que nous avons instancié, est org.springframework.scheduling.annotation.AsyncResult.

Au final, le Future renvoyé par notre méthode n’est pas si « futur » que ça puisqu’il est créé directement avec la valeur de retour. Mais il est obligatoire de renvoyer un Future. Si ce n’était pas le cas et que la méthode renvoyait un BigInteger directement, l’intercepteur de Spring renverrait null et jamais la bonne valeur.

La version récursive


La version récursive n’est pas complexe, si ce n’est que sa valeur de retour doit être un Future. On a donc une implémentation comme suit :
@Async
public Future<BigInteger> asyncRecursiveFact(final BigInteger n) throws InterruptedException, ExecutionException {
     if(n.equals(BigInteger.ZERO)){
          return new AsyncResult<BigInteger>(BigInteger.ONE);
     } else {
     return new AsyncResult<BigInteger>(n.multiply(asyncRecursiveFact(n.subtract(BigInteger.ONE)).get()));
     }
}
Elle fonctionne correctement et est beaucoup plus rapide que la première. Mais elle est un peu lourde.

A chaque itération, un objet Future est créé. Comme nous l’avons vu, ce n’est qu’un container, initialisé dès sa création avec une valeur. Il n’a donc aucun intérêt, si ce n’est de surcharger l’écriture de la méthode : récupération de la valeur par un get(), gestion des exceptions (ici simplement transmises dans un throws, mais ce n'est pas très propre).

C’est pourquoi je préfère la deuxième implémentation, plus claire (et aussi 20% plus rapide que la précédente) :
@Async
public Future<BigInteger> asyncRecursiveFactOther(final BigInteger n) {
     return new AsyncResult<BigInteger>(fact(n));
}
 
private BigInteger fact(BigInteger n){
     if(n.equals(BigInteger.ZERO)){
          return BigInteger.ONE;
     } else {
          return n.multiply(fact(n.subtract(BigInteger.ONE)));
     }
}
Le calcul récursif se fait sans utiliser les Future, mais est appelé de manière asynchrone. L’objet Future n’est créé qu'à la fin, que lorsque le calcul est terminé.

Code source


Comme expliqué dans l’article "Deux projets prototypes et didactiques", les sources de cet article sont disponibles sur GitHub :

  • les beans sont ici
  • la classe de test est ici 
  • le fichier xml de configuration ici


dimanche 9 février 2014

Spring: utiliser un BeanFactoryPostProcessor et des annotations pour un développement plus sûr

Spring offre de nombreuses possibilités d'extension, notamment en permettant d'agir à différents moment du cycle de vie des beans. C'est le cas des "PostProcessor".

Dans cet article, je vais montrer comment, dans un cas bien spécifique, réduire les risques d'erreur de développement en utilisant un BeanFactoryPostProcessor, combiné à une annotation de stéréotype "custom".

Le contexte

Afin de restreindre les droits d'accès à certaines fonctionnalités, j'ai développé un module non intrusif. L'objectif est qu'il soit simple à utiliser. Le développeur ne doit, pour sécuriser l'accès à une méthode, qu'ajouter une annotation sur ladite méthode, en précisant la règle à appliquer et des paramètres spécifiques à cette règle. Le module fera le reste.

Par exemple, il suffira d'annoter une méthode avec 
@AccessRule(rule=RoleRule.class, params={"ADMIN"})
public void foo(){
     //..
}
pour que l'accès à la méthode "foo" soit limité aux utilisateurs ayant un rôle d'administrateur.

Pour cela, une classe RoleRule doit exister. Le module fournit plusieurs règles de base, dont la RoleRule, mais les développeurs sont encouragés à créer leurs propres règles.

L'implémentation de la RoleRule pourrait ressembler à ceci:
/**
* Exemple simplifié de la RoleRule, 
* la classe réelle est plus complexe... et mieux écrite
*/
public class RoleRule{
     private String role;

     public boolean check(SecurityContext context){
          User user = (User) context.find("USER"); //Par exemple
          return user.hasRole(role);
     }

     public void setParams(String[] params){
          role = params[0];
     }
}
Cette classe est réutilisable. Par exemple:
@AccessRule(rule=RoleRule.class, params={"SUPPORT"})
public void bar(){
     //..
}
limite l'exécution de la méthode "bar" aux seuls les utilisateurs ayant le rôle "support".

Le fonctionnement du module est en dehors du scope de cet article, mais il est intéressant de savoir qu'il cherche et trouve l'annotation @Rule sur la méthode invoquée, instancie la règle déclarée dans l'annotation, y injecte les paramètres (méthode setParams) et vérifie la méthode "check" en lui passant un contexte de sécurité. Si cette méthode renvoie true, l'accès est accordé et la méthode (bar) exécutée, sinon l'accès est refusé sans que la méthode (bar) soit exécutée.

Utilisation de Spring

Ce système de gestion des droits d'accès est utilisé depuis plusieurs années et il est très stable. Il y a cependant une fonctionnalité qui est très peu documentée et que les développeurs ignorent généralement.

J'ai écrit plus haut que le module de sécurité instanciait la règle, j'ai un peu menti...

En réalité, il commence par vérifier l'existence d'un contexte Spring et s'il existe, il essaye d'y récupérer une instance de la règle.

Cela signifie donc qu'une règle peut être un bean Spring. Elle peut donc être déclarée, soit dans le fichier XML, soit via une annotation de stéréotype (typiquement @Component). L'avantage d'utiliser Spring, c'est que ça permet d'injecter dans la règle d'autres beans Spring (des services par exemple) et de les y utiliser.

Si cette possibilité n'est pas documentée, c'est qu'elle présente un risque.

Les applications que nous écrivons sont généralement des applications Web, donc forcément multithread. En regardant le code de la RoleRule ci-dessus, il est évident qu'elle ne peut être gérée par Spring qu'à condition d'être un prototype. En effet, dans un contexte multithread, une règle singleton ne pourrait survivre aux différents paramétrages dont elle fera l'objet. Dans de rares cas (pas de paramétrage par exemple), une règle pourrait être un singleton, mais c'est exceptionnel.

Le problème, c'est que Spring crée par défaut des singletons et qu'il faut donc penser à les configurer correctement.

Dans nos projets, où les beans Spring sont configurés avec des annotations, cela signifie qu'une règle devrait ressembler à ceci:
@Component
@Scope("prototype")
public class RoleRule{
     // Implémentation
}

Hélas! La majorité des beans (pour ne pas dire la totalité) sont des singletons et le risque d'oublier cette annotation @Scope est grand. Le réflexe sera d'écrire:
@Component
public class RoleRule{
     // Implémentation
}

Soit un singleton, qui donnera des résultats aléatoires dans un contexte multithread.

Solution

Il faut donc trouver un moyen pour que, lorsqu'une règle est définie comme un bean Spring, elle soit par défaut un prototype et ce, même si la configuration standard dit le contraire. Par contre, il faut laisser la possibilité de la configurer comme un singleton dans les rares cas où ce serait correct (ce qui suppose que le développeur sait ce qu'il fait).

Un stéréotype custom

La première étape consiste à configurer un stéréotype spécifique pour dire qu'une classe, un futur bean, est une règle et qu'elle doit donc obéir à certains points de configuration.

Le code pour cette annotation peut s'écrire comme suit:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Rule {
     boolean singleton() default false;
}

De cette manière, il suffit au développeur d'annoter la classe de règle pour en faire un bean (avec le scan des packages activé afin de découvrir les beans). 
@Rule
public class RoleRule{
     // Implémentation
}

Nous verrons dans un instant comment garantir que cette règle sera bien un prototype, mais notons dés à présent que le cas exceptionnel où une règle peut être un singleton doit être expressément écrit:
@Rule(singleton=true)

Automatiser la configuration

Voyons maintenant comme faire pour que, alors qu'il n'y a aucune annotation @Scope("prototype"), la configuration soit correcte.

L'idée est d'utiliser un BeanFactoryPostProcessor qui changera la configuration d'un bean règle au démarrage de Spring.

Le principe d'un BeanFactoryPostProcessor est simple: si lui-même est déclaré comme un bean Spring, il sera automatiquement appelé et il aura la possibilité de modifier les définitions contenues dans le contexte Spring, avant la création de beans. Dans notre cas, il suffira de trouver les beans annotés @Rule et modifier leur configuration en fonction des paramètres de l'annotation.

Voici son implémentation;
@Component
public class RuleBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

     public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
          String[] beanNames = beanFactory.getBeanDefinitionNames();
          for(String name:beanNames){
               BeanDefinition beanDefinition = beanFactory.getBeanDefinition(name);
               try {
                    Class<?> clazz = Class.forName(beanDefinition.getBeanClassName());
                    Rule annotated = clazz.getAnnotation(Rule.class);
                    if(annotated!=null){
                         beanDefinition.setScope(annotated.singleton()?"singleton":"prototype");
                    }
               } catch (ClassNotFoundException e) {
                    throw new BeansException("Class for bean "+name+" not found (really?)",e){};
               }
          }
     }

}

A noter l'annotation @Component pour que le PostProcessor soit un bean Spring et soit donc actif. Alternativement, il peut aussi être configuré au niveau du XML.

Une fois appelée, la méthode "postProcessBeanFactory" vérifie pour chaque définition de bean, la présence de l'annotation @Rule. Si elle est trouvée, elle change le scope dans la définition en fonction de la propriété "singleton" de l'annotation, propriété qui est false par défaut.

Tests et démo

Comme expliqué dans Deux projets prototypes et didactiques, le projet TestSpring sert de laboratoire à ce genre de d'expérimentation.

Vous y trouverez:



jeudi 6 février 2014

Deux projets prototypes et didactiques

Parmi mes tâches en tant qu'architecte, je dois vérifier le fonctionnement des frameworks utilisés, plus particulièrement de certaines de leurs propriétés, afin de bien les comprendre et d'assister les développeurs. Une autre tâche, parallèle à celle-là, est de coacher les développeurs et leur montrer comment fonctionnent les frameworks.

Pour cela, j'utilise des projets "prototypes", correctement configurés, qui me permettent d'effectuer mes divers tests. Après un peu de nettoyage, j'ai décidé de les pousser sur GitHub, où ils sont accessibles à tous.

L'objectif est surtout didactique: démontrer le fonctionnement des frameworks aux travers de petits tests, complètement indépendants les uns des autres. Et quand je dis "tests", je parle réellement de tests unitaires.

Deux projets sont disponibles:

TestsSpringHibernate (https://github.com/hittepit/TestsSpringHibernate

Ce projet teste le fonctionnement d'Hibernate, en particulier lorsqu'il est intégré avec Spring. Cependant, de nombreux tests ne profitent de Spring que pour créer une SessionFactory.

LazyLoading, mappings d'héritage, cache de second niveau et plusieurs autres fonctionnalités ont été ainsi testées. Il y aura d'autres développements en fonction des besoins rencontrés.

Voici les principales librairies utilisées et leurs versions (le laisse de côté les modules spécifiques de chaque framework, ils sont dans le pom.xml):
  • Hibernate 3.5.6
  • Spring 3.2.3
  • TestNg 6.8.5
  • Mockito 1.9.0
  • H2 1.3

TestSpring (https://github.com/hittepit/TestSpring)

Ce projet teste le fonctionnement de Spring. Il est plus récent que le précédent, donc moins complet. Il contient cependant quelques tests intéressants: l'utilisation de l'annotation @Async, de l'AOP ou des PostProcessors.

Les librairies et leurs versions sont:
  • Spring 3.2.3
  • TestNg 6.8.5

Structure des projets

Les deux projets sont similaires. Tous deux sont "maven". Le code de base se trouve donc dans src/main/java.

Les packages utilisent une base commune (be.fabrice.testspring pour TestSpring, be.fabrice pour TestsSpringHibernate), puis continuent avec un identifiant en rapport avec la fonctionnalité testée. Un sous-division existe parfois.

Par exemple, dans TestsSpringHibernate, le sous-package "inheritance" est sous-divisé en "join", "single" et "table" selon la manière dont l'héritage est fait. Dans TestSpring, le sous-package "postProcessor" est divisé en "bean", "definitionRegistry" et "factory" selon le type de PostProcesor qui est testé.

La division va plus loin dans le projet TestsSpringHibernate, où il y a systématiquement un dernier sous-package "entity" (pour les entités, avec le mapping Hibernate en annotations) ou "dao" (pour les Data Access Objects utilisés pour gérer ces entités).

Les tests, qui démontrent les propriétés, sont évidemment dans src/test/java. Le packaging est exactement pareil à celui de la fonctionnalité testée, à l'exception de TestsSpringHibernate où seule la sous-division "dao" est utilisée.

Les ressources ne sont configurées qu'au niveau des tests, c'est-à-dire dans src/test/resources, avec un découpage en répertoires basé directement sur celui des sous-packages du packaging de base (donc les ressources pour les tests sur l'héritage Hibernate sont en inheritance/join, inheritance/single et inheritance/table).

Chaque test utilise ses propres fichiers ressources, c'est-à-dire un fichier spring (xml) mais aussi un fichier SQL (uniquement pour TestsSpringHibernate) lorsque la base de données de test doit être initialisée.

Ce qu'il manque

Plusieurs fonctionnalités, clairement. Je tiens à jour le fichier README qui contient une liste de TODOs. Avec d'ailleurs un TODO qui manque:  "compléter la liste des TODOs"...

Ca manque aussi de documentation. J'essaye d'expliquer les fonctionnements dans la JavaDoc, mais je manque de discipline :-/ Le nom des méthodes de test est normalement descriptif de ce qu'elles vérifient.
J'aimerais ajouter des pages Wiki sur GitHub, mais ça prend du temps.

Patience...

mercredi 1 juin 2011

Configuration d'une application Struts2, Spring et Hibernate

Depuis quelques temps, je vois des articles qui expliquent comment créer le squelette d'une application Struts2. Malheureusement, ils s'en tiennent à la partie purement Web et négligent l'intégration avec Spring,  Hibernate (en supposant qu'on en aie besoin), et avec Maven...

Le but de cet article est de décrire, étape par étape, la création d'un squelette d'application mêlant Struts2, Spring et Hibernate, développé sur Eclipse avec le plugin d'intégration Maven et immédiatement déployable sur Tomcat (parce qu'il est bien intégré à Eclipse, mais l'application peut - sous certaines conditionss - se déployer sur n'importe quel serveur).

Allez, au boulot !

Environnement

L'environnement de développement visé est composé de :
  • jdk1.6 (mais ça fonctionne avec un jdk1.5)
  • Eclipse 3.6 (mais la procédure fonctionne globalement avec les versions 3.5 et 3.4, les plugins doivent éventuellement être adaptés). J'utilise la perspective Java EE.
  • le plugin maven-integration for Eclipse 0.12 (http://m2eclipse.sonatype.org/sites/m2e/)
  • le plugin maven-integration for WTP 0.12 ( à partir de http://m2eclipse.sonatype.org/sites/m2e-extras/)
  • un Tomcat 6 et donc jee5.

Suppositions et partis pris

Je pars du principe que vous savez utiliser Eclipse et Maven, que vous savez comment développer une application Web en Java et que vous savez déployer la dite application sur un Tomcat depuis Eclipse.

Les plugins d'intégration Maven ont été correctement installés.

Enfin, ce n'est pas non plus un tutorial pour utiliser Struts2, Spring ou Hibernate...

Création du projet de base

C'est un projet Maven. Donc, menu File>New>Other, choisir Maven Project (filtrer éventuellement sur Maven). Ne surtout pas cocher "create a simple project" parce qu'on va utiliser un archetype.

Next >

Dans la fenêtre suivante, sélectionner l'archetype. Pour cela, filtrer sur "webapp" pour plus de facilité et sélectionner org.codehaus.mojo.archetypes:webapp-jee5. (Voir note ci-dessous en cas de problème.)

Next >

Remplir les groupId, artifactId et version du projet (la valeur par défaut est généralement acceptable: 0.0.1-SNAPSHOT). Dans mon cas, j'ai choisi "be.fabrice" pour le groupId et "zeDemo" pour l'artifactId.

Le package se rempli automatiquement à partir de ces deux noms. Si ce n'est pas le cas, vous pouvez le remplir manuellement, mais l'archetype n'en fait a priori aucun cas.

Note

Il est commun dans certaines entreprises d'avoir un proxy. Dans certains cas, le plugin d'intégration Maven ne fonctionne pas correctement, en particulier en ce qui concerne les archetypes. C'est le cas pour la version 0.12. J'espère que la version suivante réglera ça.

Eventuellement, il est possible d'utiliser la ligne de commande et de créer le projet avec, en ligne de commande:

mvn archetype:generate -DgroupId=be.fabrice -DartifactId=zeDemo -DarchetypeGroupId=org.codehaus.mojo.archetypes -DarchetypeArtifactId=webapp-jee5 -DarchetypeVersion=1.2

Remplir les données demandées. Pour terminer, il faudra faire un "import existing maven projects" dans Eclipse.

Néanmoins, certaines facilités offertes par le plugin Maven ne seront pas disponibles.

Configuration de base

J'ai déjà remarqué qu'en fonction de la version plugin maven et de la version de l'archetype utilisé, certains éléments pouvaient ne pas exister. Notamment certains répertoires sources.

Dans mon cas, j'ai les répertoires src/main/java et src/test/java, mais pas les "resources". Il suffit de faire un clic droit sur le répertoire src/main pour y ajouter le répertoire "resources" et la même chose pour src/test.

Ces répertoires ne sont pas encore reconnus par Eclipse comme des répertoires sources. Il suffit d'un clic doit sur le projet et, dans le menu contextuel de choisir Maven > Upade Project Configuration.



Le projet est maintenant configuré.

Ajout des dépendances Maven

J'ai un principe quand j'ajoute les dépendances: réduire leur nombre au minimum et profiter des dépendances transitives.

Par exemple, pour utiliser Struts2, dans la mesure où l'intégration avec Spring doit se faire, la dépendance struts2-spring-plugin est suffisante. Elle importe de manière transitive struts2-core et la plupart des modules de spring 2.5.6.

Pour cela, ouvrir le fichier pom.xml dans l'éditeur par défaut (celui qui vient du plugin). Choisir l'onglet "Dependencies", cliquer sur le bouton Add et dans le champ de saisie du dialogue qui s'affiche, sélectionner struts2-spring. Rapidement, le plugin filtre les dépendances disponibles. Développez l'entrée org.apache.struts:struts2-spring-plugin et sélectionnez la version 2.2.3.



Note: de nouveau, il peut y avoir des problèmes avec un proxy... Il faudra cliquer sur "create" plutôt  que "add" et remplir les informations "à la main"...

Confirmez et sauvez le projet.

Vérifiez toujours les dépendances transitives. Les versions évoluant, elles changent parfois.

Pour cela, cliquez sur l'onglet "Dependency hierarchy". Vous y verrez la liste des dépendances importées transitivement par ce premier ajout.


Il reste à présent à ajouter les dépendances Hibernate. Souvent les annotations propres à Hibernate seront nécessaires en plus de celles venant de JPA pour pouvoir faire la jonction entre les entités et un schéma DB imposé.

Pour cela, la dépendance à importer est (même opération que pour Struts2) org.hibernate:hibernate-annotations:3.5.6-Final.

De nombreuses dépendances Spring ont déjà été importées, mais pas toutes celles dont nous avons besoin.

Pour que ce projet fonctionne, il faut encore ajouter org.springframework:spring-orm:2.5.6, org.springframework:spring-aop:2.5.6 et org.springframework:spring-jdbc:2.5.6 (apparemment nécessaire pour logback).

Car slf4j étant une dépendance transitive de nos framework, il lui faut un logger. Pour ma part, j'utilise logback et j'ajoute donc ch.qos.logback:logback-classic:0.9.17.

Je laisse de côté les dépendances de test.

Configuration xml

La configuration du projet se fait dans trois fichiers xml différents:

web.xml

Deux éléments à configurer obligatoirement: Struts2 et Spring.

Spring se charge à l'aide d'un ServletContextListener implémenté par une classe de Spring: ContextLoaderListener.

<listener>
     <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

Par défaut, cette configuration ira chercher le fichier applicationContext.xml à la racine du WEB-INF. La pratique veut toutefois que les fichiers de configuration (à l'exception du web.xml) soient placés dans le répertoire src/main/resources, ce qui aura pour effet de les placer dans le WEB-INF/classes. Il arrive aussi parfois que l'on souhaite changer le nom du fichier de configuration de spring.

Il importe donc d'ajouter les lignes suivantes:

<context-param>
     <param-name>contextConfigLocation</param-name>
     <param-value>classpath:applicationContext.xml</param-value>
</context-param>

Le <param-value> est à adapter en fonction de la configuration. Celui-ci cherchera le fichier applicationContext.xml situé à la racine du classpath (soit WEB-INF/classes).

Il faut enfin configurer l'application pour qu'elle utilise Struts2. Cela se fait en configurant un filtre:

<filter>
       <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>

<filter-mapping>
       <filter-name>struts2</filter-name>
       <url-pattern>/*</url-pattern>
</filter-mapping>

OpenSessionInViewFilter?

Une pratique assez courante dans les applications Web "simples" avec Hibernate, est d'ajouter l'OpenSessionInViewFilter qui permet de garder "en vie" la session Hibernate dans le thread d'exécution de la requête. Cela permet notamment d'accéder aux objets lazy-loadés d'Hibernate de manière transparente partout dans l'application.

La configuration se fait à l'aide d'un filtre, placé AVANT le filtre de Struts2. Il est ici configuré pour n'intercepter que les requêtes se terminant par .demo, requêtes qui seront gérées par Struts2 (comme nous le verrons dans un instant).

<filter>
       <filter-name>hibernate</filter-name>
       <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
</filter>

<filter-mapping>
       <filter-name>hibernate</filter-name>
       <url-pattern>*.demo</url-pattern>
</filter-mapping>

Fichier final

Pour récapituler voici le fichier web.xml final.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
  <display-name>ZeDemo demo application</display-name>
  <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:applicationContext.xml</param-value>
  </context-param>

  <filter>
      <filter-name>hibernate</filter-name>
      <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
  </filter>

  <filter>
      <filter-name>struts2</filter-name>
      <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
  </filter>

  <filter-mapping>
      <filter-name>hibernate</filter-name>
      <url-pattern>*.demo</url-pattern>
  </filter-mapping>

  <filter-mapping>
      <filter-name>struts2</filter-name>
      <url-pattern>/*</url-pattern>
  </filter-mapping>

  <listener>
      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
</web-app>

applicationContext.xml

Voici le fichier de configuration de Spring. Il sera placé dans src/main/resources.

Il est configuré pour fonctionner de la manière suivante:
  • les beans sont configurés à l'aide d'annotations (@Component et dérivées) à l'exception de certains beans provenant de frameworks. Spring scannera les packages be.fabrice.zeDemo pour les trouver.
  • un pool de connexions doit exister dans le serveur et être exposé comme une ressource JNDI du nom de jdbc/zeDemoDatasource.
  • les transactions seront définies à l'aide de l'annotation @Transactionnal.
  • le gestionnaire de base de données que j'utilise est PostgreSQL et je souhaite mettre le schéma à jour en fonction des entités (nous sommes en phase de développement).
  • la configuration des entités Hibernate se fait par annotations et la SessionFactory scannera les packages be.fabrice.zeDemo.entities pour les trouver.

<?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:tx="http://www.springframework.org/schema/tx"
   xmlns:context="http://www.springframework.org/schema/context"
   xmlns:jee="http://www.springframework.org/schema/jee"
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
        http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd"
        default-autowire="autodetect">

   <context:component-scan base-package="be.fabrice.zeDemo.*" annotation-config="true" />
   
   <jee:jndi-lookup id="datasource" jndi-name="jdbc/zeDemoDatasource" />
   
   <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
       <property name="dataSource" ref="datasource" />
       <property name="hibernateProperties">
           <value>
               hibernate.show_sql=true
               hibernate.hbm2ddl.auto=update
               hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
           </value>
       </property>
       <property name="packagesToScan" value="be.fabrice.zeDemo.entities" />
   </bean>

   <tx:annotation-driven transaction-manager="transactionManager" />
   
   <bean id="transactionManager"
       class="org.springframework.orm.hibernate3.HibernateTransactionManager">
       <property name="sessionFactory" ref="sessionFactory" />
   </bean>
</beans>

Config sans ressource JNDI

Une petite note pour indiquer les modifications à effectuer si on ne souhaite pas utiliser une ressource JNDI pour la configuration.

Il faut modifier le pom.xml pour y ajouter, par exemple, les apache-commons DBCP et le driver du gestionnaire de la base de données.

Dans le fichier applicationContext.xml, il faut remplacer:

<jee:jndi-lookup id="datasource" jndi-name="jdbc/zeDemoDatasource" />

par

<bean id="datasource" class="org.apache.commons.dbcp.BasicDataSource">
       <property name="driverClassName" value="driverClass" />
       <property name="username" value="username" />
       <property name="password" value="password" />
       <property name="url" value="urlDb" />
</bean>

Les valeurs "driverClass", "username", "password" et "urlDB" sont à remplacer par les valeurs adéquates...

struts.xml

Le dernier fichier à configurer est le fichier struts.xml, lui aussi dans les src/main/resources. Le seul élément que nous devons configurer pour le moment, c'est que les requêtes gérées par Struts2 doivent se terminer par "demo":

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" "http://struts.apache.org/dtds/struts-2.1.7.dtd">

<struts>
   <constant name="struts.action.extension" value="demo" />

   <include file="struts-default.xml" />

   <package name="default" extends="struts-default" >
   </package>
</struts>


logback.xml

Pas absolument nécessaire mais ça vaut mieux si vous utilisez logback. Toujours dans src/main/resources, le fichier logback.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
 <appender name="STDOUT"  class="ch.qos.logback.core.ConsoleAppender">
    <layout class="ch.qos.logback.classic.PatternLayout">
     <Pattern>ZeDemo - %d{HH:mm:ss.SSS} %-5level %logger{55} - %msg%n</Pattern>
    </layout>
 </appender>

 <root>
    <level value="INFO" />
    <appender-ref ref="STDOUT" />
 </root>
</configuration>

Test

Testons que l'application est correctement configurée. Pour cela, nous devons configurer le serveur et créer une action de test. On pourrait aller plus loin en créant un bean Spring et vérifier qu'il est bien injecté dans l'action, mais ce n'est pas nécessaire.

Configuration du serveur

Généralement, je n'ai pas grand chose à faire de ce côté car le serveur est prêt. Néanmoins, voici les éléments indispensables.

Driver de la base de données
Lorsqu'on utilise une ressource JNDI pour définir la DataSource, il faut que le driver du gestionnaire de base de données soit dans le classpath du serveur et non de celui de l'application.

Le jar du driver doit être copié dans le TOMCAT_HOME/lib, tout simplement...

Ressource JNDI
Lorsque vous créez un runtime de serveur dans Eclipse, un dossier "Servers" est créé dans le "Project Explorer". Il y a un sous-répertoire pour le runtime du Tomcat et dans ce répertoire plusieurs fichiers de configuration, dont context.xml.

La configuration de la ressource JNDI consiste à ajouter dans ce fichier les lignes suivantes:

<Resource name="jdbc/zeDemoDatasource" auth="Container" type="javax.sql.DataSource"
              maxActive="30" maxIdle="10" maxWait="10000"
              username="username" password="password" 
driverClassName="org.postgresql.Driver"
              url="urlDb"/>

Le nom de la ressource doit être le même que celui configuré dans l'applicationContext.xml. Les propriétés "username", "password" et "urlDB" sont à modifier en fonction de vos propres valeurs.

Pour que le déploiement fonctionne, la base de données doit exister.

Action de test

Il ne s'agit pas de faire compliqué.

Pour l'action, une classe Java:

package be.fabrice.zeDemo;

public class TestAction {
   public String execute(){
       return "success";
   }
}

et une JSP test.jsp (que je mets dans le WEB-INF pour le moment):

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>OK</title>
</head>
<body>
Ca marche...
</body>
</html>

Bref, ça ne fait rien, mais permet de valider la configuration.

Pour terminer, il faut ajouter l'action dans l'élément <package> du fichier struts.xml :

<action name="test" class="be.fabrice.zeDemo.TestAction">
       <result name="success">/WEB-INF/test.jsp</result>
</action>

Les valeurs sont à modifier en fonction de vos propres packages...

Déploiement

Dans la view "Servers", clic droit sur le runtime du serveur, "Add and Remove". Sélectionner le nouveau projet (zeDemo) dans la liste de gauche et cliquer sur Add puis finish.

Il suffit de lancer le serveur. Souvent, pour éviter le timeout du serveur, je force le "publish" avant.

Si tout s'est bien passé, il suffit d'entrer l'adresse suivante dans le navigateur: http://localhost:8080/zeDemo/test.demo

Normalement, "Ca marche" devrait s'afficher...

Ouf...