Tutoriel pour apprendre à embarquer Jetty dans une application web Java

Antoine Rey

Cet article explique pas à pas comment embarquer un conteneur Jetty dans sa propre application. Nul besoin d'utiliser Spring ou Scala.

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Tutoriel pour apprendre à embarquer Jetty dans une application web Java

- Auteur : Antoine Rey

- Parution : 4 mai 2016

- Licence : Copyright ® 2016 Antoine Rey. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Code source

- III. Dépendances Maven

- IV. Démarrer un Jetty

- V. Arrêter proprement Jetty

- VI. Création du package

- VI-A. 1. Appassembler

- VI-B. 2. Assembly

- VII. Conclusion

- VIII. Remerciements

I - Introduction

Une fois le développement d'une application web terminé, vient le moment (douloureux ou non) de son installation sur un serveur. En général, plusieurs prérequis sont nécessaires : JRE, serveur d'application, base de données… Aujourd'hui, Docker et/ou des outils comme Ansible et Puppet facilitent le provisionning du middleware. Néanmoins, il est possible de simplifier encore davantage cette phase d'installation. Des applications comme Sonar et Jenkins le font depuis des années : packager l'application avec son propre conteneur de Servlets et sa propre base de données. Afin de pouvoir déployer des applications les plus légères possible, les architectures microservices poussent dans ce sens. Et c'est d'ailleurs ce que proposent des frameworks comme Play Framework et Spring Boot. Ce dernier permet en effet de créer un JAR exécutable démarrant au choix un Tomcat ou un Jetty.

Pour distribuer votre application web, vous aurez le choix entre :

1. Une archive ZIP contenant JAR, scripts shells et fichiers de configuration ;

2. Un unique JAR auto-exécutable.

Le packaging est assuré par différents plugins Maven.

Disposer d'une JVM et le seul prérequis. Sachant qu'OpenJDK est installé sur la plupart des distributions Linux, ce n'est pas nécessairement une contrainte. Seule la version de Java devra être vérifiée avec soin.

II - Code source

Le code source utilisé pour illustrer ce billet provient du projet embedded-jetty-webapp hébergé sur GitHub. Pour des raisons de lisibilité, certaines parties ont été simplifiées.

Si vous souhaitez rendre autonome votre propre application, je vous conseille de vous inspirer directement du code disponible sur GitHub ( pom.xml maven et classes Java).

III - Dépendances Maven

Avant de pouvoir utiliser l'API de Jetty pour démarrer/arrêter un serveur, il faut tout d'abord tirer toutes les dépendances nécessaires au fonctionnement d'une application web. Voici la configuration Maven :

1. <properties>

2. <version.javax-servlet>3.1.0</version.javax-servlet>

3. <version.jetty>9.2.7.v20150116</version.jetty>

4. </properties>

1. <!-- Jetty -->

2. <dependency>

3. <groupId>org.eclipse.jetty</groupId>

4. <artifactId>jetty-server</artifactId>

5. <version>${version.jetty}</version>

6. </dependency>

7. <dependency>

8. <groupId>org.eclipse.jetty</groupId>

9. <artifactId>jetty-webapp</artifactId>

10. <version>${version.jetty}</version>

11. </dependency>

12. <dependency>

13. <groupId>org.eclipse.jetty</groupId>

14. <artifactId>jetty-servlet</artifactId>

15. <version>${version.jetty}</version>

16. </dependency>

17. <dependency>

18. <groupId>org.eclipse.jetty</groupId>

19. <artifactId>jetty-util</artifactId>

20. <version>${version.jetty}</version>

21. </dependency>

22. <dependency>

23. <groupId>org.eclipse.jetty</groupId>

24. <artifactId>jetty-servlets</artifactId>

25. <version>${version.jetty}</version>

26. </dependency>

27. <dependency>

28. <groupId>org.eclipse.jetty</groupId>

29. <artifactId>jetty-jsp</artifactId>

30. <version>${version.jetty}</version>

31. </dependency>

32. <dependency>

33. <groupId>org.eclipse.jetty</groupId>

34. <artifactId>jetty-http</artifactId>

35. <version>${version.jetty}</version>

36. </dependency>

38. <!--  Servlet API -->

39. <dependency>

40. <groupId>javax.servlet</groupId>

41. <artifactId>javax.servlet-api</artifactId>

42. <version>${version.javax-servlet}</version>

43. </dependency>

Comme vous pouvez le constater, Jetty est particulièrement modulaire. Si vous utilisez JSP comme technologie de rendu, il faudra ajouter l'artefact jetty-jsp sous peine du message d'erreur « JSP support not configured ».

