Java EE / Jakarta EE

Jakarta REST (JAX-RS 3.1) : créer et consommer des API HTTP

Intermédiaire9 min de lectureMis à jour

Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise

Jakarta RESTful Web Services (JAX-RS) est l'API de la plateforme, pilotée par annotations, pour les services HTTP, et le modèle qu'implémentent Jersey, RESTEasy et Apache CXF. Ce guide montre comment les requêtes sont routées vers les méthodes de ressource, comment les paramètres et les corps JSON sont liés, comment renvoyer les bons statuts et corps d'erreur, comment filtres, intercepteurs et exception mappers ajoutent des comportements transverses, et comment appeler d'autres services avec l'API cliente, avec les erreurs de conception et de sécurité courantes dans les vraies API.

Ce que vous allez apprendre

  • Déclarer une application et des classes de ressource, et comprendre comment les requêtes leur sont associées
  • Lier les paramètres de chemin, de requête, d'en-tête et de corps, et renvoyer des objets Response avec les bons codes de statut
  • Transformer les exceptions en réponses d'erreur et appliquer Bean Validation aux entrées
  • Ajouter filtres, intercepteurs et providers pour l'authentification, la journalisation et le traitement du contenu
  • Consommer des services avec l'API cliente de Jakarta REST et utiliser les réponses asynchrones et les Server-Sent Events

Jakarta RESTful Web Services — JAX-RS dans toutes les conversations, et dans le nom des packages jusqu'au renommage en jakarta.ws.rs — fait correspondre HTTP à de simples méthodes Java. Vous annotez une classe avec un chemin et ses méthodes avec des verbes HTTP, vous déclarez d'où vient chaque paramètre et vous renvoyez un objet ; le runtime s'occupe du routage, de la négociation de contenu, de la sérialisation et des réponses d'erreur. Là où l'API servlet expose HTTP tel qu'il est, Jakarta REST l'expose tel qu'un concepteur d'API le pense : ressources, représentations, codes de statut et types de médias. Les principes de conception d'une bonne API HTTP relèvent du parcours architecture ; ce guide explique comment les mettre en œuvre correctement avec cette API.

Application, ressources et correspondance

Une classe de ressource est une classe ordinaire, généralement un bean CDI, portant un @Path. Ses méthodes portent une annotation de méthode HTTP, éventuellement un sous-chemin, et la déclaration de ce qu'elles consomment et produisent. Une unique sous-classe d'Application annotée @ApplicationPath fixe la racine de l'API et, si vous la laissez vide, enregistre toutes les ressources et tous les providers que le runtime découvre.

package shop.api;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("/api")            // all resources live under /<context>/api
public class ShopApplication extends Application { }
package shop.api;

import java.net.URI;
import java.util.List;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;

@Path("/orders")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@RequestScoped
public class OrderResource {

    @Inject OrderService orders;
    @Context UriInfo uriInfo;           // request-scoped injection of URI details

    @GET
    public List<OrderSummary> list(@QueryParam("status") OrderStatus status,
                                   @QueryParam("page") @DefaultValue("0") int page,
                                   @QueryParam("size") @DefaultValue("20") int size) {
        return orders.list(status, page, size);
    }

    @GET
    @Path("/{id}")
    public OrderDetail get(@PathParam("id") long id) {
        return orders.find(id).orElseThrow(NotFoundException::new);   // 404
    }

    @POST
    public Response create(@Valid NewOrder request) {
        OrderDetail created = orders.place(request);
        URI location = uriInfo.getAbsolutePathBuilder().path(Long.toString(created.id())).build();
        return Response.created(location).entity(created).build();    // 201 + Location
    }

    @PATCH
    @Path("/{id}/status")
    public OrderDetail changeStatus(@PathParam("id") long id, StatusChange change) {
        return orders.changeStatus(id, change.status());
    }

    @DELETE
    @Path("/{id}")
    public void cancel(@PathParam("id") long id) {
        orders.cancel(id);                                             // void -> 204 No Content
    }
}

