I. Introduction▲
Tout d'abord, une première question qui peut vous venir à l'esprit :
Mais pourquoi ces plugins plutôt qu'un simple Tomcat installé sur un répertoire à part ?
L'objectif de ces plugins est multiple :
- accélérer votre cycle de développement ;
- centraliser dans votre code source une configuration qui marche.
En alternative vous auriez pu :
- installer Tomcat ;
- préconfigurer Tomcat pour accueillir votre application ;
- copier le fichier war généré dans le répertoire webapps.
Cette stratégie est fastidieuse, interrompt votre cycle de développement et nécessite que vous préconfiguriez votre conteneur Tomcat de la même façon pour tout le monde dans l'équipe.
Une variation de la méthode ci-dessus aurait été de faire pointer un fichier de contexte d'un Tomcat préinstallé sur vos sources. Cela évite la copie du fichier mais pas la nécessité d'avoir un Tomcat préconfiguré et uniforme sur toutes les machines.
Une autre variante consiste à utiliser le plugin WTP d'Eclipse. Là encore, il y a un effort de configuration à refaire sur chaque poste.
Ici nous allons insister sur un principe simple et pourtant fondamental en informatique : DRY (Don't Repeat Yourself). Si vous devez reconfigurer votre environnement de travail pour chaque nouvel arrivant, c'est de la perte de temps.
Checkout and Run : notre objectif c'est qu'un nouvel arrivant sur un projet n'ait qu'à cloner le repository et puisse être tout de suite opérationnel.
Si on souhaite accélérer son cycle de développement et éviter les redémarrages trop fréquents, on pourrait aussi utiliser JRebel. Il existe d'ailleurs un plugin Maven pour créer votre fichier rebel.xml à partir de votre POM : http://zeroturnaround.com/software/jrebel/maven/. Cette approche ne s'oppose pas à celle que nous allons détailler ici.
II. Configuration de base▲
II-A. Tomcat▲
Commençons par la configuration de base de cet article. Démarrons par le projet 1.developpez-webapp. Le seul point d'attention pour l'instant c'est que nous allons déclarer le plugin Tomcat dans le fichier pom.xml dans la section build -> pluginManagement :
<plugin>
<groupId>
org.apache.tomcat.maven</groupId>
<artifactId>
tomcat7-maven-plugin</artifactId>
<version>
2.0</version>
</plugin>
Ensuite, il vous suffit de lancer la commande suivante :
mvn tomcat7:run
Vous devriez obtenir le résultat suivant :
La commande mvn tomcat7:run invoque la phase mvn compile avant de s'exécuter elle-même. Cependant vous profitez de la compilation incrémentale de Maven et vous ne recompilerez pas l'ensemble du projet à chaque fois.
Pour résumer, le conteneur Tomcat est automatiquement téléchargé puis démarré avec le contenu de votre application Web. Le port par défaut est 8080 et le contexte correspond au nom de votre artefact. Vous pouvez donc vous connecter sur :
http://localhost:8080/developpez-webapp/
Ici rien de magique, l'application est juste constituée d'un fichier HTML statique.
II-B. Jetty▲
Si vous préférez Jetty, la configuration pour lancer votre conteneur préféré sera sensiblement très proche :
<plugin>
<groupId>
org.mortbay.jetty</groupId>
<artifactId>
jetty-maven-plugin</artifactId>
<version>
8.1.5.v20120716</version>
</plugin>
puis la commande :
mvn jetty:run
Par défaut le plugin démarre votre application sur le contexte « / ». L'URL pour y accéder est donc :
On remarquera pour le troll que le démarrage avec Jetty est légèrement plus rapide.
III. Les logs▲
Un des aspects les plus importants d'une application c'est qu'elle nous dise ce qu'elle fait.
Disons que vous ayez vos habitudes avec logback et que vous souhaitiez l'utiliser ici.
Pour que ce soit intéressant nous allons ajouter un peu de code sinon par défaut nous n'aurions que les logs de démarrage de Tomcat/Jetty, ce qui diminue un peu l'intérêt de la démonstration. Nous allons donc ajouter un service REST construit avec JAX-RS. Ce code va faire appel à plusieurs librairies tierces : Spring, Jackson, CXF, etc. et chacune de ces librairies va produire ses propres logs.
Note : pour créer mon « Hello World » j'ai utilisé la commande mvn archetype:generate, une autre commande très utile pour démarrer avec un bon squelette d'application.
Le code est présent dans le répertoire 2.developpez-webapp-jaxrs et nous ne décrirons pas l'application JAX-RS.
Rajoutons les dépendances pour les logs :
<!-- Logging -->
<dependency>
<groupId>
org.slf4j</groupId>
<artifactId>
slf4j-api</artifactId>
<version>
1.7.2</version>
</dependency>
<dependency>
<groupId>
org.slf4j</groupId>
<artifactId>
jcl-over-slf4j</artifactId>
<version>
1.7.2</version>
</dependency>
<dependency>
<groupId>
ch.qos.logback</groupId>
<artifactId>
logback-classic</artifactId>
<version>
1.0.9</version>
</dependency>
<dependency>
<groupId>
ch.qos.logback</groupId>
<artifactId>
logback-core</artifactId>
<version>
1.0.9</version>
</dependency>
Si on démarre avec mvn tomcat7:run ou mvn jetty:run, on remarque que notre application devient très bavarde. Cependant nous avons tous nos logs en DEBUG ce qui n'est pas toujours pratique.
Ajoutons une propriété système à utiliser au démarrage pour nos plugins Jetty et Tomcat afin de configurer les logs avec un fichier de configuration.
Pour le plugin Tomcat :
<configuration>
<systemProperties>
<logback.configurationFile>
src/test/resources/logback.xml</logback.configurationFile>
</systemProperties>
</configuration>
Pour le plugin Jetty c'est un tout petit peu plus verbeux :
<configuration>
<systemProperties>
<systemProperty>
<name>
logback.configurationFile</name>
<value>
src/test/resources/logback.xml</value>
</systemProperty>
</systemProperties>
</configuration>
avec notre fichier de configuration pour logback :
<?xml version="1.0" encoding="UTF-8"?>
<configuration
scan
=
"true"
scanPeriod
=
"10 seconds"
>
<appender
name
=
"STDOUT"
class
=
"ch.qos.logback.core.ConsoleAppender"
>
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root
level
=
"info"
>
<appender-ref
ref
=
"STDOUT"
/>
</root>
</configuration>
Et voilà, désormais notre application est en mode INFO. Petit bonus, vous remarquerez que nous avons configuré le rechargement automatique du fichier de log avec l'attribut scan=true dans le fichier précédent. Vous pouvez donc changer la configuration de vos logs à la volée pendant que l'application tourne.
IV. Configurer une datasource▲
Un autre besoin qui revient régulièrement, c'est celui de pouvoir configurer une datasource sur le conteneur sous forme de ressources JNDI. Ici nous allons utiliser plusieurs astuces :
- une datasource avec une base de données embarquée H2 ;
- l'utilisation du plugin maven-sql pour initialiser notre schéma (*).
Le code se trouve dans le répertoire 3.developpez-webapp-jndi
(*) Si vous utilisez JPA avec Hibernate vous pouvez aussi créer votre schéma automatiquement via la directive create-schema. Nous ne verrons pas ce point ici ce qui nous permet de continuer à illustrer des plugins Maven.
IV-A. Tomcat▲
Tout d'abord nous allons configurer le plugin Maven pour utiliser un fichier context.xml custom :
<configuration>
<contextFile>
src/test/resources/context.xml</contextFile>
</configuration>
Le fichier context.xml :
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Resource
name
=
"jdbc/myDb"
auth
=
"Container"
type
=
"javax.sql.DataSource"
driverClassName
=
"org.h2.Driver"
url
=
"jdbc:h2:target/data;MODE=PostgreSQL;DB_CLOSE_DELAY=-1"
username
=
"sa"
password
=
"sa"
maxActive
=
"10"
maxIdle
=
"2"
/>
<ResourceLink
global
=
"jdbc/myDb"
name
=
"jdbc/myDb"
type
=
"javax.sql.DataSource"
/>
</Context>
Et dernier point, votre conteneur doit désormais connaître le driver de votre base de données. Dans un cas classique vous poseriez votre jar dans le répertoire lib de Tomcat. Ici vous allez utiliser le tag dependency lié au plugin :
<plugin>
<groupId>
org.apache.tomcat.maven</groupId>
<artifactId>
tomcat7-maven-plugin</artifactId>
<version>
2.0</version>
<dependencies>
<dependency>
<groupId>
com.h2database</groupId>
<artifactId>
h2</artifactId>
<version>
1.3.170</version>
</dependency>
</dependencies>
[...]
</plugin>
Si vous lancez mvn tomcat7:run et que vous vous connectez via JMX, vous constatez la présence de votre datasource dans l'annuaire JNDI :
Par contre cela n'empêchera pas quelques logs désagréables au moment de l'invocation de votre service :
Normal, vous n'avez pas créé votre schéma. Pour cela nous allons utiliser le plugin maven-sql qui va nous permettre d'exécuter des commandes SQL au démarrage de l'application. Voici la configuration avec des commentaires pour expliquer :
<plugin>
<groupId>
org.codehaus.mojo</groupId>
<artifactId>
sql-maven-plugin</artifactId>
<version>
1.5</version>
<dependencies>
<!-- Le plugin nécessite lui aussi de connaitre le driver utilisé -->
<dependency>
<groupId>
com.h2database</groupId>
<artifactId>
h2</artifactId>
<version>
1.3.170</version>
</dependency>
</dependencies>
<!-- La configuration du plugin doit contenir les informations d'accès à la base de données -->
<configuration>
<driver>
org.h2.Driver</driver>
<url>
jdbc:h2:target/data;MODE=PostgreSql;DB_CLOSE_DELAY=-1</url>
<username>
sa</username>
<password>
sa</password>
</configuration>
<executions>
<!-- on pourrait paramétrer plusieurs phases, ici on en crée une qui va exécuter le script de création -->
<execution>
<id>
create-db</id>
<phase>
process-test-resources</phase>
<goals>
<goal>
execute</goal>
</goals>
<configuration>
<autocommit>
true</autocommit>
<srcFiles>
<srcFile>
src/test/resources/schema.sql</srcFile>
</srcFiles>
</configuration>
</execution>
</executions>
</plugin>
Désormais vous pouvez lancer votre application avec mvn tomcat7:run et une datasource JNDI accessible dans votre application.
IV-B. Jetty▲
Avec Jetty les étapes sont relativement semblables. Tout d'abord nous créons un fichier qui contient la définition de la datasource : jetty-ds.xml
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC
"-//Mort Bay Consulting//DTD Configure//EN"
"http://jetty.mortbay.org/configure.dtd"
>
<Configure
id
=
"DS"
class
=
"org.eclipse.jetty.webapp.WebAppContext"
>
<New
class
=
"org.eclipse.jetty.plus.jndi.Resource"
>
<Arg><Ref
id
=
"DS"
/></Arg>
<Arg>
jdbc/myDb</Arg>
<Arg>
<New
class
=
"org.h2.jdbcx.JdbcDataSource"
>
<Set
name
=
"uRL"
>
jdbc:h2:target/data;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;</Set>
<Set
name
=
"user"
>
sa</Set>
<Set
name
=
"password"
>
sa</Set>
</New>
</Arg>
</New>
</Configure>
Puis nous faisons référence à cette datasource dans le pom.xml :
<plugin>
<groupId>
org.mortbay.jetty</groupId>
<artifactId>
jetty-maven-plugin</artifactId>
<version>
8.1.5.v20120716</version>
<configuration>
<webAppConfig>
<jettyEnvXml>
src/test/resources/jetty-ds.xml</jettyEnvXml>
</webAppConfig>
</configuration>
<dependencies>
<dependency>
<groupId>
com.h2database</groupId>
<artifactId>
h2</artifactId>
<version>
1.3.170</version>
</dependency>
</dependencies>
</plugin>
Le lecteur averti aura remarqué en exécutant mvn jetty:run que l'exemple ne fonctionne pas et que l'on obtient l'erreur suivante : org.h2.jdbc.JdbcSQLException: Database may be already in use: "Locked by another process". Possible solutions: close all other connection(s); use the server mode [90020-170]
Effectivement après pas mal de tentatives je n'ai pas réussi à faire fonctionner H2 et le plugin Jetty ensemble. Cependant le code source ainsi que l'article sont disponibles sous GitHub donc n'hésitez pas à faire un pull-request si vous parvenez à trouver la solution ;)
V. Débogage▲
Étape inévitable en cours de développement, le débogage avec un conteneur nécessite en général de rajouter des options de lancement dans les propriétés systèmes utilisées au lancement. Les options en questions sont les suivantes :
-Xrunjdwp:transport
=
dt_socket,server
=
y,suspend
=
y,address
=
8000
Et là, bonne nouvelle, pas besoin de modifier les scripts Tomcat avec Maven, il vous suffit de lancer la commande habituelle mais avec le script mvnDebug et non mvn. mvnDebug est souvent oublié, il s'agit du script de lancement de Maven avec les bonnes propriétés de débogage déjà positionnées.
Récapitulons, si je veux lancer mon appli en debug :
mvnDebug tomcat7:run ou mvnDebug jetty:run
Puis dans Eclipse :
- Debug As ;
- => run configurations ;
- => créer une nouvelle configuration Remote Debug ;
- choisir le projet et le port 8000 ;
- cliquer sur Run.
VI. Jouez vos tests d'intégration▲
Vous avez l'habitude de faire des tests unitaires ? C'est très bien, je vous en félicite. Mais testez-vous ensuite vos services sur Tomcat en intégration continue ?
Bien souvent une fois les tests unitaires passés on voit le schéma suivant :
- packaging de l'application au format war ;
- installation manuelle ;
- test manuel par une équipe de QA sur le war déployé.
Et pourquoi ne pas automatiser tout ça et le lancer à chaque build ? (*)
(*) En pratique les tests d'intégration peuvent prendre du temps. Pour éviter de casser votre belle dynamique de test avec des feedbacks rapides vous pouvez aussi splitter TU et TI. Les tests d'intégration joués à part permettent aux TU de renvoyer plus rapidement un résultat.
Pour cela nous allons utiliser le plugin maven-failsafe qui va nous permettre de configurer la phase integration-test pour lancer tous les tests de nom : **/IT*.java, **/*IT.java, et **/*ITCase.java
Vous vous demandez la différence entre maven-surefire et maven-failsafe ? Dites-vous que les deux jouent le même rôle, celui de lancer vos tests. Sauf que maven-failsafe utilise d'autres conventions de nommage pour les tests à lancer et vous facilite ainsi la séparation entre tests unitaires et tests d'intégration.
<plugin>
<groupId>
org.apache.maven.plugins</groupId>
<artifactId>
maven-failsafe-plugin</artifactId>
<version>
2.13</version>
<configuration>
</configuration>
<executions>
<execution>
<id>
failsafe-integration-tests</id>
<phase>
integration-test</phase>
<goals>
<goal>
integration-test</goal>
</goals>
</execution>
<execution>
<id>
failsafe-verify</id>
<phase>
verify</phase>
<goals>
<goal>
verify</goal>
</goals>
</execution>
</executions>
</plugin>
Ensuite nous allons configurer les plugin Tomcat et Jetty pour démarrer avant les tests d'intégration.
Pour être capable de conserver les configurations Jetty et Tomcat dans le même POM, j'ai dû utiliser des profils Maven afin de conserver le choix du conteneur utilisé en test d'intégration. L'utilisation de profil n'est pas obligatoire, elle ne le devient que si vous tenez à laisser le choix d'utilisation des deux.
Les deux plugins utilisent la même méthode, il faut se binder(s'attacher) sur les phases d'intégration pour démarrer le conteneur avant les tests et l'éteindre ensuite.
VI-A. Tomcat▲
<plugin>
<groupId>
org.apache.tomcat.maven</groupId>
<artifactId>
tomcat7-maven-plugin</artifactId>
<executions>
<execution>
<id>
run-tomcat</id>
<phase>
pre-integration-test</phase>
<goals>
<goal>
run</goal>
</goals>
</execution>
<execution>
<id>
stop-tomcat</id>
<phase>
post-integration-test</phase>
<goals>
<goal>
shutdown</goal>
</goals>
</execution>
</executions>
</plugin>
VI-B. Jetty▲
<plugin>
<groupId>
org.mortbay.jetty</groupId>
<artifactId>
jetty-maven-plugin</artifactId>
<executions>
<execution>
<id>
run-jetty</id>
<phase>
pre-integration-test</phase>
<goals>
<goal>
run</goal>
</goals>
</execution>
<execution>
<id>
stop-jetty</id>
<phase>
post-integration-test</phase>
<goals>
<goal>
stop</goal>
</goals>
</execution>
</executions>
</plugin>
À noter cependant pour pouvoir appeler le goal stop sur le plugin Jetty, vous devrez avoir ajouté les clés suivantes dans la configuration du plugin :
<stopKey>
key</stopKey>
<stopPort>
8087</stopPort>
Désormais si vous lancez le build avec la commande : mvn verify -Pjetty ou mvn verify -Ptomcat, vous devriez voir le démarrage de votre serveur avant la phase de tests d'intégration.
Ici avec Jetty :
À vous de jouer maintenant pour coder un test d'intégration qui profite de cette astuce.
VII. Précompilation des JSP▲
Mettons cette fois que votre objectif ne soit plus uniquement le développement mais le lancement de votre application en production. Voici quelques astuces intéressantes.
Vous l'aurez sans doute remarqué, la première fois que vous arrivez sur une page après avoir relancé votre Tomcat celle-ci est plus lente à s'afficher. C'est parce que Tomcat compile vos JSP en servlets lors de leur première visite. En plus d'être relativement désagréable pour vos utilisateurs, c'est aussi un peu tardif pour découvrir des erreurs de compilations, vous ne trouvez pas ?
Application du principe fail fast, on va mettre en place la précompilation des JSP.
VII-A. Tomcat▲
Première étape, déclarez le plugin de compilation de JSP :
<plugin>
<groupId>
org.codehaus.mojo</groupId>
<artifactId>
jspc-maven-plugin</artifactId>
<executions>
<execution>
<id>
jspc</id>
<goals>
<goal>
compile</goal>
</goals>
</execution>
</executions>
</plugin>
Si vous compilez votre application vous remarquerez que désormais les JSP sont compilées pendant la phase de compilation (logique c'est vrai, mais ce n'était pas le cas sans ce plugin) :
Cette étape a pour but de précompiler chaque JSP en une classe de Servlet. Les résultats de cette opération sont :
- l'ensemble de vos servlets compilés dans le répertoire target/classes ;
- un fichier jspweb.xml qui dérive de votre fichier web.xml initial et qui contient en plus la déclaration de chaque servlet.
Il faut donc modifier la configuration du plugin war pour qu'il utilise le fichier jspweb.xml construit par le plugin de compilation des JSP.
<plugin>
<groupId>
org.apache.maven.plugins</groupId>
<artifactId>
maven-war-plugin</artifactId>
<version>
2.0</version>
<configuration>
<webXml>
${basedir}/target/jspweb.xml</webXml>
</configuration>
</plugin>
Je vous laisse observer le contenu du war afin de vérifier qu'il contient bien la servlet déjà compilée et le fichier web.xml avec la déclaration attendue.
VII-B. Jetty▲
Le principe pour Jetty est identique, vous pouvez donc vous reporter au paragraphe précédent pour comprendre le fonctionnement.
Spécifiquement, voici la configuration du plugin de compilation :
<plugin>
<groupId>
org.mortbay.jetty</groupId>
<artifactId>
jetty-jspc-maven-plugin</artifactId>
<version>
8.1.5.v20120716</version>
<executions>
<execution>
<id>
jspc</id>
<goals>
<goal>
jspc</goal>
</goals>
</execution>
</executions>
</plugin>
Et le plugin war modifié (attention, le plugin Jetty crée un fichier web.xml et le plugin Tomcat crée un fichier jspweb.xml !!)
<plugin>
<groupId>
org.apache.maven.plugins</groupId>
<artifactId>
maven-war-plugin</artifactId>
<version>
2.0</version>
<configuration>
<webXml>
${basedir}/target/web.xml</webXml>
</configuration>
</plugin>
Astuce : cette étape étant coûteuse en temps, vous pouvez la déplacer dans un profil pour ne pas la jouer en phase de développement.
VIII. Ressources▲
- Code de l'article sous GitHub : https://github.com/hlassiege/maven-tomcat-jetty
- L'article sous GitHub : https://github.com/hlassiege/art-maven-tomcat-jetty
- Le très bon blog de Khan (jetoile) qui parle aussi de Maven : http://blog.jetoile.fr/
- Un article sur le débogage avec Eclipse et une appli Java sur developpez.com : https://aldian.developpez.com/cours/le-debogage-en-java-javaee/
IX. Conclusion▲
Voilà, nous avons fait le tour de quelques plugins Maven nécessaires pour améliorer notre productivité.
- Nous avons mis en œuvre le « checkout and run » afin que chaque nouvel entrant sur le projet puisse rapidement démarrer.
- Nous avons centralisé des configurations dans notre pom.xml pour éviter à chacun de refaire son intégration et pour éviter aussi le syndrome « homme-clé », le type seul capable de monter votre environnement de travail.
- Nous savons comment déboguer, jouer nos tests.
- Et nous avons même vu une astuce bien pratique pour nos performances en production avec la précompilation des JSP.
Cet article sera suivi par d'autres dans la même veine expliquant notamment comment faire du SOA ou du développement JavaScript avec Maven. Stay tuned.
Vos retours nous aident à améliorer nos publications. N'hésitez donc pas à commenter cet article sur le forum : 11 commentaires
X. Remerciements▲
Et pour vraiment conclure je tiens à remercier keulkeul, thierryler et Khanh Tuong Maudoux pour leur relecture technique ainsi que f-leb pour la relecture orthographique.