Tutoriel pour réduire l'empreinte serveur d'une API REST en JAVA en la compilant en code natif avec Quarkus

François-Xavier Robin

Ce tutoriel Quarkus-JPA-PostgreSQL met en œuvre :

- une API Rest partielle (GET) avec JAX-RS et Quarkus sur une source de données JPA ;

- des tests unitaires ;

- des tests d'intégration au niveau API (http) avec un PostGreSQL lancé par un plugin maven Docker ;

- une distribution native, compilée avec GraalVM et une image docker de l'application compilée.

Réalisé sous Linux Mint 19 mais devrait convenir à de nombreuses distributions, voire à Windows.

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

- Titre : Tutoriel pour réduire l'empreinte serveur d'une API REST en JAVA en la compilant en code natif avec Quarkus

- Auteur : François-Xavier Robin

- Parution : 13 mai 2020

- Licence : Copyright ® 2020 François-Xavier Robin. 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. Choix techniques

- II. Qu'est-ce que Quarkus

- III. Structure globale du projet

- IV. Maven et son pom.xml

- V. Les plugins de build

- VI. Un simple /ping

- VII. Compilation en binaire avec GraalVM

- VII-A. Installation de SDKMAN

- VII-B. Installation de GraalVM

- VII-C. Compilation en code natif

- VIII. Configuration de l'accès aux données

- VIII-A. Lancement de PostgreSQL via Docker

- VIII-B. Paramétrage de l'application

- IX. Des entités à rendre persistantes

- IX-A. VideoGame et Genre

- IX-B. Producers CDI et annotation

- IX-C. Le repository CRUD avec Spring Data JPA

- X. Le endpoint REST JAX-RS

- XI. Convertisseur générique pour les valeurs de l'enum

- XII. Tests

- XII-A. Tests unitaires

- XII-B. Tests d'intégration

- XIII. Version native et image Docker

- XIV. Health Check et Metrics

- XV. Conclusions

- XVI. Remerciements

I - Choix techniques

L'objectif de cet article est de faire tourner une API REST avec Quarkus fonctionnant avec :

- JAX-RS 2 : Avec RESTEasy et Jackson pour la sérialisation JSON ;

- CDI 2 : avec l'implémentation interne partielle de QUARKUS ;

- JPA 2 : avec Hibernate ;

- Bean Validation : avec Hibernate Validator ;

- Health Check et Metrics : avec SmallRye Health et SmallRye Metrics.

On équipera le projet de diverses bibliothèques pour accélérer le développement :

- Spring Data JPA : pour ses Repository CRUD JPA ;

- Lombok : pour réduire le boiler plate (> voir mon article sur Lombok) ;

- Open API avec Swagger 2 (mais ce n'est pas l'objet de ce tutoriel).

Nativement, Quarkus est fourni avec Google Guava, ce qui servira dans le cadre de ce tutoriel.

Le projet au complet est disponible sur GitHub.

II - Qu'est-ce que Quarkus

Sur la page d'accueil de Quarkus, on peut lire :

Quarkus : Supersonic Subatomic Java A Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM, crafted from the best of breed Java libraries and standards.

En substance, c'est un framework constitué des meilleurs standards et bibliothèques Java pour réaliser des applications pour le cloud en mode REST.

Ses concurrents directs sont les fameux :

- Micronaut ;

- Thorntail ;

- SpringBoot (et toute la plateforme Spring) ;

- dans une moindre mesure, Payara-micro.

III - Structure globale du projet

Avant de commencer à entrer dans le détail des divers éléments, voici la structure du projet Maven :

[quarkus-tuto]
├── src
│   ├── main
│   │   ├── docker
│   │   │   ├── Dockerfile.jvm
│   │   │   └── Dockerfile.native
│   │   ├── java
│   │   │   └── fr
│   │   │       └── fxjavadevblog
│   │   │           └── qjg
│   │   │               ├── genre
│   │   │               │   ├── Genre.java
│   │   │               │   ├── GenreConverterProvider.java
│   │   │               │   └── GenreResource.java
│   │   │               ├── health
│   │   │               │   └── SimpleHealthCheck.java
│   │   │               ├── ping
│   │   │               │   └── PingService.java
│   │   │               ├── utils
│   │   │               │   ├── GenericEnumConverter.java
│   │   │               │   ├── InjectUUID.java
│   │   │               │   └── UUIDProducer.java
│   │   │               ├── videogame
│   │   │               │   ├── VideoGame.java
│   │   │               │   ├── VideoGameFactory.java
│   │   │               │   ├── VideoGameRepository.java
│   │   │               │   └── VideoGameResource.java
│   │   │               └── ApiDefinition.java
│   │   └── resources
│   │       ├── application.properties
│   │       └── import.sql
│   ├── test
│   │   ├── java
│   │   │   └── fr
│   │   │       └── fxjavadevblog
│   │   │           └── qjg
│   │   │               ├── global
│   │   │               │   └── TestingGroups.java
│   │   │               ├── ping
│   │   │               │   └── PingTest.java
│   │   │               └── utils
│   │   │                   ├── CDITest.java
│   │   │                   ├── DummyTest.java
│   │   │                   └── GenericEnumConverterTest.java
│   │   └── resources
│   │       └── application.properties
│   └── test-integration
│       ├── java
│       │   └── fr
│       │       └── fxjavadevblog
│       │           └── qjg
│       │               ├── utils
│       │               │   └── IT_DummyTest.java
│       │               └── videogame
│       │                   └── IT_VideoGameResource.java
│       └── resources
│           └── application.properties
├── target
│   ├── classes
│   │   ├── application.properties
│   │   └── import.sql
│   └── test-classes
│       └── application.properties
├── .dockerignore
├── README.md
└── pom.xml

La structure du projet se décompose ainsi :

- src/main : contient les sources Java main/java et les ressources pour Quarkus main\resources : application.properties et import.sql ;

- src/test : contient les tests unitaires test/java et les ressources pour les tests unitaires sans base de données PostgreSQL test\resources ;

- src/test-integration : contient les tests d'intégration test-integration/java et les ressources pour les tests d’intégration avec PostgreSQL test-integration\resources ;

- src/main/docker : contient les Dockerfile nécessaires à la génération de l'image conteneurisée de l'application.

La partie JAVA se décompose en cinq packages :

fxjavadevblog
└── qjg
├── genre
│   ├── Genre.java                    : enum qui contient tous les genres de jeux vidéo
│   ├── GenreConverterProvider.java   : fournisseur de conversion de Genre pour les paramètres JAX-RS
│   └── GenreResource.java            : point d’accès REST via JAX-RS aux genres de jeux vidéo
├── health
│   └── SimpleHealthCheck.java        : retour simple de Health Check (optionnel)
├── ping
│   └── PingService.java              : pour vérifier que JAX-RS est bien opérationnel
├── utils
│   ├── GenericEnumConverter.java     : convertisseur générique d’enum en Json
│   ├── InjectUUID.java               : annotation pour injecter des UUID sous forme de String
│   └── UUIDProducer.java             : générateur de UUID
├── videogame
│   ├── VideoGame.java                : classe métier, persistante via JPA (Hibernate)
│   ├── VideoGameFactory.java         : Factory de jeux video via CDI pour bénéficier de @InjectUUID en mode programmatique
│   ├── VideoGameRepository.java      : un repository CRUD JPA généré par Spring Data JPA
│   └── VideoGameResource.java        : le point d’accès REST via JAX-RS aux jeux vidéo
└── ApiDefinition.java                : pour les informations de l’API via Swagger

La partie tests unitaires est constituée des éléments suivants :

test
├── java
│   └── fr
│       └── fxjavadevblog
│           └── qjg
│               ├── global
│               │   └── TestingGroups.java   : définitions de constantes pour les groupes de tests JUnit 5
│               ├── ping
│               │   └── PingTest.java        : Vérifie que le “ping” fonctionne
│               └── utils
│                   ├── CDITest.java                    : permet de vérifier que CDI est opérationnel
│                   ├── DummyTest.java                  : un test vide
│                   └── GenericEnumConverterTest.java   : vérification de la conversion générique d’enum
└── resources
└── application.properties               : fichier de paramétrage de Quarkus spécifique pour les tests unitaires

DummyTest.java : un test vide afin de vérifier que les tests unitaires s'exécutent correctement (un métatest, lol)

IV - Maven et son pom.xml

D'abord il nous faut quelques paramétrages classiques Maven :

1. <project xmlns="http://maven.apache.org/POM/4.0.0"

2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

4. <modelVersion>4.0.0</modelVersion>

5. <groupId>fr.fxjavadevblog</groupId>

6. <artifactId>quarkus-tuto</artifactId>

7. <version>0.0.1-SNAPSHOT</version>

8. <name>Quarkus-JPA-PostgreSQL</name>

9. <properties>

10. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

11. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

12. <maven.compiler.target>1.8</maven.compiler.target>

13. <maven.compiler.source>1.8</maven.compiler.source>

14. <lombok.version>1.18.12</lombok.version>

15. <quarkus-version>1.3.1.Final</quarkus-version>

16. <surefire-plugin.version>2.22.2</surefire-plugin.version>

17. </properties>

18. </project>

Pour ce tutoriel, on utilisera Java 8.

On ajoute les dépendances classiques :

1. <dependencies>

2. <dependency>

3. <groupId>org.projectlombok</groupId>

4. <artifactId>lombok</artifactId>

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

6. <scope>provided</scope>

7. </dependency>

8. </dependencies>

Pour utiliser Quarkus :

1. <dependencyManagement>

2. <dependencies>

3. <dependency>

4. <groupId>io.quarkus</groupId>

5. <artifactId>quarkus-bom</artifactId>

6. <version>${quarkus-version}</version>

7. <type>pom</type>

8. <scope>import</scope>

9. </dependency>

10. </dependencies>

11. </dependencyManagement>

puis :

1. <dependency>

2. <groupId>io.quarkus</groupId>

3. <artifactId>quarkus-spring-data-jpa</artifactId>

4. </dependency>

5. <dependency>

6. <groupId>io.quarkus</groupId>

7. <artifactId>quarkus-jackson</artifactId>

8. </dependency>

9. <dependency>

10. <groupId>io.quarkus</groupId>

11. <artifactId>quarkus-resteasy-jackson</artifactId>

12. </dependency>

13. <dependency>

14. <groupId>io.quarkus</groupId>

15. <artifactId>quarkus-jdbc-postgresql</artifactId>

16. </dependency>

17. <!-- OPEN API via Swagger 2 -->

18. <dependency>

19. <groupId>io.quarkus</groupId>

20. <artifactId>quarkus-smallrye-openapi</artifactId>

21. </dependency>

22. <!-- Health Check -->

23. <dependency>

24. <groupId>io.quarkus</groupId>

25. <artifactId>quarkus-smallrye-health</artifactId>

26. </dependency>

27. <!-- Metrics -->

28. <dependency>

29. <groupId>io.quarkus</groupId>

