Using the REST Client
This guide explains how to use the REST Client Reactive in order to interact with REST APIs. REST Client Reactive is the REST Client implementation compatible with RESTEasy Reactive.
If your application uses a client and exposes REST endpoints, please use RESTEasy Reactive for the server part.
准备
要完成本指南,您需要:
-
Roughly 15 minutes
-
An IDE
-
JDK 11+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.9.6
-
Optionally the Quarkus CLI if you want to use it
-
Optionally Mandrel or GraalVM installed and configured appropriately if you want to build a native executable (or Docker if you use a native container build)
完整源码
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
Clone the Git repository: git clone https://github.com/quarkusio/quarkus-quickstarts.git
, or download
an archive.
The solution is located in the rest-client-reactive-quickstart
directory.
Creating the Maven project
First, we need a new project. Create a new project with the following command:
For Windows users:
-
If using cmd, (don’t use backward slash
\
and put everything on the same line) -
If using Powershell, wrap
-D
parameters in double quotes e.g."-DprojectArtifactId=rest-client-reactive-quickstart"
This command generates the Maven project with a REST endpoint and imports:
-
the
resteasy-reactive-jackson
extension for the REST server support. Useresteasy-reactive
instead if you do not wish to use Jackson; -
the
rest-client-reactive-jackson
extension for the REST client support. Userest-client-reactive
instead if you do not wish to use Jackson
If you already have your Quarkus project configured, you can add the
rest-client-reactive-jackson
extension to your project by running the
following command in your project base directory:
quarkus extension add rest-client-reactive-jackson
./mvnw quarkus:add-extension -Dextensions='rest-client-reactive-jackson'
./gradlew addExtension --extensions='rest-client-reactive-jackson'
This will add the following to your build file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
</dependency>
implementation("io.quarkus:quarkus-rest-client-reactive-jackson")
Setting up the model
In this guide we will be demonstrating how to consume part of the REST API
supplied by the stage.code.quarkus.io
service. Our first order of business is to set up the model we will be
using, in the form of a Extension
POJO.
Create a src/main/java/org/acme/rest/client/Extension.java
file and set
the following content:
package org.acme.rest.client;
import java.util.List;
public class Extension {
public String id;
public String name;
public String shortName;
public List<String> keywords;
}
The model above is only a subset of the fields provided by the service, but it suffices for the purposes of this guide.
Create the interface
Using the REST Client Reactive is as simple as creating an interface using
the proper Jakarta REST and MicroProfile annotations. In our case the
interface should be created at
src/main/java/org/acme/rest/client/ExtensionsService.java
and have the
following content:
package org.acme.rest.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.Set;
@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {
@GET
Set<Extension> getById(@QueryParam("id") String id);
}
The getById
method gives our code the ability to get an extension by id
from the Code Quarkus API. The client will handle all the networking and
marshalling leaving our code clean of such technical details.
The purpose of the annotations in the code above is the following:
-
@RegisterRestClient
allows Quarkus to know that this interface is meant to be available for CDI injection as a REST Client -
@Path
,@GET
and@QueryParam
are the standard Jakarta REST annotations used to define how to access the service
When the If you don’t rely on the JSON default, it is heavily recommended to annotate
your endpoints with the |
The |
Query Parameters
The easiest way to specify a query parameter is to annotate a client method
parameter with the @QueryParam
or the @RestQuery
. The @RestQuery
is
equivalent of the @QueryParam
, but with optional name. Additionally, it
can be also used to pass query parameters as a Map
, which is convenient if
parameters are not known in advance.
package org.acme.rest.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.reactive.RestQuery;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MultivaluedMap;
import java.util.Map;
import java.util.Set;
@Path("/extensions")
@RegisterRestClient(configKey = "extensions-api")
public interface ExtensionsService {
@GET
Set<Extension> getById(@QueryParam("id") Integer id);
@GET
Set<Extension> getByName(@RestQuery String name); (1)
@GET
Set<Extension> getByFilter(@RestQuery Map<String, String> filter); (2)
@GET
Set<Extension> getByFilters(@RestQuery MultivaluedMap<String, String> filters); (3)
}
1 | Request query will include parameter with key name |
2 | Each Map entry represents exactly one query parameter |
3 | MultivaluedMap allows you to send array values |
Using @ClientQueryParam
Another way to add query parameters to a request is to use
@io.quarkus.rest.client.reactive.ClientQueryParam
on either the REST
client interface or a specific method of the interface. The annotation can
specify the query parameter name while the value can either be a constant, a
configuration property or it can be determined by invoking a method.
The following example shows the various possible usages:
@ClientQueryParam(name = "my-param", value = "${my.property-value}") (1)
public interface Client {
@GET
String getWithParam();
@GET
@ClientQueryParam(name = "some-other-param", value = "other") (2)
String getWithOtherParam();
@GET
@ClientQueryParam(name = "param-from-method", value = "{with-param}") (3)
String getFromMethod();
default String withParam(String name) {
if ("param-from-method".equals(name)) {
return "test";
}
throw new IllegalArgumentException();
}
}
1 | By placing @ClientQueryParam on the interface, we ensure that my-param
will be added to all requests of the client. Because we used the ${…}
syntax, the actual value of the parameter will be obtained using the
my.property-value configuration property. |
2 | When getWithOtherParam is called, in addition to the my-param query
parameter, some-other-param with the value of other will also be added. |
3 | when getFromMethod is called, in addition to the my-param query
parameter, param-from-method with the value of test (because that’s what
the withParam method returns when invoked with param-from-method ) will
also be added. |
Note that if an interface method contains an argument annotated with
|
More information about this annotation can be found on the javadoc of
@ClientQueryParam
.
Form Parameters
Form parameters can be specified using @RestForm
(or @FormParam
)
annotations:
package org.acme.rest.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.reactive.RestForm;
import jakarta.ws.rs.PORT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.core.MultivaluedMap;
import java.util.Map;
import java.util.Set;
@Path("/extensions")
@RegisterRestClient(configKey = "extensions-api")
public interface ExtensionsService {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
Set<Extension> postId(@FormParam("id") Integer id);
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
Set<Extension> postName(@RestForm String name);
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
Set<Extension> postFilter(@RestForm Map<String, String> filter);
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
Set<Extension> postFilters(@RestForm MultivaluedMap<String, String> filters);
}
Using @ClientFormParam
Form parameters can also be specified using @ClientFormParam
, similar to
@ClientQueryParam
:
@ClientFormParam(name = "my-param", value = "${my.property-value}")
public interface Client {
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
String postWithParam();
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@ClientFormParam(name = "some-other-param", value = "other")
String postWithOtherParam();
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@ClientFormParam(name = "param-from-method", value = "{with-param}")
String postFromMethod();
default String withParam(String name) {
if ("param-from-method".equals(name)) {
return "test";
}
throw new IllegalArgumentException();
}
}
More information about this annotation can be found on the javadoc of
@ClientFormParam
.
Path Parameters
If the GET request requires path parameters you can leverage the
@PathParam("parameter-name")
annotation instead of (or in addition to) the
@QueryParam
. Path and query parameters can be combined, as required, as
illustrated in the example below.
package org.acme.rest.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.QueryParam;
import java.util.Set;
@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {
@GET
@Path("/stream/{stream}")
Set<Extension> getByStream(@PathParam("stream") String stream, @QueryParam("id") String id);
}
Create the configuration
In order to determine the base URL to which REST calls will be made, the
REST Client uses configuration from application.properties
. The name of
the property needs to follow a certain convention which is best displayed in
the following code:
# Your configuration properties
quarkus.rest-client."org.acme.rest.client.ExtensionsService".url=https://stage.code.quarkus.io/api # (1)
1 | Having this configuration means that all requests performed using
org.acme.rest.client.ExtensionsService will use
https://stage.code.quarkus.io/api as the base URL. Using the
configuration above, calling the getById method of ExtensionsService
with a value of io.quarkus:quarkus-rest-client-reactive would result in an
HTTP GET request being made to
https://stage.code.quarkus.io/api/extensions?id=io.quarkus:quarkus-rest-client-reactive . |
Note that org.acme.rest.client.ExtensionsService
must match the fully
qualified name of the ExtensionsService
interface we created in the
previous section.
To facilitate the configuration, you can use the @RegisterRestClient
configKey
property that allows to use different configuration root than
the fully qualified name of your interface.
@RegisterRestClient(configKey="extensions-api")
public interface ExtensionsService {
[...]
}
# Your configuration properties
quarkus.rest-client.extensions-api.url=https://stage.code.quarkus.io/api
quarkus.rest-client.extensions-api.scope=jakarta.inject.Singleton
Disabling Hostname Verification
To disable the SSL hostname verification for a specific REST client, add the following property to your configuration:
quarkus.rest-client.extensions-api.verify-host=false
This setting should not be used in production as it will disable the SSL hostname verification. |
HTTP/2 Support
HTTP/2 is disabled by default in REST Client. If you want to enable it, you can set:
// for all REST Clients:
quarkus.rest-client.http2=true
// or for a single REST Client:
quarkus.rest-client.extensions-api.http2=true
Alternatively, you can enable the Application-Layer Protocol Negotiation (alpn) TLS extension and the client will negotiate which HTTP version to use over the ones compatible by the server. By default, it will try to use HTTP/2 first and if it’s not enabled, it will use HTTP/1.1. If you want to enable it, you can set:
quarkus.rest-client.alpn=true
// or for a single REST Client:
quarkus.rest-client.extensions-api.alpn=true
Create the Jakarta REST resource
Create the src/main/java/org/acme/rest/client/ExtensionsResource.java
file
with the following content:
package org.acme.rest.client;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.Set;
@Path("/extension")
public class ExtensionsResource {
@RestClient (1)
ExtensionsService extensionsService;
@GET
@Path("/id/{id}")
public Set<Extension> id(String id) {
return extensionsService.getById(id);
}
}
There are two interesting parts in this listing:
1 | the client stub is injected with the @RestClient annotation instead of the
usual CDI @Inject |
Programmatic client creation with QuarkusRestClientBuilder
Instead of annotating the client with @RegisterRestClient
, and injecting a
client with @RestClient
, you can also create REST Client
programmatically. You do that with the QuarkusRestClientBuilder
.
With this approach the client interface could look as follows:
package org.acme.rest.client;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.Set;
@Path("/extensions")
public interface ExtensionsService {
@GET
Set<Extension> getById(@QueryParam("id") String id);
}
And the service as follows:
package org.acme.rest.client;
import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.net.URI;
import java.util.Set;
@Path("/extension")
public class ExtensionsResource {
private final ExtensionsService extensionsService;
public ExtensionsResource() {
extensionsService = QuarkusRestClientBuilder.newBuilder()
.baseUri(URI.create("https://stage.code.quarkus.io/api"))
.build(ExtensionsService.class);
}
@GET
@Path("/id/{id}")
public Set<Extension> id(String id) {
return extensionsService.getById(id);
}
}
The
|
Use Custom HTTP Options
The REST Client Reactive internally uses the Vert.x HTTP Client to make the network connections. The REST Client Reactive extensions allows configuring some settings via properties, for example:
-
quarkus.rest-client.client-prefix.connect-timeout
to configure the connect timeout in milliseconds. -
quarkus.rest-client.client-prefix.max-redirects
to limit the number of redirects.
However, there are many more options within the Vert.x HTTP Client to configure the connections. See all the options in the Vert.x HTTP Client Options API in this link.
To fully customize the Vert.x HTTP Client instance that the REST Client Reactive is internally using, you can provide your custom HTTP Client Options instance via CDI or when programmatically creating your client.
Let’s see an example about how to provide the HTTP Client Options via CDI:
package org.acme.rest.client;
import jakarta.enterprise.inject.Produces;
import jakarta.ws.rs.ext.ContextResolver;
import io.vertx.core.http.HttpClientOptions;
import io.quarkus.arc.Unremovable;
@Provider
public class CustomHttpClientOptions implements ContextResolver<HttpClientOptions> {
@Override
public HttpClientOptions getContext(Class<?> aClass) {
HttpClientOptions options = new HttpClientOptions();
// ...
return options;
}
}
Now, all the REST Clients will be using your custom HTTP Client Options.
Another approach is to provide the custom HTTP Client options when creating the client programmatically:
package org.acme.rest.client;
import io.quarkus.rest.client.reactive.QuarkusRestClientBuilder;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.net.URI;
import java.util.Set;
import io.vertx.core.http.HttpClientOptions;
@Path("/extension")
public class ExtensionsResource {
private final ExtensionsService extensionsService;
public ExtensionsResource() {
HttpClientOptions options = new HttpClientOptions();
// ...
extensionsService = QuarkusRestClientBuilder.newBuilder()
.baseUri(URI.create("https://stage.code.quarkus.io/api"))
.httpClientOptions(options) (1)
.build(ExtensionsService.class);
}
// ...
}
1 | the client will use the registered HTTP Client options over the HTTP Client options provided via CDI if any. |
Redirection
A HTTP server can redirect a response to another location by sending a response with a status code that starts with "3" and a HTTP header "Location" holding the URL to be redirected to. When the REST Client receives a redirection response from a HTTP server, it won’t automatically perform another request to the new location. We can enable the automatic redirection in REST Client by adding the "follow-redirects" property:
-
quarkus.rest-client.follow-redirects
to enable redirection for all REST clients. -
quarkus.rest-client.<client-prefix>.follow-redirects
to enable redirection for a specific REST client.
If this property is true, then REST Client will perform a new request that it receives a redirection response from the HTTP server.
Additionally, we can limit the number of redirections using the property "max-redirects".
One important note is that according to the
RFC2616 specs, by
default the redirection will only happen for GET or HEAD methods. However,
in REST Client, you can provide your custom redirect handler to enable
redirection on POST or PUT methods, or to follow a more complex logic, via
either using the @ClientRedirectHandler
annotation, CDI or
programmatically when creating your client.
Let’s see an example about how to register your own custom redirect handler
using the @ClientRedirectHandler
annotation:
import jakarta.ws.rs.core.Response;
import io.quarkus.rest.client.reactive.ClientRedirectHandler;
@RegisterRestClient(configKey="extensions-api")
public interface ExtensionsService {
@ClientRedirectHandler
static URI alwaysRedirect(Response response) {
if (Response.Status.Family.familyOf(response.getStatus()) == Response.Status.Family.REDIRECTION) {
return response.getLocation();
}
return null;
}
}
The "alwaysRedirect" redirect handler will only be used by the specified REST Client which in this example is the "ExtensionsService" client.
Alternatively, you can also provide a custom redirect handler for all your REST Clients via CDI:
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ContextResolver;
import jakarta.ws.rs.ext.Provider;
import org.jboss.resteasy.reactive.client.handlers.RedirectHandler;
@Provider
public class AlwaysRedirectHandler implements ContextResolver<RedirectHandler> {
@Override
public RedirectHandler getContext(Class<?> aClass) {
return response -> {
if (Response.Status.Family.familyOf(response.getStatus()) == Response.Status.Family.REDIRECTION) {
return response.getLocation();
}
// no redirect
return null;
};
}
}
Now, all the REST Clients will be using your custom redirect handler.
Another approach is to provide it programmatically when creating the client:
@Path("/extension")
public class ExtensionsResource {
private final ExtensionsService extensionsService;
public ExtensionsResource() {
extensionsService = QuarkusRestClientBuilder.newBuilder()
.baseUri(URI.create("https://stage.code.quarkus.io/api"))
.register(AlwaysRedirectHandler.class) (1)
.build(ExtensionsService.class);
}
// ...
}
1 | the client will use the registered redirect handler over the redirect handler provided via CDI if any. |
Update the test
Next, we need to update the functional test to reflect the changes made to
the endpoint. Edit the
src/test/java/org/acme/rest/client/ExtensionsResourceTest.java
file and
change the content of the test to:
package org.acme.rest.client;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.greaterThan;
@QuarkusTest
public class ExtensionsResourceTest {
@Test
public void testExtensionsIdEndpoint() {
given()
.when().get("/extension/id/io.quarkus:quarkus-rest-client-reactive")
.then()
.statusCode(200)
.body("$.size()", is(1),
"[0].id", is("io.quarkus:quarkus-rest-client-reactive"),
"[0].name", is("REST Client Reactive"),
"[0].keywords.size()", greaterThan(1),
"[0].keywords", hasItem("rest-client"));
}
}
The code above uses REST Assured's json-path capabilities.
Async Support
To get the full power of the reactive nature of the client, you can use the
non-blocking flavor of REST Client Reactive extension, which comes with
support for CompletionStage
and Uni
. Let’s see it in action by adding a
getByIdAsync
method in our ExtensionsService
REST interface. The code
should look like:
package org.acme.rest.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.Set;
import java.util.concurrent.CompletionStage;
@Path("/extensions")
@RegisterRestClient(configKey = "extensions-api")
public interface ExtensionsService {
@GET
Set<Extension> getById(@QueryParam("id") String id);
@GET
CompletionStage<Set<Extension>> getByIdAsync(@QueryParam("id") String id);
}
Open the src/main/java/org/acme/rest/client/ExtensionsResource.java
file
and update it with the following content:
package org.acme.rest.client;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.Set;
import java.util.concurrent.CompletionStage;
@Path("/extension")
public class ExtensionsResource {
@RestClient
ExtensionsService extensionsService;
@GET
@Path("/id/{id}")
public Set<Extension> id(String id) {
return extensionsService.getById(id);
}
@GET
@Path("/id-async/{id}")
public CompletionStage<Set<Extension>> idAsync(String id) {
return extensionsService.getByIdAsync(id);
}
}
Please note that since the invocation is now non-blocking, the idAsync
method will be invoked on the event loop, i.e. will not get offloaded to a
worker pool thread and thus reducing hardware resource utilization. See
Resteasy reactive execution
model for more details.
To test asynchronous methods, add the test method below in
ExtensionsResourceTest
:
@Test
public void testExtensionIdAsyncEndpoint() {
given()
.when().get("/extension/id-async/io.quarkus:quarkus-rest-client-reactive")
.then()
.statusCode(200)
.body("$.size()", is(1),
"[0].id", is("io.quarkus:quarkus-rest-client-reactive"),
"[0].name", is("REST Client Reactive"),
"[0].keywords.size()", greaterThan(1),
"[0].keywords", hasItem("rest-client"));
}
The Uni
version is very similar:
package org.acme.rest.client;
import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.Set;
@Path("/extensions")
@RegisterRestClient(configKey = "extensions-api")
public interface ExtensionsService {
// ...
@GET
Uni<Set<Extension>> getByIdAsUni(@QueryParam("id") String id);
}
The ExtensionsResource
becomes:
package org.acme.rest.client;
import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.Set;
@Path("/extension")
public class ExtensionsResource {
@RestClient
ExtensionsService extensionsService;
// ...
@GET
@Path("/id-uni/{id}")
public Uni<Set<Extension>> idUni(String id) {
return extensionsService.getByIdAsUni(id);
}
}
Mutiny
The previous snippet uses Mutiny reactive types. If you are not familiar with Mutiny, check Mutiny - an intuitive reactive programming library. |
When returning a Uni
, every subscription invokes the remote service. It
means you can re-send the request by re-subscribing on the Uni
, or use a
retry
as follows:
@RestClient ExtensionsService extensionsService;
// ...
extensionsService.getByIdAsUni(id)
.onFailure().retry().atMost(10);
If you use a CompletionStage
, you would need to call the service’s method
to retry. This difference comes from the laziness aspect of Mutiny and its
subscription protocol. More details about this can be found in
the
Mutiny documentation.
Server-Sent Event (SSE) support
Consuming SSE events is possible simply by declaring the result type as a
io.smallrye.mutiny.Multi
.
The simplest example is:
package org.acme.rest.client;
import io.smallrye.mutiny.Multi;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("/sse")
@RegisterRestClient(configKey = "some-api")
public interface SseClient {
@GET
@Produces(MediaType.SERVER_SENT_EVENTS)
Multi<String> get();
}
All the IO involved in streaming the SSE results is done in a non-blocking manner. |
Results are not limited to strings - for example when the server returns
JSON payload for each event, Quarkus automatically deserializes it into the
generic type used in the Multi
.
Users can also access the entire SSE event by using the
A simple example where the event payloads are
|
Filtering out events
On occasion, the stream of SSE events may contain some events that should
not be returned by the client - an example of this is having the server send
heartbeat events in order to keep the underlying TCP connection open. The
REST Client supports filtering out such events by providing the
@org.jboss.resteasy.reactive.client.SseEventFilter
.
Here is an example of filtering out heartbeat events:
package org.acme.rest.client;
import io.smallrye.mutiny.Uni;
import java.util.function.Predicate;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.reactive.client.SseEvent;
import org.jboss.resteasy.reactive.client.SseEventFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
@Path("/sse")
@RegisterRestClient(configKey = "some-api")
public interface SseClient {
@GET
@Produces(MediaType.SERVER_SENT_EVENTS)
@SseEventFilter(HeartbeatFilter.class)
Multi<SseEvent<Long>> get();
class HeartbeatFilter implements Predicate<SseEvent<String>> {
@Override
public boolean test(SseEvent<String> event) {
return !"heartbeat".equals(event.id());
}
}
}
Custom headers support
There are a few ways in which you can specify custom headers for your REST calls:
-
by registering a
ClientHeadersFactory
or aReactiveClientHeadersFactory
with the@RegisterClientHeaders
annotation -
by programmatically registering a
ClientHeadersFactory
or aReactiveClientHeadersFactory
with theQuarkusRestClientBuilder.clientHeadersFactory(factory)
method -
by specifying the value of the header with
@ClientHeaderParam
-
by specifying the value of the header by
@HeaderParam
The code below demonstrates how to use each of these techniques:
package org.acme.rest.client;
import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import java.util.Set;
import io.quarkus.rest.client.reactive.NotBody;
@Path("/extensions")
@RegisterRestClient
@RegisterClientHeaders(RequestUUIDHeaderFactory.class) (1)
@ClientHeaderParam(name = "my-header", value = "constant-header-value") (2)
@ClientHeaderParam(name = "computed-header", value = "{org.acme.rest.client.Util.computeHeader}") (3)
public interface ExtensionsService {
@GET
@ClientHeaderParam(name = "header-from-properties", value = "${header.value}") (4)
@ClientHeaderParam(name = "header-from-method-param", value = "Bearer {token}") (5)
Set<Extension> getById(@QueryParam("id") String id, @HeaderParam("jaxrs-style-header") String headerValue, @NotBody String token); (6)
}
1 | There can be only one ClientHeadersFactory per class. With it, you can not
only add custom headers, but you can also transform existing ones. See the
RequestUUIDHeaderFactory class below for an example of the factory. |
2 | @ClientHeaderParam can be used on the client interface and on methods. It
can specify a constant header value… |
3 | … and a name of a method that should compute the value of the header. It
can either be a static method or a default method in this interface. The
method can take either no parameters, a single String parameter or a single
io.quarkus.rest.client.reactive.ComputedParamContext parameter (which is
very useful for code that needs to compute headers based on method
parameters and naturally complements
@io.quarkus.rest.client.reactive.NotBody ). |
4 | … as well as a value from your application’s configuration |
5 | … or even any mixture of verbatim text, method parameters (referenced by name), a configuration value (as mentioned previously) and method invocations (as mentioned before) |
6 | … or as a normal Jakarta REST @HeaderParam annotated argument |
When using Kotlin, if default methods are going to be leveraged, then the Kotlin compiler needs to be configured to use Java’s default interface capabilities. See this for more details. |
A ClientHeadersFactory
can look as follows:
package org.acme.rest.client;
import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import java.util.UUID;
@ApplicationScoped
public class RequestUUIDHeaderFactory implements ClientHeadersFactory {
@Override
public MultivaluedMap<String, String> update(MultivaluedMap<String, String> incomingHeaders, MultivaluedMap<String, String> clientOutgoingHeaders) {
MultivaluedMap<String, String> result = new MultivaluedHashMap<>();
result.add("X-request-uuid", UUID.randomUUID().toString());
return result;
}
}
As you see in the example above, you can make your ClientHeadersFactory
implementation a CDI bean by annotating it with a scope-defining annotation,
such as @Singleton
, @ApplicationScoped
, etc.
To specify a value for ${header.value}
, simply put the following in your
application.properties
:
header.value=value of the header
Also, there is a reactive flavor of ClientHeadersFactory
that allows doing
blocking operations. For example:
package org.acme.rest.client;
import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import java.util.UUID;
@ApplicationScoped
public class GetTokenReactiveClientHeadersFactory extends ReactiveClientHeadersFactory {
@Inject
Service service;
@Override
public Uni<MultivaluedMap<String, String>> getHeaders(
MultivaluedMap<String, String> incomingHeaders,
MultivaluedMap<String, String> clientOutgoingHeaders) {
return Uni.createFrom().item(() -> {
MultivaluedHashMap<String, String> newHeaders = new MultivaluedHashMap<>();
// perform blocking call
newHeaders.add(HEADER_NAME, service.getToken());
return newHeaders;
});
}
}
Default header factory
The @RegisterClientHeaders
annotation can also be used without any custom
factory specified. In that case the DefaultClientHeadersFactoryImpl
factory will be used. If you make a REST client call from a REST resource,
this factory will propagate all the headers listed in
org.eclipse.microprofile.rest.client.propagateHeaders
configuration
property from the resource request to the client request. Individual header
names are comma-separated.
@Path("/extensions")
@RegisterRestClient
@RegisterClientHeaders
public interface ExtensionsService {
@GET
Set<Extension> getById(@QueryParam("id") String id);
@GET
CompletionStage<Set<Extension>> getByIdAsync(@QueryParam("id") String id);
}
org.eclipse.microprofile.rest.client.propagateHeaders=Authorization,Proxy-Authorization
Customizing the request
The REST Client Reactive supports further customization of the final request
to be sent to the server via filters. The filters must implement either the
interface ClientRequestFilter
or ResteasyReactiveClientRequestFilter
.
A simple example of customizing the request would be to add a custom header:
@Provider
public class TestClientRequestFilter implements ClientRequestFilter {
@Override
public void filter(ClientRequestContext requestContext) {
requestContext.getHeaders().add("my_header", "value");
}
}
Next, you can register your filter using the @RegisterProvider
annotation:
@Path("/extensions")
@RegisterProvider(TestClientRequestFilter.class)
public interface ExtensionsService {
// ...
}
Or programmatically using the .register()
method:
QuarkusRestClientBuilder.newBuilder()
.register(TestClientRequestFilter.class)
.build(ExtensionsService.class)
Injecting the jakarta.ws.rs.ext.Providers
instance in filters
The jakarta.ws.rs.ext.Providers
is useful when we need to lookup the
provider instances of the current client.
We can get the Providers
instance in our filters from the request context
as follows:
@Provider
public class TestClientRequestFilter implements ClientRequestFilter {
@Override
public void filter(ClientRequestContext requestContext) {
Providers providers = ((ResteasyReactiveClientRequestContext) requestContext).getProviders();
// ...
}
}
Alternatively, you can implement the ResteasyReactiveClientRequestFilter
interface instead of the ClientRequestFilter
interface that will directly
provide the ResteasyReactiveClientRequestContext
context:
@Provider
public class TestClientRequestFilter implements ResteasyReactiveClientRequestFilter {
@Override
public void filter(ResteasyReactiveClientRequestFilter requestContext) {
Providers providers = requestContext.getProviders();
// ...
}
}
Customizing the ObjectMapper in REST Client Reactive Jackson
The REST Client Reactive supports adding a custom ObjectMapper to be used
only the Client using the annotation @ClientObjectMapper
.
A simple example is to provide a custom ObjectMapper to the REST Client Reactive Jackson extension by doing:
@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {
@GET
Set<Extension> getById(@QueryParam("id") String id);
@ClientObjectMapper (1)
static ObjectMapper objectMapper(ObjectMapper defaultObjectMapper) { (2)
return defaultObjectMapper.copy() (3)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(DeserializationFeature.UNWRAP_ROOT_VALUE);
}
}
1 | The method must be annotated with @ClientObjectMapper . |
2 | It’s must be a static method. Also, the parameter defaultObjectMapper will
be resolved via CDI. If not found, it will throw an exception at runtime. |
3 | In this example, we’re creating a copy of the default object mapper. You should NEVER modify the default object mapper, but create a copy instead. |
Exception handling
The MicroProfile REST Client specification introduces the
org.eclipse.microprofile.rest.client.ext.ResponseExceptionMapper
whose
purpose is to convert an HTTP response to an exception.
A simple example of implementing such a ResponseExceptionMapper
for the
ExtensionsService
discussed above, could be:
public class MyResponseExceptionMapper implements ResponseExceptionMapper<RuntimeException> {
@Override
public RuntimeException toThrowable(Response response) {
if (response.getStatus() == 500) {
throw new RuntimeException("The remote service responded with HTTP 500");
}
return null;
}
}
ResponseExceptionMapper
also defines the getPriority
method which is
used in order to determine the priority with which ResponseExceptionMapper
implementations will be called (implementations with a lower value for
getPriority
will be invoked first). If toThrowable
returns an
exception, then that exception will be thrown. If null
is returned, the
next implementation of ResponseExceptionMapper
in the chain will be called
(if there is any).
The class as written above, would not be automatically be used by any REST
Client. To make it available to every REST Client of the application, the
class needs to be annotated with @Provider
(as long as
quarkus.rest-client-reactive.provider-autodiscovery
is not set to
false
). Alternatively, if the exception handling class should only apply
to specific REST Client interfaces, you can either annotate the interfaces
with @RegisterProvider(MyResponseExceptionMapper.class)
, or register it
using configuration using the providers
property of the proper
quarkus.rest-client
configuration group.
Using @ClientExceptionMapper
A simpler way to convert HTTP response codes of 400 or above is to use the
@ClientExceptionMapper
annotation.
For the ExtensionsService
REST Client interface defined above, an example
use of @ClientExceptionMapper
would be:
@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {
@GET
Set<Extension> getById(@QueryParam("id") String id);
@GET
CompletionStage<Set<Extension>> getByIdAsync(@QueryParam("id") String id);
@ClientExceptionMapper
static RuntimeException toException(Response response) {
if (response.getStatus() == 500) {
return new RuntimeException("The remote service responded with HTTP 500");
}
return null;
}
}
Naturally this handling is per REST Client. @ClientExceptionMapper
uses
the default priority if the priority
attribute is not set and the normal
rules of invoking all handlers in turn apply.
Methods annotated with @ClientExceptionMapper can also take a
java.lang.reflect.Method parameter which is useful if the exception
mapping code needs to know the REST Client method that was invoked and
caused the exception mapping code to engage.
|
Using @Blocking annotation in exception mappers
In cases that warrant using InputStream
as the return type of REST Client
method (such as when large amounts of data need to be read):
@Path("/echo")
@RegisterRestClient
public interface EchoClient {
@GET
InputStream get();
}
This will work as expected, but if you try to read this InputStream object
in a custom exception mapper, you will receive a
BlockingNotAllowedException
exception. This is because
ResponseExceptionMapper
classes are run on the Event Loop thread executor
by default - which does not allow to perform IO operations.
To make your exception mapper blocking, you can annotate the exception
mapper with the @Blocking
annotation:
@Provider
@Blocking (1)
public class MyResponseExceptionMapper implements ResponseExceptionMapper<RuntimeException> {
@Override
public RuntimeException toThrowable(Response response) {
if (response.getStatus() == 500) {
response.readEntity(String.class); (2)
return new RuntimeException("The remote service responded with HTTP 500");
}
return null;
}
}
1 | With the @Blocking annotation, the MyResponseExceptionMapper exception
mapper will be executed in the worker thread pool. |
2 | Reading the entity is now allowed because we’re executing the mapper on the worker thread pool. |
Note that you can also use the @Blocking
annotation when using
@ClientExceptionMapper:
@Path("/echo")
@RegisterRestClient
public interface EchoClient {
@GET
InputStream get();
@ClientExceptionMapper
@Blocking
static RuntimeException toException(Response response) {
if (response.getStatus() == 500) {
response.readEntity(String.class);
return new RuntimeException("The remote service responded with HTTP 500");
}
return null;
}
}
Multipart Form support
REST Client Reactive support multipart messages.
Sending Multipart messages
REST Client Reactive allows sending data as multipart forms. This way you can for example send files efficiently.
To send data as a multipart form, you can just use the regular @RestForm
(or @FormParam
) annotations:
@POST
@Path("/binary")
String sendMultipart(@RestForm File file, @RestForm String otherField);
Parameters specified as File
, Path
, byte[]
or Buffer
are sent as
files and default to the application/octet-stream
MIME type. Other
@RestForm
parameter types default to the text/plain
MIME type. You can
override these defaults with the @PartType
annotation.
Naturally, you can also group these parameters into a containing class:
public static class Parameters {
@RestForm
File file;
@RestForm
String otherField;
}
@POST
@Path("/binary")
String sendMultipart(Parameters parameters);
Any @RestForm
parameter of the type File
, Path
, byte[]
or Buffer
,
as well as any annotated with @PartType
automatically imply a
@Consumes(MediaType.MULTIPART_FORM_DATA)
on the method if there is no
@Consumes
present.
If there are @RestForm parameters that are not multipart-implying, then
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) is implied.
|
There are a few modes in which the form data can be encoded. By default,
Rest Client Reactive uses RFC1738. You can override it by specifying the
mode either on the client level, by setting
io.quarkus.rest.client.multipart-post-encoder-mode
RestBuilder property to
the selected value of HttpPostRequestEncoder.EncoderMode
or by specifying
quarkus.rest-client.multipart-post-encoder-mode
in your
application.properties
. Please note that the latter works only for clients
created with the @RegisterRestClient
annotation. All the available modes
are described in the
Netty
documentation
You can also send JSON multiparts by specifying the @PartType
annotation:
public static class Person {
public String firstName;
public String lastName;
}
@POST
@Path("/json")
String sendMultipart(@RestForm @PartType(MediaType.APPLICATION_JSON) Person person);
Receiving Multipart Messages
REST Client Reactive also supports receiving multipart messages. As with sending, to parse a multipart response, you need to create a class that describes the response data, e.g.
public class FormDto {
@RestForm (1)
@PartType(MediaType.APPLICATION_OCTET_STREAM)
public File file;
@FormParam("otherField") (2)
@PartType(MediaType.TEXT_PLAIN)
public String textProperty;
}
1 | uses the shorthand @RestForm annotation to make a field as a part of a
multipart form |
2 | the standard @FormParam can also be used. It allows to override the name
of the multipart part. |
Then, create an interface method that corresponds to the call and make it
return the FormDto
:
@GET
@Produces(MediaType.MULTIPART_FORM_DATA)
@Path("/get-file")
FormDto data receiveMultipart();
At the moment, multipart response support is subject to the following limitations:
-
files sent in multipart responses can only be parsed to
File
,Path
andFileDownload
-
each field of the response type has to be annotated with
@PartType
- fields without this annotation are ignored
REST Client Reactive needs to know the classes used as multipart return
types upfront. If you have an interface method that produces
multipart/form-data
, the return type will be discovered
automatically. However, if you intend to use the ClientBuilder
API to
parse a response as multipart, you need to annotate your DTO class with
@MultipartForm
.
The files you download are not automatically removed and can take up a lot of disk space. Consider removing the files when you are done working with them. |
Multipart mixed / OData usage
It is not uncommon that an application has to interact with enterprise
systems (like CRM systems) using a special protocol called
OData.
This protocol essentially uses a custom HTTP Content-Type
which needs some
glue code to work with the REST Client (creating the body is entirely up to
the application - the REST Client can’t do much to help).
An example looks like the following:
@Path("/crm")
@RegisterRestClient
public interface CRMService {
@POST
@ClientHeaderParam(name = "Content-Type", value = "{calculateContentType}") (1)
String performBatch(@HeaderParam("Authorization") String accessToken, @NotBody String batchId, String body); (2)
default String calculateContentType(ComputedParamContext context) {
return "multipart/mixed;boundary=batch_" + context.methodParameters().get(1).value(); (3)
}
}
The code uses the following pieces:
1 | @ClientHeaderParam(name = "Content-Type", value =
"{calculateContentType}") which ensures that the Content-Type header is
created by calling the interface’s calculateContentType default method. |
2 | The aforementioned parameter needs to be annotated with @NotBody because
it is only used to aid the construction of HTTP headers. |
3 | context.methodParameters().get(1).value() which allows the
calculateContentType method to obtain the proper method parameter passed
to the REST Client method. |
As previously mentioned, the body parameter needs to be properly crafted by the application code to conform to the service’s requirements.
Receiving compressed messages
REST Client Reactive also supports receiving compressed messages using
GZIP. You can enable the HTTP compression support by adding the property
quarkus.http.enable-compression=true
. When this feature is enabled and a
server returns a response that includes the header Content-Encoding: gzip
,
REST Client Reactive will automatically decode the content and proceed with
the message handling.
Proxy support
REST Client Reactive supports sending requests through a proxy. It honors the JVM settings for it but also allows to specify both:
-
global client proxy settings, with
quarkus.rest-client.proxy-address
,quarkus.rest-client.proxy-user
,quarkus.rest-client.proxy-password
,quarkus.rest-client.non-proxy-hosts
-
per-client proxy settings, with
quarkus.rest-client.<my-client>.proxy-address
, etc. These are applied only to clients injected with CDI, that is the ones created with@RegisterRestClient
If proxy-address
is set on the client level, the client uses its specific
proxy settings. No proxy settings are propagated from the global
configuration or JVM properties.
If proxy-address
is not set for the client but is set on the global level,
the client uses the global settings. Otherwise, the client uses the JVM
settings.
An example configuration for setting proxy:
# global proxy configuration is used for all clients
quarkus.rest-client.proxy-address=localhost:8182
quarkus.rest-client.proxy-user=<proxy user name>
quarkus.rest-client.proxy-password=<proxy password>
quarkus.rest-client.non-proxy-hosts=example.com
# per-client configuration overrides the global settings for a specific client
quarkus.rest-client.my-client.proxy-address=localhost:8183
quarkus.rest-client.my-client.proxy-user=<proxy user name>
quarkus.rest-client.my-client.proxy-password=<proxy password>
quarkus.rest-client.my-client.url=...
MicroProfile REST Client specification does not allow setting proxy
credentials. In order to specify proxy user and proxy password
programmatically, you need to cast your RestClientBuilder to
RestClientBuilderImpl .
|
Package and run the application
Run the application with:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
Open your browser to http://localhost:8080/extension/id/io.quarkus:quarkus-rest-client-reactive.
You should see a JSON object containing some basic information about this extension.
As usual, the application can be packaged using:
quarkus build
./mvnw install
./gradlew build
And executed with java -jar target/quarkus-app/quarkus-run.jar
.
You can also generate the native executable with:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.package.type=native
Logging traffic
REST Client Reactive can log the requests it sends and the responses it
receives. To enable logging, add the quarkus.rest-client.logging.scope
property to your application.properties
and set it to:
-
request-response
to log the request and response contents, or -
all
to also enable low level logging of the underlying libraries.
As HTTP messages can have large bodies, we limit the amount of body
characters logged. The default limit is 100
, you can change it by
specifying quarkus.rest-client.logging.body-limit
.
REST Client Reactive is logging the traffic with level DEBUG and does not alter logger properties. You may need to adjust your logger configuration to use this feature. |
An example logging configuration:
quarkus.rest-client.logging.scope=request-response
quarkus.rest-client.logging.body-limit=50
quarkus.log.category."org.jboss.resteasy.reactive.client.logging".level=DEBUG
REST Client Reactive uses a default ClientLogger implementation. You can
change it by providing a custom ClientLogger instance through CDI or when
programmatically creating your client.
|
Mocking the client for tests
If you use a client injected with the @RestClient
annotation, you can
easily mock it for tests. You can do it with Mockito’s @InjectMock
or
with QuarkusMock
.
This section shows how to replace your client with a mock. If you would like to get a more in-depth understanding of how mocking works in Quarkus, see the blog post on Mocking CDI beans.
Mocking does not work when using @QuarkusIntegrationTest .
|
Let’s assume you have the following client:
package io.quarkus.it.rest.client.main;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@Path("/")
@RegisterRestClient
public interface Client {
@GET
String get();
}
Mocking with InjectMock
The simplest approach to mock a client for tests is to use Mockito and
@InjectMock
.
First, add the following dependency to your application:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-junit5-mockito")
Then, in your test you can simply use @InjectMock
to create and inject a
mock:
package io.quarkus.it.rest.client.main;
import static org.mockito.Mockito.when;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
@QuarkusTest
public class InjectMockTest {
@InjectMock
@RestClient
Client mock;
@BeforeEach
public void setUp() {
when(mock.get()).thenReturn("MockAnswer");
}
@Test
void doTest() {
// ...
}
}
Mocking with QuarkusMock
If Mockito doesn’t meet your needs, you can create a mock programmatically
using QuarkusMock
, e.g.:
package io.quarkus.it.rest.client.main;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusMock;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
public class QuarkusMockTest {
@BeforeEach
public void setUp() {
Client customMock = new Client() { (1)
@Override
public String get() {
return "MockAnswer";
}
};
QuarkusMock.installMockForType(customMock, Client.class, RestClient.LITERAL); (2)
}
@Test
void doTest() {
// ...
}
}
1 | here we use a manually created implementation of the client interface to replace the actual Client |
2 | note that RestClient.LITERAL has to be passed as the last argument of the
installMockForType method |
Using a Mock HTTP Server for tests
In some cases you may want to mock the remote endpoint - the HTTP server - instead of mocking the client itself. This may be especially useful for native tests, or for programmatically created clients.
You can easily mock an HTTP Server with Wiremock. The Wiremock section of the Quarkus - Using the REST Client describes how to set it up in detail.
Known limitations
While the REST Client Reactive extension aims to be a drop-in replacement for the REST Client extension, there are some differences and limitations:
-
the default scope of the client for the new extension is
@ApplicationScoped
while thequarkus-rest-client
defaults to@Dependent
To change this behavior, set thequarkus.rest-client-reactive.scope
property to the fully qualified scope name. -
it is not possible to set
HostnameVerifier
orSSLContext
-
a few things that don’t make sense for a non-blocking implementations, such as setting the
ExecutorService
, don’t work