Summit Art Creations - stock.ado

Comment bâtir un microservice avec Quarkus en 15 minutes

Ce tutoriel pratique sur Quarkus vous guide pas à pas dans la création d'un microservice de catalogue de produits réactif, avec PostgreSQL, des API CRUD, des tests et un déploiement rapide.

Les entreprises recherchent de plus en plus des applications Java rapides, distribuées et capables d'évoluer efficacement. Le framework Quarkus, un projet open source maintenu principalement par Red Hat, est parfaitement adapté à ce cas d'usage.

Dans ce tutoriel pratique, nous allons créer un microservice de catalogue de produits prêt à être déployé en production à l'aide de ce framework. Si vous connaissez déjà Spring Boot et son approche gourmande en ressources d'exécution, vous apprendrez peut-être de nouvelles techniques pour créer et déployer des applications Java.

Le code source complet de ce tutoriel est disponible sur ce dépôt GitHub.

Ce que nous allons développer

L'objectif final de ce tutoriel est de créer une API REST réactive qui gère un catalogue de produits avec une intégration PostgreSQL. Pour y parvenir, nous allons développer un microservice doté des fonctionnalités suivantes :

  • Opérations CRUD complètes pour la gestion des produits.
  • Persistance en base de données avec l'ORM Hibernate.
  • Fonctionnalités de recherche et de filtrage.
  • Couverture de test complète.

Prérequis

Avant de commencer, assurez-vous d'avoir installé les outils suivants :

  • JDK 21 ou plus – GraalVM est recommandé pour la compilation native.
  • Apache Maven 3.8+
  • Un environnement d'exécution de conteneurs Docker ou Podman.
  • Votre IDE préféré -- IntelliJ IDEA, VS Code ou Eclipse.

Astuce : utilisez SDKMAN! pour gérer plusieurs versions de JDK et vous assurer que JAVA_HOME est correctement configuré. Téléchargez-le et installez-le, puis exécutez ces commandes :

sdk install java 21.0.5-tem
sdk install maven

Configuration du projet

La première étape consiste à créer la structure du projet. Créez le répertoire de votre projet et accédez-y :

mkdir product-catalog
cd product-catalog

Lancez maintenant le projet Quarkus avec les extensions nécessaires :

mvn io.quarkus.platform:quarkus-maven-plugin:3.20.0:create \
    -DprojectGroupId=ca.bazlur \
    -DprojectArtifactId=product-catalog \
    -DclassName="ca.bazlur.ProductResource" \
    -Dpath="/products" \
    -Dextensions="hibernate-orm-panache,resteasy-jackson,jdbc-postgresql"

Remarquez que nous ajoutons des extensions au moment de la création. Les extensions Quarkus offrent des fonctionnalités préintégrées sans aucune configuration supplémentaire. Voici les extensions que nous avons ajoutées :

  • hibernate-orm-panache. Cela simplifie l'utilisation de JPA grâce au modèle « active record ».
  • resteasy-jackson. Implémentation de JAX-RS avec prise en charge de JSON.
  • jdbc-postgresql. Pilote de base de données PostgreSQL.

Cette commande crée la structure de projet suivante :

product-catalog/
├── mvnw
├── mvnw.cmd
├── pom.xml
├── README.md
└── src/
    ├── main/
    │   ├── docker/
    │   │   ├── Dockerfile.jvm
    │   │   ├── Dockerfile.legacy-jar
    │   │   ├── Dockerfile.native
    │   │   └── Dockerfile.native-micro
    │   ├── java/
    │   │   └── ca/
    │   │       └── bazlur/
    │   │           ├── MyEntity.java
    │   │           └── ProductResource.java
    │   └── resources/
    │       ├── application.properties
    │       └── import.sql
    └── test/
        └── java/
            └── ca/
                └── bazlur/
                    ├── ProductResourceIT.java
                    └── ProductResourceTest.java

 

Obtenir et tester le point de terminaison REST initial

Quarkus génère un point de terminaison REST de base pour nous aider à démarrer. Ouvrez le fichier ProductResource.java pour le consulter :

package ca.bazlur;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/products")
public class ProductResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello from RESTEasy";
    }
}

Testons ce point de terminaison. Lancez l'application en mode développement :

./mvnw quarkus:dev

Quarkus prend en charge la mise à jour en direct, ce qui permet de voir les modifications apportées au code s'appliquer immédiatement sans avoir à redémarrer l'application. Ce provisionnement automatisé des services en modes développement et test est appelé « Dev Services ».

Nous allons maintenant tester le point de terminaison :

curl -i localhost:8080/products

Et recevoir les en-têtes suivants en réponse :

HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
content-length: 18

Et voici ce qui figure dans le corps de la réponse :

Hello from RESTEasy

Créer l'entité produit

