Cette page couvre les patterns courants d’utilisation d’un client REST typé Cyrano. Tous les exemples compilent et s’exécutent contre un endpoint java.net.http.HttpClient ; le proxy d’interface est généré à process-classes par la Class-File API et chargé à la première invocation.
Méthodes HTTP
Les sept verbes JAX-RS standards sont supportés :
@RegisterRestClient(configKey = "blog-api")
@Path("/posts")
public interface BlogClient {
@GET
List<Post> list();
@GET
@Path("/{id}")
Post get(@PathParam("id") long id);
@POST
@Consumes(MediaType.APPLICATION_JSON)
Post create(Post body);
@PUT
@Path("/{id}")
Post replace(@PathParam("id") long id, Post body);
@PATCH
@Path("/{id}")
Post update(@PathParam("id") long id, Post body);
@DELETE
@Path("/{id}")
void delete(@PathParam("id") long id);
@HEAD
@Path("/{id}")
Response head(@PathParam("id") long id);
}
Paramètres
| Annotation | Sémantique côté client |
|---|---|
|
Remplace |
|
Ajoute |
|
Ajoute un header HTTP de valeur calculée au runtime. |
|
Ajoute un cookie HTTP. |
|
Ajoute un paramètre matriciel ( |
|
Ajoute un champ à un body |
|
Valeur de repli si l’argument est |
|
Agrège plusieurs paramètres dans un objet ( |
Exemple @BeanParam :
public record SearchFilters(
@QueryParam("q") String query,
@QueryParam("limit") @DefaultValue("20") int limit,
@HeaderParam("X-Trace") String trace
) {}
@GET
@Path("/search")
List<Post> search(@BeanParam SearchFilters filters);
Headers statiques et dynamiques
@ClientHeaderParam permet d’attacher un header sans le passer en paramètre à chaque appel. Valeur littérale ou méthode default :
@RegisterRestClient(configKey = "blog-api")
@ClientHeaderParam(name = "User-Agent", value = "cyrano/0.1")
public interface BlogClient {
@GET
@ClientHeaderParam(name = "X-Request-Id", value = "{newRequestId}")
List<Post> list();
default String newRequestId() {
return java.util.UUID.randomUUID().toString();
}
}
@RegisterClientHeaders branche un ClientHeadersFactory qui peut lire les headers de la requête entrante côté serveur — utile pour propager Authorization, X-Forwarded-For, etc.
Sérialisation JSON
Les types Java communs sont sérialisés/désérialisés via Jakarta JSON-B (impl Champollion) :
-
Records, POJO, classes annotées
@JsonbProperty. -
Optional<T>,List<T>,Set<T>,Map<K,V>. -
Types primitifs et leurs wrappers,
String,BigDecimal,LocalDate,Instant. -
jakarta.ws.rs.core.Response— accès au statut et aux headers bruts.
Penser à opens com.example.dto to jakarta.json.bind; dans le module-info.java du module qui contient les DTO.
Async — CompletionStage<T>
Un retour CompletionStage<T> déclenche HttpClient.sendAsync. Le CompletionStage est complété sur un virtual thread.
@GET
CompletionStage<List<UserDto>> listUsersAsync();
CompletionStage<Greeting> chained = client.listUsersAsync()
.thenApply(users -> users.size())
.thenApply(n -> new Greeting("Trouvé " + n + " utilisateurs"));
Aucune configuration n’est requise — l'`HttpClient` interne est déjà construit avec Executors.newVirtualThreadPerTaskExecutor().
Exception mapping
Par défaut, toute réponse de statut HTTP ≥ 400 est convertie en jakarta.ws.rs.WebApplicationException (spec §8, priorité 1). Pour mapper finement, implémenter ResponseExceptionMapper :
public class NotFoundMapper implements ResponseExceptionMapper<UserNotFound> {
@Override
public boolean handles(int status, jakarta.ws.rs.core.MultivaluedMap<String, Object> headers) {
return status == 404;
}
@Override
public UserNotFound toThrowable(jakarta.ws.rs.core.Response response) {
return new UserNotFound("Utilisateur introuvable");
}
@Override
public int getPriority() {
return 100; // < 1 (défaut) — gagne contre WebApplicationException
}
}
Enregistrement :
@RegisterRestClient(configKey = "users-api")
@RegisterProvider(NotFoundMapper.class)
public interface UsersClient { /* ... */ }
ou via builder :
RestClientBuilder.newBuilder()
.register(NotFoundMapper.class)
.build(UsersClient.class);
Filtres requête/réponse
Pour logger, ajouter des headers globaux, mesurer la latence — utiliser ClientRequestFilter et ClientResponseFilter JAX-RS :
public class LoggingRequestFilter implements ClientRequestFilter {
@Override
public void filter(ClientRequestContext ctx) {
System.out.println(">>> " + ctx.getMethod() + " " + ctx.getUri());
}
}
Enregistrement identique aux exception mappers (@RegisterProvider ou register(…)).
Builder programmatique
Sans CDI ou pour des cas dynamiques (URL résolue runtime, providers conditionnels) :
RestClientBuilder builder = RestClientBuilder.newBuilder()
.baseUri(URI.create(System.getenv("BLOG_API_URL")))
.connectTimeout(2, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.register(LoggingRequestFilter.class)
.register(NotFoundMapper.class);
BlogClient blog = builder.build(BlogClient.class);
Configuration via MP Config
Tout (URL, timeouts, providers, scope CDI, follow redirects, query param style) peut être surchargé sans toucher au code :
blog-api/mp-rest/url=https://staging.example.com
blog-api/mp-rest/connectTimeout=2000
blog-api/mp-rest/readTimeout=5000
blog-api/mp-rest/scope=jakarta.enterprise.context.ApplicationScoped
blog-api/mp-rest/providers=com.example.LoggingRequestFilter,com.example.NotFoundMapper
blog-api/mp-rest/followRedirects=true
blog-api/mp-rest/queryParamStyle=COMMA_SEPARATED
Voir la liste complète des clés supportées.
Intégration Vidocq Runtime
Dans un runtime Vidocq complet, l’extension vidocq-mps-cyrano-extension (en cours d’intégration) déclare le module Cyrano comme side-effect-free runtime extension : les beans synthétiques @RestClient deviennent disponibles dès l’amorçage de l’application, et la résolution MP Config est branchée sur Ravel sans configuration supplémentaire.
Pour publier côté serveur l’interface consommée côté client, voir Cassini — le même @Path("/users") peut servir de contrat partagé.