30. <artifactId>quarkus-smallrye-metrics</artifactId>

31. </dependency>

32. <!-- for testing -->

33. <dependency>

34. <groupId>io.quarkus</groupId>

35. <artifactId>quarkus-junit5</artifactId>

36. <scope>test</scope>

37. </dependency>

38. <!-- for testing -->

39. <dependency>

40. <groupId>io.rest-assured</groupId>

41. <artifactId>rest-assured</artifactId>

42. <scope>test</scope>

43. </dependency>

V - Les plugins de build

Attention, ils sont nombreux, mais ce n'est pas rare pour des projets Maven.

Il nous faut de quoi :

- générer tout ce qui est traité par Quarkus ;

- lancer les tests unitaires sans base de données ;

- démarrer notre base de données PostgreSQL avec Docker pendant les tests d'intégration JUnit 5. On est ainsi à mi-chemin entre des tests unitaires et des tests d'intégration. Je préfère cette solution plutôt que de mocker les tests. Cela nécessite évidemment que Docker soit installé sur l'environnement.

1. <build>

2. <plugins>

3. <plugin>

4. <artifactId>maven-compiler-plugin</artifactId>

5. <version>3.8.1</version>

6. </plugin>

7. <plugin>

8. <groupId>io.quarkus</groupId>

9. <artifactId>quarkus-maven-plugin</artifactId>

10. <version>${quarkus-version}</version>

11. <executions>

12. <execution>

13. <goals>

14. <goal>build</goal>

15. </goals>

16. </execution>

17. </executions>

18. </plugin>

19. <plugin>

20. <artifactId>maven-resources-plugin</artifactId>

21. <version>3.1.0</version>

22. <executions>

23. <execution>

24. <id>copy-resources</id>

25. <phase>pre-integration-test</phase>

26. <goals>

27. <goal>copy-resources</goal>

28. </goals>

29. <configuration>

30. <overwrite>true</overwrite>

31. <outputDirectory>${basedir}/target/test-classes</outputDirectory>

32. <resources>

33. <resource>

34. <directory>src/test-integration/resources</directory>

35. <filtering>true</filtering>

36. </resource>

37. </resources>

38. </configuration>

39. </execution>

40. </executions>

41. </plugin>

42. <!-- tests unitaires -->

43. <plugin>

44. <artifactId>maven-surefire-plugin</artifactId>

45. <version>${surefire-plugin.version}</version>

46. <configuration>

47. <excludes>

48. <exclude>**/IT_*.java</exclude>

49. </excludes>

50. <systemProperties>

51. <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>

52. </systemProperties>

53. <skipTests>${skip.surefire.tests}</skipTests>

54. </configuration>

55. </plugin>

56. <!-- tests d'integration -->

57. <plugin>

58. <groupId>org.apache.maven.plugins</groupId>

59. <artifactId>maven-failsafe-plugin</artifactId>

60. <version>${surefire-plugin.version}</version>

61. <executions>

62. <execution>

63. <goals>

64. <goal>integration-test</goal>

65. <goal>verify</goal>

66. </goals>

67. </execution>

68. </executions>

69. </plugin>

70. <plugin>

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

72. <artifactId>build-helper-maven-plugin</artifactId>

73. <version>3.1.0</version>

74. <executions>

75. <execution>

76. <id>add-integration-test-sources</id>

77. <phase>generate-test-sources</phase>

78. <goals>

79. <goal>add-test-source</goal>

80. </goals>

81. <configuration>

82. <sources>

83. <source>src/test-integration/java</source>

84. </sources>

85. </configuration>

86. </execution>

87. <execution>

88. <id>add-integration-test-resource</id>

89. <phase>generate-test-resources</phase>

90. <goals>

91. <goal>add-test-resource</goal>

92. </goals>

93. <configuration>

94. <resources>

95. <resource>

96. <directory>src/test-integration/resources</directory>

97. </resource>

98. </resources>

99. </configuration>

100. </execution>

101. </executions>

102. </plugin>

103. <plugin>

104. <groupId>io.fabric8</groupId>

105. <artifactId>docker-maven-plugin</artifactId>

106. <version>0.33.0</version>

107. <configuration>

108. <skip>${skip.integration.tests}</skip>

109. <images>

110. <image>

111. <name>postgres:12.2</name>

112. <alias>postgresql</alias>

113. <run>

114. <env>

115. <POSTGRES_USER>quarkus_tuto</POSTGRES_USER>

116. <POSTGRES_PASSWORD>quarkus_tuto</POSTGRES_PASSWORD>

117. <POSTGRES_DB>quarkus_tuto</POSTGRES_DB>

118. </env>

119. <ports>

120. <port>5432:5432</port>

121. </ports>

122. <log>

123. <prefix>PostgreSQL Server : </prefix>

124. <date>default</date>

125. <color>green</color>

126. </log>

127. <wait>

128. <tcp>

129. <mode>mapped</mode>

130. <ports>

131. <port>5432</port>

132. </ports>

133. </tcp>

134. <time>10000</time>

135. </wait>

136. </run>

137. </image>

138. </images>

139. </configuration>

140. <executions>

141. <execution>

142. <id>docker:start</id>

143. <phase>pre-integration-test</phase>

144. <goals>

145. <goal>stop</goal>

146. <goal>start</goal>

147. </goals>

148. </execution>

149. <execution>

150. <id>docker:stop</id>

151. <phase>post-integration-test</phase>

152. <goals>

153. <goal>stop</goal>

154. </goals>

155. </execution>

156. </executions>

157. </plugin>

158. </plugins>

159. </build>

Et enfin, pour atteindre le Graal du code Java compilé en binaire natif, il nous faut rajouter un petit profil Maven :

1. <profiles>

2. <profile>

3. <id>native</id>

4. <activation>

5. <property>

6. <name>native</name>

7. </property>

8. </activation>

9. <build>

10. <plugins>

11. <plugin>

12. <groupId>io.quarkus</groupId>

13. <artifactId>quarkus-maven-plugin</artifactId>

14. <version>${quarkus-plugin.version}</version>

15. <executions>

16. <execution>

17. <goals>

18. <goal>native-image</goal>

19. </goals>

20. <configuration>

21. <enableHttpUrlHandler>true</enableHttpUrlHandler>

22. </configuration>

23. </execution>

24. </executions>

25. </plugin>

26. <plugin>

27. <groupId>org.apache.maven.plugins</groupId>

28. <artifactId>maven-failsafe-plugin</artifactId>

29. <version>${surefire-plugin.version}</version>

30. <executions>

31. <execution>

32. <goals>

33. <goal>integration-test</goal>

34. <goal>verify</goal>

35. </goals>

36. <configuration>

37. <systemProperties>

38. <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>

39. </systemProperties>

40. </configuration>

41. </execution>

42. </executions>

43. </plugin>

44. </plugins>

45. </build>

46. </profile>

47. </profiles>

Et voilà, le pom.xml est entièrement configuré.

Il est temps de coder quelques classes Quarkus dans votre IDE favori.

VI - Un simple /ping

Pour vérifier que tout va bien, on va faire un simple endpoint HTTP Rest avec JAX-RS qui va répondre à /api/ping/v1.

1. package fr.fxjavadevblog.qjg.ping;

2. import javax.ws.rs.GET;

3. import javax.ws.rs.Path;

4. import javax.ws.rs.Produces;

5. /**

6. * Simple JAX-RS endoint to check if the application is running.

7. *

8. * @author François-Xavier Robin

9. *

10. */

11. @Path("/api/ping")

12. public class PingService

13. {

14. @Path("/v1")

15. @GET

16. @Produces("text/plain")

17. public String ping()

18. {

19. return "pong";

20. }

21. }

On compile et on lance Quarkus en mode DEV.

1. $ mvn clean compile quarkus:dev

2. ...

3. ... ( build maven ...)

4. ...

5. Listening for transport dt_socket at address: 5005

6. SLF4J: Class path contains multiple SLF4J bindings.

7. SLF4J: Found binding in [jar:file:/home/robin/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]

8. SLF4J: Found binding in [jar:file:/home/robin/.m2/repository/org/jboss/slf4j/slf4j-jboss-logging/1.2.0.Final/slf4j-jboss-logging-1.2.0.Final.jar!/org/slf4j/impl/StaticLoggerBinder.class]

9. SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.

10. SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]

11. 11:26:09.593 [Thread-20] DEBUG io.netty.util.internal.logging.InternalLoggerFactory - Using SLF4J as the default logging framework

12. 11:26:09.599 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - -Dio.netty.noUnsafe: false

13. 11:26:09.600 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - Java version: 8

14. 11:26:09.601 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - sun.misc.Unsafe.theUnsafe: available

15. 11:26:09.601 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - sun.misc.Unsafe.copyMemory: available

16. 11:26:09.602 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - java.nio.Buffer.address: available

17. 11:26:09.603 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - direct buffer constructor: available

18. 11:26:09.604 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - java.nio.Bits.unaligned: available, true

19. 11:26:09.605 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - jdk.internal.misc.Unsafe.allocateUninitializedArray(int): unavailable prior to Java9

20. 11:26:09.605 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - java.nio.DirectByteBuffer.<init>(long, int): available

21. 11:26:09.606 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent - sun.misc.Unsafe: available

22. 11:26:09.607 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent - -Dio.netty.tmpdir: /tmp (java.io.tmpdir)

23. 11:26:09.607 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent - -Dio.netty.bitMode: 64 (sun.arch.data.model)

24. 11:26:09.608 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent - -Dio.netty.maxDirectMemory: 3704094720 bytes

25. 11:26:09.608 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent - -Dio.netty.uninitializedArrayAllocationThreshold: -1

26. __  ____  __  _____   ___  __ ____  ______

27. --/ __ \/ / / / _ | / _ \/ //_/ / / / __/

28. -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \

29. --\___\_\____/_/ |_/_/|_/_/|_|\____/___/

30. 2020-04-02 11:26:08,775 WARN  [io.qua.agr.dep.AgroalProcessor] (build-13) The Agroal dependency is present but no JDBC datasources have been defined.

31. 11:26:09.608 [Thread-20] DEBUG io.netty.util.internal.CleanerJava6 - java.nio.ByteBuffer.cleaner(): available

32. 11:26:09.608 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent - -Dio.netty.noPreferDirect: false

33. 11:26:09.610 [Thread-20] DEBUG io.netty.channel.DefaultChannelId - -Dio.netty.processId: 12872 (auto-detected)

34. 11:26:09.611 [Thread-20] DEBUG io.netty.util.NetUtil - -Djava.net.preferIPv4Stack: false

35. 11:26:09.611 [Thread-20] DEBUG io.netty.util.NetUtil - -Djava.net.preferIPv6Addresses: false

36. 11:26:09.612 [Thread-20] DEBUG io.netty.util.NetUtil - Loopback interface: lo (lo, 0:0:0:0:0:0:0:1%lo)

37. 11:26:09.613 [Thread-20] DEBUG io.netty.util.NetUtil - /proc/sys/net/core/somaxconn: 128