Créons maintenant notre entité produit. Remplacez le fichier MyEntity.java généré par un nouveau fichier Product.java:

package ca.bazlur;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

@Entity
@Table(name = "products")
public class Product extends PanacheEntity { // ①

    @Column(nullable = false)
    public String name;

    @Column(length = 1000)
    public String description;

    @Column(nullable = false, precision = 10, scale = 2) // ②
    public BigDecimal price;

    @Column(nullable = false)
    public String category;

    @Column(name = "created_at", updatable = false) // ③
    public LocalDateTime createdAt;

    // Custom query methods
    public static List<Product> findByCategory(String category) { // ④
        return find("category", category).list();
    }

    public static List<Product> searchByName(String searchTerm) { // ⑤
        return find("LOWER(name) LIKE LOWER(?1)",
                    "%" + searchTerm + "%").list();
    }

    @PrePersist
    void onCreate() { // ⑥
        createdAt = LocalDateTime.now();
    }
}

Voici ce que nous avons mis en œuvre dans le code, comme indiqué dans l'extrait ci-dessus :

PanacheEntity met en œuvre le modèle « active record » avec des opérations CRUD intégrées et un champ ID généré automatiquement.

BigDecimal, avec ses paramètres de précision et d'échelle, garantit des calculs monétaires précis sans erreurs de virgule flottant

③ L'attribut updatable = false garantit que l'horodatage de création est immuable après la persistance initiale.

④ Les méthodes de recherche statiques fournissent des requêtes sans risque de type, sans nécessiter de couche de référentiel distincte.

⑤ La recherche insensible à la casse à l'aide des fonctions LOWER()fonctionne sur différentes bases de données.

⑥ Les callbacks du cycle de vie JPA définissent automatiquement les horodatages sans intervention manuelle.

La puissance de Panache

Panache est une fonctionnalité de Quarkus qui simplifie l'accès aux bases de données en Java.

Ceux qui utilisent Spring Data JPA s'attendent généralement à devoir écrire une interface de référentiel pour chaque entité. Panache adopte une approche différente qui transforme l'entité en un puissant enregistrement actif doté de capacités de requête intégrées. Panache prend en charge les modèles de référentiel, mais il n'est pas nécessaire de créer des classes de référentiel distinctes.

Voici ce qu'il offre d'emblée avec le modèle d'enregistrement :

// Basic CRUD operations
Product product = Product.findById(1L);
List<Product> products = Product.listAll();
long count = Product.count();
Product.deleteById(1L);

// Pagination support
PanacheQuery<Product> query = Product.findAll();
List<Product> firstPage = query.page(0, 20).list();

// Type-safe queries with parameters
List<Product> electronics = Product.list("category", "Electronics");
Product cheapest = Product.find("price < ?1", 50.0)
        .firstResult();

// Sorting
List<Product> sorted = Product.listAll(
        Sort.by("price").descending()
);

// Complex queries remain readable
List<Product> results = Product.find(
        "category = ?1 and price between ?2 and ?3",
        "Electronics", 20.0, 100.0
).list();

Créer l'API REST

Créons maintenant une API REST complète. Modifiez le fichier ProductResource.java, comme suit :

package ca.bazlur;

import jakarta.transaction.Transactional;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;

import java.net.URI;
import java.util.List;

@Path("/products")
@Produces(MediaType.APPLICATION_JSON) // ①
@Consumes(MediaType.APPLICATION_JSON)
public class ProductResource {

    @GET
    public List<Product> getAllProducts(
            @QueryParam("category") String category, // ②
            @QueryParam("search") String search) {

        if (category != null) {
            return Product.findByCategory(category);
        }

        if (search != null) {
            return Product.searchByName(search);
        }

        return Product.listAll();
    }

    @GET
    @Path("/{id}")
    public Response getProduct(@PathParam("id") Long id) { // ③
        Product product = Product.findById(id);
        if (product != null) {
            return Response.ok(product).build();
        }
        return Response.status(Response.Status.NOT_FOUND).build();
    }

    @POST
    @Transactional // ④
    public Response createProduct(Product product) {
        product.persist();
        URI location = UriBuilder.fromResource(ProductResource.class)
                .path("{id}")
                .build(product.id); // ⑤
        return Response.created(location)
                .entity(product)
                .build();
    }

    @PUT
    @Path("/{id}")
    @Transactional
    public Response updateProduct(@PathParam("id") Long id,
                                 Product product) {
        Product existing = Product.findById(id);
        if (existing == null) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }

        existing.name = product.name; // ⑥
        existing.description = product.description;
        existing.price = product.price;
        existing.category = product.category;

        return Response.ok(existing).build();
    }

    @DELETE
    @Path("/{id}")
    @Transactional
    public Response deleteProduct(@PathParam("id") Long id) {
        boolean deleted = Product.deleteById(id); // ⑦
        if (deleted) {
            return Response.noContent().build();
        }
        return Response.status(Response.Status.NOT_FOUND).build();
    }
}