IV - Démarrer un Jetty

Manipuler l'API Jetty pour démarrer un conteneur de servlet depuis une classe Main ne présente pas de difficulté :

1. public static void main(String[] args) throws Exception {

3. Server server = new Server(8080);

5. WebAppContext root = new WebAppContext();

6. root.setContextPath("/");

7. root.setDescriptor("webapp/WEB-INF/web.xml");

8. URL webAppDir = Thread.currentThread().getContextClassLoader().getResource("webapp");

9. if (webAppDir == null) {

10. throw new RuntimeException("No webapp directory was found into the JAR file");

11. }

12. root.setResourceBase(webAppDir.toURI().toString());

13. root.setParentLoaderPriority(true);

15. server.setHandler(root);

16. server.start();

17. }

Une première subtilité réside dans l'utilisation du ClassLoader du thread courant. Sans quoi, en dehors d'un IDE, le répertoire webapp ne sera pas trouvé.

La seconde subtilité vient du fait que l'artefact construit est de type JAR et non un WAR. Bien qu'elle y ressemble, l'arborescence du projet n'est donc pas celle d'un WAR.

Le répertoire webapp ne se trouve pas dans le répertoire src/main/webapp, mais dans src/main/resources/webapp. Ainsi, lors de la construction du JAR, le répertoire webapp sera copié à la racine du JAR sans configuration maven particulière.

Dans notre exemple, l'application web utilise un descripteur de déploiement web.xml. Optionnel depuis Servlet 3.0, l'appel à la méthode setDescriptor est facultatif.

Enfin, le port HTTP utilisé dans notre exemple est le 8080. Ce dernier aurait pu être passé en paramètre du main() ou bien chargé depuis un fichier de configuration.

Lors de l'appel à la méthode start(), le conteneur Jetty démarre. L'application web est ensuite aussitôt démarrée. Il n'y a pas réellement de phase de déploiement.

V - Arrêter proprement Jetty

Pour arrêter le serveur, une solution peu recommandée est d'utiliser un kill -9 sur le PID du process Java. Les traitements en cours s'arrêtent brutalement et les ressources ne sont pas correctement libérées.

Une solution plus élégante est de demander au serveur Jetty de s'arrêter proprement. Le contexte de servlets est alors fermé par Jetty. Les listeners JEE implémentant l'interface ServletContextListener en sont notifiés.

Pour communiquer avec Jetty, une solution possible est d'utiliser un socket TCP. Je me suis grandement inspiré du code Java utilisé par le plugin Jetty pour maven.

Le principe est simple, un thread Monitor est démarré à la suite du serveur Jetty, et ceci dans la même JVM :

1. server.start();

3. Monitor monitor = new Monitor(8090, new Server[] {server});

4. monitor.start();

5. server.join();

Ce thread démarre un SocketServer écoutant sur le port 8090. Il attend que l'instruction stop lui soit envoyée.

Pour davantage de détails, vous pouvez vous reporter à la méthode statique stop de la classe JettyServer ainsi qu'à la classe Monitor.

Une autre technique serait d'utiliser JMX pour communiquer avec Jetty. L'ajout du module jetty-jmx est alors nécessaire.

VI - Création du package

Comme je vous l'indiquais en introduction, je vous propose de packager votre application web de deux manières différentes.

VI-A - 1. Appassembler

Le plugin Appassembler pour maven permet de créer un répertoire target/appass__embler qu'il suffit de copier/coller pour installer l'application. Ce dernier contient trois sous-répertoires :

1. bin : scripts start.sh, start.bat, stop.sh et stop.bat permettant de démarrer/arrêter la webapp. Ces scripts se chargent de trouver le JRE, sont compatibles avec cygwin et positionnent le classpath ;

2. conf : facultatif, ce répertoire contient la configuration de l'application (fichiers properties ou YAML, logback.xml…) ;

3. lib : tous les JAR nécessaires au fonctionnement de l'application.

Activé par défaut, le profile maven appassembler regroupe la configuration nécessaire :

1. <profile>

2. <id>appassembler</id>

3. <activation>

4. <activeByDefault>true</activeByDefault>

5. </activation>

6. <build>

7. <plugins>

8. <!-- Generate both Windows and Linux bash shell execution scripts -->

9. <plugin>

10. <groupId>org.codehaus.mojo</groupId>

11. <artifactId>appassembler-maven-plugin</artifactId>

12. <version>${version.plugin.appassembler-maven-plugin}</version>

13. <executions>

14. <execution>

15. <goals>

16. <goal>assemble</goal>

17. </goals>

18. </execution>

19. </executions>

