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.