La correspondance (matching) se fait par étapes : le runtime choisit la classe de ressource dont le @Path correspond au plus long préfixe, puis une méthode dont le sous-chemin et le verbe HTTP correspondent, puis filtre selon le Content-Type de la requête face à @Consumes et l'en-tête Accept face à @Produces. Un chemin qui correspond sans qu'aucun verbe ne corresponde donne 405 Method Not Allowed ; un verbe qui correspond sans type de média compatible donne 415 Unsupported Media Type ou 406 Not Acceptable. Les modèles de chemin peuvent contenir des expressions régulières — @Path("/{id: \\d+}") — pour que /orders/abc renvoie un 404 plutôt qu'une erreur de conversion. Une méthode qui porte un @Path sans annotation de verbe est un sub-resource locator : elle renvoie un autre objet ressource qui poursuit la correspondance, et c'est ainsi que l'on isole /orders/{id}/lines dans sa propre classe.

Les classes de ressource qui ne sont pas des beans CDI ont par défaut une portée requête ; déclarer explicitement @RequestScoped ou @ApplicationScoped rend le cycle de vie évident et permet à CDI d'y injecter des dépendances. Une ressource de portée application doit être thread-safe, et les champs @Context qu'elle contient sont des proxys résolus à chaque requête.

Paramètres et corps

Les paramètres proviennent de plusieurs sources, chacune avec son annotation : @PathParam, @QueryParam, @HeaderParam, @CookieParam, @FormParam (corps de formulaire URL-encoded) et @MatrixParam. La conversion depuis la chaîne gère les types primitifs, les enums, tout type doté d'un constructeur String ou d'une méthode statique valueOf/fromString, ainsi que List<T> pour les valeurs répétées ; @DefaultValue couvre les paramètres absents. Un groupe de paramètres liés peut être regroupé dans une classe dont les champs sont annotés, injectée avec @BeanParam, ce qui garde des signatures courtes pour les endpoints de recherche.

Le seul paramètre non annoté est le corps de l'entité. Il est lu par un MessageBodyReader choisi d'après le Content-Type : les corps JSON deviennent des objets via JSON Binding, String, byte[], InputStream et File sont pris en charge nativement, et Jakarta REST 3.1 a enfin standardisé le multipart avec EntityPart :

@POST
@Path("/{id}/attachments")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response attach(@PathParam("id") long id, List<EntityPart> parts) throws IOException {
    for (EntityPart part : parts) {
        if (part.getFileName().isPresent()) {
            orders.attach(id, part.getFileName().get(), part.getMediaType(), part.getContent());
        }
    }
    return Response.noContent().build();
}

Pour les classes classiques, JSON Binding exige un constructeur public sans argument et des getters/setters ou des champs publics ; les records sont pris en charge par Yasson 3, l'implémentation compatible JSON-B, depuis JSON-B 3.0. Le nommage des champs, les formats de date, le traitement des null et l'ordre des propriétés se règlent avec @JsonbProperty, @JsonbDateFormat, @JsonbNillable et @JsonbPropertyOrder, ou globalement avec un JsonbConfig enregistré via un ContextResolver<Jsonb>. Séparez les types de l'API (NewOrder, OrderDetail) des entités JPA : sérialiser une entité parcourt les associations lazy, divulgue des champs internes et couple votre API à votre schéma.

Réponses et codes de statut

Une méthode qui renvoie un objet produit 200 OK avec l'entité sérialisée, void produit 204 No Content, et null produit également un 204. Tout le reste passe par Response :

SituationConstructionStatut
Ressource crééeResponse.created(uri).entity(body)201 avec Location
Acceptée pour un traitement en arrière-planResponse.accepted()202
Rien à renvoyerResponse.noContent()204
GET conditionnel satisfaitrequest.evaluatePreconditions(etag) renvoie une valeur non nulle304
Erreur client avec un corpsResponse.status(Status.CONFLICT).entity(problem)409, etc.

On ajoute des en-têtes avec .header(...), on gère le cache avec .cacheControl(...) et .tag(...), et Response.temporaryRedirect(uri) couvre les redirections. Response peut aussi transporter une GenericEntity pour renvoyer une List<T> en conservant l'information de type quand le runtime ne peut pas l'inférer. Les codes de statut ont un sens pour les clients comme pour les intermédiaires ; le parcours web propose une référence des codes de statut pour vérifier leur sémantique exacte avant d'en choisir un.

Erreurs : exceptions et mappers

Lever une sous-classe de WebApplicationException — NotFoundException, BadRequestException, ForbiddenException, NotAuthorizedException, ClientErrorException(Response) — envoie le statut correspondant. Pour un corps d'erreur cohérent sur toute l'API, enregistrez un ExceptionMapper par type d'exception :

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

@Provider
public class DomainExceptionMapper implements ExceptionMapper<DomainException> {

