Cette page recense les recettes courantes au-delà du Hello World — sub-resources, providers, filtres et intercepteurs, content negotiation, async non-bloquant, SSE, et l’intégration CDI via Vauban.

Sub-resources et sub-resource locators

@Path("/users")
public class UsersResource {

    // Sub-resource method
    @GET @Produces(MediaType.APPLICATION_JSON)
    public List<User> list() { ... }

    // Sub-resource locator : retourne une instance qui prend la suite du dispatch
    @Path("/{id}")
    public UserResource user(@PathParam("id") long id) {
        return new UserResource(id);
    }
}

public class UserResource {
    private final long id;
    public UserResource(long id) { this.id = id; }

    @GET @Produces(MediaType.APPLICATION_JSON)
    public User get() { ... }

    @PUT @Consumes(MediaType.APPLICATION_JSON)
    public Response update(User u) { ... }
}

Providers — MessageBodyReader / MessageBodyWriter

Cassini fournit CassiniJsonbReaderWriter qui couvre application/json via Champollion. Pour ajouter un format custom :

@Provider
@Produces("application/x-protobuf")
@Consumes("application/x-protobuf")
public class ProtobufProvider
        implements MessageBodyReader<Message>, MessageBodyWriter<Message> {
    // ...
}

Les providers sont enregistrés via Application.getClasses(), le BeanProvider (CDI), ou META-INF/services/jakarta.ws.rs.ext.MessageBodyReader. La résolution best-match est gérée par MessageBodyRegistry dans cassini-core.

ContextResolver et Providers

@Provider
public class JsonbContextResolver implements ContextResolver<Jsonb> {
    private final Jsonb jsonb = JsonbBuilder.create(
        new JsonbConfig().withFormatting(true));

    @Override public Jsonb getContext(Class<?> type) { return jsonb; }
}

Injection via @Context Providers providers dans une ressource pour récupérer un MessageBodyWriter ou un ContextResolver arbitraire.

ExceptionMapper

@Provider
public class NotFoundExceptionMapper implements ExceptionMapper<NotFoundException> {
    @Override
    public Response toResponse(NotFoundException e) {
        return Response.status(Response.Status.NOT_FOUND)
            .entity(Map.of("error", e.getMessage()))
            .type(MediaType.APPLICATION_JSON)
            .build();
    }
}

Filtres et intercepteurs

@Provider
@PreMatching
public class CorsFilter implements ContainerRequestFilter {
    @Override
    public void filter(ContainerRequestContext ctx) {
        ctx.getHeaders().add("Access-Control-Allow-Origin", "*");
    }
}

@Provider
@Logged                        // @NameBinding custom
public class LoggingFilter implements ContainerRequestFilter, ContainerResponseFilter {
    @Override public void filter(ContainerRequestContext req) {
        System.out.printf("[->] %s %s%n", req.getMethod(), req.getUriInfo().getPath());
    }
    @Override public void filter(ContainerRequestContext req, ContainerResponseContext res) {
        System.out.printf("[<-] %d%n", res.getStatus());
    }
}

L’ordre est résolu via @Priority au démarrage du stack (pas par requête).

Content negotiation

Cassini implémente intégralement la résolution best-match prévue par §3.7 et Request.selectVariant (§5.1) :

@GET
public Response negotiate(@Context Request request) {
    var variants = Variant.mediaTypes(
        MediaType.APPLICATION_JSON_TYPE,
        MediaType.APPLICATION_XML_TYPE).build();
    var best = request.selectVariant(variants);
    if (best == null) return Response.notAcceptable(variants).build();
    return Response.ok(payload(), best).build();
}

Async — @Suspended AsyncResponse

@GET @Path("/long")
public void longRunning(@Suspended AsyncResponse async) {
    Thread.startVirtualThread(() -> {
        try {
            var result = compute();        // peut bloquer
            async.resume(result);
        } catch (Exception e) {
            async.resume(e);
        }
    });
}

L’exécution async repose sur les virtual threads (Java 25). Le CassiniAsyncContext (SPI interne) orchestre la propagation CompletionStage jusqu’au transport. Le streaming chunked complet est planifié pour M2h — voir état TCK.

Server-Sent Events

@GET @Path("/events") @Produces(MediaType.SERVER_SENT_EVENTS)
public void events(@Context SseEventSink sink, @Context Sse sse) {
    try (sink) {
        for (int i = 0; i < 10; i++) {
            sink.send(sse.newEvent("tick-" + i));
        }
    }
}
L’implémentation actuelle (CassiniSseEventSink) bufférise puis émet en bloc — conforme spec §11 sur le contrat, mais le streaming réel chunked est planifié sur le jalon M2i (voir lien:https://codeberg.org/Vidocq/cassini/src/branch/main/ASYNC.md[ASYNC.md]).

Client JAX-RS

Ajoutez cassini-client au path et ClientBuilder.newClient() retourne un client Cassini (découvert via ServiceLoader). C’est un Client Jakarta REST 4.0 zéro-dépendance bâti sur java.net.http + virtual threads, qui mutualise la (dé)sérialisation JSON-B avec le côté serveur.

try (Client client = ClientBuilder.newClient()) {
    User u = client.target("https://api.example.com")
        .path("/users/{id}").resolveTemplate("id", 42)
        .request(MediaType.APPLICATION_JSON)
        .get(User.class);
}

Supportés : construction d’URI via WebTarget, ClientRequestFilter / ClientResponseFilter (ordonnés par @Priority), ReaderInterceptor / WriterInterceptor, invocation async sur virtual thread (.async().get()), et Feature`s auto-enregistrées découvertes via `ServiceLoader (ex. instrumentation MicroProfile Telemetry — sans .register() explicite).

Response r = client.target(base).path("/orders")
    .request()
    .async()                       // tourne sur un virtual thread
    .post(Entity.json(order))
    .get();

Intégration CDI / Vauban

Avec cassini-cdi-vauban sur le classpath, VaubanBeanProvider.getResourceClasses() itère le BeanManager et expose toutes les classes annotées @Path / @Provider au stack. Aucune liste manuelle de singletons n’est nécessaire.

var container = VaubanContainer.builder()
        .addBeanClass(TodoService.class)
        .addBeanClass(TodoResource.class)
        .build();

SeBootstrap.start(new Application() {}, config);   // Cassini scanne via Vauban

Le CassiniScopeExtension (BCE) ajoute automatiquement @RequestScoped aux classes @Path sans scope explicite. Voir fonctionnement interne pour le diagramme.