Aller au contenu
BackJavaquarkusExceptionserreurs

Cartographier les pièges d’un donjon : industrialiser la gestion des erreurs avec Quarkus

Dans Quarkus, la gestion des erreurs demande une approche structurée. Entre ExceptionMapper, standardisation des réponses et logique métier, découvrons comment passer d’une gestion dispersée à un système clair, robuste et véritablement industrialisé.

Comment bien gérer ses exceptions dans Quarkus

La grande salle de la taverne est calme.
Sur la table centrale, une carte du donjon est déployée.

Les aventuriers ne discutent pas de gloire.
Ils discutent de survie.

Chaque couloir est étudié.
Chaque salle est analysée.
Chaque piège est anticipé.

Car une chose est certaine :
le danger n’est pas dans ce que l’on voit…
mais dans ce que l’on n’a pas prévu.

En développement, les erreurs suivent exactement la même règle.
Elles ne sont pas exceptionnelles. Elles sont inévitables.

La vraie question devient alors :

avons-nous une carte claire… ou avançons-nous à l’aveugle ?

Différence avec Spring Boot : une philosophie différente

Dans un univers familier comme Spring Boot, la gestion des erreurs est centralisée très tôt.

Un simple @ControllerAdvice, quelques @ExceptionHandler, et l’on obtient :

  • un point unique de gestion
  • plusieurs méthodes dédiées par type d’erreur
  • une structure homogène facile à imposer
@ExceptionHandler(TavernNotFoundException.class)
public ResponseEntity<ProblemDetail> handleNotFound(...) { ... }

un handler classic de spring

  • Une seule classe
  • Plusieurs cas
  • Une vision centralisée

C’est une approche confortable, presque naturelle :

un maître de guilde qui gère toutes les situations depuis un seul endroit

Avec Quarkus, le choix est différent.

Basé sur JAX-RS, il repose sur :

  • ExceptionMapper
  • une classe par exception
  • une résolution automatique par type
@Provider
public class TavernNotFoundExceptionMapper
        implements ExceptionMapper<TavernNotFoundException> {
}

mapper d'une exception dans Quarkus

  • Une exception = un mapper
  • Pas de point central unique
  • Une approche plus modulaire

Ce n’est pas une limitation.
C’est un changement de philosophie :

chaque aventurier connaît son rôle, plutôt qu’un seul chef qui décide de tout

Problème initial : une exception = un mapper

Au début, les aventuriers notent les dangers comme ils peuvent.

Un parchemin pour les pièges mécaniques.
Un autre pour les malédictions.
Un troisième pour les embuscades.

Mais rien n’est standardisé.

Dans notre API, c’est exactement ce qu’il se passe.

Le service est propre :

public Tavern getTavern(String name) {
    return repository.findByName(name)
            .orElseThrow(() -> new TavernNotFoundException(name));
}

extrait de notre service

Les erreurs sont identifiées.
Mais leur gestion ne l’est pas.

Chaque mapper construit sa réponse :

return Response.status(404)
        .entity("Not found")
        .build();

Et rapidement :

  • les formats divergent
  • les informations varient
  • la duplication s’installe
chaque aventurier note les pièges… mais personne ne dessine la carte

Solution : éviter la duplication et structurer

Un silence s’installe autour de la table.

Puis quelqu’un propose une idée simple :

et si on arrêtait de noter les pièges…
pour commencer à dessiner une carte ?

Un format unique : Problem

On commence par définir une représentation unique.

Un symbole pour chaque piège.
Une structure claire.
Une lecture immédiate.

public class Problem {
    private String type;
    private String title;
    private int status;
    private String detail;
    private Map<String, Object> additional;
}

la structure commune qui sera renvoyée

Toutes les erreurs parlent le même langage.
Le client sait toujours à quoi s’attendre.

la carte commence à prendre forme

Une base métier : BusinessException

Plutôt que de décrire les pièges après coup, on les définit dès le départ.

public abstract class BusinessException extends RuntimeException {
    private final int status;
    private final String title;
}

l'abstraction de nos exceptions

Puis :

public class TavernNotFoundException extends BusinessException {
    public TavernNotFoundException(String name) {
        super(404, "Ressource introuvable",
              "La taverne '" + name + "' n'existe pas.");
    }
}

une exception

Chaque piège devient :

  • identifié
  • catégorisé
  • documenté
les dangers ne sont plus subis, ils sont connus

Centraliser avec AbstractExceptionMapper

La carte ne suffit pas.
Encore faut-il une méthode pour la dessiner.

public abstract class AbstractExceptionMapper<T extends Throwable>
        implements ExceptionMapper<T> {

    protected abstract int status(T ex);
    protected abstract String title(T ex);

    protected String detail(T ex) {
        return ex.getMessage();
    }

    protected void enrich(Problem problem, T ex) {}

    public Response toResponse(T ex) {
        Problem problem = Problem.of(status(ex), title(ex))
                .detail(detail(ex));

        enrich(problem, ex);

        return Response.status(problem.getStatus())
                .type("application/problem+json")
                .entity(problem)
                .build();
    }
}

