Using the legacy REST Client
This guide is about the REST Client compatible with RESTEasy Classic which used to be the default Jakarta REST (formerly known as JAX-RS) implementation until Quarkus 2.8. It is now recommended to use RESTEasy Reactive, which supports equally well traditional blocking workloads and reactive workloads. For more information about RESTEasy Reactive, please see the REST Client Reactive guide and, for the server side, the introductory REST JSON guide or the more detailed RESTEasy Reactive guide. |
This guide explains how to use the RESTEasy REST Client in order to interact with REST APIs with very little effort.
there is another guide if you need to write server JSON REST APIs. |
准备
要完成本指南,您需要:
-
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-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-quickstart"
This command generates the Maven project with a REST endpoint and imports:
-
the
resteasy
andresteasy-jackson
extensions for the REST server support; -
the
rest-client
andrest-client-jackson
extensions for the REST client support.
If you already have your Quarkus project configured, you can add the
rest-client
and the rest-client-jackson
extensions to your project by
running the following command in your project base directory:
quarkus extension add rest-client,rest-client-jackson
./mvnw quarkus:add-extension -Dextensions='rest-client,rest-client-jackson'
./gradlew addExtension --extensions='rest-client,rest-client-jackson'
This will add the following to your pom.xml
:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
implementation("io.quarkus:quarkus-rest-client")
implementation("io.quarkus:quarkus-rest-client-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 an 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 RESTEasy REST Client 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 org.jboss.resteasy.annotations.jaxrs.QueryParam;
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 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 a JSON extension is installed such as If you don’t want JSON by default you can set
If you don’t rely on the JSON default, it is heavily recommended to annotate
your endpoints with the |
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 a mock example below.
package org.acme.rest.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.annotations.jaxrs.PathParam;
import org.jboss.resteasy.annotations.jaxrs.QueryParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.Set;
@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {
@GET
@Path("/stream/{stream}")
Set<Extension> getByStream(@PathParam 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)
quarkus.rest-client."org.acme.rest.client.ExtensionsService".scope=jakarta.inject.Singleton # (2)
1 | Having this configuration means that all requests performed using
ExtensionsService will use https://stage.code.quarkus.io as the base
URL. Using the configuration above, calling the getById method of
ExtensionsService with a value of io.quarkus:quarkus-rest-client would
result in an HTTP GET request being made to
https://stage.code.quarkus.io/api/extensions?id=io.quarkus:quarkus-rest-client . |
2 | Having this configuration means that the default scope of
ExtensionsService will be @Singleton . Supported scope values are
@Singleton , @Dependent , @ApplicationScoped and @RequestScoped . The
default scope is @Dependent . The default scope can also be defined on the
interface. |
Note that org.acme.rest.client.ExtensionsService
must match the fully
qualified name of the ExtensionsService
interface we created in the
previous section.
The standard MicroProfile Rest Client properties notation can also be used to configure the client:
If a property is specified via both the Quarkus notation and the MicroProfile notation, the Quarkus notation takes a precedence. |
To facilitate the configuration, you can use the @RegisterRestClient
configKey
property that allows to use another 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. |
Moreover, you can configure a REST client to use your custom hostname verify
strategy. All you need to do is to provide a class that implements the
interface javax.net.ssl.HostnameVerifier
and add the following property to
your configuration:
quarkus.rest-client.extensions-api.hostname-verifier=<full qualified custom hostname verifier class name>
Quarkus REST client provides an embedded hostname verifier strategy to
disable the hostname verification called
|
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 org.jboss.resteasy.annotations.jaxrs.PathParam;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.Set;
@Path("/extension")
public class ExtensionsResource {
@Inject
@RestClient
ExtensionsService extensionsService;
@GET
@Path("/id/{id}")
public Set<Extension> id(@PathParam String id) {
return extensionsService.getById(id);
}
}
Note that in addition to the standard CDI @Inject
annotation, we also need
to use the MicroProfile @RestClient
annotation to inject
ExtensionsService
.
Update the test
We also 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 testExtensionIdEndpoint
method to:
package org.acme.rest.client;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.greaterThan;
import org.acme.rest.client.resources.WireMockExtensionsResource;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
@QuarkusTestResource(WireMockExtensionsResource.class)
public class ExtensionsResourceTest {
@Test
public void testExtensionsIdEndpoint() {
given()
.when().get("/extension/id/io.quarkus:quarkus-rest-client")
.then()
.statusCode(200)
.body("$.size()", is(1),
"[0].id", is("io.quarkus:quarkus-rest-client"),
"[0].name", is("REST Client Classic"),
"[0].keywords.size()", greaterThan(1),
"[0].keywords", hasItem("rest-client"));
}
}
The code above uses REST Assured's json-path capabilities.
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. However, you can enable the automatic redirection by enabling 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.
Async Support
The rest client supports asynchronous rest calls. Async support comes in 2
flavors: you can return a CompletionStage
or a Uni
(requires the
quarkus-rest-client-mutiny
extension). 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 java.util.Set;
import java.util.concurrent.CompletionStage;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.annotations.jaxrs.QueryParam;
@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {
@GET
Set<Extension> getById(@QueryParam String id);
@GET
CompletionStage<Set<Extension>> getByIdAsync(@QueryParam 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 java.util.Set;
import java.util.concurrent.CompletionStage;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.resteasy.annotations.jaxrs.PathParam;
@Path("/extension")
public class ExtensionsResource {
@Inject
@RestClient
ExtensionsService extensionsService;
@GET
@Path("/id/{id}")
public Set<Extension> id(@PathParam String id) {
return extensionsService.getById(id);
}
@GET
@Path("/id-async/{id}")
public CompletionStage<Set<Extension>> idAsync(@PathParam String id) {
return extensionsService.getByIdAsync(id);
}
}
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")
.then()
.statusCode(200)
.body("$.size()", is(1),
"[0].id", is("io.quarkus:quarkus-rest-client"),
"[0].name", is("REST Client Classic"),
"[0].keywords.size()", greaterThan(1),
"[0].keywords", hasItem("rest-client"));
}
The Uni
version is very similar:
package org.acme.rest.client;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.annotations.jaxrs.QueryParam;
import io.smallrye.mutiny.Uni;
@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {
// ...
@GET
Uni<Set<Extension>> getByIdAsUni(@QueryParam String id);
}
The ExtensionsResource
becomes:
package org.acme.rest.client;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.resteasy.annotations.jaxrs.PathParam;
import io.smallrye.mutiny.Uni;
@Path("/extension")
public class ExtensionsResource {
@Inject
@RestClient
ExtensionsService extensionsService;
// ...
@GET
@Path("/id-uni/{id}")
public Uni<Set<Extension>> idMutiny(@PathParam 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:
@Inject @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.
Custom headers support
The MicroProfile REST client allows amending request headers by registering
a ClientHeadersFactory
with the @RegisterClientHeaders
annotation.
Let’s see it in action by adding a @RegisterClientHeaders
annotation
pointing to a RequestUUIDHeaderFactory
class in our ExtensionsService
REST interface:
package org.acme.rest.client;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.annotations.jaxrs.QueryParam;
import io.smallrye.mutiny.Uni;
@Path("/extensions")
@RegisterRestClient
@RegisterClientHeaders(RequestUUIDHeaderFactory.class)
public interface ExtensionsService {
@GET
Set<Extension> getById(@QueryParam String id);
@GET
CompletionStage<Set<Extension>> getByIdAsync(@QueryParam String id);
@GET
Uni<Set<Extension>> getByIdAsUni(@QueryParam String id);
}
And the RequestUUIDHeaderFactory
would look like:
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.
Default header factory
You can also use @RegisterClientHeaders
annotation without any custom
factory specified. In that case the DefaultClientHeadersFactoryImpl
factory will be used and all headers listed in
org.eclipse.microprofile.rest.client.propagateHeaders
configuration
property will be amended. Individual header names are comma-separated.
@Path("/extensions")
@RegisterRestClient
@RegisterClientHeaders
public interface ExtensionsService {
@GET
Set<Extension> getById(@QueryParam String id);
@GET
CompletionStage<Set<Extension>> getByIdAsync(@QueryParam String id);
@GET
Uni<Set<Extension>> getByIdAsUni(@QueryParam String id);
}
org.eclipse.microprofile.rest.client.propagateHeaders=Authorization,Proxy-Authorization
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.
You should see a JSON object containing some basic information about the REST Client 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
REST Client and RESTEasy interactions
In Quarkus, the REST Client extension and the RESTEasy extension share the same infrastructure. One important consequence of this consideration is that they share the same list of providers (in the Jakarta REST meaning of the word).
For instance, if you declare a WriterInterceptor
, it will by default
intercept both the servers calls and the client calls, which might not be
the desired behavior.
However, you can change this default behavior and constrain a provider to:
-
only consider client calls by adding the
@ConstrainedTo(RuntimeType.CLIENT)
annotation to your provider; -
only consider server calls by adding the
@ConstrainedTo(RuntimeType.SERVER)
annotation to your provider.
Using a Mock HTTP Server for tests
Setting up a mock HTTP server, against which tests are run, is a common
testing pattern. Examples of such servers are
Wiremock and
Hoverfly.
In this section we’ll demonstrate how Wiremock can be leveraged for testing
the ExtensionsService
which was developed above.
First, Wiremock needs to be added as a test dependency. For a Maven project that would happen like so:
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<scope>test</scope>
<version>${wiremock.version}</version> (1)
</dependency>
1 | Use a proper Wiremock version. All available versions can be found here. |
testImplementation("org.wiremock:wiremock:$wiremockVersion") (1)
1 | Use a proper Wiremock version. All available versions can be found here. |
In Quarkus tests when some service needs to be started before the Quarkus
tests are ran, we utilize the @io.quarkus.test.common.QuarkusTestResource
annotation to specify a
io.quarkus.test.common.QuarkusTestResourceLifecycleManager
which can start
the service and supply configuration values that Quarkus will use.
For more details about |
Let’s create an implementation of QuarkusTestResourceLifecycleManager
called WiremockExtensions
like so:
package org.acme.rest.client;
import java.util.Map;
import com.github.tomakehurst.wiremock.WireMockServer;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import static com.github.tomakehurst.wiremock.client.WireMock.*; (1)
public class WireMockExtensions implements QuarkusTestResourceLifecycleManager { (2)
private WireMockServer wireMockServer;
@Override
public Map<String, String> start() {
wireMockServer = new WireMockServer();
wireMockServer.start(); (3)
wireMockServer.stubFor(get(urlEqualTo("/extensions?id=io.quarkus:quarkus-rest-client")) (4)
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody(
"[{" +
"\"id\": \"io.quarkus:quarkus-rest-client\"," +
"\"name\": \"REST Client Classic\"" +
"}]"
)));
wireMockServer.stubFor(get(urlMatching(".*")).atPriority(10).willReturn(aResponse().proxiedFrom("https://stage.code.quarkus.io/api"))); (5)
return Map.of("quarkus.rest-client.\"org.acme.rest.client.ExtensionsService\".url", wireMockServer.baseUrl()); (6)
}
@Override
public void stop() {
if (null != wireMockServer) {
wireMockServer.stop(); (7)
}
}
}
1 | Statically importing the methods in the Wiremock package makes it easier to read the test. |
2 | The start method is invoked by Quarkus before any test is run and returns
a Map of configuration properties that apply during the test execution. |
3 | Launch Wiremock. |
4 | Configure Wiremock to stub the calls to
/extensions?id=io.quarkus:quarkus-rest-client by returning a specific
canned response. |
5 | All HTTP calls that have not been stubbed are handled by calling the real service. This is done for demonstration purposes, as it is not something that would usually happen in a real test. |
6 | As the start method returns configuration that applies for tests, we set
the rest-client property that controls the base URL which is used by the
implementation of ExtensionsService to the base URL where Wiremock is
listening for incoming requests. |
7 | When all tests have finished, shutdown Wiremock. |
The ExtensionsResourceTest
test class needs to be annotated like so:
@QuarkusTest
@QuarkusTestResource(WireMockExtensions.class)
public class ExtensionsResourceTest {
}
|