    @Override
    public Response toResponse(DomainException e) {
        var problem = new Problem("https://api.example.com/problems/" + e.code(),
                                  e.getMessage(), e.status());
        return Response.status(e.status())
                       .type("application/problem+json")     // RFC 9457 problem details
                       .entity(problem)
                       .build();
    }
}

Les classes @Provider sont découvertes automatiquement. Le runtime choisit le mapper associé à la superclasse la plus proche de l'exception levée : un mapper pour Throwable attrape donc tout ce que les autres laissent passer — et il doit journaliser la stack trace et renvoyer un 500 générique sans le message de l'exception, qui peut contenir du SQL ou des chemins internes. Les échecs de Bean Validation (ConstraintViolationException) sont convertis par le runtime en 400 lorsqu'ils portent sur les données de la requête ; ajoutez votre propre mapper si vous voulez lister les violations dans le corps. Ne laissez jamais une PersistenceException atteindre le client sans mapping.

Validation

Annotez les champs des DTO avec des contraintes Bean Validation et le paramètre avec @Valid, comme dans create plus haut ; les contraintes placées directement sur les paramètres (@QueryParam("size") @Max(100) int size) fonctionnent aussi. La validation a lieu avant l'exécution de la méthode, dans la même chaîne d'intercepteurs que tout le reste : une méthode de ressource peut donc considérer ses entrées comme structurellement valides et se concentrer sur les règles métier. La validation des valeurs de retour (@Valid sur la méthode) existe, mais justifie rarement son coût.

Filtres, intercepteurs et autres providers

Les comportements transverses vivent dans des providers :

  • ContainerRequestFilter s'exécute avant la méthode de ressource — authentification, identifiants de corrélation, limitation de débit. Annotez-le @PreMatching pour qu'il s'exécute avant le routage (nécessaire pour réécrire la méthode ou l'URI), ou laissez-le post-matching pour qu'il puisse inspecter la ressource retenue via ResourceInfo. Interrompez la requête avec requestContext.abortWith(Response.status(401).build()).
  • ContainerResponseFilter s'exécute après — en-têtes CORS et de sécurité, journalisation des réponses.
  • ReaderInterceptor / WriterInterceptor enveloppent les flux de l'entité — compression, signature, chiffrement.
  • MessageBodyReader / MessageBodyWriter ajoutent des types de médias que le runtime ne connaît pas, comme CSV ou Protocol Buffers.
  • ParamConverterProvider apprend au runtime à convertir des paramètres vers vos propres types, comme un Money ou un OrderId.

Par défaut, les filtres s'appliquent globalement. Pour cibler des méthodes précises, définissez un name binding : une annotation méta-annotée avec @NameBinding, placée à la fois sur le filtre et sur les méthodes de ressource concernées, de sorte que @Secured sur une méthode n'attache le filtre d'authentification qu'à cet endroit. L'ordre entre filtres se règle avec @Priority et les constantes de jakarta.ws.rs.Priorities (AUTHENTICATION passe avant AUTHORIZATION, puis viennent HEADER_DECORATOR, ENTITY_CODER et USER).

La sécurité proprement dite est déléguée : avec Jakarta Security configuré sur le serveur, @RolesAllowed sur une méthode de ressource (un enterprise bean, ou un bean CDI sur les serveurs qui le permettent) ou securityContext.isUserInRole("admin") depuis un SecurityContext injecté effectue le contrôle ; pour les API à base de jetons, un filtre de requête valide le bearer token et installe son propre SecurityContext. Le guide JWT traite le côté jetons.

L'API cliente

Jakarta REST fournit un client fluent pour appeler d'autres services, qui sert aussi de base au Rest Client type-safe de MicroProfile :

import jakarta.ws.rs.client.*;
import jakarta.ws.rs.core.*;

Client client = ClientBuilder.newBuilder()
        .connectTimeout(2, TimeUnit.SECONDS)
        .readTimeout(5, TimeUnit.SECONDS)
        .build();                                    // heavyweight: create once, reuse, close at shutdown

WebTarget orders = client.target("https://shop.example.com/api/orders");

try (Response response = orders.path("{id}").resolveTemplate("id", 42)
                               .request(MediaType.APPLICATION_JSON)
                               .header("Authorization", "Bearer " + token)
                               .get()) {
    if (response.getStatus() == 200) {
        OrderDetail order = response.readEntity(OrderDetail.class);
    }
}