20. <configuration>

21. <repositoryLayout>flat</repositoryLayout>

22. <useWildcardClassPath>true</useWildcardClassPath>

23. <!-- Set the target configuration directory to be used in the bin scripts -->

24. <configurationDirectory>conf</configurationDirectory>

25. <!-- Copy the contents from "/src/main/config" to the target

26. configuration directory in the assembled application -->

27. <copyConfigurationDirectory>true</copyConfigurationDirectory>

28. <!-- Include the target configuration directory in the beginning of

29. the classpath declaration in the bin scripts -->

30. <includeConfigurationDirectoryInClasspath>true</includeConfigurationDirectoryInClasspath>

31. <!-- Extra JVM arguments that will be included in the bin scripts -->

32. <extraJvmArguments>-Xmx1024m</extraJvmArguments>

34. <programs>

35. <program>

36. <id>start</id>

37. <mainClass>com.javaetmoi.jetty.JettyServer</mainClass>

38. <name>start</name>

39. </program>

40. <program>

41. <id>stop</id>

42. <mainClass>com.javaetmoi.jetty.Stop</mainClass>

43. <name>stop</name>

44. </program>

45. </programs>

46. <binFileExtensions>

47. <unix>.sh</unix>

48. </binFileExtensions>

49. </configuration>

50. </plugin>

51. </plugins>

52. </build>

53. </profile>

Voici les commandes à exécuter pour tester ce type de packaging :

1. git clone git://github.com/arey/embedded-jetty-webapp.git

2. cd embedded-jetty-webapp

3. mvn clean install

4. target/appassembler/bin/start.sh &

5. curl http://localhost:8080/HelloWorld

6. target/appassembler/bin/stop.sh

VI-B - 2. Assembly

L'une des fonctionnalités offertes par le plugin Assembly pour Maven est de rassembler tous les JAR d'une application en un seul gros JAR couramment suffixé par jar-with-dependencies (exemple : jetty-webapp-1.0.0-SNAPSHOT-jar-with-dependencies.jar). Afin de rendre ce JAR auto-exécutable, sa class main doit être spécifiée dans son manifeste.

Voici la configuration du profile maven flatjar :

1. <profile>

2. <id>fatjar</id>

3. <build>

4. <plugins>

5. <plugin>

6. <artifactId>maven-assembly-plugin</artifactId>

7. <version>${version.plugin.maven-assembly-plugin}</version>

8. <configuration>

9. <descriptorRefs>

10. <descriptorRef>jar-with-dependencies</descriptorRef>

11. </descriptorRefs>

12. <archive>

13. <manifest>

14. <mainClass>com.javaetmoi.jetty.JettyServer</mainClass>

15. </manifest>

16. </archive>

17. </configuration>

18. <executions>

19. <execution>

20. <id>make-assembly</id>

21. <phase>package</phase>

22. <goals>

23. <goal>single</goal>

24. </goals>

25. </execution>

26. </executions>

27. </plugin>

28. </plugins>

29. </build>

30. </profile>

Voici les commandes à exécuter pour tester ce type de packaging :

1. git clone git://github.com/arey/embedded-jetty-webapp.git

2. cd embedded-jetty-webapp

3. mvn clean install -Pflatjar

4. java -jar target/jetty-webapp-1.0.0-SNAPSHOT-jar-with-dependencies.jar &

5. curl http://localhost:8080/HelloWorld

6. java -cp target/jetty-webapp-1.0.0-SNAPSHOT-jar-with-dependencies.jar com.javaetmoi.jetty.Stop

VII - Conclusion

Par cet article, j'espère vous avoir convaincu de la facilité d'embarquer Jetty dans n'importe quelle application web. Tomcat s'intègre d'une manière similaire.

Avec cette approche, la mise à jour de Jetty ne nécessite qu'une simple montée de version de Jetty dans le pom.xml.

Autre atout : l'exécution d'un Jetty au démarrage de son application est profitable lors du développement. En effet, il n'est plus nécessaire d'installer et/ou d'utiliser le moindre plugin dans son IDE. L'application web est démarrée par un simple Run ou Debug sur la classe Main.

Références :

- Appassembler maven plugin

- Eclipse Maven Jetty Plugin

- Apache Assembly Maven Plugin

- WAR-less Java Web Apps de James Ward

VIII - Remerciements

Cet article a été publié avec l'aimable autorisation d'Antoine Rey. L'article original (Embarquer Jetty dans une web app) a été rédigé par Antoine Rey.

Nous tenons à remercier Claude Leloup pour la relecture orthographique attentive de cet article et Mickael Baron pour la mise au gabarit.