38. 11:26:09.613 [Thread-20] DEBUG io.netty.channel.DefaultChannelId - -Dio.netty.machineId: 18:31:bf:ff:fe:17:85:36 (auto-detected)

39. 11:26:09.625 [main] DEBUG io.netty.util.ResourceLeakDetector - -Dio.netty.leakDetection.level: simple

40. 11:26:09.625 [main] DEBUG io.netty.util.ResourceLeakDetector - -Dio.netty.leakDetection.targetRecords: 4

41. 11:26:09.632 [main] DEBUG io.netty.util.internal.InternalThreadLocalMap - -Dio.netty.threadLocalMap.stringBuilder.initialSize: 1024

42. 11:26:09.632 [main] DEBUG io.netty.util.internal.InternalThreadLocalMap - -Dio.netty.threadLocalMap.stringBuilder.maxSize: 4096

43. 11:26:09.639 [main] DEBUG io.netty.channel.MultithreadEventLoopGroup - -Dio.netty.eventLoopThreads: 16

44. 11:26:09.648 [main] DEBUG io.netty.channel.nio.NioEventLoop - -Dio.netty.noKeySetOptimization: false

45. 11:26:09.649 [main] DEBUG io.netty.channel.nio.NioEventLoop - -Dio.netty.selectorAutoRebuildThreshold: 512

46. 11:26:09.649 [main] DEBUG io.netty.util.internal.PlatformDependent - org.jctools-core.MpscChunkedArrayQueue: available

47. 11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.numHeapArenas: 16

48. 11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.numDirectArenas: 16

49. 11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.pageSize: 8192

50. 11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.maxOrder: 1

51. 11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.chunkSize: 16384

52. 11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.tinyCacheSize: 512

53. 11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.smallCacheSize: 256

54. 11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.normalCacheSize: 64

55. 11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.maxCachedBufferCapacity: 32768

56. 11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.cacheTrimInterval: 8192

57. 11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.cacheTrimIntervalMillis: 0

58. 11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.useCacheForAllThreads: true

59. 11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.maxCachedByteBuffersPerChunk: 1023

60. 11:26:09.809 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.allocator.type: pooled

61. 11:26:09.809 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.threadLocalDirectBufferSize: 0

62. 11:26:09.809 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.maxThreadLocalCharBufferSize: 16384

63. 2020-04-02 11:26:09,816 INFO  [io.quarkus] (main) quarkus-tuto 0.0.1-SNAPSHOT (powered by Quarkus 1.3.1.Final) started in 1.415s. Listening on: http://0.0.0.0:8080

64. 2020-04-02 11:26:09,818 INFO  [io.quarkus] (main) Profile dev activated. Live Coding activated.

65. 2020-04-02 11:26:09,819 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, hibernate-orm-panache, hibernate-validator, jdbc-postgresql, narayana-jta, resteasy, resteasy-jsonb, spring-data-jpa, spring-di]

Quakus se lance en très peu de temps alors qu'il est simplement en mode JVM classique. Vivement le build GraalVM natif… patience.

On peut tester le service manuellement :

$ curl http://localhost:8080/api/ping/v1
pong

Si on modifie le code java et qu'on le sauvegarde, il se recompile automatiquement grâce au mode dev de Quarkus. Par exemple : on remplace return "pong"; par return "PONG"; et on sauvegarde le fichier.

$ curl http://localhost:8080/api/ping/v1
PONG

C'est vraiment très pratique ce rechargement à chaud (live reload) !

Attention avec Lombok toutefois, Quarkus ne semble pas relancer l'annotation processor et donc il ne génère pas le bytecode de Lombok. Lien vers cette anomalie : https://github.com/quarkusio/quarkus/issues/4224

VII - Compilation en binaire avec GraalVM

En prérequis, il faut s'assurer que GraalVM est bien installé.

Je vous conseille d'utiliser pour cela SDKMAN qui est une plate-forme pour gérer plusieurs outils de développement présents sur votre poste en plusieurs versions et vous permet de les activer simplement et rapidement, même le temps d'une session shell (terminal).

VII-A - Installation de SDKMAN

Rien de bien compliqué, sous Linux tout du moins :

$ curl -s "https://get.sdkman.io" | bash
$ source "$HOME/.sdkman/bin/sdkman-init.sh"
$ sdk version
SDKMAN 5.7.4+362

VII-B - Installation de GraalVM

Grâce à SDKMAN c'est vraiment simple :

1. $ sdk install java 19.3.1.r11-grl

2. Downloading: java 19.3.1.r11-grl

3. In progress...

4. 0%###################################################################### 100,0%

5. Repackaging Java 19.3.1.r11-grl...

6. Done repackaging...

7. Installing: java 19.3.1.r11-grl

8. Done installing!

9. Do you want java 19.3.1.r11-grl to be set as default? (Y/n): Y

10. Setting java 19.3.1.r11-grl as default.

Dans cet exemple, j'ai choisi de mettre GraalVM en version 19.3.1 et de le déclarer comme JDK par défaut.

GraalVM s'est installé dans le répertoire de SDKMAN /home/robin/.sdkman/candidates/java/19.3.1.r11-grl et tout a été linké correctement pour en faire le JDK par défaut.

1. $ echo $JAVA_HOME

2. /home/robin/.sdkman/candidates/java/current

3. $ ll /home/robin/.sdkman/candidates/java/current

4. lrwxrwxrwx 1 robin robin 50 avril  2 10:25 /home/robin/.sdkman/candidates/java/current -> /home/robin/.sdkman/candidates/java/19.3.1.r11-grl/

5. $ whereis java

6. java: /usr/bin/java /usr/lib/java /usr/share/java /home/robin/.sdkman/candidates/java/19.3.1.r11-grl/bin/java

Pour pouvoir compiler du code Java en code natif, il faut rajouter une variable d'environnement au fichier ~/.mavenrc (fichier à créer s'il n'existe pas).

export JAVA_HOME=/home/robin/.sdkman/candidates/java/current
export GRAALVM_HOME=$JAVA_HOME

En relançant un shell, on vérifie que tout est correctement affecté :

1. $ mvn -version

2. Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)

3. Maven home: /home/robin/.sdkman/candidates/maven/current

4. Java version: 11.0.6, vendor: Oracle Corporation, runtime: /home/robin/.sdkman/candidates/java/19.3.1.r11-grl

5. Default locale: fr_FR, platform encoding: UTF-8

6. OS name: "linux", version: "5.3.0-45-generic", arch: "amd64", family: "unix"

Ensuite il faut installer l'outil native-image :

1. $ gu install native-image

2. Downloading: Component catalog from www.graalvm.org

3. Processing Component: Native Image

4. Downloading: Component native-image: Native Image  from github.com

5. Installing new component: Native Image (org.graalvm.native-image, version 19.3.1)

Tout est prêt pour pouvoir compiler notre application en code natif.

VII-C - Compilation en code natif

Il suffit de lancer Maven avec le profil native qui est présent dans le pom XML. C'est un peu long, c'est normal, mais le résultat en vaut la chandelle.

1. $ mvn package -Pnative

2. ...

3. ...

4. ... (vous pouvez allez vous aérer la compilation est assez longue ...)

5. ...

6. ...

7. ...

8. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]    classlist:  10 076,23 ms

9. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]        (cap):   1 186,64 ms

10. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]        setup:   3 325,77 ms

11. 17:25:16,623 INFO  [org.hib.val.int.uti.Version] HV000001: Hibernate Validator 6.1.2.Final

12. 17:25:18,675 INFO  [org.jbo.threads] JBoss Threads version 3.0.1.Final

13. 17:25:42,598 INFO  [org.hib.Version] HHH000412: Hibernate ORM core version 5.4.12.Final

14. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]   (typeflow):  20 083,90 ms

15. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]    (objects):  16 238,17 ms

16. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]   (features):     742,27 ms

17. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]     analysis:  38 874,58 ms

18. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]     (clinit):     658,58 ms

19. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]     universe:   2 171,74 ms

20. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]      (parse):   2 662,69 ms

21. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]     (inline):   4 485,66 ms

22. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]    (compile):  29 844,86 ms

23. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]      compile:  39 558,51 ms

24. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]        image:   2 916,45 ms

25. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]        write:     817,69 ms

26. [quarkus-tuto-0.0.1-SNAPSHOT-runner:1965]      [total]:  98 480,45 ms

27. [INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 101129ms

28. [INFO] ------------------------------------------------------------------------

29. [INFO] BUILD SUCCESS

30. [INFO] ------------------------------------------------------------------------

31. [INFO] Total time:  01:51 min

32. [INFO] Finished at: 2020-04-02T17:26:40+02:00

33. [INFO] ------------------------------------------------------------------------

La construction du binaire natif a pris presque deux minutes !

Mais on va lancer l'application qui a été générée, classiquement, dans le répertoire target :

1. $ ./target/quarkus-tuto-0.0.1-SNAPSHOT-runner

2. __  ____  __  _____   ___  __ ____  ______

3. --/ __ \/ / / / _ | / _ \/ //_/ / / / __/

4. -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \

5. --\___\_\____/_/ |_/_/|_/_/|_|\____/___/

6. 2020-04-02 17:29:01,484 INFO  [io.quarkus] (main) quarkus-tuto 0.0.1-SNAPSHOT (powered by Quarkus 1.3.1.Final) started in 0.016s. Listening on: http://0.0.0.0:8080

7. 2020-04-02 17:29:01,484 INFO  [io.quarkus] (main) Profile prod activated.

8. 2020-04-02 17:29:01,484 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, hibernate-orm-panache, hibernate-validator, jdbc-postgresql, narayana-jta, resteasy, resteasy-jsonb, spring-data-jpa, spring-di]

Oui, c'est bien la réalité : notre application a démarré en 0.016 seconde !

Pour l'arrêter, il suffit de tuer le process ou d’effectuer un CTRL+C dans le terminal.

VIII - Configuration de l'accès aux données

VIII-A - Lancement de PostgreSQL via Docker

Dans la configuration Maven, on a paramétré une image Docker de PostgreSQL 12 qui se lance pendant la phase de tests d'intégration.

Pendant la phase de développement, il faut donc une instance de PostgreSQL qui va fournir la base de données de test. Je vais utiliser encore une fois Docker pour cela.

1. $ docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name quarkus_tuto -e POSTGRES_USER=quarkus_tuto -e POSTGRES_PASSWORD=quarkus_tuto -e POSTGRES_DB=quarkus_tuto -p 5432:5432 postgres:12.2

2. Unable to find image 'postgres:12.2' locally

3. 12.2: Pulling from library/postgres

4. c499e6d256d6: Pull complete

5. 67a768c93810: Pull complete

6. 3befaea70a64: Pull complete

7. b72dde2f70c9: Pull complete

8. 9af5f5958937: Pull complete

9. 79f4f06e2acc: Pull complete

10. bc35aa1d8687: Pull complete