OrderDetail created = orders.request()
        .post(Entity.json(new NewOrder("C-17", List.of(new Line("SKU-1", 2)))), OrderDetail.class);

WebTarget est immuable et réutilisable ; Invocation.Builder définit les en-têtes appel par appel. Lire directement l'entité (get(OrderDetail.class)) lève une WebApplicationException sur les statuts d'erreur, ce qui est pratique dans un script et pénible dans un service : préférez lire la Response et traiter les statuts explicitement. Définissez toujours des timeouts : ils sont infinis par défaut dans la plupart des implémentations. Le client prend en charge les mêmes filtres et providers que le serveur, ainsi que l'invocation réactive (.rx().get(...) qui renvoie un CompletionStage).

Réponses asynchrones et Server-Sent Events

Une méthode de ressource peut libérer le thread du conteneur pendant qu'elle attend : déclarez un paramètre @Suspended AsyncResponse, renvoyez void et appelez asyncResponse.resume(result) depuis un autre thread — un managed executor ou un message listener. setTimeout associé à un TimeoutHandler évite que des requêtes restent bloquées indéfiniment. C'est la déclinaison Jakarta REST du traitement asynchrone des servlets, et elle ne vaut la peine que si l'attente porte sur quelque chose d'externe.

Les Server-Sent Events diffusent une suite d'événements sur une seule réponse de longue durée, ce qui convient aux notifications et aux mises à jour de progression sans recourir à WebSocket :

@GET
@Path("/{id}/events")
@Produces(MediaType.SERVER_SENT_EVENTS)
public void events(@PathParam("id") long id, @Context SseEventSink sink, @Context Sse sse) {
    subscriptions.register(id, status ->
        sink.send(sse.newEventBuilder().name("status").data(status).mediaType(MediaType.APPLICATION_JSON).build()));
}

SseBroadcaster diffuse un même événement vers de nombreux sinks. N'oubliez pas que chaque sink ouvert est une connexion maintenue : configurez un timeout d'inactivité sur le load balancer et fermez les sinks quand le client disparaît.

Bootstrap Java SE

Jakarta REST 3.1 a ajouté SeBootstrap, qui permet d'exécuter une application sans conteneur de servlets : SeBootstrap.start(new ShopApplication(), SeBootstrap.Configuration.builder().port(8080).build()) démarre le serveur embarqué de l'implémentation. C'est pratique pour des outils et des tests, même si, dans un déploiement Jakarta EE, le conteneur de servlets reste l'hôte.

Erreurs fréquentes

  • Exposer les entités JPA. Exceptions de lazy loading pendant la sérialisation, champs divulgués et API calquée sur le schéma. Passez par des DTO.
  • Renvoyer 200 avec un corps d'erreur. Les clients ne peuvent plus se fier aux codes de statut et le monitoring ne peut plus compter les échecs. Levez une exception et mappez-la.
  • Un mapper fourre-tout qui renvoie e.getMessage(). Il divulgue des détails internes. Journalisez côté serveur et renvoyez un problème générique.
  • Aucun timeout côté client. Un service en aval bloqué épuise votre pool de threads. Définissez des timeouts de connexion et de lecture, et envisagez des circuit breakers.
  • Créer un Client par requête. C'est coûteux et cela provoque des fuites de connexions. Un seul par application, fermé à l'arrêt.
  • Un @Path sur une classe sans portée. Cela fonctionne, mais le cycle de vie dépend de l'implémentation et l'injection CDI peut ne pas fonctionner. Déclarez @RequestScoped ou @ApplicationScoped.
  • Compter sur @Consumes pour valider les entrées. Il ne vérifie que le type de média. Ajoutez Bean Validation et des contrôles métier.
  • Utiliser GET pour des actions. Les caches et les mécanismes de préchargement finiront par l'appeler. Utilisez POST pour tout ce qui a des effets de bord.

