Consumindo um serviço gRPC
Os clientes gRPC podem ser injectados no código da sua aplicação.
Consumir serviços gRPC requer que as classes gRPC sejam geradas. Coloque seus arquivos proto em src/main/proto e execute mvn compile .
|
Stubs e Injeção
A geração do gRPC fornece vários stubs, oferecendo diferentes maneiras de consumir um serviço gRPC. Você pode injetar:
-
uma interface de serviço utilizando a API Mutiny,
-
um stub bloqueante utilizando a API gRPC,
-
um stub reativo baseado no Mutiny,
-
o
io.grpc.Channel
do gRPC, que permite criar outros tipos de stubs.
import io.quarkus.grpc.GrpcClient;
import hello.Greeter;
import hello.GreeterGrpc.GreeterBlockingStub;
import hello.MutinyGreeterGrpc.MutinyGreeterStub;
class MyBean {
// A service interface using the Mutiny API
@GrpcClient("helloService") (1)
Greeter greeter;
// A reactive stub based on Mutiny
@GrpcClient("helloService")
MutinyGreeterGrpc.MutinyGreeterStub mutiny;
// A blocking stub using the gRPC API
@GrpcClient
GreeterGrpc.GreeterBlockingStub helloService; (2)
@GrpcClient("hello-service")
Channel channel;
}
1 | O ponto de injeção do cliente gRPC deve ser anotado com o qualificador @GrpcClient . Este qualificador pode ser usado para especificar o nome que é utilizado para configurar o cliente gRPC subjacente. Por exemplo, se você defini-lo como hello-service , a configuração do host do serviço é feita usando o qualificador quarkus.grpc.clients. hello-service.host . |
2 | Se o nome não for especificado através do endereço GrpcClient#value() , então o nome do campo é usado em seu lugar, por exemplo, helloService neste caso específico. |
Os nomes das classes stub são derivados do nome do serviço usado em seu arquivo proto
. Por exemplo, se você usar Greeter
como nome de serviço, como no seguinte caso:
option java_package = "hello";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
Nesse caso, o nome da interface do serviço é: hello.Greeter
, o nome do stub Mutiny é: hello.MutinyGreeterGrpc.MutinyGreeterStub
e o nome do stub bloqueante é: hello.GreeterGrpc.GreeterBlockingStub
.
Exemplos
Interface de serviço
import io.quarkus.grpc.GrpcClient;
import io.smallrye.mutiny.Uni;
import hello.Greeter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class ExampleResource {
@GrpcClient (1)
Greeter hello;
@GET
@Path("/mutiny/{name}")
public Uni<String> helloMutiny(String name) {
return hello.sayHello(HelloRequest.newBuilder().setName(name).build())
.onItem().transform(HelloReply::getMessage);
}
}
1 | O nome do serviço é derivado do ponto de injeção - o nome do campo é usado. A propriedade quarkus.grpc.clients.hello.host deve ser definida. |
Stub Bloqueante
import io.quarkus.grpc.GrpcClient;
import hello.GreeterGrpc.GreeterBlockingStub;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class ExampleResource {
@GrpcClient("hello") (1)
GreeterGrpc.GreeterBlockingStub blockingHelloService;
@GET
@Path("/blocking/{name}")
public String helloBlocking(String name) {
return blockingHelloService.sayHello(HelloRequest.newBuilder().setName(name).build()).getMessage();
}
}
1 | A propriedade quarkus.grpc.clients.hello.host deve ser configurada. |
Lidando com fluxos
O gRPC permite o envia e recebimento de fluxos:
service Streaming {
rpc Source(Empty) returns (stream Item) {} // Returns a stream
rpc Sink(stream Item) returns (Empty) {} // Reads a stream
rpc Pipe(stream Item) returns (stream Item) {} // Reads a streams and return a streams
}
Utilizando o stub Mutiny, você pode interagir com eles da seguinte forma:
package io.quarkus.grpc.example.streaming;
import io.grpc.examples.streaming.Empty;
import io.grpc.examples.streaming.Item;
import io.grpc.examples.streaming.MutinyStreamingGrpc;
import io.quarkus.grpc.GrpcClient;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/streaming")
@Produces(MediaType.APPLICATION_JSON)
public class StreamingEndpoint {
@GrpcClient
MutinyStreamingGrpc.MutinyStreamingStub streaming;
@GET
public Multi<String> invokeSource() {
// Retrieve a stream
return streaming.source(Empty.newBuilder().build())
.onItem().transform(Item::getValue);
}
@GET
@Path("sink/{max}")
public Uni<Void> invokeSink(int max) {
// Send a stream and wait for completion
Multi<Item> inputs = Multi.createFrom().range(0, max)
.map(i -> Integer.toString(i))
.map(i -> Item.newBuilder().setValue(i).build());
return streaming.sink(inputs).onItem().ignore().andContinueWithNull();
}
@GET
@Path("/{max}")
public Multi<String> invokePipe(int max) {
// Send a stream and retrieve a stream
Multi<Item> inputs = Multi.createFrom().range(0, max)
.map(i -> Integer.toString(i))
.map(i -> Item.newBuilder().setValue(i).build());
return streaming.pipe(inputs).onItem().transform(Item::getValue);
}
}
Configuração do cliente
Para cada serviço gRPC que você injeta em sua aplicação, você pode configurar os seguintes atributos:
Propriedade de Configuração Fixa no Momento da Compilação - Todas as outras propriedades de configuração podem ser sobrepostas em tempo de execução.
Configuration property |
Tipo |
Padrão |
---|---|---|
If set to true, and a Stork load balancer is used, connections with all available service instances will be requested proactively. This means better load balancing at the cost of having multiple active connections. Environment variable: Show more |
boolean |
|
O client-name
é o nome definido no @GrpcClient
ou derivado do ponto de injeção, se não for explicitamente definido.
Os exemplos a seguir usam hello como o nome do cliente. Não se esqueça de substituí-lo pelo nome que você usou na anotação @GrpcClient
.
Quando você habilita quarkus.grpc.clients."client-name".xds.enabled , é o xDS que deve tratar da maior parte da configuração acima.
|
Habilitando o TLS
Para habilitar o TLS, use a seguinte configuração. Lembre-se de que todos os caminhos na configuração podem especificar um recurso no classpath (geralmente em src/main/resources
ou de sua subpasta) ou um arquivo externo.
quarkus.grpc.clients.hello.host=localhost
# either a path to a classpath resource or to a file:
quarkus.grpc.clients.hello.ssl.trust-store=tls/ca.pem
Quando SSL/TLS é configurado, plain-text é automaticamente desativado.
|
TLS com autenticação mútua
Para utilizar o TLS com autenticação mútua, utilize a seguinte configuração:
quarkus.grpc.clients.hello.host=localhost
quarkus.grpc.clients.hello.plain-text=false
# all the following may use either a path to a classpath resource or to a file:
quarkus.grpc.clients.hello.ssl.certificate=tls/client.pem
quarkus.grpc.clients.hello.ssl.key=tls/client.key
quarkus.grpc.clients.hello.ssl.trust-store=tls/ca.pem
Prazos (Deadlines) para o Stub do Cliente
Se o senhor precisar configurar um prazo (deadline) para um stub de gRPC, ou seja, para especificar uma duração de tempo após o qual o stub sempre retornará o erro de status DEADLINE_EXCEEDED
. Você pode especificar o prazo através da propriedade de configuração quarkus.grpc.clients."service-name".deadline
, por exemplo:
quarkus.grpc.clients.hello.host=localhost
quarkus.grpc.clients.hello.deadline=2s (1)
1 | Defina o prazo (deadline) para todos os stubs injetados. |
Não utilize esse recurso para implementar um tempo limite (timeout) de RPC. Para implementar um timeout de RPC, use Mutiny call.ifNoItem().after(…) ou Fault Tolerance @Timeout .
|
Cabeçalhos gRPC
Da mesma forma que no HTTP, chamadas gRPC podem transportar cabeçalhos junto com a mensagem. Cabeçalhos podem ser úteis, por exemplo, para autenticação.
Para definir cabeçalhos para uma chamada gRPC, crie um cliente com cabeçalhos anexados e, em seguida, realize a chamada neste cliente:
import jakarta.enterprise.context.ApplicationScoped;
import examples.Greeter;
import examples.HelloReply;
import examples.HelloRequest;
import io.grpc.Metadata;
import io.quarkus.grpc.GrpcClient;
import io.quarkus.grpc.GrpcClientUtils;
import io.smallrye.mutiny.Uni;
@ApplicationScoped
public class MyService {
@GrpcClient
Greeter client;
public Uni<HelloReply> doTheCall() {
Metadata extraHeaders = new Metadata();
if (headers) {
extraHeaders.put("my-header", "my-interface-value");
}
Greeter alteredClient = GrpcClientUtils.attachHeaders(client, extraHeaders); (1)
return alteredClient.sayHello(HelloRequest.newBuilder().setName(name).build()); (2)
}
}
1 | Alterar o cliente para fazer chamadas com os extraHeaders anexados |
2 | Realize a chamada com o cliente alterado. O cliente original permanece inalterado. |
GrpcClientUtils
funciona com todas as variações de clientes.
Interceptores de clientes
Um interceptor de cliente gRPC pode ser implementado por um bean CDI que também implementa a interface io.grpc.ClientInterceptor
. Você pode anotar um cliente injetado com @io.quarkus.grpc.RegisterClientInterceptor
para registrar o interceptor especificado para a instância específica do cliente. A anotação @RegisterClientInterceptor
pode ser repetida. Como alternativa, se você deseja aplicar o interceptador a qualquer cliente injetado, anote o bean do interceptador com @io.quarkus.grpc.GlobalInterceptor
.
import io.quarkus.grpc.GlobalInterceptor;
import io.grpc.ClientInterceptor;
@GlobalInterceptor (1)
@ApplicationScoped
public class MyInterceptor implements ClientInterceptor {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method,
CallOptions callOptions, Channel next) {
// ...
}
}
1 | Este interceptor é aplicado a todos os clientes gRPC injectados. |
Também é possível anotar um método produtor como um interceptor global:
import io.quarkus.grpc.GlobalInterceptor;
import jakarta.enterprise.inject.Produces;
public class MyProducer {
@GlobalInterceptor
@Produces
public MyInterceptor myInterceptor() {
return new MyInterceptor();
}
}
Consulte o JavaDoc do ClientInterceptor para implementar corretamente o seu interceptor. |
@RegisterClientInterceptor
import io.quarkus.grpc.GrpcClient;
import io.quarkus.grpc.RegisterClientInterceptor;
import hello.Greeter;
@ApplicationScoped
class MyBean {
@RegisterClientInterceptor(MySpecialInterceptor.class) (1)
@GrpcClient("helloService")
Greeter greeter;
}
1 | Regista o MySpecialInterceptor para este cliente específico. |
Quando tem vários interceptadores de clientes, pode ordená-los implementando a interface jakarta.enterprise.inject.spi.Prioritized
:
@ApplicationScoped
public class MyInterceptor implements ClientInterceptor, Prioritized {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method,
CallOptions callOptions, Channel next) {
// ...
}
@Override
public int getPriority() {
return 10;
}
}
Os interceptadores com a maior prioridade são chamados primeiro. A prioridade padrão, usada se o interceptador não implementar a interface Prioritized
, é 0
.
Métricas de Cliente gRPC
Habilitando a coleta de métricas
As métricas do cliente gRPC são habilitadas automaticamente quando a aplicação também utiliza a extensão quarkus-micrometer. O Micrometer coleta as métricas de todos os clientes gRPC usados pelo aplicação.
Como exemplo, se você exportar as métricas para o Prometheus, obterá:
# HELP grpc_client_responses_received_messages_total The total number of responses received
# TYPE grpc_client_responses_received_messages_total counter
grpc_client_responses_received_messages_total{method="SayHello",methodType="UNARY",service="helloworld.Greeter",} 6.0
# HELP grpc_client_requests_sent_messages_total The total number of requests sent
# TYPE grpc_client_requests_sent_messages_total counter
grpc_client_requests_sent_messages_total{method="SayHello",methodType="UNARY",service="helloworld.Greeter",} 6.0
# HELP grpc_client_processing_duration_seconds The total time taken for the client to complete the call, including network delay
# TYPE grpc_client_processing_duration_seconds summary
grpc_client_processing_duration_seconds_count{method="SayHello",methodType="UNARY",service="helloworld.Greeter",statusCode="OK",} 6.0
grpc_client_processing_duration_seconds_sum{method="SayHello",methodType="UNARY",service="helloworld.Greeter",statusCode="OK",} 0.167411625
# HELP grpc_client_processing_duration_seconds_max The total time taken for the client to complete the call, including network delay
# TYPE grpc_client_processing_duration_seconds_max gauge
grpc_client_processing_duration_seconds_max{method="SayHello",methodType="UNARY",service="helloworld.Greeter",statusCode="OK",} 0.136478028
O nome do serviço, o método e o tipo podem ser encontrados nas tags.
Tratamento personalizado de excepções
Se algum dos serviços gRPC ou interceptadores de servidor lançar uma exceção (personalizada), você poderá adicionar seu próprio ExceptionHandlerProvider como um bean CDI em sua aplicação, para fornecer um tratamento personalizado dessas exceções.
por exemplo.
@ApplicationScoped
public class HelloExceptionHandlerProvider implements ExceptionHandlerProvider {
@Override
public <ReqT, RespT> ExceptionHandler<ReqT, RespT> createHandler(ServerCall.Listener<ReqT> listener,
ServerCall<ReqT, RespT> serverCall, Metadata metadata) {
return new HelloExceptionHandler<>(listener, serverCall, metadata);
}
@Override
public Throwable transform(Throwable t) {
if (t instanceof HelloException he) {
return new StatusRuntimeException(Status.ABORTED.withDescription(he.getName()));
} else {
return ExceptionHandlerProvider.toStatusException(t, true);
}
}
private static class HelloExceptionHandler<A, B> extends ExceptionHandler<A, B> {
public HelloExceptionHandler(ServerCall.Listener<A> listener, ServerCall<A, B> call, Metadata metadata) {
super(listener, call, metadata);
}
@Override
protected void handleException(Throwable t, ServerCall<A, B> call, Metadata metadata) {
StatusRuntimeException sre = (StatusRuntimeException) ExceptionHandlerProvider.toStatusException(t, true);
Metadata trailers = sre.getTrailers() != null ? sre.getTrailers() : metadata;
call.close(sre.getStatus(), trailers);
}
}
}
Modo de desenvolvimento
Por padrão, ao iniciar a aplicação no modo de desenvolvimento, um servidor gRPC é iniciado, mesmo que nenhum serviço esteja configurado. Você pode configurar o comportamento do modo de desenvolvimento da extensão gRPC usando as seguintes propriedades.
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Configuration property |
Tipo |
Padrão |
---|---|---|
Start gRPC server in dev mode even if no gRPC services are implemented. By default set to Environment variable: Show more |
boolean |
|
Injetar clientes falsos
No seu @QuarkusTest
, você pode utilizar @InjectMock
para injetar o cliente Mutiny de um serviço gRPC:
@QuarkusTest
public class GrpcMockTest {
@InjectMock
@GrpcClient("hello")
Greeter greeter;
@Test
void test1() {
HelloRequest request = HelloRequest.newBuilder().setName("neo").build();
Mockito.when(greeter.sayHello(Mockito.any(HelloRequest.class)))
.thenReturn(Uni.createFrom().item(HelloReply.newBuilder().setMessage("hello neo").build()));
Assertions.assertEquals(greeter.sayHello(request).await().indefinitely().getMessage(), "hello neo");
}
}
Apenas o cliente Mutiny pode ser mocked (simulado), canais e outros stubs não podem ser simulados. |