11. 276504d44bd7: Pull complete

12. 56cfad4df2a4: Pull complete

13. 28bfa2f917aa: Pull complete

14. bbbebba2bc39: Pull complete

15. d2407cea5efb: Pull complete

16. 92dae474b380: Pull complete

17. c71da770d20d: Pull complete

18. Digest: sha256:d480b197ab8e01edced54cbbbba9707373473f42006468b60be04da07ce97823

19. Status: Downloaded newer image for postgres:12.2

20. The files belonging to this database system will be owned by user "postgres".

21. This user must also own the server process.

23. The database cluster will be initialized with locale "en_US.utf8".

24. The default database encoding has accordingly been set to "UTF8".

25. The default text search configuration will be set to "english".

27. Data page checksums are disabled.

29. fixing permissions on existing directory /var/lib/postgresql/data ... ok

30. creating subdirectories ... ok

31. selecting dynamic shared memory implementation ... posix

32. selecting default max_connections ... 100

33. selecting default shared_buffers ... 128MB

34. selecting default time zone ... Etc/UTC

35. creating configuration files ... ok

36. running bootstrap script ... ok

37. performing post-bootstrap initialization ... ok

38. syncing data to disk ... ok

39. initdb: warning: enabling "trust" authentication for local connections

40. You can change this by editing pg_hba.conf or using the option -A, or

41. --auth-local and --auth-host, the next time you run initdb.

42. Success. You can now start the database server using:

43. pg_ctl -D /var/lib/postgresql/data -l logfile start

