Usando o Cliente REST
|
This guide is about the REST Client compatible with RESTEasy Classic which used to be the default 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. |
Pré-requisitos
Para concluir este guia, você precisa:
-
Cerca de 15 minutos
-
Um IDE
-
JDK 11+ instalado com 'JAVA_HOME' configurado adequadamente
-
Apache Maven 3.8.6
-
Opcionalmente, o Quarkus CLI se você quiser usá-lo
-
Opcionalmente, Mandrel ou GraalVM instalado e configurado apropriadamente se você quiser criar um executável nativo (ou Docker se você usar uma compilação de contêiner nativo)
Solução
Recomendamos que siga as instruções nas seções seguintes e crie a aplicação passo a passo. No entanto, você pode ir diretamente para o exemplo completo.
Clone o repositório Git: git clone https://github.com/quarkusio/quarkus-quickstarts.git, ou baixe um arquivo.
The solution is located in the rest-client-quickstart directory.
Criar o projeto Maven
Primeiro, precisamos de um novo projeto. Crie um novo projeto com o seguinte comando:
Este comando gera o projeto Maven com um endpoint REST e com importações:
-
the
resteasyandresteasy-jacksonextensions for the REST server support; -
the
rest-clientandrest-client-jacksonextensions 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'
Isso adicionará o seguinte ao seu arquivo 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")
Configurando o modelo
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.
Crie um arquivo src/main/java/org/acme/rest/client/Extension.java e defina o seguinte conteúdo:
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;
}
O modelo acima é apenas um subconjunto dos campos fornecidos pelo serviço, mas é suficiente para os objetivos deste guia.
Crie a interface
Using the RESTEasy REST Client is as simple as creating an interface using the proper JAX-RS 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 javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import java.util.Set;
@Path("/extensions")
@RegisterRestClient
public interface ExtensionsService {
@GET
Set<Extension> getById(@QueryParam String id);
}
O método getById dá ao nosso código a capacidade de obter uma extensão por id a partir da API do Code Quarkus. O cliente tratará de toda a ligação em rede e da organização, deixando o nosso código livre desses pormenores técnicos.
O objetivo das anotações no código acima é o seguinte:
-
@RegisterRestClientpermite que o Quarkus saiba que essa interface deve estar disponível para injeção de CDI como um Cliente REST -
@Path,@GETand@QueryParamare the standard JAX-RS 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 |
Parâmetros do Caminho
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 javax.ws.rs.GET;
import javax.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);
}
Crie a configuração
Para determinar o URL de base para o qual as chamadas REST serão feitas, o Cliente REST usa a configuração de application.properties. O nome da propriedade precisa seguir uma determinada convenção que é melhor exibida no código a seguir:
# 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=javax.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 que org.acme.rest.client.ExtensionsService deve corresponder ao nome totalmente qualificado da interface ExtensionsService que criamos na seção anterior.
|
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=javax.inject.Singleton
Desabilitando a Verificação do Nome do Host
Para desabilitar a verificação do nome do host SSL para um cliente REST específico, adicione a seguinte propriedade à sua configuração:
quarkus.rest-client.extensions-api.verify-host=false
|
Esta definição não deve ser utilizada em produção, uma vez que irá desativar a verificação do nome do host SSL. |
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 JAX-RS resource
Crie o arquivo src/main/java/org/acme/rest/client/ExtensionsResource.java com o seguinte conteúdo:
package org.acme.rest.client;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.resteasy.annotations.jaxrs.PathParam;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.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.
Atualize o teste
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"));
}
}
O código acima utiliza as capacidades json-path do REST Assured.
Suporte Assíncrono
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 javax.ws.rs.GET;
import javax.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);
}
Abra o arquivo src/main/java/org/acme/rest/client/ExtensionsResource.java e atualize-o com o seguinte conteúdo:
package org.acme.rest.client;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.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);
}
}
Para testar métodos assíncronos, adicione o método de teste abaixo em 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"));
}
A versão Uni é muito semelhante:
package org.acme.rest.client;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import javax.ws.rs.GET;
import javax.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);
}
O ExtensionsResource torna-se:
package org.acme.rest.client;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.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
O código anterior usa tipos reativos do Mutiny. Se você não estiver familiarizado com o Mutiny, consulte Mutiny - uma biblioteca de programação reativa intuitiva . |
Ao retornar um Uni , cada assinatura invoca o serviço remoto. Isso significa que você pode reenviar a requisição assinando novamente o Uni , ou usar um retry da seguinte forma:
@Inject @RestClient ExtensionsService extensionsService;
// ...
extensionsService.getByIdAsUni(id)
.onFailure().retry().atMost(10);
Se você usar um CompletionStage , precisará chamar o método do serviço para tentar novamente. Essa diferença vem do aspecto preguiçoso do Mutiny e de seu protocolo de assinatura. Mais detalhes sobre isso podem ser encontrados na documentação do Mutiny .
Suporte para cabeçalhos personalizados
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 javax.ws.rs.GET;
import javax.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 javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.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;
}
}
Como você vê no exemplo acima, é possível tornar a implementação do ClientHeadersFactory um bean CDI anotando-o com uma anotação de definição de escopo, como @Singleton , @ApplicationScoped , etc.
Fábrica de cabeçalhos padrão
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
Empacote e execute a aplicação
Execute a aplicação com:
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.
Como de costume, a aplicação pode ser empacotada utilizando:
quarkus build
./mvnw install
./gradlew build
E executado com java -jar target/quarkus-app/quarkus-run.jar.
Também é possível gerar o executável nativo com:
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 JAX-RS 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.
Usando um Servidor HTTP Simulado para testes
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>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<scope>test</scope>
<version>${wiremock.version}</version> (1)
</dependency>
| 1 | Use a proper Wiremock version. All available versions can be found here. |
testImplementation("com.github.tomakehurst:wiremock-jre8:$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.Collections;
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 Collections.singletonMap("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 {
}
|
|