Désormais :

  • la structure est centralisée
  • les règles sont communes
  • les variations sont contrôlées
la carte est dessinée avec une seule encre

Des mappers métier ultra simples

Chaque aventurier reprend sa place.

Mais cette fois, avec une méthode commune.

@Provider
public class TavernNotFoundExceptionMapper
        extends AbstractExceptionMapper<TavernNotFoundException> {

    protected int status(TavernNotFoundException ex) {
        return ex.getStatus();
    }

    protected String title(TavernNotFoundException ex) {
        return ex.getTitle();
    }
}

un de nos mapper

  • Lisible
  • Sans duplication
  • Axé métier

Enrichir quand c’est utile

Certains pièges nécessitent plus de détails.

Une salle trop remplie.
Un passage bloqué.
Une capacité atteinte.

protected void enrich(Problem problem, TavernCapacityReachedException ex) {
    problem.additional(Map.of(
        "currentCapacity", ex.getCurrentCapacity(),
        "maxCapacity", ex.getMaxCapacity()
    ));
}

dire ce qui est nécessaire

On fournit des informations utiles au client.
Sans casser le format.

Un filet de sécurité global

Même la meilleure carte ne peut tout prévoir.

Alors on ajoute une dernière règle :

@Provider
public class GlobalExceptionMapper
        extends AbstractExceptionMapper<Throwable> {
}

Toutes les erreurs sont couvertes.
Aucun comportement imprévisible.

même l’inconnu a sa place sur la carte

Une approche alternative : centraliser avec le pattern matching

Alors que la carte commence à prendre forme, un des aventuriers propose une autre stratégie.

Plutôt que de multiplier les rôles, pourquoi ne pas désigner un seul stratège chargé de tout interpréter ?

Un registre unique.
Une lecture centralisée.
Et une analyse des pièges basée sur leur nature.

En Java moderne, cela peut se traduire par l’utilisation du pattern matching :

return switch (ex) {
    case BusinessException be -> buildProblem(be);
    case ConstraintViolationException ve -> buildValidationProblem(ve);
    default -> buildTechnicalProblem(ex);
};

on centralise

L’idée est séduisante :

  • une seule entrée
  • une logique centralisée
  • une lecture immédiate des cas

Et sur de petits systèmes, cela fonctionne très bien.

un seul maître de guilde qui lit la carte et donne les ordres

Mais à mesure que le donjon se complexifie, la carte s’alourdit.

De nouveaux pièges apparaissent :

  • des règles métier spécifiques
  • des enrichissements particuliers
  • des cas techniques imprévus

Et le registre central devient progressivement plus dense, plus fragile.

Chaque nouveau piège oblige à modifier ce point unique.
Chaque oubli peut avoir des conséquences inattendues.

le stratège devient indispensable… et donc vulnérable

Une approche plus équilibrée consiste alors à combiner les deux mondes :

  • garder une base commune (BusinessException, Problem)
  • centraliser ce qui est générique
  • déléguer ce qui est spécifique à des mappers dédiés

Le pattern matching garde alors sa place, mais de manière ciblée :

  • pour enrichir certains cas proches
  • pour factoriser sans sur-centraliser
la carte reste lisible… sans dépendre d’un seul homme
@Provider
public class BusinessExceptionMapper
        extends AbstractExceptionMapper<BusinessException> {

    @Override
    protected int status(BusinessException ex) {
        return ex.getStatus();
    }

    @Override
    protected String title(BusinessException ex) {
        return ex.getTitle();
    }

    @Override
    protected void enrich(Problem problem, BusinessException ex) {

        switch (ex) {
            case TavernCapacityReachedException e -> problem.additional(Map.of(
                "currentCapacity", e.getCurrentCapacity(),
                "maxCapacity", e.getMaxCapacity()
            ));
            default -> {
                // rien
            }
        }
    }
}

un mapper dédié pour les BusinessException

Cette approche hybride permet de conserver :

  • la lisibilité d’un système centralisé
  • la robustesse d’un système modulaire

Et surtout, elle respecte un principe essentiel :

dans un donjon complexe, mieux vaut une équipe bien organisée… qu’un seul héros surchargé

Conclusion

Les aventuriers replient la carte.

Le donjon n’a pas changé.
Les pièges sont toujours là.

Mais quelque chose est différent.

Chaque danger est identifié.
Chaque réaction est prévue.
Chaque situation a une réponse.

Avec Quarkus, la gestion des erreurs demande plus de rigueur qu’avec Spring Boot.
Moins de magie, plus de structure.

Mais cette discipline apporte un avantage décisif :

un système clair, maîtrisé, et fidèle à ton métier.

Et dans un donjon comme dans une API…
c’est souvent ce qui fait toute la différence.


Tout le code relatif a cet article est consultable ici :

GitHub - ErwanLT/quarkus-demo: Demo project for quarkus possibility
Demo project for quarkus possibility. Contribute to ErwanLT/quarkus-demo development by creating an account on GitHub.

Dernier