Cette ressource REST illustre plusieurs modèles importants de Jakarta RESTful Web Services et de Quarkus :

① Les annotations Jakarta RESTful Web Service au niveau de la classe s’appliquent à toutes les méthodes, ce qui réduit les répétitions.

② Les paramètres de requête permettent de filtrer et d’effectuer des recherches sans avoir recours à des points de terminaison distincts.

③ Les objets de réponse offrent un contrôle précis sur les codes d’état HTTP et les en-têtes.

④ L'annotation @Transactional garantit que les opérations sur la base de données sont atomiques, ce qui signifie qu'elles réussissent toutes ou échouent toutes.

⑤ Les en-têtes de localisation respectent les meilleures pratiques REST en indiquant où se trouve la ressource créée.

⑥ L'accès direct aux champs dans les entités Panache permet de conserver un code concis et lisible.

⑦ Les valeurs de retour booléennes des méthodes Panache indiquent si l'opération a réussi.

Configuration de la base de données avec Dev Services

Comme mentionné précédemment, l'une des fonctionnalités les plus impressionnantes de Quarkus est Dev Services. Pour l'utiliser ici, nous ajoutons des données de test au fichier src/main/resources/import.sql:

INSERT INTO products (id, name, description, price, category, created_at)
VALUES 
    (1, 'Laptop Pro', 'High-performance laptop for professionals', 
     1499.99, 'Electronics', '2024-10-26T10:00:00'),
    (2, 'Wireless Mouse', 'Ergonomic wireless mouse with long battery life', 
     29.99, 'Accessories', '2024-10-26T10:05:00'),
    (3, 'Mechanical Keyboard', 'RGB mechanical keyboard with blue switches', 
     129.50, 'Accessories', '2024-10-25T14:30:00'),
    (4, '4K Monitor', '27-inch 4K UHD monitor with HDR support', 
     399.00, 'Electronics', '2024-10-26T11:15:00'),
    (5, 'Java Programming Book', 'Comprehensive guide to modern Java', 
     45.99, 'Books', '2024-10-24T09:00:00');

ALTER SEQUENCE products_SEQ RESTART WITH 6;

Ces requêtes d'insertion s'exécuteront au démarrage.

Assurez-vous que Docker ou Podman est en cours d'exécution, puis redémarrez l'application

./mvnw quarkus:dev

Et voilà ! Quarkus s'occupe automatiquement du reste.

D’abord, il détecte que vous avez besoin de PostgreSQL. Ensuite, il lance une instance de la base de données en conteneur.

Il configure la source de données, puis crée le schéma de la base de données avant de charger les données de test.

Pas besoin de fichiers Docker Compose ni de configuration manuelle. Juste une productivité de développement optimale.

Maintenant, testons l'API :

# Get all products
curl localhost:8080/products | jq

# Filter by category
curl "localhost:8080/products?category=Electronics" | jq

# Search by name
curl "localhost:8080/products?search=laptop" | jq

# Create a new product
curl -X POST localhost:8080/products \
  -H "Content-Type: application/json" \
  -d '{
    "name": "USB-C Hub",
    "description": "7-in-1 USB-C hub with HDMI",
    "price": 49.99,
    "category": "Accessories"
  }' | jq

Tests complets avec Quarkus

Quarkus simplifie les tests. Il utilise une seule annotation, @QuarkusTest. Contrairement aux différentes annotations de Spring Boot (@WebMvcTest, @DataJpaTest, @SpringBootTest), Quarkus adopte une approche unifiée. L'annotation @QuarkusTest lance l'application dans son intégralité avec Dev Services. Il fournit ainsi de véritables bases de données et services. Nous n'avons pas besoin de simulations ni de substituts à H2.

Mettez à jour ProductResourceTest.java:

package ca.bazlur;

import io.quarkus.test.junit.QuarkusTest;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.core.MediaType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.Matchers.*;

@QuarkusTest // ①
class ProductResourceTest {

    @BeforeEach
    @Transactional
    void cleanupDatabase() { // ②
        Product.deleteAll();
    }

    @Test
    void testGetAllProductsEmpty() {
        given()
            .when().get("/products")
            .then()
            .statusCode(200)
            .contentType(MediaType.APPLICATION_JSON)
            .body("$", empty()); // ③
    }

    @Test
    void testGetAllProductsWithData() {
        createTestProducts();

        given()
            .when().get("/products")
            .then()
            .statusCode(200)
            .contentType(MediaType.APPLICATION_JSON)
            .body("$", hasSize(3))
            .body("name", hasItems("Laptop", "Mouse", "Book")); // ④
    }