44. waiting for server to start....2020-04-02 15:40:28.357 UTC [47] LOG:  starting PostgreSQL 12.2 (Debian 12.2-2.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit

45. 2020-04-02 15:40:28.360 UTC [47] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"

46. 2020-04-02 15:40:28.383 UTC [48] LOG:  database system was shut down at 2020-04-02 15:40:28 UTC

47. 2020-04-02 15:40:28.389 UTC [47] LOG:  database system is ready to accept connections

48. done

49. server started

50. CREATE DATABASE

51. /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*

52. 2020-04-02 15:40:28.603 UTC [47] LOG:  received fast shutdown request

53. waiting for server to shut down....2020-04-02 15:40:28.605 UTC [47] LOG:  aborting any active transactions

54. 2020-04-02 15:40:28.608 UTC [47] LOG:  background worker "logical replication launcher" (PID 54) exited with exit code 1

55. 2020-04-02 15:40:28.609 UTC [49] LOG:  shutting down

56. 2020-04-02 15:40:28.633 UTC [47] LOG:  database system is shut down

57. done

58. server stopped

59. PostgreSQL init process complete; ready for start up.

60. 2020-04-02 15:40:28.739 UTC [1] LOG:  starting PostgreSQL 12.2 (Debian 12.2-2.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit

61. 2020-04-02 15:40:28.740 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432

62. 2020-04-02 15:40:28.740 UTC [1] LOG:  listening on IPv6 address "::", port 5432

63. 2020-04-02 15:40:28.744 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"

64. 2020-04-02 15:40:28.764 UTC [65] LOG:  database system was shut down at 2020-04-02 15:40:28 UTC

65. 2020-04-02 15:40:28.770 UTC [1] LOG:  database system is ready to accept connections

Laissons PostgreSQL fonctionner dans son terminal.

VIII-B - Paramétrage de l'application

Il faut créer un fichier application.properties comme ressource du projet Maven dans la partie Java.

1. # CDI

2. quarkus.arc.remove-unused-beans=false

3. %dev.quarkus.smallrye-openapi.path=/openapi

4. %dev.quarkus.swagger-ui.always-include=true

5. %dev.quarkus.swagger-ui.path=/openapi-ui

6. # DEV with PostgreSQL

7. %dev.quarkus.datasource.db-kind=postgresql

8. %dev.quarkus.datasource.jdbc.url=jdbc:postgresql:quarkus_tuto

9. %dev.quarkus.datasource.username=quarkus_tuto

10. %dev.quarkus.datasource.password=quarkus_tuto

11. %dev.quarkus.datasource.jdbc.max-size=8

12. %dev.us.datasource.jdbc.min-size=2

13. %dev.quarkus.hibernate-orm.log.sql=false

14. %dev.quarkus.hibernate-orm.database.generation=drop-and-create

15. %dev.quarkus.hibernate-orm.sql-load-script=import.sql

16. %dev.quarkus.log.level=INFO

17. %dev.quarkus.log.category."org.hibernate".level=INFO

18. %dev.quarkus.log.category."fr.fxjavadevblog".level=DEBUG

19. # PROD

20. %prod.quarkus.datasource.db-kind=postgresql

21. %prod.quarkus.datasource.jdbc.url=jdbc:postgresql:quarkus_tuto

22. %prod.quarkus.datasource.username=quarkus_tuto

23. %prod.quarkus.datasource.password=quarkus_tuto

24. %prod.quarkus.hibernate-orm.database.generation=drop-and-create

25. %prod.quarkus.hibernate-orm.sql-load-script=import.sql

Remarque importante : quand on construit l'image native de l'application, Quarkus se met automatiquement en mode prod. Cela signifie que certains paramètres sont ignorés par défaut comme le drop-and-create et le sql-load-script. C'est une très bonne pratique, cependant, dans le cadre de ce tutoriel où les données ne persistent pas, je force, même en mode prod, la création de la base de données et l'import du script SQL. Dans le fichier ci-dessus, ce sont les lignes %prod.* qui s'activent en production AUSSI. Je le redis : à ne pas faire dans un vrai projet !

Pour les tests unitaires et les tests d'intégration, nous aurons donc des fichiers application.properties différents.

IX - Des entités à rendre persistantes

Bien évidemment, il nous faut au moins une classe persistante. J'ai repris ici des exemples d'un précédent article :

- VideoGame : classe persistante JPA ;

- Genre : une enum JAVA persistante sous forme de String ;

- Producers : des producers CDI pour les UUID qui serviront de @Id dans la classe persistante comme clef primaire ;

- VideoGameReposity : le CRUD issu de Spring Data JPA ;

- VideoGameFactory : de quoi créer des instances de la classe VideoGame en bénéficiant de l'injection automatique de UUID.

IX-A - VideoGame et Genre

1. package fr.fxjavadevblog.qjg.videogame;

3. import java.io.Serializable;

5. import javax.enterprise.context.Dependent;

6. import javax.inject.Inject;

7. import javax.persistence.Column;

8. import javax.persistence.Entity;

9. import javax.persistence.EnumType;

10. import javax.persistence.Enumerated;

11. import javax.persistence.Id;

12. import javax.persistence.Table;

13. import javax.persistence.Version;

14. import fr.fxjavadevblog.qjg.utils.InjectUUID;

15. import lombok.AccessLevel;

16. import lombok.EqualsAndHashCode;

17. import lombok.Getter;

18. import lombok.NoArgsConstructor;

19. import lombok.Setter;

20. import lombok.ToString;

21. @SuppressWarnings("serial")

22. // Lombok

23. @NoArgsConstructor(access = AccessLevel.PROTECTED)

24. @EqualsAndHashCode(of = "id")

25. @ToString(of = { "id", "name" })

26. // CDI Annotation

27. @Dependent

28. // JPA Annotation

29. @Entity

30. @Table(name = "VIDEO_GAME")

31. public class VideoGame implements Serializable

32. {

33. @Id

34. @Inject

35. @InjectUUID

36. @Getter

37. @Column(length = 36)

38. private String id;

39. @Getter

40. @Setter

41. @Column(name = "NAME", nullable = false, unique = true)

42. private String name;

43. @Getter

44. @Setter

45. @Enumerated(EnumType.STRING)

46. @Column(name = "GENRE", nullable = false)

47. private Genre genre;

48. @Version

49. @Getter

50. @Column(name = "VERSION")

51. private Long version;

52. }

1. /**

2. * Enumeration of genres of Video Games for Atari ST.

3. *

4. * @author François-Xavier Robin

5. *

6. */

7. public enum Genre

8. {

9. ARCADE, EDUCATION, FIGHTING, PINBALL, PLATFORM, REFLEXION, RPG, SHOOT_THEM_UP, SIMULATION, SPORT;

10. }

IX-B - Producers CDI et annotation

L'annotation @InjectUUID est utilisée sur la classe VideoGame.

1. package fr.fxjavadevblog.qjg;

2. import java.lang.annotation.ElementType;

3. import java.lang.annotation.Retention;

4. import java.lang.annotation.RetentionPolicy;

5. import java.lang.annotation.Target;

6. import javax.inject.Qualifier;

7. /**

8. * annotation to mark a field to be injected by CDI with a UUID.

9. *

10. * @author François-Xavier Robin

11. *

12. */

13. @Qualifier

14. @Retention(RetentionPolicy.RUNTIME)

15. @Target({ElementType.FIELD, ElementType.METHOD})

16. public @interface InjectUUID

17. {

18. }

Et son « traitement » par le Producer CDI suivant :

1. package fr.fxjavadevblog.qjg.utils;

2. import java.util.UUID;

3. import javax.enterprise.context.ApplicationScoped;

4. import javax.enterprise.inject.Produces;

5. @ApplicationScoped

6. public class Producers

7. {

8. /**

9. * produces randomly generated UUID for primary keys.

10. *

11. * @return UUID as a HEXA-STRING

12. *

13. */

14. @Produces

15. @InjectUUID

16. public String produceUUIDAsString()

17. {

18. return UUID.randomUUID().toString();

19. }

20. }

IX-C - Le repository CRUD avec Spring Data JPA

1. package fr.fxjavadevblog.qjg.videogame;

2. import java.util.List;

3. import org.springframework.data.repository.CrudRepository;

4. /**

5. * CRUD repository for the VideoGame class. Primary key is a UUID represented by a String.

6. * This repository is created by Hibernate Data JPA.

7. *

8. * @author François-Xavier Robin

9. *

10. */

11. public interface VideoGameRepository extends CrudRepository<VideoGame, String>

12. {

13. /**

14. * gets every Video Game filtered by the given Genre.

15. *

16. * @param genre

17. *    genre of video game

18. *    @see Genre

19. *

20. * @return

21. *    a list of video games

22. */

23. List<VideoGame> findByGenre(Genre genre);

24. }

X - Le endpoint REST JAX-RS

Et enfin de quoi servir le tout sur HTTP avec JAX-WS qui répond à l'URL /api/videogames/v1 :

1. package fr.fxjavadevblog.qjg.videogame;

2. import java.util.List;

3. import javax.ws.rs.BadRequestException;

4. import javax.ws.rs.GET;

5. import javax.ws.rs.Path;

6. import javax.ws.rs.PathParam;

7. import javax.ws.rs.Produces;

8. /**

9. * JAX-RS endpoint for Video Games.

10. *

11. * @author François-Xavier Robin

12. *

13. */

14. @Path("/api/videogames/v1")

15. public class VideoGameResource

16. {

17. private final VideoGameRepository videoGameRepository;

18. public VideoGameResource(VideoGameRepository videoGameRepository)

19. {

20. this.videoGameRepository = videoGameRepository;

21. }

22. @GET

23. @Produces("application/json")

24. public Iterable<VideoGame> findAll()

25. {

26. return videoGameRepository.findAll();

27. }

28. @GET

29. @Path("/genre/{genre}")

30. @Produces("application/json")

31. public List<VideoGame> findByGenre(@PathParam(value = "genre") String genre)

32. {

33. try

34. {

35. Genre convertedGenre = Genre.valueOf(genre.replace("-", "_").toUpperCase());

36. return videoGameRepository.findByGenre(convertedGenre);

37. }

38. catch (java.lang.IllegalArgumentException e)

39. {

40. throw new BadRequestException("Genre " + genre + "does not exist.");

41. }

42. }

43. }

Il suffit ensuite de déclencher la requête REST suivante pour obtenir tous les jeux vidéo :

1. $ curl http://localhost:8080//api/videogames/v1

2. [

3. {

4. "id": "896b9c77-4f6d-4bd6-b681-2791acfa0d51",

5. "name": "100 4 1",

6. "genre": "REFLEXION",

7. "version": 1

8. },

9. {

10. "id": "6bf157fa-bd95-46ce-bbca-58afb87ebb9b",

11. "name": "10TH FRAME",

12. "genre": "SPORT",

13. "version": 1

14. },

15. {

16. "id": "e603e430-0853-46b0-9f44-d4f662f56c51",

17. "name": "1943 : THE BATTLE OF MIDWAY",

18. "genre": "SHOOT_THEM_UP",

19. "version": 1

20. },

21. {

22. "id": "61ec5869-d9f5-497a-9ffc-8e3612892c4b",

23. "name": "1ST DIVISION MANAGER",

24. "genre": "SPORT",

25. "version": 1

26. },

27. {

28. "id": "ed1233c4-c130-49f4-b329-314a6dd957a3",

29. "name": "1ST SERVE TENNIS",

30. "genre": "SPORT",

31. "version": 1

32. },

33. {

34. "id": "85d071ca-95bc-488a-afdc-494c430f03d9",

35. "name": "20000 LEAGUES UNDER THE SEA",

36. "genre": "RPG",

37. "version": 1

38. },

39. ... etc ...

De la même manière, pour filtrer sur le genre de jeu :

1. $ curl http://localhost:8080/api/videogames/v1/genre/SHOOT_THEM_UP

2. [

3. {

4. "id": "e603e430-0853-46b0-9f44-d4f662f56c51",

5. "name": "1943 : THE BATTLE OF MIDWAY",

6. "genre": "SHOOT_THEM_UP",

7. "version": 1

8. },

9. {

10. "id": "fa02815b-f6d2-4ba1-9f63-5c8f575d06b6",

11. "name": "AIR SUPPLY",

12. "genre": "SHOOT_THEM_UP",

13. "version": 1

14. },

15. {

16. "id": "07638287-8f45-4be8-a887-c57f350a9ce7",

17. "name": "ALBEDO",

18. "genre": "SHOOT_THEM_UP",

19. "version": 1

20. },

21. {

22. "id": "56417168-1e57-4197-a490-56258e5405eb",

23. "name": "ALCON",

24. "genre": "SHOOT_THEM_UP",

25. "version": 1

26. },

27. {

28. "id": "d5bd6aaa-674d-4e7f-ae8e-53118de897c6",

29. "name": "ALIEN BLAST",

30. "genre": "SHOOT_THEM_UP",

31. "version": 1

32. },

33. {

34. "id": "2589d502-4619-4728-b688-9cece2a8a3ab",

35. "name": "ALIEN BUSTERS IV",

36. "genre": "SHOOT_THEM_UP",

37. "version": 1

38. }

39. ... etc ...

Cela fonctionne, mais je trouve que ce n'est pas satisfaisant concernant les URL d'invocation pour les genres, ainsi que le résultat JSON qui sérialise en majuscules les valeurs de l'enum. C'est normal, mais ce n'est pas très « HTTP Friendly ».

On va y remédier dans le paragraphe qui suit !

XI - Convertisseur générique pour les valeurs de l'enum

Pour résumer le problème, les URL d'appel ainsi que le contenu du retour JSON ne respectent pas les conventions classiques de nommage de REST.

En plus, les manipulations comme Genre.valueOf(genre.replace("-", "_").toUpperCase()); ne sont pas très jolies, à mon goût.

Ce que l'on souhaite pour les URL d'appel et les retours JSON :

- utiliser des « - » au lieu des « _ » pour séparer les mots clefs ;

- basculer tout en minuscules.

Vous pouvez retrouver ces recommandations ici : https://restfulapi.net/resource-naming/

Exemple : l'appel de /api/videogames/v1/genre/shoot-them-up doit retourner :

1. [

2. ...

3. ...

4. {

5. "id": "d5bd6aaa-674d-4e7f-ae8e-53118de897c6",

6. "name": "ALIEN BLAST",

7. "genre": "shoot-them-up",

8. "version": 1

9. },

10. ...

11. ...

12. ]

Mais, on ne veut pas toucher aux conventions de nommage de l'enum Genre ! C'est du Java et on doit pouvoir garder les choses ainsi !

Il y a plein de solutions pour cela. Celle que je vais privilégier est l'usage de l'annotation @JsonProperty de Jackson que l'on va placer sur chacune des valeurs de l'enum :

1. public enum Genre

2. {

3. @JsonProperty(value = "arcade")

4. ARCADE,

6. @JsonProperty(value = "education")

7. EDUCATION,

8. @JsonProperty(value = "fighting")

9. FIGHTING,

10. @JsonProperty(value = "pinball")

11. PINBALL,

12. @JsonProperty(value = "platform")

13. PLATFORM,

14. @JsonProperty(value = "reflexion")

15. REFLEXION,

16. @JsonProperty(value = "rpg")

17. RPG,

18. @JsonProperty(value = "shoot-them-up")

19. SHOOT_THEM_UP,

20. @JsonProperty(value = "simulation")

21. SIMULATION,

22. @JsonProperty(value = "sport")

23. SPORT;

24. }

Avec cela, on règle déjà un premier problème : le contenu du retour JSON !

1. $ curl http://localhost:8080//api/videogames/v1/genre/SHOOT_THEM_UP

2. [

3. {

4. "id": "e603e430-0853-46b0-9f44-d4f662f56c51",

5. "name": "1943 : THE BATTLE OF MIDWAY",

6. "genre": "shoot-them-up",

7. "version": 1

8. },

9. ... etc ...

Mais le problème persiste pour l'URL ! Il faut donc créer un ParamConverter JAX-RS !

Un quoi ?

Un ParamConverter<T> est un convertisseur JAX-RS qui va être invoqué à différents moments du traitement de la requête. Son travail est de fournir une conversion bidirectionnelle de <T> vers String et de String vers <T>.

Mais on va en créer un générique qui va aller chercher la valeur de l'annotation Jackson @JsonProperty.

1. package fr.fxjavadevblog.qjg.utils;

3. import java.util.EnumSet;

4. import java.util.Optional;

6. import javax.ws.rs.ext.ParamConverter;

8. import org.slf4j.Logger;

9. import org.slf4j.LoggerFactory;

11. import com.fasterxml.jackson.annotation.JsonProperty;

12. import com.google.common.collect.BiMap;

13. import com.google.common.collect.HashBiMap;

15. /**

16. * Generic JAX-RS Enum converter based on the Jackson JsonProperty annotation to

17. * get the mapping.

18. *

19. * @author François-Xavier Robin

20. *

21. * @param <T>

22. */

24. public class GenericEnumConverter<T extends Enum<T>> implements ParamConverter<T>

25. {

26. private static final Logger log = LoggerFactory.getLogger(GenericEnumConverter.class);

27. /**

28. * bi-directionnal Map to store enum value as key and its string representation as value.

29. * The string representation is retrieved through the JsonProperty annotation put on the enum constant.

30. */

31. private final BiMap<T, String> biMap =  HashBiMap.create();

32. /**

33. * returns a Generic converter for an enum class in the context of JAX-RS ParamConverter.

34. *

35. * @param <T>

36. *    enum type

37. * @param t

38. *    enum type class

39. * @return

40. *    a generic converter used by JAX-RS.

41. */

42. public static <T extends Enum<T>> ParamConverter<T> of(Class<T> t)

43. {

44. return new GenericEnumConverter<>(t);

45. }

46. private GenericEnumConverter(Class<T> t)

47. {

48. log.debug("Generating conversion map for enum {}", t);

49. EnumSet.allOf(t).forEach(v -> {

50. try

51. {

52. String enumValue = v.name();

53. JsonProperty annotation =  v.getClass().getDeclaredField(enumValue).getAnnotation(JsonProperty.class);

54. // get the annotation if it exists or take the classic enum representation

55. String result = Optional.ofNullable(annotation).map(JsonProperty::value).orElse(enumValue);

56. log.debug("Enum value {}.{} is mapped to \"{}\"", t.getSimpleName(), v.name(), result);

57. biMap.put(v, result);

58. }

59. catch (NoSuchFieldException | SecurityException e)

60. {

61. log.error("Error while populating BiMap of enum {}", t.getClass());

62. log.error("Thrown by ", e);

63. }

64. });

65. }

66. /**

67. * returns the enum type constant from this string representation.

68. */

69. @Override

70. public T fromString(String value)

71. {

72. T returnedValue = biMap.inverse().get(value);

73. log.debug("Converting String \"{}\" to {}.{}", value, returnedValue.getClass(), returnedValue);

74. return returnedValue;

75. }

76. /**

77. * returns the string represenation from this enum type constant.

78. */

79. @Override

80. public String toString(T t)

81. {

82. String returnedValue = biMap.get(t);

83. log.debug("Converting Enum {}.{} to String \"{}\"", t.getClass(), t, returnedValue);

84. return returnedValue;

85. }

86. }

Les concepts de cette classe sont les suivants :

- elle est instanciée en prenant une enum comme argument : ParamConverter<Genre> converter = GenericEnumConverter.of(Genre.class); ;

- elle introspecte l'enum pendant sa construction à la recherche des annotations @JsonProperty sur chaque valeur ;

- pour chaque valeur, elle récupère le contenu de l'annotation @JsonProperty et peuple une BiMap (Map bidirectionnelle Guava, incluse dans Quarkus) ;

- si l'annotation n'est pas présente (on ne sait jamais), la valeur toString() de l'enum sera prise par défaut.

La partie « générique » permet de s'adapter à n'importe quelle enum.

À la fin de la construction de cette classe, la BiMap contient les tuples suivants :

1. Generating conversion map for enum class fr.fxjavadevblog.qjg.genre.Genre

2. Enum value Genre.ARCADE is mapped to "arcade"

3. Enum value Genre.EDUCATION is mapped to "education"

4. Enum value Genre.FIGHTING is mapped to "fighting"

5. Enum value Genre.PINBALL is mapped to "pinball"

6. Enum value Genre.PLATFORM is mapped to "platform"

7. Enum value Genre.REFLEXION is mapped to "reflexion"

8. Enum value Genre.RPG is mapped to "rpg"

9. Enum value Genre.SHOOT_THEM_UP is mapped to "shoot-them-up"

10. Enum value Genre.SIMULATION is mapped to "simulation"

11. Enum value Genre.SPORT is mapped to "sport"

Ensuite, il faut enregistrer ce convertisseur automatique auprès de JAX-RS : cela se fait au moyen d'une classe qui implémente l'interface ParamConverterProvider et d'une annotation @Provider :

1. package fr.fxjavadevblog.qjg.genre;

2. import java.lang.annotation.Annotation;

3. import java.lang.reflect.Type;

4. import javax.ws.rs.ext.ParamConverter;

5. import javax.ws.rs.ext.ParamConverterProvider;

6. import javax.ws.rs.ext.Provider;

7. import org.slf4j.Logger;

8. import org.slf4j.LoggerFactory;

9. import fr.fxjavadevblog.qjg.utils.GenericEnumConverter;

10. /**

11. * JAX-RS provider for Genre conversion. This converter is registered because of

12. * the Provider annotation.

13. *

14. * @author François-Xavier Robin

15. */

16. @Provider

17. public class GenreConverterProvider implements ParamConverterProvider

18. {

19. private final Logger log = LoggerFactory.getLogger(GenreConverterProvider.class);

20. private final ParamConverter<Genre> converter = GenericEnumConverter.of(Genre.class);

21. @SuppressWarnings("unchecked")

22. @Override

23. public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations)

24. {

25. log.debug("Registering GenreConverter");

26. return (Genre.class.equals(rawType)) ? (ParamConverter<T>) converter : null;

27. }

28. }

Le endpoint REST de la classe VideoGameResource change légèrement pour se simplifier :

1. @GET

2. @Path("/genre/{genre}")

3. public List<VideoGame> findByGenre(@PathParam("genre") Genre genre)

4. {

5. return videoGameRepository.findByGenre(genre);

6. }

Notez ici l'appel au convertisseur générique précédemment codé.

Grâce à tout ceci, on obtient le comportement souhaité :

1. $ curl http://localhost:8080//api/videogames/v1/genre/shoot-them-up

2. [

3. {

4. "id": "e603e430-0853-46b0-9f44-d4f662f56c51",

5. "name": "1943 : THE BATTLE OF MIDWAY",

6. "genre": "shoot-them-up",

7. "version": 1

8. },

9. {

10. "id": "fa02815b-f6d2-4ba1-9f63-5c8f575d06b6",

11. "name": "AIR SUPPLY",

12. "genre": "shoot-them-up",

13. "version": 1

14. },

15. {

16. "id": "07638287-8f45-4be8-a887-c57f350a9ce7",

17. "name": "ALBEDO",

18. "genre": "shoot-them-up",

19. "version": 1

20. },

21. {

22. "id": "56417168-1e57-4197-a490-56258e5405eb",

23. "name": "ALCON",

24. "genre": "shoot-them-up",

25. "version": 1

26. },

27. {

28. "id": "d5bd6aaa-674d-4e7f-ae8e-53118de897c6",

29. "name": "ALIEN BLAST",

30. "genre": "shoot-them-up",

31. "version": 1

32. },

33. {

34. "id": "2589d502-4619-4728-b688-9cece2a8a3ab",

35. "name": "ALIEN BUSTERS IV",

36. "genre": "shoot-them-up",

37. "version": 1

38. }

39. ... etc.

De plus, quand on invoque l'URL avec un genre qui ne peut pas être mappé, on obtient une erreur 404. Ce comportement est très satisfaisant.

1. $ curl -s -o /dev/null -D - http://localhost:8080//api/videogames/v1/genre/SHOOT_THEM_UP

2. HTTP/1.1 404 Not Found

3. Content-Length: 0

4. Content-Type: application/json

XII - Tests

XII-A - Tests unitaires

Ces tests sont classiquement dans le répertoire « test ». Les points particuliers sont les suivants :

- la base de données n'est pas créée dans ce cas ;

- @QuarkusTest est présent sur quelques classes de tests unitaires pour vérifier le comportement de CDI ;

- les tests unitaires nommés IT_* sont ignorés par convention (tests d'intégration) ;

- le profil test de Quarkus est automatique, un fichier spécifique application.properties est utilisé à cet effet ;

- la propriété quarkus.arc.remove-unused-beans=false permet de conserver dans le contexte CDI tous les beans injectables.

Pour lancer les tests unitaires, procédez comme ci-dessous :

1. $ mvn clean test

2. ...

3. ...

4. [INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ quarkus-tuto ---

5. [INFO]

6. [INFO] -------------------------------------------------------

7. [INFO]  T E S T S

8. [INFO] -------------------------------------------------------

9. [INFO] Running fr.fxjavadevblog.qjg.ping.PingTest

10. 2020-04-14 19:25:27,022 INFO  [io.quarkus] (main) Quarkus 1.3.1.Final started in 4.701s. Listening on: http://0.0.0.0:8081

11. 2020-04-14 19:25:27,030 INFO  [io.quarkus] (main) Profile test activated.

12. 2020-04-14 19:25:27,031 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, hibernate-orm-panache, hibernate-validator, jdbc-postgresql, narayana-jta, resteasy, resteasy-jackson, smallrye-openapi, spring-data-jpa, spring-di, swagger-ui]

13. [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.947 s - in fr.fxjavadevblog.qjg.ping.PingTest

14. [INFO] Running fr.fxjavadevblog.qjg.utils.GenericEnumConverterTest

15. [INFO] Tests run: 100, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.174 s - in fr.fxjavadevblog.qjg.utils.GenericEnumConverterTest

16. [INFO] Running fr.fxjavadevblog.qjg.utils.CDITest

17. [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 s - in fr.fxjavadevblog.qjg.utils.CDITest

18. [INFO] Running fr.fxjavadevblog.qjg.utils.DummyTest

19. [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 s - in fr.fxjavadevblog.qjg.utils.DummyTest

20. 2020-04-14 19:25:28,697 INFO  [io.quarkus] (main) Quarkus stopped in 0.056s

21. [INFO]

22. [INFO] Results:

23. [INFO]

24. [INFO] Tests run: 105, Failures: 0, Errors: 0, Skipped: 0

25. [INFO]

26. [INFO] ------------------------------------------------------------------------

27. [INFO] BUILD SUCCESS

28. [INFO] ------------------------------------------------------------------------

29. [INFO] Total time:  13.888 s

30. [INFO] Finished at: 2020-04-14T19:25:29+02:00

31. [INFO] ------------------------------------------------------------------------

XII-B - Tests d'intégration

Ces tests sont placés dans le répertoire « test-integration ». Les points particuliers sont les suivants :

- une image Docker PostgreSQL pour les tests d'intégration est lancée ;

- @QuarkusTest est présent sur tous les tests afin de disposer de l'environnement complet ;

- Les tests sont réalisés avec Rest Assured.

1. package fr.fxjavadevblog.qjg.videogame;

2. import static io.restassured.RestAssured.given;

3. import static org.hamcrest.CoreMatchers.containsString;

4. import org.junit.jupiter.api.DisplayName;

5. import org.junit.jupiter.api.Tag;

6. import org.junit.jupiter.api.Test;

7. import fr.fxjavadevblog.qjg.global.TestingGroups;

8. import io.quarkus.test.junit.QuarkusTest;

9. @QuarkusTest

10. @Tag(TestingGroups.INTEGRATION_TESTING)

11. class IT_VideoGameResource

12. {

13. public static final String ENDPOINT = "/api/videogames/v1";

14. @Test

15. @DisplayName("Get " + ENDPOINT)

16. void testGetAllVideoGames()

17. {

18. given().when()

19. .get(ENDPOINT + "/")

20. .then()

21. .statusCode(200)

22. .assertThat().body(containsString("XENON"),

23. containsString("RICK"),

24. containsString("LOTUS"));

25. }

26. }

Pour lancer les tests d'intégration :

- s'assurer que le PostgreSQL de dev n'est pas lancé ;

- lancer la commande $ mvn -Dskip.surefire.tests verify.

1. $ mvn -Dskip.surefire.tests verify

2. Scanning for projects...

3. [INFO]

4. [INFO] -------------------< fr.fxjavadevblog:quarkus-tuto >--------------------

5. [INFO] Building Quarkus-JPA-PostgreSQL 0.0.1-SNAPSHOT

6. [INFO] --------------------------------[ jar ]---------------------------------

7. [INFO]

8. [INFO] --- maven-resources-plugin:3.1.0:resources (default-resources) @ quarkus-tuto ---

9. [INFO] Using 'UTF-8' encoding to copy filtered resources.

10. [INFO] Copying 2 resources

11. [INFO]

12. [INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ quarkus-tuto ---

13. [INFO] Nothing to compile - all classes are up to date

14. [INFO]

15. [INFO] --- build-helper-maven-plugin:3.1.0:add-test-source (add-integration-test-sources) @ quarkus-tuto ---

16. [INFO] Test Source directory: /home/robin/git/quarkus-tuto/src/test-integration/java added.

17. [INFO]

18. [INFO] --- build-helper-maven-plugin:3.1.0:add-test-resource (add-integration-test-resource) @ quarkus-tuto ---

19. [INFO]

20. [INFO] --- maven-resources-plugin:3.1.0:testResources (default-testResources) @ quarkus-tuto ---

21. [INFO] Using 'UTF-8' encoding to copy filtered resources.

22. [INFO] Copying 1 resource

23. [INFO] Copying 1 resource

24. [INFO]

25. [INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ quarkus-tuto ---

26. [INFO] Nothing to compile - all classes are up to date

27. [INFO]

28. [INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ quarkus-tuto ---

29. [INFO] Tests are skipped.

30. [INFO]

31. [INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ quarkus-tuto ---

32. [INFO]

33. [INFO] --- quarkus-maven-plugin:1.3.1.Final:build (default) @ quarkus-tuto ---

34. [INFO] [org.jboss.threads] JBoss Threads version 3.0.1.Final

35. [INFO] [org.hibernate.Version] HHH000412: Hibernate ORM core version 5.4.12.Final

36. [INFO] [io.quarkus.deployment.pkg.steps.JarResultBuildStep] Building thin jar: /home/robin/git/quarkus-tuto/target/quarkus-tuto-0.0.1-SNAPSHOT-runner.jar

37. [INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 1647ms

38. [INFO]

39. [INFO] --- maven-resources-plugin:3.1.0:copy-resources (copy-resources) @ quarkus-tuto ---

40. [INFO] Using 'UTF-8' encoding to copy filtered resources.

41. [INFO] Copying 1 resource

42. [INFO]

43. [INFO] --- docker-maven-plugin:0.33.0:stop (docker:start) @ quarkus-tuto ---

44. [INFO]

45. [INFO] --- docker-maven-plugin:0.33.0:start (docker:start) @ quarkus-tuto ---

46. [INFO] DOCKER> [postgres:12.2] "postgresql": Start container ca6a77a7d1e3

47. [INFO] DOCKER> [postgres:12.2] "postgresql": Waiting for mapped ports [5432] on host localhost

48. [INFO] DOCKER> [postgres:12.2] "postgresql": Waited on tcp port '[localhost/127.0.0.1:5432]' 4 ms

49. [INFO]

50. [INFO] --- maven-failsafe-plugin:2.22.2:integration-test (default) @ quarkus-tuto ---

51. 19:29:19.154 PostgreSQL Server :The files belonging to this database system will be owned by user "postgres".

52. 19:29:19.154 PostgreSQL Server :This user must also own the server process.

53. 19:29:19.154 PostgreSQL Server :

54. 19:29:19.154 PostgreSQL Server :The database cluster will be initialized with locale "en_US.utf8".

55. 19:29:19.154 PostgreSQL Server :The default database encoding has accordingly been set to "UTF8".

56. 19:29:19.154 PostgreSQL Server :The default text search configuration will be set to "english".

57. 19:29:19.154 PostgreSQL Server :

58. 19:29:19.154 PostgreSQL Server :Data page checksums are disabled.

59. 19:29:19.154 PostgreSQL Server :

60. 19:29:19.154 PostgreSQL Server :fixing permissions on existing directory /var/lib/postgresql/data ... ok

61. 19:29:19.155 PostgreSQL Server :creating subdirectories ... ok

62. 19:29:19.155 PostgreSQL Server :selecting dynamic shared memory implementation ... posix

63. 19:29:19.171 PostgreSQL Server :selecting default max_connections ... 100

64. 19:29:19.197 PostgreSQL Server :selecting default shared_buffers ... 128MB

65. 19:29:19.243 PostgreSQL Server :selecting default time zone ... Etc/UTC

66. 19:29:19.244 PostgreSQL Server :creating configuration files ... ok

67. [INFO]

68. [INFO] -------------------------------------------------------

69. [INFO]  T E S T S

70. [INFO] -------------------------------------------------------

71. 19:29:19.393 PostgreSQL Server :running bootstrap script ... ok

72. 19:29:20.011 PostgreSQL Server :performing post-bootstrap initialization ... ok

73. [INFO] Running fr.fxjavadevblog.qjg.utils.IT_DummyTest

74. [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.032 s - in fr.fxjavadevblog.qjg.utils.IT_DummyTest

75. [INFO] Running fr.fxjavadevblog.qjg.videogame.IT_VideoGameResource

76. 19:29:20.174 PostgreSQL Server :syncing data to disk ... ok

77. 19:29:20.174 PostgreSQL Server :

78. 19:29:20.174 PostgreSQL Server :

79. 19:29:20.174 PostgreSQL Server :Success. You can now start the database server using:

80. 19:29:20.174 PostgreSQL Server :

81. 19:29:20.174 PostgreSQL Server :    pg_ctl -D /var/lib/postgresql/data -l logfile start

82. 19:29:20.174 PostgreSQL Server :

83. 19:29:20.174 PostgreSQL Server :initdb: warning: enabling "trust" authentication for local connections

84. 19:29:20.174 PostgreSQL Server :You can change this by editing pg_hba.conf or using the option -A, or

85. 19:29:20.174 PostgreSQL Server :--auth-local and --auth-host, the next time you run initdb.

86. 19:29:20.201 PostgreSQL Server :waiting for server to start....2020-04-14 17:29:20.201 UTC [47] LOG:  starting PostgreSQL 12.2 (Debian 12.2-2.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit

87. 19:29:20.203 PostgreSQL Server :2020-04-14 17:29:20.203 UTC [47] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"

88. 19:29:20.218 PostgreSQL Server :2020-04-14 17:29:20.218 UTC [48] LOG:  database system was shut down at 2020-04-14 17:29:19 UTC

89. 19:29:20.223 PostgreSQL Server :2020-04-14 17:29:20.223 UTC [47] LOG:  database system is ready to accept connections

90. 19:29:20.292 PostgreSQL Server : done

91. 19:29:20.292 PostgreSQL Server :server started

92. 19:29:20.418 PostgreSQL Server :CREATE DATABASE

93. 19:29:20.419 PostgreSQL Server :

94. 19:29:20.419 PostgreSQL Server :

95. 19:29:20.419 PostgreSQL Server :/usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*

96. 19:29:20.419 PostgreSQL Server :

97. 19:29:20.420 PostgreSQL Server :2020-04-14 17:29:20.420 UTC [47] LOG:  received fast shutdown request

98. 19:29:20.422 PostgreSQL Server :waiting for server to shut down....2020-04-14 17:29:20.422 UTC [47] LOG:  aborting any active transactions

99. 19:29:20.423 PostgreSQL Server :2020-04-14 17:29:20.423 UTC [47] LOG:  background worker "logical replication launcher" (PID 54) exited with exit code 1

100. 19:29:20.424 PostgreSQL Server :2020-04-14 17:29:20.424 UTC [49] LOG:  shutting down

101. 19:29:20.437 PostgreSQL Server :2020-04-14 17:29:20.436 UTC [47] LOG:  database system is shut down

102. 19:29:20.520 PostgreSQL Server : done

103. 19:29:20.520 PostgreSQL Server :server stopped

104. 19:29:20.520 PostgreSQL Server :

105. 19:29:20.520 PostgreSQL Server :PostgreSQL init process complete; ready for start up.

106. 19:29:20.520 PostgreSQL Server :

107. 19:29:20.537 PostgreSQL Server :2020-04-14 17:29:20.537 UTC [1] LOG:  starting PostgreSQL 12.2 (Debian 12.2-2.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit

108. 19:29:20.538 PostgreSQL Server :2020-04-14 17:29:20.537 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432

109. 19:29:20.538 PostgreSQL Server :2020-04-14 17:29:20.537 UTC [1] LOG:  listening on IPv6 address "::", port 5432

110. 19:29:20.541 PostgreSQL Server :2020-04-14 17:29:20.541 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"

111. 19:29:20.555 PostgreSQL Server :2020-04-14 17:29:20.555 UTC [65] LOG:  database system was shut down at 2020-04-14 17:29:20 UTC

112. 19:29:20.560 PostgreSQL Server :2020-04-14 17:29:20.560 UTC [1] LOG:  database system is ready to accept connections

113. Hibernate:

115. drop table if exists VIDEO_GAME cascade

116. Hibernate:

118. create table VIDEO_GAME (

119. id varchar(36) not null,

120. GENRE varchar(255) not null,

121. NAME varchar(255) not null,

122. VERSION int8,

123. primary key (id)

124. )

125. Hibernate:

127. alter table if exists VIDEO_GAME

128. add constraint UK_jg5tlrbpecd0wd8c9vjo6b429 unique (NAME)

130. Hibernate:

131. INSERT INTO VIDEO_GAME(ID,NAME,GENRE,VERSION) VALUES ('896b9c77-4f6d-4bd6-b681-2791acfa0d51','100 4 1','REFLEXION',1)

132. Hibernate:

133. INSERT INTO VIDEO_GAME(ID,NAME,GENRE,VERSION) VALUES ('6bf157fa-bd95-46ce-bbca-58afb87ebb9b','10TH FRAME','SPORT',1)

134. Hibernate:

135. INSERT INTO VIDEO_GAME(ID,NAME,GENRE,VERSION) VALUES ('e603e430-0853-46b0-9f44-d4f662f56c51','1943 : THE BATTLE OF MIDWAY','SHOOT_THEM_UP',1)

136. Hibernate:

137. INSERT INTO VIDEO_GAME(ID,NAME,GENRE,VERSION) VALUES ('61ec5869-d9f5-497a-9ffc-8e3612892c4b','1ST DIVISION MANAGER','SPORT',1)

139. ... etc ...

140. Hibernate:

141. select

142. videogame0_.id as id1_0_,

143. videogame0_.GENRE as genre2_0_,

144. videogame0_.NAME as name3_0_,

145. videogame0_.VERSION as version4_0_

146. from

147. VIDEO_GAME videogame0_ limit ?

148. [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 8.89 s - in fr.fxjavadevblog.qjg.videogame.IT_VideoGameResource

149. [INFO]

150. [INFO] Results:

151. [INFO]

152. [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

153. [INFO]

154. [INFO]

155. [INFO] --- docker-maven-plugin:0.33.0:stop (docker:stop) @ quarkus-tuto ---

156. 19:29:29.455 PostgreSQL Server :2020-04-14 17:29:29.454 UTC [1] LOG:  received smart shutdown request

157. 19:29:29.460 PostgreSQL Server :2020-04-14 17:29:29.459 UTC [1] LOG:  background worker "logical replication launcher" (PID 71) exited with exit code 1

158. 19:29:29.460 PostgreSQL Server :2020-04-14 17:29:29.460 UTC [66] LOG:  shutting down

159. 19:29:29.496 PostgreSQL Server :2020-04-14 17:29:29.496 UTC [1] LOG:  database system is shut down

160. [INFO] DOCKER> [postgres:12.2] "postgresql": Stop and removed container ca6a77a7d1e3 after 0 ms

161. [INFO]

162. [INFO] --- maven-failsafe-plugin:2.22.2:verify (default) @ quarkus-tuto ---

163. [INFO] ------------------------------------------------------------------------

164. [INFO] BUILD SUCCESS

165. [INFO] ------------------------------------------------------------------------

XIII - Version native et image Docker

Pour générer l'image Docker de l'application native, rien de plus facile que de lancer :

1. $ mvn clean package -Pnative -Dquarkus.native.container-build=true -Dskip.surefire.tests

2. ...

3. ...

4. [INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Pulling image quay.io/quarkus/ubi-quarkus-native-image:19.3.1-java11

5. 19.3.1-java11: Pulling from quarkus/ubi-quarkus-native-image

6. 57de4da701b5: Pull complete

7. cf0f3ebe9f53: Pull complete

8. e9da77aa316d: Pull complete

9. Digest: sha256:b18b701bd6f9d0a7778129f63b9f2dd666be2a2574854b56cd60e3cbd42b73d3

10. Status: Downloaded newer image for quay.io/quarkus/ubi-quarkus-native-image:19.3.1-java11

11. quay.io/quarkus/ubi-quarkus-native-image:19.3.1-java11

12. [INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Running Quarkus native-image plugin on GraalVM Version 19.3.1 CE

13. [INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] docker run -v /home/robin/git/quarkus-tuto/target/quarkus-tuto-0.0.1-SNAPSHOT-native-image-source-jar:/project:z --env LANG=C --user 1000:1000 --rm quay.io/quarkus/ubi-quarkus-native-image:19.3.1-java11 -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager -J-Dsun.nio.ch.maxUpdateArraySize=100 -J-DCoordinatorEnvironmentBean.transactionStatusManagerEnable=false -J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory -J-Dvertx.disableDnsResolver=true -J-Dio.netty.leakDetection.level=DISABLED -J-Dio.netty.allocator.maxOrder=1 -J-Duser.language=fr -J-Dfile.encoding=UTF-8 --initialize-at-build-time= -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime -H:+JNI -jar quarkus-tuto-0.0.1-SNAPSHOT-runner.jar -H:FallbackThreshold=0 -H:+ReportExceptionStackTraces -H:-AddAllCharsets -H:-IncludeAllTimeZones -H:EnableURLProtocols=http,https --enable-all-security-services --no-server -H:-UseServiceLoaderFeature -H:+StackTrace quarkus-tuto-0.0.1-SNAPSHOT-runner

14. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]    classlist:  10 740,32 ms

15. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]        (cap):     793,01 ms

16. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]        setup:   2 161,94 ms

17. 08:35:25,649 INFO  [org.hib.Version] HHH000412: Hibernate ORM core version 5.4.12.Final

18. 08:35:25,663 INFO  [org.hib.ann.com.Version] HCANN000001: Hibernate Commons Annotations {5.1.0.Final}

19. 08:35:25,705 INFO  [org.hib.dia.Dialect] HHH000400: Using dialect: io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect

20. 08:35:25,848 INFO  [org.hib.val.int.uti.Version] HV000001: Hibernate Validator 6.1.2.Final

21. 08:35:27,547 INFO  [org.jbo.threads] JBoss Threads version 3.0.1.Final

22. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]   (typeflow):  47 866,54 ms

23. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]    (objects):  33 462,47 ms

24. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]   (features):   1 692,11 ms

25. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]     analysis:  87 791,76 ms

26. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]     (clinit):   1 211,71 ms

27. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]     universe:   5 195,59 ms

28. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]      (parse):   6 258,47 ms

29. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]     (inline):   8 432,95 ms

30. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]    (compile):  55 066,12 ms

31. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]      compile:  74 232,51 ms

32. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]        image:   5 742,28 ms

33. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]        write:   1 469,31 ms

34. [quarkus-tuto-0.0.1-SNAPSHOT-runner:24]      [total]: 187 812,32 ms

35. [INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 223001ms

36. [INFO] ------------------------------------------------------------------------

37. [INFO] BUILD SUCCESS

38. [INFO] ------------------------------------------------------------------------

39. [INFO] Total time:  03:50 min

40. [INFO] Finished at: 2020-04-15T10:38:21+02:00

41. [INFO] ------------------------------------------------------------------------

Cette commande a réalisé une chose essentielle : elle a compilé toute l'application en une version native LINUX, quel que soit votre environnement de travail, au moyen d'un conteneur dédié à la compilation.

Pour faire simple, un conteneur Docker ubi-quarkus-native-image:19.3.1-java11 a été récupéré puis lancé pour compiler l'application au format LINUX, même si vous êtes sous Windows. Cela nécessite toutefois d'avoir GraalVM installé nativement, mais cela peut-être contourné
(cf : https://quarkus.io/guides/building-native-image)

Une fois l'application compilée, il faut maintenant créer l'image du conteneur Docker. Cette création est possible en ayant préalablement créé 2 fichiers dans /src/main/docker :

- /src/main/docker/Dockerfile.native : fichier pour la génération en mode natif ;

- .dockerignore : fichier pour la génération en mode JVM normal à la racine du projet Maven.

Il peut aussi exister un fichier Dockerfile.jvm mais ce n'est pas l'objet de ce tutoriel.

Voici le contenu de ces fichiers :

Dockerfile.native

1. FROM registry.access.redhat.com/ubi8/ubi-minimal

2. WORKDIR /work/

3. COPY target/*-runner /work/application

4. RUN chmod 775 /work

5. EXPOSE 8080

6. CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

.dockerignore

*
!target/*-runner
!target/*-runner.jar

On peut alors lancer la création de l'image Docker :

1. $ docker build -f src/main/docker/Dockerfile.native -t quarkus-tuto .

2. Sending build context to Docker daemon  102.9MB

3. Step 1/6 : FROM registry.access.redhat.com/ubi8/ubi-minimal

4. latest: Pulling from ubi8/ubi-minimal

5. b26afdf22be4: Pull complete

6. 218f593046ab: Pull complete

7. Digest: sha256:df6f9e5d689e4a0b295ff12abc6e2ae2932a1f3e479ae1124ab76cf40c3a8cdd

8. Status: Downloaded newer image for registry.access.redhat.com/ubi8/ubi-minimal:latest

9. ---> 91d23a64fdf2

10. Step 2/6 : WORKDIR /work/

11. ---> Running in 40a5ee141273

12. Removing intermediate container 40a5ee141273

13. ---> 6ab61dca9bf2

14. Step 3/6 : COPY target/*-runner /work/application

15. ---> 3aae05f5ee7d

16. Step 4/6 : RUN chmod 775 /work

17. ---> Running in 8387b2b6e071

18. Removing intermediate container 8387b2b6e071

19. ---> 0c03e70d6326

20. Step 5/6 : EXPOSE 8080

21. ---> Running in 1813737fd08e

22. Removing intermediate container 1813737fd08e

23. ---> 978251ac9f2b

24. Step 6/6 : CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

25. ---> Running in c93ab35754d4

26. Removing intermediate container c93ab35754d4

27. ---> 6009aee9381b

28. Successfully built 6009aee9381b

29. Successfully tagged quarkus-tuto:latest

Une fois le conteneur créé, il suffit de le lancer, en ayant lancé préalablement une instance de PostgreSQL (encore avec Docker, comme en DEV) :

$ docker run -i --rm -p 8080:8080 --network="host" quarkus-tuto

Le paramètre --network="host" permet à l'application de se connecter au PostgreSQL exposé par Docker.

1. $ curl http://localhost:8080/api/videogames/v1/genre/rpg

2. [

3. {

4. "id": "75a9b985-c5a9-40a0-87ba-086850683bfc",

5. "name": "20000 LIEUES SOUS LES MERS",

6. "genre": "rpg",

7. "version": 1

8. },

9. {

10. "id": "2b412dd7-d090-4328-8180-869b60bbc106",

11. "name": "ADVENTURE",

12. "genre": "rpg",

13. "version": 1

14. },

15. {

16. "id": "885a3475-63c9-49f3-b120-e218ce9c9510",

17. "name": "ADVENTURER, THE",

18. "genre": "rpg",

19. "version": 1

20. },

21. {

22. "id": "a500c1a6-6008-45d4-b3f4-5b668b77499a",

23. "name": "ADVENTURES OF ROBIN HOOD, THE",

24. "genre": "rpg",

25. "version": 1

26. },

27. ... etc ...

28. ]

XIV - Health Check et Metrics

Quarkus embarque les extensions SmallRye Health et Metrics, qui sont les implémentations respectives de Eclipse MicroProfile Health et Metrics.

Le simple ajout dans le pom de ces deux dépendances rend ces fonctionnalités opérationnelles :

1. <!-- Health Check -->

2. <dependency>

3. <groupId>io.quarkus</groupId>

4. <artifactId>quarkus-smallrye-health</artifactId>

5. </dependency>

6. <!-- Metrics -->

7. <dependency>

8. <groupId>io.quarkus</groupId>

9. <artifactId>quarkus-smallrye-metrics</artifactId>

10. </dependency>

Une fois l'application lancée et qu'elle est sollicitée, on peut obtenir un état de son bon fonctionnement :

1. $ curl http://localhost:8080/health

2. {

3. "status": "UP",

4. "checks": [

5. {

6. "name": "Application",

7. "status": "UP"

8. },

9. {

10. "name": "Database connections health check",

11. "status": "UP"

12. }

13. ]

14. }

On peut obtenir quelques mesures qui auront été calculées au moyen de l'annotation @Timed sur les méthodes de la classe VideoGameResource :

1. @Timed(name = "videogames-find-by-all", absolute = true,

2. description = "A measure of how long it takes to fetch all video games.",

3. unit = MetricUnits.MILLISECONDS)

4. public Iterable<VideoGame> findAll()

5. {

6. return videoGameRepository.findAll();

7. }

8. @Timed(name = "videogames-find-by-genre", absolute = true,

9. description = "A measure of how long it takes to fetch all video games filtered by a given genre.",

10. unit = MetricUnits.MILLISECONDS)

11. public List<VideoGame> findByGenre(@PathParam("genre") Genre genre)

12. {

13. return videoGameRepository.findByGenre(genre);

14. }

L'attribut absolute=true empêche la concaténation du nom du package et de la classe au nom de la mesure. Ceci sera plus agréable à lire dans les outils de restitution qui exploiteront cette information du retour JSON. Je préfère cette notation, car elle aura un impact direct sur les URL d'appel des mesures.

Voici les mesures obtenues après 20 appels de l'URL /api/videogames/v1/genre/shoot-them-up :

1. $ curl -H"Accept: application/json" localhost:8080/metrics/application/videogames-find-by-genre

2. {

3. "videogames-find-by-genre": {

4. "p99": 10.044791,

5. "min": 2.335977,

6. "max": 10.044791,

7. "mean": 3.6114086322681627,

8. "p50": 3.19797,

9. "p999": 10.044791,

10. "stddev": 1.6164326637605608,

11. "p95": 5.663952,

12. "p98": 10.044791,

13. "p75": 3.752217,

14. "fiveMinRate": 0.06305266722909629,

15. "fifteenMinRate": 0.021812705995763727,

16. "meanRate": 0.008589683736876096,

17. "count": 20,

18. "oneMinRate": 0.252757448780742

19. }

20. }

Il faut ensuite utiliser un collecteur de Metrics comme Prometheus couplé à Grafana pour obtenir de jolis tableaux de bord.

XV - Conclusions

Quarkus est, à mon humble avis, un framework de développement de Web Services REST très intéressant sur de nombreux aspects :

- il est facile à prendre en main ;

- le mode dev et le hot reload offrent un gain de temps important, même si l'usage conjoint de Lombok n'est pas encore optimum ;

- la documentation est claire et il y a de nombreux exemples officiels sur GitHub ;

- la conformité aux specs Java EE et Microprofile est très intéressante et rassurante ( JAX-RS, etc.) : pas de nouvelle API propriétaire à apprendre ;

- le plugin de compilation native avec GraalVM est fourni et le résultat est à la hauteur des espérances ;

- l'usage du Health Check et des Metrics est vraiment bien intégré et facile à mettre en œuvre ;

- il est facile de rajouter la gestion de token JWT et la liaison avec KeyCloak ;

- la communauté semble très active.

Je vous encourage donc vivement à vous pencher sérieusement sur Quarkus !

XVI - Remerciements

Cet article a été publié avec l'aimable autorisation François-Xavier Robin.

Nous tenons à remercier escartefigue pour sa relecture orthographique attentive de cet article et Winjerome pour la mise au gabarit.