À retenir

  • Une application @ApplicationPath et des classes de ressource @Path suffisent à constituer une API complète ; la correspondance examine le chemin, le verbe puis les types de médias, dans cet ordre, et les incompatibilités produisent automatiquement des 404, 405, 415 ou 406.
  • Liez les entrées avec @PathParam, @QueryParam, @HeaderParam, @BeanParam et un corps non annoté ; JSON Binding gère le JSON, et la 3.1 a standardisé le multipart avec EntityPart.
  • Renvoyez des objets pour les simples 200 et une Response pour tout le reste ; utilisez created(uri), noContent() et les préconditions plutôt que des statuts construits à la main.
  • Levez des exceptions et traduisez-les dans des providers ExceptionMapper, idéalement en problem details RFC 9457 ; ne divulguez jamais de messages internes.
  • Filtres, intercepteurs, readers, writers et convertisseurs de paramètres sont les points d'extension ; les name bindings et @Priority déterminent où et dans quel ordre ils s'appliquent.
  • L'API cliente est fluent et réutilisable : un seul Client par application, des timeouts explicites, une gestion explicite des statuts.
  • @Suspended AsyncResponse, les Server-Sent Events et SeBootstrap couvrent les longues attentes, le streaming et l'exécution autonome.

Sources et lectures complémentaires

Questions fréquentes

Quelle implémentation de Jakarta REST choisir ?
Celle que fournit votre runtime : Jersey dans GlassFish et Payara, RESTEasy dans WildFly et Quarkus, l'implémentation de Liberty (basée sur RESTEasy) dans Open Liberty, Apache CXF dans TomEE. Du code écrit selon la spécification tourne sur toutes. Ne choisissez délibérément une implémentation que pour un usage autonome, sur Tomcat ou avec l'API de bootstrap Java SE.
Comment Jakarta REST transforme-t-il mon objet en JSON ?
Par l'intermédiaire d'un MessageBodyWriter. Les serveurs Jakarta EE en fournissent un qui s'appuie sur JSON Binding (JSON-B) : toute classe dotée de getters ou de champs publics est sérialisée automatiquement, et des annotations comme @JsonbProperty et @JsonbTransient permettent d'ajuster le résultat. Un déploiement autonome doit ajouter lui-même un provider JSON (Yasson pour JSON-B, ou Jackson).
Une méthode de ressource doit-elle renvoyer l'entité ou une Response ?
Renvoyez l'entité quand le succès est toujours un 200 avec un corps : le framework se charge de la sérialisation et de la négociation de contenu. Renvoyez une Response quand il vous faut un autre statut, des en-têtes comme Location ou un résultat conditionnel. Ne renvoyez jamais une Response uniquement pour emballer une erreur : levez une exception et laissez un ExceptionMapper produire le corps d'erreur de façon cohérente.
Jakarta REST, est-ce la même chose que le @RestController de Spring ?
Même idée, annotations et conteneur différents. Les deux associent méthodes HTTP et chemins à des méthodes Java, lient les paramètres et sérialisent le JSON. Jakarta REST est le standard implémenté par plusieurs éditeurs et tourne sur n'importe quel serveur Jakarta EE ; Spring MVC est propre à Spring. Les runtimes MicroProfile comme Quarkus et Helidon utilisent Jakarta REST, ce qui en fait la compétence la plus portable d'une stack Java à l'autre.
Jakarta Servlet 6 : servlets, filtres, sessions et tier webComment le conteneur de servlets traite HTTP : cycle de vie, requête et réponse, filtres, listeners, sessions, mode asynchrone et nouveautés de Servlet 6.0.Débutant10 min de lectureEnterprise Beans 4 : session beans stateless, stateful et singletonCe qu'apportent les session beans EJB (pooling, transactions, sécurité, timers, appels asynchrones), leurs différences et quand préférer un bean CDI.Intermédiaire10 min de lectureArchitectureConception d'API REST : ressources, sémantique HTTP, versionnement et erreursConcevoir des API HTTP qui vieillissent bien : modélisation des ressources, méthodes et statuts, pagination, versionnement, erreurs RFC 9457 et ETag.Intermédiaire12 min de lectureWeb et mobileCodes de statut HTTP : référence complète, signification et bon usageTous les codes de statut HTTP de 100 à 511, leur sens, les paires que l'on confond, redirections, cache et conseils pour concevoir une API.Débutant17 min de lectureSécuritéSécuriser des API REST avec JWT : vérification, claims et révocationVérifier un JWT sans faille : structure JWS, liste blanche d'algorithmes, rotation JWKS, validation des claims, révocation, stockage et défenses de l'API.Avancé9 min de lectureSpringConstruire des API REST avec @RestController dans Spring MVCAPI JSON avec @RestController, ResponseEntity et Jackson : validation, erreurs RFC 9457 via @RestControllerAdvice, appels sortants avec RestClient.Intermédiaire6 min de lecture