    @Test
    void testSearchProductsCaseInsensitive() {
        createTestProducts();

        given()
            .queryParam("search", "LAPTOP") // ⑤
            .when().get("/products")
            .then()
            .statusCode(200)
            .body("$", hasSize(1))
            .body("[0].name", equalTo("Laptop"));
    }

    @Test
    void testCreateProduct() {
        Product product = new Product();
        product.name = "New Product";
        product.description = "A new product description";
        product.price = new BigDecimal("49.99");
        product.category = "New Category";

        given()
            .contentType(MediaType.APPLICATION_JSON)
            .body(product)
            .when().post("/products")
            .then()
            .statusCode(201) // ⑥
            .header("Location", matchesPattern(".*/products/\\d+"))
            .body("id", notNullValue())
            .body("createdAt", notNullValue());
    }

    @Test
    void testUpdateProduct() {
        Long productId = createProduct("Original Product",
            "Original Description",
            new BigDecimal("19.99"), "Original Category");

        Product updatedProduct = new Product();
        updatedProduct.name = "Updated Product";
        updatedProduct.description = "Updated Description";
        updatedProduct.price = new BigDecimal("29.99");
        updatedProduct.category = "Updated Category";

        given()
            .contentType(MediaType.APPLICATION_JSON)
            .body(updatedProduct)
            .when().put("/products/" + productId)
            .then()
            .statusCode(200)
            .body("name", equalTo("Updated Product")); // ⑦

        // Verify persistence
        Product persisted = Product.findById(productId);
        assert persisted.name.equals("Updated Product");
    }

    @Test
    void testDeleteProduct() {
        Long productId = createProduct("Product to Delete",
            "Will be deleted",
            new BigDecimal("99.99"), "Test");

        given()
            .when().delete("/products/" + productId)
            .then()
            .statusCode(204); // ⑧

        // Verify deletion
        given()
            .when().get("/products/" + productId)
            .then()
            .statusCode(404);
    }

    @Transactional
    void createTestProducts() {
        createProduct("Laptop", "High-performance laptop",
            new BigDecimal("999.99"), "Electronics");
        createProduct("Mouse", "Wireless mouse",
            new BigDecimal("29.99"), "Electronics");
        createProduct("Book", "Programming book",
            new BigDecimal("49.99"), "Books");
    }

    @Transactional
    Long createProduct(String name, String description,
                       BigDecimal price, String category) {
        Product product = new Product();
        product.name = name;
        product.description = description;
        product.price = price;
        product.category = category;
        product.persist();
        return product.id;
    }
}

Notre classe de test illustre les bonnes pratiques de test de Quarkus et l'intégration de la méthode REST-assured :

① L'annotation @QuarkusTest lance l'application dans son intégralité avec de véritables services, et non des simulacres.

② Le nettoyage de la base de données garantit l'isolation des tests sans recourir à des stratégies de rollback complexes.

③ Les expressions JSONPath utilisant le caractère $ pour représenter la racine permettent des assertions JSON puissantes.

④ Les matchers Hamcrest fournissent des assertions lisibles pour les collections et les objets complexes.

⑤ Les tests de recherche insensible à la casse vérifient que nos fonctions SQL LOWER() fonctionnent correctement.

⑥ La vérification des codes d'état HTTP garantit le respect des conventions REST.

⑦ La validation du corps de la réponse confirme que les valeurs mises à jour ont été persistées.

⑧ Le code d'état 204 No Content est la réponse correcte pour les opérations DELETE réussies.

Nous exécutons maintenant les tests :

./mvnw test

Exécuter Quarkus en mode JVM

Bien que nous ayons utilisé le mode dev jusqu'à présent, vous pouvez également compiler et exécuter votre application sous forme de fichier JAR classique, comme suit :

# Build the application
./mvnw clean package

# Run the JAR
java -jar target/quarkus-app/quarkus-run.jar

Les résultats montrent que Quarkus démarre remarquablement rapidement, en 1,310 seconde :

__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/

2025-06-15 08:29:34,378 INFO  [io.quarkus] (main) product-catalog 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.23.3) started in 1.310s. Listening on: http://0.0.0.0:8080
2025-06-15 08:29:34,382 INFO  [io.quarkus] (main) Profile prod activated.
2025-06-15 08:29:34,382 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, hibernate-orm-panache, jdbc-postgresql, narayana-jta, resteasy, resteasy-jackson, smallrye-context-propagation, vertx]

Conclusion

En seulement 15 minutes, nous avons développé un microservice prêt à être déployé en production, ce qui aurait normalement pris des heures avec les frameworks traditionnels.

A N M Bazlur Rahman est Java Champion et développeur logiciel chez DNAstack. Il est également fondateur et modérateur du Java User Group au Bangladesh.

Cet article a été initialement publié en langue anglaise sur SearchAppArchitecture.

 

Pour approfondir sur Outils de développement