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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
<project
xmlns
=
"http://maven.apache.org/POM/4.0.0"
xmlns
:
xsi
=
"http://www.w3.org/2001/XMLSchema-instance"
xsi
:
schemaLocation
=
"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<modelVersion>
4.0.0</modelVersion>
<groupId>
fr.fxjavadevblog</groupId>
<artifactId>
quarkus-tuto</artifactId>
<version>
0.0.1-SNAPSHOT</version>
<name>
Quarkus-JPA-PostgreSQL</name>
<properties>
<project.build.sourceEncoding>
UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>
UTF-8</project.reporting.outputEncoding>
<maven.compiler.target>
1.8</maven.compiler.target>
<maven.compiler.source>
1.8</maven.compiler.source>
<lombok.version>
1.18.12</lombok.version>
<quarkus-version>
1.3.1.Final</quarkus-version>
<surefire-plugin.version>
2.22.2</surefire-plugin.version>
</properties>
</project>
Pour ce tutoriel, on utilisera Java 8.
On ajoute les dépendances classiques :
2.
3.
4.
5.
6.
7.
8.
<dependencies>
<dependency>
<groupId>
org.projectlombok</groupId>
<artifactId>
lombok</artifactId>
<version>
${lombok.version}</version>
<scope>
provided</scope>
</dependency>
</dependencies>
Pour utiliser Quarkus :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>
io.quarkus</groupId>
<artifactId>
quarkus-bom</artifactId>
<version>
${quarkus-version}</version>
<type>
pom</type>
<scope>
import</scope>
</dependency>
</dependencies>
</dependencyManagement>
puis :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
<dependency>
<groupId>
io.quarkus</groupId>
<artifactId>
quarkus-spring-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>
io.quarkus</groupId>
<artifactId>
quarkus-jackson</artifactId>
</dependency>
<dependency>
<groupId>
io.quarkus</groupId>
<artifactId>
quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
<groupId>
io.quarkus</groupId>
<artifactId>
quarkus-jdbc-postgresql</artifactId>
</dependency>
<!-- OPEN API via Swagger 2 -->
<dependency>
<groupId>
io.quarkus</groupId>
<artifactId>
quarkus-smallrye-openapi</artifactId>
</dependency>
<!-- Health Check -->
<dependency>
<groupId>
io.quarkus</groupId>
<artifactId>
quarkus-smallrye-health</artifactId>
</dependency>
<!-- Metrics -->
<dependency>
<groupId>
io.quarkus</groupId>
<artifactId>
quarkus-smallrye-metrics</artifactId>
</dependency>
<!-- for testing -->
<dependency>
<groupId>
io.quarkus</groupId>
<artifactId>
quarkus-junit5</artifactId>
<scope>
test</scope>
</dependency>
<!-- for testing -->
<dependency>
<groupId>
io.rest-assured</groupId>
<artifactId>
rest-assured</artifactId>
<scope>
test</scope>
</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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
<build>
<plugins>
<plugin>
<artifactId>
maven-compiler-plugin</artifactId>
<version>
3.8.1</version>
</plugin>
<plugin>
<groupId>
io.quarkus</groupId>
<artifactId>
quarkus-maven-plugin</artifactId>
<version>
${quarkus-version}</version>
<executions>
<execution>
<goals>
<goal>
build</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>
maven-resources-plugin</artifactId>
<version>
3.1.0</version>
<executions>
<execution>
<id>
copy-resources</id>
<phase>
pre-integration-test</phase>
<goals>
<goal>
copy-resources</goal>
</goals>
<configuration>
<overwrite>
true</overwrite>
<outputDirectory>
${basedir}/target/test-classes</outputDirectory>
<resources>
<resource>
<directory>
src/test-integration/resources</directory>
<filtering>
true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<!-- tests unitaires -->
<plugin>
<artifactId>
maven-surefire-plugin</artifactId>
<version>
${surefire-plugin.version}</version>
<configuration>
<excludes>
<exclude>
**/IT_*.java</exclude>
</excludes>
<systemProperties>
<java.util.logging.manager>
org.jboss.logmanager.LogManager</java.util.logging.manager>
</systemProperties>
<skipTests>
${skip.surefire.tests}</skipTests>
</configuration>
</plugin>
<!-- tests d'integration -->
<plugin>
<groupId>
org.apache.maven.plugins</groupId>
<artifactId>
maven-failsafe-plugin</artifactId>
<version>
${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>
integration-test</goal>
<goal>
verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>
org.codehaus.mojo</groupId>
<artifactId>
build-helper-maven-plugin</artifactId>
<version>
3.1.0</version>
<executions>
<execution>
<id>
add-integration-test-sources</id>
<phase>
generate-test-sources</phase>
<goals>
<goal>
add-test-source</goal>
</goals>
<configuration>
<sources>
<source>
src/test-integration/java</source>
</sources>
</configuration>
</execution>
<execution>
<id>
add-integration-test-resource</id>
<phase>
generate-test-resources</phase>
<goals>
<goal>
add-test-resource</goal>
</goals>
<configuration>
<resources>
<resource>
<directory>
src/test-integration/resources</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>
io.fabric8</groupId>
<artifactId>
docker-maven-plugin</artifactId>
<version>
0.33.0</version>
<configuration>
<skip>
${skip.integration.tests}</skip>
<images>
<image>
<name>
postgres:12.2</name>
<alias>
postgresql</alias>
<run>
<env>
<POSTGRES_USER>
quarkus_tuto</POSTGRES_USER>
<POSTGRES_PASSWORD>
quarkus_tuto</POSTGRES_PASSWORD>
<POSTGRES_DB>
quarkus_tuto</POSTGRES_DB>
</env>
<ports>
<port>
5432:5432</port>
</ports>
<log>
<prefix>
PostgreSQL Server : </prefix>
<date>
default</date>
<color>
green</color>
</log>
<wait>
<tcp>
<mode>
mapped</mode>
<ports>
<port>
5432</port>
</ports>
</tcp>
<time>
10000</time>
</wait>
</run>
</image>
</images>
</configuration>
<executions>
<execution>
<id>
docker:start</id>
<phase>
pre-integration-test</phase>
<goals>
<goal>
stop</goal>
<goal>
start</goal>
</goals>
</execution>
<execution>
<id>
docker:stop</id>
<phase>
post-integration-test</phase>
<goals>
<goal>
stop</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Et enfin, pour atteindre le Graal du code Java compilé en binaire natif, il nous faut rajouter un petit profil Maven :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
<profiles>
<profile>
<id>
native</id>
<activation>
<property>
<name>
native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>
io.quarkus</groupId>
<artifactId>
quarkus-maven-plugin</artifactId>
<version>
${quarkus-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>
native-image</goal>
</goals>
<configuration>
<enableHttpUrlHandler>
true</enableHttpUrlHandler>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>
org.apache.maven.plugins</groupId>
<artifactId>
maven-failsafe-plugin</artifactId>
<version>
${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>
integration-test</goal>
<goal>
verify</goal>
</goals>
<configuration>
<systemProperties>
<native.image.path>
${project.build.directory}/${project.build.finalName}-runner</native.image.path>
</systemProperties>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
package
fr.fxjavadevblog.qjg.ping;
import
javax.ws.rs.GET;
import
javax.ws.rs.Path;
import
javax.ws.rs.Produces;
/**
* Simple JAX-RS endoint to check if the application is running.
*
*
@author
François-Xavier Robin
*
*/
@Path
(
"/api/ping"
)
public
class
PingService
{
@Path
(
"/v1"
)
@GET
@Produces
(
"text/plain"
)
public
String ping
(
)
{
return
"pong"
;
}
}
On compile et on lance Quarkus en mode DEV.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
$ mvn clean compile quarkus:dev
...
... ( build maven ...)
...
Listening for transport dt_socket at address: 5005
SLF4J: Class path contains multiple SLF4J bindings.
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]
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]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]
11:26:09.593 [Thread-20] DEBUG io.netty.util.internal.logging.InternalLoggerFactory - Using SLF4J as the default logging framework
11:26:09.599 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - -Dio.netty.noUnsafe: false
11:26:09.600 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - Java version: 8
11:26:09.601 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - sun.misc.Unsafe.theUnsafe: available
11:26:09.601 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - sun.misc.Unsafe.copyMemory: available
11:26:09.602 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - java.nio.Buffer.address: available
11:26:09.603 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - direct buffer constructor: available
11:26:09.604 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - java.nio.Bits.unaligned: available, true
11:26:09.605 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - jdk.internal.misc.Unsafe.allocateUninitializedArray(int): unavailable prior to Java9
11:26:09.605 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent0 - java.nio.DirectByteBuffer.<init>(long, int): available
11:26:09.606 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent - sun.misc.Unsafe: available
11:26:09.607 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent - -Dio.netty.tmpdir: /tmp (java.io.tmpdir)
11:26:09.607 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent - -Dio.netty.bitMode: 64 (sun.arch.data.model)
11:26:09.608 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent - -Dio.netty.maxDirectMemory: 3704094720 bytes
11:26:09.608 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent - -Dio.netty.uninitializedArrayAllocationThreshold: -1
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
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.
11:26:09.608 [Thread-20] DEBUG io.netty.util.internal.CleanerJava6 - java.nio.ByteBuffer.cleaner(): available
11:26:09.608 [Thread-20] DEBUG io.netty.util.internal.PlatformDependent - -Dio.netty.noPreferDirect: false
11:26:09.610 [Thread-20] DEBUG io.netty.channel.DefaultChannelId - -Dio.netty.processId: 12872 (auto-detected)
11:26:09.611 [Thread-20] DEBUG io.netty.util.NetUtil - -Djava.net.preferIPv4Stack: false
11:26:09.611 [Thread-20] DEBUG io.netty.util.NetUtil - -Djava.net.preferIPv6Addresses: false
11:26:09.612 [Thread-20] DEBUG io.netty.util.NetUtil - Loopback interface: lo (lo, 0:0:0:0:0:0:0:1%lo)
11:26:09.613 [Thread-20] DEBUG io.netty.util.NetUtil - /proc/sys/net/core/somaxconn: 128
11:26:09.613 [Thread-20] DEBUG io.netty.channel.DefaultChannelId - -Dio.netty.machineId: 18:31:bf:ff:fe:17:85:36 (auto-detected)
11:26:09.625 [main] DEBUG io.netty.util.ResourceLeakDetector - -Dio.netty.leakDetection.level: simple
11:26:09.625 [main] DEBUG io.netty.util.ResourceLeakDetector - -Dio.netty.leakDetection.targetRecords: 4
11:26:09.632 [main] DEBUG io.netty.util.internal.InternalThreadLocalMap - -Dio.netty.threadLocalMap.stringBuilder.initialSize: 1024
11:26:09.632 [main] DEBUG io.netty.util.internal.InternalThreadLocalMap - -Dio.netty.threadLocalMap.stringBuilder.maxSize: 4096
11:26:09.639 [main] DEBUG io.netty.channel.MultithreadEventLoopGroup - -Dio.netty.eventLoopThreads: 16
11:26:09.648 [main] DEBUG io.netty.channel.nio.NioEventLoop - -Dio.netty.noKeySetOptimization: false
11:26:09.649 [main] DEBUG io.netty.channel.nio.NioEventLoop - -Dio.netty.selectorAutoRebuildThreshold: 512
11:26:09.649 [main] DEBUG io.netty.util.internal.PlatformDependent - org.jctools-core.MpscChunkedArrayQueue: available
11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.numHeapArenas: 16
11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.numDirectArenas: 16
11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.pageSize: 8192
11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.maxOrder: 1
11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.chunkSize: 16384
11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.tinyCacheSize: 512
11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.smallCacheSize: 256
11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.normalCacheSize: 64
11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.maxCachedBufferCapacity: 32768
11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.cacheTrimInterval: 8192
11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.cacheTrimIntervalMillis: 0
11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.useCacheForAllThreads: true
11:26:09.704 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.PooledByteBufAllocator - -Dio.netty.allocator.maxCachedByteBuffersPerChunk: 1023
11:26:09.809 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.allocator.type: pooled
11:26:09.809 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.threadLocalDirectBufferSize: 0
11:26:09.809 [vert.x-eventloop-thread-1] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.maxThreadLocalCharBufferSize: 16384
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
2020-04-02 11:26:09,818 INFO [io.quarkus] (main) Profile dev activated. Live Coding activated.
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
$
sdk install java 19
.3
.1
.r11-grl
Downloading: java 19
.3
.1
.r11-grl
In
progress...
0
%
###################################################################### 100,0%
Repackaging Java 19
.3
.1
.r11-grl...
Done
repackaging...
Installing: java 19
.3
.1
.r11-grl
Done
installing!
Do
you want java 19
.3
.1
.r11-grl to be set as default? (
Y/n): Y
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.
2.
3.
4.
5.
6.
$
echo $JAVA_HOME
/home/robin/.sdkman/candidates/java/current
$
ll /home/robin/.sdkman/candidates/java/current
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/
$
whereis java
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é :
2.
3.
4.
5.
6.
$
mvn -version
Apache Maven 3
.6
.3
(
cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: /home/robin/.sdkman/candidates/maven/current
Java version: 11
.0
.6
, vendor: Oracle Corporation, runtime: /home/robin/.sdkman/candidates/java/19
.3
.1
.r11-grl
Default locale: fr_FR, platform encoding: UTF-8
OS name: "linux"
, version: "5.3.0-45-generic"
, arch: "amd64"
, family: "unix"
Ensuite il faut installer l'outil native-image :
2.
3.
4.
5.
$
gu install native-image
Downloading: Component catalog from www.graalvm.org
Processing Component: Native Image
Downloading: Component native-image: Native Image from github.com
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
$
mvn package -Pnative
...
...
... (
vous pouvez allez vous aérer la compilation est assez longue ...)
...
...
...
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] classlist: 10
076
,23
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] (
cap): 1
186
,64
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] setup: 3
325
,77
ms
17
:25
:16
,623
INFO [org.hib.val.int.uti.Version] HV000001: Hibernate Validator 6
.1
.2
.Final
17
:25
:18
,675
INFO [org.jbo.threads] JBoss Threads version 3
.0
.1
.Final
17
:25
:42
,598
INFO [org.hib.Version] HHH000412: Hibernate ORM core version 5
.4
.12
.Final
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] (
typeflow): 20
083
,90
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] (
objects): 16
238
,17
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] (
features): 742
,27
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] analysis: 38
874
,58
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] (
clinit): 658
,58
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] universe: 2
171
,74
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] (
parse): 2
662
,69
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] (
inline): 4
485
,66
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] (
compile): 29
844
,86
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] compile: 39
558
,51
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] image: 2
916
,45
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] write: 817
,69
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:1965
] [total]: 98
480
,45
ms
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in
101129ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01
:51
min
[INFO] Finished at: 2020
-04
-02T17:26
:40
+02
:00
[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 :
2.
3.
4.
5.
6.
7.
8.
$
./target/quarkus-tuto-0
.0
.1
-SNAPSHOT-runner
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ |
/ _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |
/ , _/ ,<
/ /_/ /\ \
--\___\_\____/_/ |
_/_/|
_/_/|
_|
\____/___/
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
2020
-04
-02
17
:29
:01
,484
INFO [io.quarkus] (
main) Profile prod activated.
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
$
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
Unable to find image 'postgres:12.2'
locally
12
.2
: Pulling from library/postgres
c499e6d256d6: Pull complete
67a768c93810: Pull complete
3befaea70a64: Pull complete
b72dde2f70c9: Pull complete
9af5f5958937: Pull complete
79f4f06e2acc: Pull complete
bc35aa1d8687: Pull complete
276504d44bd7: Pull complete
56cfad4df2a4: Pull complete
28bfa2f917aa: Pull complete
bbbebba2bc39: Pull complete
d2407cea5efb: Pull complete
92dae474b380: Pull complete
c71da770d20d: Pull complete
Digest: sha256:d480b197ab8e01edced54cbbbba9707373473f42006468b60be04da07ce97823
Status: Downloaded newer image for
postgres:12
.2
The files belonging to this database system will be owned by user "postgres"
.
This user must also own the server process.
The database cluster will be initialized with locale "en_US.utf8"
.
The default database encoding has accordingly been set to "UTF8"
.
The default text search configuration will be set to "english"
.
Data page checksums are disabled.
fixing permissions on existing directory /var/lib/postgresql/data ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Etc/UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
initdb: warning: enabling "trust"
authentication for
local
connections
You can change this by editing pg_hba.conf or using the option -A, or
--auth-local
and --auth-host, the next time you run initdb.
Success. You can now start the database server using:
pg_ctl -D /var/lib/postgresql/data -l logfile start
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
2020
-04
-02
15
:40
:28
.360
UTC [47
] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2020
-04
-02
15
:40
:28
.383
UTC [48
] LOG: database system was shut down at 2020
-04
-02
15
:40
:28
UTC
2020
-04
-02
15
:40
:28
.389
UTC [47
] LOG: database system is ready to accept connections
done
server started
CREATE DATABASE
/usr/local
/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*
2020
-04
-02
15
:40
:28
.603
UTC [47
] LOG: received fast shutdown request
waiting for
server to shut down....2020
-04
-02
15
:40
:28
.605
UTC [47
] LOG: aborting any active transactions
2020
-04
-02
15
:40
:28
.608
UTC [47
] LOG: background worker "logical replication launcher"
(
PID 54
) exited with exit code 1
2020
-04
-02
15
:40
:28
.609
UTC [49
] LOG: shutting down
2020
-04
-02
15
:40
:28
.633
UTC [47
] LOG: database system is shut down
done
server stopped
PostgreSQL init process complete; ready for
start up.
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
2020
-04
-02
15
:40
:28
.740
UTC [1
] LOG: listening on IPv4 address "0.0.0.0"
, port 5432
2020
-04
-02
15
:40
:28
.740
UTC [1
] LOG: listening on IPv6 address "::"
, port 5432
2020
-04
-02
15
:40
:28
.744
UTC [1
] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2020
-04
-02
15
:40
:28
.764
UTC [65
] LOG: database system was shut down at 2020
-04
-02
15
:40
:28
UTC
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
# CDI
quarkus.arc.remove-unused-beans
=
false
%dev
.quarkus.smallrye-openapi.path
=
/openapi
%dev
.quarkus.swagger-ui.always-include
=
true
%dev
.quarkus.swagger-ui.path
=
/openapi-ui
# DEV with PostgreSQL
%dev
.quarkus.datasource.db-kind
=
postgresql
%dev
.quarkus.datasource.jdbc.url
=
jdbc:postgresql:quarkus_tuto
%dev
.quarkus.datasource.username
=
quarkus_tuto
%dev
.quarkus.datasource.password
=
quarkus_tuto
%dev
.quarkus.datasource.jdbc.max-size
=
8
%dev
.us.datasource.jdbc.min-size
=
2
%dev
.quarkus.hibernate-orm.log.sql
=
false
%dev
.quarkus.hibernate-orm.database.generation
=
drop-and-create
%dev
.quarkus.hibernate-orm.sql-load-script
=
import.sql
%dev
.quarkus.log.level
=
INFO
%dev
.quarkus.log.category."org.hibernate"
.level
=
INFO
%dev
.quarkus.log.category."fr.fxjavadevblog"
.level
=
DEBUG
# PROD
%prod
.quarkus.datasource.db-kind
=
postgresql
%prod
.quarkus.datasource.jdbc.url
=
jdbc:postgresql:quarkus_tuto
%prod
.quarkus.datasource.username
=
quarkus_tuto
%prod
.quarkus.datasource.password
=
quarkus_tuto
%prod
.quarkus.hibernate-orm.database.generation
=
drop-and-create
%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▲
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
package
fr.fxjavadevblog.qjg.videogame;
import
java.io.Serializable;
import
javax.enterprise.context.Dependent;
import
javax.inject.Inject;
import
javax.persistence.Column;
import
javax.persistence.Entity;
import
javax.persistence.EnumType;
import
javax.persistence.Enumerated;
import
javax.persistence.Id;
import
javax.persistence.Table;
import
javax.persistence.Version;
import
fr.fxjavadevblog.qjg.utils.InjectUUID;
import
lombok.AccessLevel;
import
lombok.EqualsAndHashCode;
import
lombok.Getter;
import
lombok.NoArgsConstructor;
import
lombok.Setter;
import
lombok.ToString;
@SuppressWarnings
(
"serial"
)
// Lombok
@NoArgsConstructor
(
access =
AccessLevel.PROTECTED)
@EqualsAndHashCode
(
of =
"id"
)
@ToString
(
of =
{
"id"
, "name"
}
)
// CDI Annotation
@Dependent
// JPA Annotation
@Entity
@Table
(
name =
"VIDEO_GAME"
)
public
class
VideoGame implements
Serializable
{
@Id
@Inject
@InjectUUID
@Getter
@Column
(
length =
36
)
private
String id;
@Getter
@Setter
@Column
(
name =
"NAME"
, nullable =
false
, unique =
true
)
private
String name;
@Getter
@Setter
@Enumerated
(
EnumType.STRING)
@Column
(
name =
"GENRE"
, nullable =
false
)
private
Genre genre;
@Version
@Getter
@Column
(
name =
"VERSION"
)
private
Long version;
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
/**
* Enumeration of genres of Video Games for Atari ST.
*
*
@author
François-Xavier Robin
*
*/
public
enum
Genre
{
ARCADE, EDUCATION, FIGHTING, PINBALL, PLATFORM, REFLEXION, RPG, SHOOT_THEM_UP, SIMULATION, SPORT;
}
IX-B. Producers CDI et annotation▲
L'annotation @InjectUUID
est utilisée sur la classe VideoGame.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
package
fr.fxjavadevblog.qjg;
import
java.lang.annotation.ElementType;
import
java.lang.annotation.Retention;
import
java.lang.annotation.RetentionPolicy;
import
java.lang.annotation.Target;
import
javax.inject.Qualifier;
/**
* annotation to mark a field to be injected by CDI with a UUID.
*
*
@author
François-Xavier Robin
*
*/
@Qualifier
@Retention
(
RetentionPolicy.RUNTIME)
@Target
({
ElementType.FIELD, ElementType.METHOD}
)
public
@interface
InjectUUID
{
}
Et son « traitement » par le Producer CDI suivant :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
package
fr.fxjavadevblog.qjg.utils;
import
java.util.UUID;
import
javax.enterprise.context.ApplicationScoped;
import
javax.enterprise.inject.Produces;
@ApplicationScoped
public
class
Producers
{
/**
* produces randomly generated UUID for primary keys.
*
*
@return
UUID as a HEXA-STRING
*
*/
@Produces
@InjectUUID
public
String produceUUIDAsString
(
)
{
return
UUID.randomUUID
(
).toString
(
);
}
}
IX-C. Le repository CRUD avec Spring Data JPA▲
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
package
fr.fxjavadevblog.qjg.videogame;
import
java.util.List;
import
org.springframework.data.repository.CrudRepository;
/**
* CRUD repository for the VideoGame class. Primary key is a UUID represented by a String.
* This repository is created by Hibernate Data JPA.
*
*
@author
François-Xavier Robin
*
*/
public
interface
VideoGameRepository extends
CrudRepository<
VideoGame, String>
{
/**
* gets every Video Game filtered by the given Genre.
*
*
@param
genre
* genre of video game
*
@see
Genre
*
*
@return
* a list of video games
*/
List<
VideoGame>
findByGenre
(
Genre genre);
}
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
package
fr.fxjavadevblog.qjg.videogame;
import
java.util.List;
import
javax.ws.rs.BadRequestException;
import
javax.ws.rs.GET;
import
javax.ws.rs.Path;
import
javax.ws.rs.PathParam;
import
javax.ws.rs.Produces;
/**
* JAX-RS endpoint for Video Games.
*
*
@author
François-Xavier Robin
*
*/
@Path
(
"/api/videogames/v1"
)
public
class
VideoGameResource
{
private
final
VideoGameRepository videoGameRepository;
public
VideoGameResource
(
VideoGameRepository videoGameRepository)
{
this
.videoGameRepository =
videoGameRepository;
}
@GET
@Produces
(
"application/json"
)
public
Iterable<
VideoGame>
findAll
(
)
{
return
videoGameRepository.findAll
(
);
}
@GET
@Path
(
"/genre/{genre}"
)
@Produces
(
"application/json"
)
public
List<
VideoGame>
findByGenre
(
@PathParam
(
value =
"genre"
) String genre)
{
try
{
Genre convertedGenre =
Genre.valueOf
(
genre.replace
(
"-"
, "_"
).toUpperCase
(
));
return
videoGameRepository.findByGenre
(
convertedGenre);
}
catch
(
java.lang.IllegalArgumentException e)
{
throw
new
BadRequestException
(
"Genre "
+
genre +
"does not exist."
);
}
}
}
Il suffit ensuite de déclencher la requête REST suivante pour obtenir tous les jeux vidéo :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
$
curl http://localhost:8080
//api/videogames/v1
[
{
"id"
: "896b9c77-4f6d-4bd6-b681-2791acfa0d51"
,
"name"
: "100 4 1"
,
"genre"
: "REFLEXION"
,
"version"
: 1
},
{
"id"
: "6bf157fa-bd95-46ce-bbca-58afb87ebb9b"
,
"name"
: "10TH FRAME"
,
"genre"
: "SPORT"
,
"version"
: 1
},
{
"id"
: "e603e430-0853-46b0-9f44-d4f662f56c51"
,
"name"
: "1943 : THE BATTLE OF MIDWAY"
,
"genre"
: "SHOOT_THEM_UP"
,
"version"
: 1
},
{
"id"
: "61ec5869-d9f5-497a-9ffc-8e3612892c4b"
,
"name"
: "1ST DIVISION MANAGER"
,
"genre"
: "SPORT"
,
"version"
: 1
},
{
"id"
: "ed1233c4-c130-49f4-b329-314a6dd957a3"
,
"name"
: "1ST SERVE TENNIS"
,
"genre"
: "SPORT"
,
"version"
: 1
},
{
"id"
: "85d071ca-95bc-488a-afdc-494c430f03d9"
,
"name"
: "20000 LEAGUES UNDER THE SEA"
,
"genre"
: "RPG"
,
"version"
: 1
},
... etc ...
De la même manière, pour filtrer sur le genre de jeu :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
$
curl http://localhost:8080
/api/videogames/v1/genre/SHOOT_THEM_UP
[
{
"id"
: "e603e430-0853-46b0-9f44-d4f662f56c51"
,
"name"
: "1943 : THE BATTLE OF MIDWAY"
,
"genre"
: "SHOOT_THEM_UP"
,
"version"
: 1
},
{
"id"
: "fa02815b-f6d2-4ba1-9f63-5c8f575d06b6"
,
"name"
: "AIR SUPPLY"
,
"genre"
: "SHOOT_THEM_UP"
,
"version"
: 1
},
{
"id"
: "07638287-8f45-4be8-a887-c57f350a9ce7"
,
"name"
: "ALBEDO"
,
"genre"
: "SHOOT_THEM_UP"
,
"version"
: 1
},
{
"id"
: "56417168-1e57-4197-a490-56258e5405eb"
,
"name"
: "ALCON"
,
"genre"
: "SHOOT_THEM_UP"
,
"version"
: 1
},
{
"id"
: "d5bd6aaa-674d-4e7f-ae8e-53118de897c6"
,
"name"
: "ALIEN BLAST"
,
"genre"
: "SHOOT_THEM_UP"
,
"version"
: 1
},
{
"id"
: "2589d502-4619-4728-b688-9cece2a8a3ab"
,
"name"
: "ALIEN BUSTERS IV"
,
"genre"
: "SHOOT_THEM_UP"
,
"version"
: 1
}
... 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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
[
...
...
{
"id"
: "d5bd6aaa-674d-4e7f-ae8e-53118de897c6"
,
"name"
: "ALIEN BLAST"
,
"genre"
: "shoot-them-up"
,
"version"
: 1
},
...
...
]
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
public
enum
Genre
{
@JsonProperty
(
value =
"arcade"
)
ARCADE,
@JsonProperty
(
value =
"education"
)
EDUCATION,
@JsonProperty
(
value =
"fighting"
)
FIGHTING,
@JsonProperty
(
value =
"pinball"
)
PINBALL,
@JsonProperty
(
value =
"platform"
)
PLATFORM,
@JsonProperty
(
value =
"reflexion"
)
REFLEXION,
@JsonProperty
(
value =
"rpg"
)
RPG,
@JsonProperty
(
value =
"shoot-them-up"
)
SHOOT_THEM_UP,
@JsonProperty
(
value =
"simulation"
)
SIMULATION,
@JsonProperty
(
value =
"sport"
)
SPORT;
}
Avec cela, on règle déjà un premier problème : le contenu du retour JSON !
2.
3.
4.
5.
6.
7.
8.
9.
$
curl http://localhost:8080
//api/videogames/v1/genre/SHOOT_THEM_UP
[
{
"id"
: "e603e430-0853-46b0-9f44-d4f662f56c51"
,
"name"
: "1943 : THE BATTLE OF MIDWAY"
,
"genre"
: "shoot-them-up"
,
"version"
: 1
},
... 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
.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
package
fr.fxjavadevblog.qjg.utils;
import
java.util.EnumSet;
import
java.util.Optional;
import
javax.ws.rs.ext.ParamConverter;
import
org.slf4j.Logger;
import
org.slf4j.LoggerFactory;
import
com.fasterxml.jackson.annotation.JsonProperty;
import
com.google.common.collect.BiMap;
import
com.google.common.collect.HashBiMap;
/**
* Generic JAX-RS Enum converter based on the Jackson JsonProperty annotation to
* get the mapping.
*
*
@author
François-Xavier Robin
*
*
@param
<T>
*/
public
class
GenericEnumConverter<
T extends
Enum<
T>>
implements
ParamConverter<
T>
{
private
static
final
Logger log =
LoggerFactory.getLogger
(
GenericEnumConverter.class
);
/**
* bi-directionnal Map to store enum value as key and its string representation as value.
* The string representation is retrieved through the JsonProperty annotation put on the enum constant.
*/
private
final
BiMap<
T, String>
biMap =
HashBiMap.create
(
);
/**
* returns a Generic converter for an enum class in the context of JAX-RS ParamConverter.
*
*
@param
<T>
* enum type
*
@param
t
* enum type class
*
@return
* a generic converter used by JAX-RS.
*/
public
static
<
T extends
Enum<
T>>
ParamConverter<
T>
of
(
Class<
T>
t)
{
return
new
GenericEnumConverter<>(
t);
}
private
GenericEnumConverter
(
Class<
T>
t)
{
log.debug
(
"Generating conversion map for enum {}"
, t);
EnumSet.allOf
(
t).forEach
(
v ->
{
try
{
String enumValue =
v.name
(
);
JsonProperty annotation =
v.getClass
(
).getDeclaredField
(
enumValue).getAnnotation
(
JsonProperty.class
);
// get the annotation if it exists or take the classic enum representation
String result =
Optional.ofNullable
(
annotation).map
(
JsonProperty::value).orElse
(
enumValue);
log.debug
(
"Enum value {}.{} is mapped to
\"
{}
\"
"
, t.getSimpleName
(
), v.name
(
), result);
biMap.put
(
v, result);
}
catch
(
NoSuchFieldException |
SecurityException e)
{
log.error
(
"Error while populating BiMap of enum {}"
, t.getClass
(
));
log.error
(
"Thrown by "
, e);
}
}
);
}
/**
* returns the enum type constant from this string representation.
*/
@Override
public
T fromString
(
String value)
{
T returnedValue =
biMap.inverse
(
).get
(
value);
log.debug
(
"Converting String
\"
{}
\"
to {}.{}"
, value, returnedValue.getClass
(
), returnedValue);
return
returnedValue;
}
/**
* returns the string represenation from this enum type constant.
*/
@Override
public
String toString
(
T t)
{
String returnedValue =
biMap.get
(
t);
log.debug
(
"Converting Enum {}.{} to String
\"
{}
\"
"
, t.getClass
(
), t, returnedValue);
return
returnedValue;
}
}
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
Generating conversion map for enum class fr.fxjavadevblog.qjg.genre.Genre
Enum value Genre.ARCADE is mapped to "arcade"
Enum value Genre.EDUCATION is mapped to "education"
Enum value Genre.FIGHTING is mapped to "fighting"
Enum value Genre.PINBALL is mapped to "pinball"
Enum value Genre.PLATFORM is mapped to "platform"
Enum value Genre.REFLEXION is mapped to "reflexion"
Enum value Genre.RPG is mapped to "rpg"
Enum value Genre.SHOOT_THEM_UP is mapped to "shoot-them-up"
Enum value Genre.SIMULATION is mapped to "simulation"
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
:
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
package
fr.fxjavadevblog.qjg.genre;
import
java.lang.annotation.Annotation;
import
java.lang.reflect.Type;
import
javax.ws.rs.ext.ParamConverter;
import
javax.ws.rs.ext.ParamConverterProvider;
import
javax.ws.rs.ext.Provider;
import
org.slf4j.Logger;
import
org.slf4j.LoggerFactory;
import
fr.fxjavadevblog.qjg.utils.GenericEnumConverter;
/**
* JAX-RS provider for Genre conversion. This converter is registered because of
* the Provider annotation.
*
*
@author
François-Xavier Robin
*/
@Provider
public
class
GenreConverterProvider implements
ParamConverterProvider
{
private
final
Logger log =
LoggerFactory.getLogger
(
GenreConverterProvider.class
);
private
final
ParamConverter<
Genre>
converter =
GenericEnumConverter.of
(
Genre.class
);
@SuppressWarnings
(
"unchecked"
)
@Override
public
<
T>
ParamConverter<
T>
getConverter
(
Class<
T>
rawType, Type genericType, Annotation[] annotations)
{
log.debug
(
"Registering GenreConverter"
);
return
(
Genre.class
.equals
(
rawType)) ? (
ParamConverter<
T>
) converter : null
;
}
}
Le endpoint REST de la classe VideoGameResource change légèrement pour se simplifier :
2.
3.
4.
5.
6.
@GET
@Path
(
"/genre/{genre}"
)
public
List<
VideoGame>
findByGenre
(
@PathParam
(
"genre"
) Genre genre)
{
return
videoGameRepository.findByGenre
(
genre);
}
Notez ici l'appel au convertisseur générique précédemment codé.
Grâce à tout ceci, on obtient le comportement souhaité :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
$
curl http://localhost:8080
//api/videogames/v1/genre/shoot-them-up
[
{
"id"
: "e603e430-0853-46b0-9f44-d4f662f56c51"
,
"name"
: "1943 : THE BATTLE OF MIDWAY"
,
"genre"
: "shoot-them-up"
,
"version"
: 1
},
{
"id"
: "fa02815b-f6d2-4ba1-9f63-5c8f575d06b6"
,
"name"
: "AIR SUPPLY"
,
"genre"
: "shoot-them-up"
,
"version"
: 1
},
{
"id"
: "07638287-8f45-4be8-a887-c57f350a9ce7"
,
"name"
: "ALBEDO"
,
"genre"
: "shoot-them-up"
,
"version"
: 1
},
{
"id"
: "56417168-1e57-4197-a490-56258e5405eb"
,
"name"
: "ALCON"
,
"genre"
: "shoot-them-up"
,
"version"
: 1
},
{
"id"
: "d5bd6aaa-674d-4e7f-ae8e-53118de897c6"
,
"name"
: "ALIEN BLAST"
,
"genre"
: "shoot-them-up"
,
"version"
: 1
},
{
"id"
: "2589d502-4619-4728-b688-9cece2a8a3ab"
,
"name"
: "ALIEN BUSTERS IV"
,
"genre"
: "shoot-them-up"
,
"version"
: 1
}
... 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.
2.
3.
4.
$
curl -s -o /dev/null -D - http://localhost:8080
//api/videogames/v1/genre/SHOOT_THEM_UP
HTTP/1
.1
404
Not Found
Content-Length: 0
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
$
mvn clean test
...
...
[INFO] --- maven-surefire-plugin:2
.22
.2
:test (
default-test) @ quarkus-tuto ---
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running fr.fxjavadevblog.qjg.ping.PingTest
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
2020
-04
-14
19
:25
:27
,030
INFO [io.quarkus] (
main) Profile test activated.
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]
[INFO] Tests run: 1
, Failures: 0
, Errors: 0
, Skipped: 0
, Time elapsed: 7
.947
s - in
fr.fxjavadevblog.qjg.ping.PingTest
[INFO] Running fr.fxjavadevblog.qjg.utils.GenericEnumConverterTest
[INFO] Tests run: 100
, Failures: 0
, Errors: 0
, Skipped: 0
, Time elapsed: 0
.174
s - in
fr.fxjavadevblog.qjg.utils.GenericEnumConverterTest
[INFO] Running fr.fxjavadevblog.qjg.utils.CDITest
[INFO] Tests run: 3
, Failures: 0
, Errors: 0
, Skipped: 0
, Time elapsed: 0
.001
s - in
fr.fxjavadevblog.qjg.utils.CDITest
[INFO] Running fr.fxjavadevblog.qjg.utils.DummyTest
[INFO] Tests run: 1
, Failures: 0
, Errors: 0
, Skipped: 0
, Time elapsed: 0
s - in
fr.fxjavadevblog.qjg.utils.DummyTest
2020
-04
-14
19
:25
:28
,697
INFO [io.quarkus] (
main) Quarkus stopped in
0
.056s
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 105
, Failures: 0
, Errors: 0
, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 13
.888
s
[INFO] Finished at: 2020
-04
-14T19:25
:29
+02
:00
[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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
package
fr.fxjavadevblog.qjg.videogame;
import
static
io.restassured.RestAssured.given;
import
static
org.hamcrest.CoreMatchers.containsString;
import
org.junit.jupiter.api.DisplayName;
import
org.junit.jupiter.api.Tag;
import
org.junit.jupiter.api.Test;
import
fr.fxjavadevblog.qjg.global.TestingGroups;
import
io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
@Tag
(
TestingGroups.INTEGRATION_TESTING)
class
IT_VideoGameResource
{
public
static
final
String ENDPOINT =
"/api/videogames/v1"
;
@Test
@DisplayName
(
"Get "
+
ENDPOINT)
void
testGetAllVideoGames
(
)
{
given
(
).when
(
)
.get
(
ENDPOINT +
"/"
)
.then
(
)
.statusCode
(
200
)
.assertThat
(
).body
(
containsString
(
"XENON"
),
containsString
(
"RICK"
),
containsString
(
"LOTUS"
));
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
$
mvn -Dskip.surefire.tests verify
Scanning for
projects...
[INFO]
[INFO] -------------------<
fr.fxjavadevblog:quarkus-tuto >
--------------------
[INFO] Building Quarkus-JPA-PostgreSQL 0
.0
.1
-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:3
.1
.0
:resources (
default-resources) @ quarkus-tuto ---
[INFO] Using 'UTF-8'
encoding to copy filtered resources.
[INFO] Copying 2
resources
[INFO]
[INFO] --- maven-compiler-plugin:3
.8
.1
:compile (
default-compile) @ quarkus-tuto ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- build-helper-maven-plugin:3
.1
.0
:add-test-source (
add-integration-test-sources) @ quarkus-tuto ---
[INFO] Test Source directory: /home/robin/git/quarkus-tuto/src/test-integration/java added.
[INFO]
[INFO] --- build-helper-maven-plugin:3
.1
.0
:add-test-resource (
add-integration-test-resource) @ quarkus-tuto ---
[INFO]
[INFO] --- maven-resources-plugin:3
.1
.0
:testResources (
default-testResources) @ quarkus-tuto ---
[INFO] Using 'UTF-8'
encoding to copy filtered resources.
[INFO] Copying 1
resource
[INFO] Copying 1
resource
[INFO]
[INFO] --- maven-compiler-plugin:3
.8
.1
:testCompile (
default-testCompile) @ quarkus-tuto ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2
.22
.2
:test (
default-test) @ quarkus-tuto ---
[INFO] Tests are skipped.
[INFO]
[INFO] --- maven-jar-plugin:2
.4
:jar (
default-jar) @ quarkus-tuto ---
[INFO]
[INFO] --- quarkus-maven-plugin:1
.3
.1
.Final:build (
default) @ quarkus-tuto ---
[INFO] [org.jboss.threads] JBoss Threads version 3
.0
.1
.Final
[INFO] [org.hibernate.Version] HHH000412: Hibernate ORM core version 5
.4
.12
.Final
[INFO] [io.quarkus.deployment.pkg.steps.JarResultBuildStep] Building thin jar: /home/robin/git/quarkus-tuto/target/quarkus-tuto-0
.0
.1
-SNAPSHOT-runner.jar
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in
1647ms
[INFO]
[INFO] --- maven-resources-plugin:3
.1
.0
:copy-resources (
copy-resources) @ quarkus-tuto ---
[INFO] Using 'UTF-8'
encoding to copy filtered resources.
[INFO] Copying 1
resource
[INFO]
[INFO] --- docker-maven-plugin:0
.33
.0
:stop (
docker:start) @ quarkus-tuto ---
[INFO]
[INFO] --- docker-maven-plugin:0
.33
.0
:start (
docker:start) @ quarkus-tuto ---
[INFO] DOCKER>
[postgres:12
.2
] "postgresql"
: Start container ca6a77a7d1e3
[INFO] DOCKER>
[postgres:12
.2
] "postgresql"
: Waiting for
mapped ports [5432
] on host localhost
[INFO] DOCKER>
[postgres:12
.2
] "postgresql"
: Waited on tcp port '[localhost/127.0.0.1:5432]'
4
ms
[INFO]
[INFO] --- maven-failsafe-plugin:2
.22
.2
:integration-test (
default) @ quarkus-tuto ---
19
:29
:19
.154
PostgreSQL Server :The files belonging to this database system will be owned by user "postgres"
.
19
:29
:19
.154
PostgreSQL Server :This user must also own the server process.
19
:29
:19
.154
PostgreSQL Server :
19
:29
:19
.154
PostgreSQL Server :The database cluster will be initialized with locale "en_US.utf8"
.
19
:29
:19
.154
PostgreSQL Server :The default database encoding has accordingly been set to "UTF8"
.
19
:29
:19
.154
PostgreSQL Server :The default text search configuration will be set to "english"
.
19
:29
:19
.154
PostgreSQL Server :
19
:29
:19
.154
PostgreSQL Server :Data page checksums are disabled.
19
:29
:19
.154
PostgreSQL Server :
19
:29
:19
.154
PostgreSQL Server :fixing permissions on existing directory /var/lib/postgresql/data ... ok
19
:29
:19
.155
PostgreSQL Server :creating subdirectories ... ok
19
:29
:19
.155
PostgreSQL Server :selecting dynamic shared memory implementation ... posix
19
:29
:19
.171
PostgreSQL Server :selecting default max_connections ... 100
19
:29
:19
.197
PostgreSQL Server :selecting default shared_buffers ... 128MB
19
:29
:19
.243
PostgreSQL Server :selecting default time zone ... Etc/UTC
19
:29
:19
.244
PostgreSQL Server :creating configuration files ... ok
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
19
:29
:19
.393
PostgreSQL Server :running bootstrap script ... ok
19
:29
:20
.011
PostgreSQL Server :performing post-bootstrap initialization ... ok
[INFO] Running fr.fxjavadevblog.qjg.utils.IT_DummyTest
[INFO] Tests run: 1
, Failures: 0
, Errors: 0
, Skipped: 0
, Time elapsed: 0
.032
s - in
fr.fxjavadevblog.qjg.utils.IT_DummyTest
[INFO] Running fr.fxjavadevblog.qjg.videogame.IT_VideoGameResource
19
:29
:20
.174
PostgreSQL Server :syncing data to disk ... ok
19
:29
:20
.174
PostgreSQL Server :
19
:29
:20
.174
PostgreSQL Server :
19
:29
:20
.174
PostgreSQL Server :Success. You can now start the database server using:
19
:29
:20
.174
PostgreSQL Server :
19
:29
:20
.174
PostgreSQL Server : pg_ctl -D /var/lib/postgresql/data -l logfile start
19
:29
:20
.174
PostgreSQL Server :
19
:29
:20
.174
PostgreSQL Server :initdb: warning: enabling "trust"
authentication for
local
connections
19
:29
:20
.174
PostgreSQL Server :You can change this by editing pg_hba.conf or using the option -A, or
19
:29
:20
.174
PostgreSQL Server :--auth-local
and --auth-host, the next time you run initdb.
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
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"
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
19
:29
:20
.223
PostgreSQL Server :2020
-04
-14
17
:29
:20
.223
UTC [47
] LOG: database system is ready to accept connections
19
:29
:20
.292
PostgreSQL Server : done
19
:29
:20
.292
PostgreSQL Server :server started
19
:29
:20
.418
PostgreSQL Server :CREATE DATABASE
19
:29
:20
.419
PostgreSQL Server :
19
:29
:20
.419
PostgreSQL Server :
19
:29
:20
.419
PostgreSQL Server :/usr/local
/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*
19
:29
:20
.419
PostgreSQL Server :
19
:29
:20
.420
PostgreSQL Server :2020
-04
-14
17
:29
:20
.420
UTC [47
] LOG: received fast shutdown request
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
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
19
:29
:20
.424
PostgreSQL Server :2020
-04
-14
17
:29
:20
.424
UTC [49
] LOG: shutting down
19
:29
:20
.437
PostgreSQL Server :2020
-04
-14
17
:29
:20
.436
UTC [47
] LOG: database system is shut down
19
:29
:20
.520
PostgreSQL Server : done
19
:29
:20
.520
PostgreSQL Server :server stopped
19
:29
:20
.520
PostgreSQL Server :
19
:29
:20
.520
PostgreSQL Server :PostgreSQL init process complete; ready for
start up.
19
:29
:20
.520
PostgreSQL Server :
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
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
19
:29
:20
.538
PostgreSQL Server :2020
-04
-14
17
:29
:20
.537
UTC [1
] LOG: listening on IPv6 address "::"
, port 5432
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"
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
19
:29
:20
.560
PostgreSQL Server :2020
-04
-14
17
:29
:20
.560
UTC [1
] LOG: database system is ready to accept connections
Hibernate:
drop table if
exists VIDEO_GAME cascade
Hibernate:
create table VIDEO_GAME (
id varchar
(
36
) not null,
GENRE varchar
(
255
) not null,
NAME varchar
(
255
) not null,
VERSION int8,
primary key (
id)
)
Hibernate:
alter table if
exists VIDEO_GAME
add constraint UK_jg5tlrbpecd0wd8c9vjo6b429 unique (
NAME)
Hibernate:
INSERT INTO VIDEO_GAME
(
ID,NAME,GENRE,VERSION) VALUES (
'896b9c77-4f6d-4bd6-b681-2791acfa0d51'
,'100 4 1'
,'REFLEXION'
,1
)
Hibernate:
INSERT INTO VIDEO_GAME
(
ID,NAME,GENRE,VERSION) VALUES (
'6bf157fa-bd95-46ce-bbca-58afb87ebb9b'
,'10TH FRAME'
,'SPORT'
,1
)
Hibernate:
INSERT INTO VIDEO_GAME
(
ID,NAME,GENRE,VERSION) VALUES (
'e603e430-0853-46b0-9f44-d4f662f56c51'
,'1943 : THE BATTLE OF MIDWAY'
,'SHOOT_THEM_UP'
,1
)
Hibernate:
INSERT INTO VIDEO_GAME
(
ID,NAME,GENRE,VERSION) VALUES (
'61ec5869-d9f5-497a-9ffc-8e3612892c4b'
,'1ST DIVISION MANAGER'
,'SPORT'
,1
)
... etc ...
Hibernate:
select
videogame0_.id as id1_0_,
videogame0_.GENRE as genre2_0_,
videogame0_.NAME as name3_0_,
videogame0_.VERSION as version4_0_
from
VIDEO_GAME videogame0_ limit ?
[INFO] Tests run: 1
, Failures: 0
, Errors: 0
, Skipped: 0
, Time elapsed: 8
.89
s - in
fr.fxjavadevblog.qjg.videogame.IT_VideoGameResource
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2
, Failures: 0
, Errors: 0
, Skipped: 0
[INFO]
[INFO]
[INFO] --- docker-maven-plugin:0
.33
.0
:stop (
docker:stop) @ quarkus-tuto ---
19
:29
:29
.455
PostgreSQL Server :2020
-04
-14
17
:29
:29
.454
UTC [1
] LOG: received smart shutdown request
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
19
:29
:29
.460
PostgreSQL Server :2020
-04
-14
17
:29
:29
.460
UTC [66
] LOG: shutting down
19
:29
:29
.496
PostgreSQL Server :2020
-04
-14
17
:29
:29
.496
UTC [1
] LOG: database system is shut down
[INFO] DOCKER>
[postgres:12
.2
] "postgresql"
: Stop and removed container ca6a77a7d1e3 after 0
ms
[INFO]
[INFO] --- maven-failsafe-plugin:2
.22
.2
:verify (
default) @ quarkus-tuto ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
$
mvn clean package -Pnative -Dquarkus.native.container-build
=
true -Dskip.surefire.tests
...
...
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Pulling image quay.io/quarkus/ubi-quarkus-native-image:19
.3
.1
-java11
19
.3
.1
-java11: Pulling from quarkus/ubi-quarkus-native-image
57de4da701b5: Pull complete
cf0f3ebe9f53: Pull complete
e9da77aa316d: Pull complete
Digest: sha256:b18b701bd6f9d0a7778129f63b9f2dd666be2a2574854b56cd60e3cbd42b73d3
Status: Downloaded newer image for
quay.io/quarkus/ubi-quarkus-native-image:19
.3
.1
-java11
quay.io/quarkus/ubi-quarkus-native-image:19
.3
.1
-java11
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Running Quarkus native-image plugin on GraalVM Version 19
.3
.1
CE
[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
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] classlist: 10
740
,32
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] (
cap): 793
,01
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] setup: 2
161
,94
ms
08
:35
:25
,649
INFO [org.hib.Version] HHH000412: Hibernate ORM core version 5
.4
.12
.Final
08
:35
:25
,663
INFO [org.hib.ann.com.Version] HCANN000001: Hibernate Commons Annotations {5
.1
.0
.Final}
08
:35
:25
,705
INFO [org.hib.dia.Dialect] HHH000400: Using dialect: io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect
08
:35
:25
,848
INFO [org.hib.val.int.uti.Version] HV000001: Hibernate Validator 6
.1
.2
.Final
08
:35
:27
,547
INFO [org.jbo.threads] JBoss Threads version 3
.0
.1
.Final
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] (
typeflow): 47
866
,54
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] (
objects): 33
462
,47
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] (
features): 1
692
,11
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] analysis: 87
791
,76
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] (
clinit): 1
211
,71
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] universe: 5
195
,59
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] (
parse): 6
258
,47
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] (
inline): 8
432
,95
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] (
compile): 55
066
,12
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] compile: 74
232
,51
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] image: 5
742
,28
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] write: 1
469
,31
ms
[quarkus-tuto-0
.0
.1
-SNAPSHOT-runner:24
] [total]: 187
812
,32
ms
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in
223001ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 03
:50
min
[INFO] Finished at: 2020
-04
-15T10:38
:21
+02
:00
[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 :
2.
3.
4.
5.
6.
FROM
registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR
/work/
COPY
target/*-runner /work/application
RUN
chmod 775
/work
EXPOSE
8080
CMD
["./application"
, "-Dquarkus.http.host=0.0.0.0"
]
*
!
target/*-runner
!
target/*-runner.jar
On peut alors lancer la création de l'image Docker :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
$
docker build -f src/main/docker/Dockerfile.native -t quarkus-tuto .
Sending build context to Docker daemon 102
.9MB
Step 1
/6
: FROM registry.access.redhat.com/ubi8/ubi-minimal
latest: Pulling from ubi8/ubi-minimal
b26afdf22be4: Pull complete
218f593046ab: Pull complete
Digest: sha256:df6f9e5d689e4a0b295ff12abc6e2ae2932a1f3e479ae1124ab76cf40c3a8cdd
Status: Downloaded newer image for
registry.access.redhat.com/ubi8/ubi-minimal:latest
--->
91d23a64fdf2
Step 2
/6
: WORKDIR /work/
--->
Running in
40a5ee141273
Removing intermediate container 40a5ee141273
--->
6ab61dca9bf2
Step 3
/6
: COPY target/*-runner /work/application
--->
3aae05f5ee7d
Step 4
/6
: RUN chmod 775
/work
--->
Running in
8387b2b6e071
Removing intermediate container 8387b2b6e071
--->
0c03e70d6326
Step 5
/6
: EXPOSE 8080
--->
Running in
1813737fd08e
Removing intermediate container 1813737fd08e
--->
978251ac9f2b
Step 6
/6
: CMD ["./application"
, "-Dquarkus.http.host=0.0.0.0"
]
--->
Running in
c93ab35754d4
Removing intermediate container c93ab35754d4
--->
6009aee9381b
Successfully built 6009aee9381b
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
$
curl http://localhost:8080
/api/videogames/v1/genre/rpg
[
{
"id"
: "75a9b985-c5a9-40a0-87ba-086850683bfc"
,
"name"
: "20000 LIEUES SOUS LES MERS"
,
"genre"
: "rpg"
,
"version"
: 1
},
{
"id"
: "2b412dd7-d090-4328-8180-869b60bbc106"
,
"name"
: "ADVENTURE"
,
"genre"
: "rpg"
,
"version"
: 1
},
{
"id"
: "885a3475-63c9-49f3-b120-e218ce9c9510"
,
"name"
: "ADVENTURER, THE"
,
"genre"
: "rpg"
,
"version"
: 1
},
{
"id"
: "a500c1a6-6008-45d4-b3f4-5b668b77499a"
,
"name"
: "ADVENTURES OF ROBIN HOOD, THE"
,
"genre"
: "rpg"
,
"version"
: 1
},
... etc ...
]
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
<!-- Health Check -->
<dependency>
<groupId>
io.quarkus</groupId>
<artifactId>
quarkus-smallrye-health</artifactId>
</dependency>
<!-- Metrics -->
<dependency>
<groupId>
io.quarkus</groupId>
<artifactId>
quarkus-smallrye-metrics</artifactId>
</dependency>
Une fois l'application lancée et qu'elle est sollicitée, on peut obtenir un état de son bon fonctionnement :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
$
curl http://localhost:8080
/health
{
"status"
: "UP"
,
"checks"
: [
{
"name"
: "Application"
,
"status"
: "UP"
},
{
"name"
: "Database connections health check"
,
"status"
: "UP"
}
]
}
On peut obtenir quelques mesures qui auront été calculées au moyen de l'annotation @Timed
sur les méthodes de la classe VideoGameResource :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
@Timed
(
name =
"videogames-find-by-all"
, absolute =
true
,
description =
"A measure of how long it takes to fetch all video games."
,
unit =
MetricUnits.MILLISECONDS)
public
Iterable<
VideoGame>
findAll
(
)
{
return
videoGameRepository.findAll
(
);
}
@Timed
(
name =
"videogames-find-by-genre"
, absolute =
true
,
description =
"A measure of how long it takes to fetch all video games filtered by a given genre."
,
unit =
MetricUnits.MILLISECONDS)
public
List<
VideoGame>
findByGenre
(
@PathParam
(
"genre"
) Genre genre)
{
return
videoGameRepository.findByGenre
(
genre);
}
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
$
curl -H"Accept: application/json"
localhost:8080
/metrics/application/videogames-find-by-genre
{
"videogames-find-by-genre"
: {
"p99"
: 10
.044791
,
"min"
: 2
.335977
,
"max"
: 10
.044791
,
"mean"
: 3
.6114086322681627
,
"p50"
: 3
.19797
,
"p999"
: 10
.044791
,
"stddev"
: 1
.6164326637605608
,
"p95"
: 5
.663952
,
"p98"
: 10
.044791
,
"p75"
: 3
.752217
,
"fiveMinRate"
: 0
.06305266722909629
,
"fifteenMinRate"
: 0
.021812705995763727
,
"meanRate"
: 0
.008589683736876096
,
"count"
: 20
,
"oneMinRate"
: 0
.252757448780742
}
}
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.