Consuming a gRPC Service
gRPC clients can be injected in your application code.
Consuming gRPC services requires the gRPC classes to be generated. Place
your proto files in src/main/proto and run mvn compile .
|
Stubs and Injection
gRPC generation provides several stubs, providing different ways to consume a gRPC service. You can inject:
-
a service interface using the Mutiny API,
-
a blocking stub using the gRPC API,
-
a reactive stub based on Mutiny,
-
the gRPC
io.grpc.Channel
, that lets you create other types of 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 | A gRPC client injection point must be annotated with the @GrpcClient
qualifier. This qualifier can be used to specify the name that is used to
configure the underlying gRPC client. For example, if you set it to
hello-service , configuring the host of the service is done using the
quarkus.grpc.clients.hello-service.host . |
2 | If the name is not specified via the GrpcClient#value() then the field
name is used instead, e.g. helloService in this particular case. |
The stub class names are derived from the service name used in your proto
file. For example, if you use Greeter
as a service name as in:
option java_package = "hello";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
Then the service interface name is: hello.Greeter
, the Mutiny stub name
is: hello.MutinyGreeterGrpc.MutinyGreeterStub
and the blocking stub name
is: hello.GreeterGrpc.GreeterBlockingStub
.
Examples
Service Interface
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 | The service name is derived from the injection point - the field name is
used. The quarkus.grpc.clients.hello.host property must be set. |
Blocking Stub
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 | The quarkus.grpc.clients.hello.host property must be set. |
Handling streams
gRPC allows sending and receiving streams:
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
}
Using the Mutiny stub, you can interact with these as follows:
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);
}
}
Client configuration
For each gRPC service you inject in your application, you can configure the following attributes:
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Type |
Default |
|
---|---|---|
Use new Vert.x gRPC client support. By default, we still use previous Java gRPC support. Environment variable: Show more |
boolean |
|
Explicitly enable use of XDS. Environment variable: Show more |
boolean |
|
Use secure credentials. Environment variable: Show more |
boolean |
|
Optional explicit target. Environment variable: Show more |
string |
|
Explicitly enable use of in-process. Environment variable: Show more |
boolean |
|
Set in-process name. Environment variable: Show more |
string |
|
Number of threads on a delayed gRPC ClientCall Environment variable: Show more |
int |
|
Deadline in milliseconds of delayed gRPC call Environment variable: Show more |
long |
|
Number of retries on a gRPC ClientCall Environment variable: Show more |
int |
|
Initial delay in seconds on refresh check Environment variable: Show more |
long |
|
Refresh period in seconds Environment variable: Show more |
long |
|
The gRPC service port. Environment variable: Show more |
int |
|
The gRPC service test port. Environment variable: Show more |
int |
|
The host name / IP on which the service is exposed. Environment variable: Show more |
string |
|
The classpath path or file path to a server certificate or certificate chain in PEM format. Environment variable: Show more |
path |
|
The classpath path or file path to the corresponding certificate private key file in PEM format. Environment variable: Show more |
path |
|
An optional trust store which holds the certificate information of the certificates to trust The trust store can be either on classpath or in an external file. Environment variable: Show more |
path |
|
Use a name resolver. Defaults to dns. If set to "stork", host will be treated as SmallRye Stork service name Environment variable: Show more |
string |
|
Whether Environment variable: Show more |
boolean |
|
The duration after which a keep alive ping is sent. Environment variable: Show more |
||
The flow control window in bytes. Default is 1MiB. Environment variable: Show more |
int |
|
The duration without ongoing RPCs before going to idle mode. Environment variable: Show more |
||
The amount of time the sender of a keep alive ping waits for an acknowledgement. Environment variable: Show more |
||
Whether keep-alive will be performed when there are no outstanding RPC on a connection. Environment variable: Show more |
boolean |
|
The max number of hedged attempts. Environment variable: Show more |
int |
|
The max number of retry attempts. Retry must be explicitly enabled. Environment variable: Show more |
int |
|
The maximum number of channel trace events to keep in the tracer for each channel or sub-channel. Environment variable: Show more |
int |
|
The maximum message size allowed for a single gRPC frame (in bytes). Default is 4 MiB. Environment variable: Show more |
int |
|
The maximum size of metadata allowed to be received (in bytes). Default is 8192B. Environment variable: Show more |
int |
|
The negotiation type for the HTTP/2 connection. Accepted values are: Environment variable: Show more |
string |
|
Overrides the authority used with TLS and HTTP virtual hosting. Environment variable: Show more |
string |
|
The per RPC buffer limit in bytes used for retry. Environment variable: Show more |
long |
|
Whether retry is enabled. Note that retry is disabled by default. Environment variable: Show more |
boolean |
|
The retry buffer size in bytes. Environment variable: Show more |
long |
|
Use a custom user-agent. Environment variable: Show more |
string |
|
Use a custom load balancing policy. Accepted values are: Environment variable: Show more |
string |
|
The compression to use for each call. The accepted values are Environment variable: Show more |
string |
|
The deadline used for each call. Environment variable: Show more |
About the Duration format
To write duration values, use the standard You can also use a simplified format, starting with a number:
In other cases, the simplified format is translated to the
|
The client-name
is the name set in the @GrpcClient
or derived from the
injection point if not explicitly defined.
The following examples uses hello as the client name. Don’t forget to
replace it with the name you used in the @GrpcClient
annotation.
When you enable
quarkus.grpc.clients."client-name".use-quarkus-grpc-client , you are then
using the new Vert.x gRPC channel implementation, so not all configuration
properties can still be applied. And currently there is no Stork support
yet.
|
When you enable quarkus.grpc.clients."client-name".xds.enabled , it’s the
xDS that should handle most of the configuration above.
|
Enabling TLS
To enable TLS, use the following configuration. Note that all paths in the
configuration may either specify a resource on the classpath (typically from
src/main/resources
or its subfolder) or an external file.
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
When SSL/TLS is configured, plain-text is automatically disabled.
|
TLS with Mutual Auth
To use TLS with mutual authentication, use the following configuration:
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
Client Stub Deadlines
If you need to configure a deadline for a gRPC stub, i.e. to specify a
duration of time after which the stub will always return the status error
DEADLINE_EXCEEDED
. You can specify the deadline via the
quarkus.grpc.clients."service-name".deadline
configuration property, e.g.:
quarkus.grpc.clients.hello.host=localhost
quarkus.grpc.clients.hello.deadline=2s (1)
1 | Set the deadline for all injected stubs. |
Do not use this feature to implement an RPC timeout. To implement an RPC
timeout, either use Mutiny call.ifNoItem().after(…) or Fault Tolerance
@Timeout .
|
gRPC Headers
Similarly to HTTP, alongside the message, gRPC calls can carry headers. Headers can be useful e.g. for authentication.
To set headers for a gRPC call, create a client with headers attached and then perform the call on this client:
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 | Alter the client to make calls with the extraHeaders attached |
2 | Perform the call with the altered client. The original client remains unmodified |
GrpcClientUtils
work with all flavors of clients.
Client Interceptors
A gRPC client interceptor can be implemented by a CDI bean that also
implements the io.grpc.ClientInterceptor
interface. You can annotate an
injected client with @io.quarkus.grpc.RegisterClientInterceptor
to
register the specified interceptor for the particular client instance. The
@RegisterClientInterceptor
annotation is repeatable. Alternatively, if
you want to apply the interceptor to any injected client then annotate the
interceptor bean with @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 | This interceptor is applied to all injected gRPC clients. |
It’s also possible to annotate a producer method as a global interceptor:
import io.quarkus.grpc.GlobalInterceptor;
import jakarta.enterprise.inject.Produces;
public class MyProducer {
@GlobalInterceptor
@Produces
public MyInterceptor myInterceptor() {
return new MyInterceptor();
}
}
Check the ClientInterceptor JavaDoc to properly implement your interceptor. |
@RegisterClientInterceptor
Exampleimport io.quarkus.grpc.GrpcClient;
import io.quarkus.grpc.RegisterClientInterceptor;
import hello.Greeter;
@ApplicationScoped
class MyBean {
@RegisterClientInterceptor(MySpecialInterceptor.class) (1)
@GrpcClient("helloService")
Greeter greeter;
}
1 | Registers the MySpecialInterceptor for this particular client. |
When you have multiple client interceptors, you can order them by
implementing the jakarta.enterprise.inject.spi.Prioritized
interface:
@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;
}
}
Interceptors with the highest priority are called first. The default
priority, used if the interceptor does not implement the Prioritized
interface, is 0
.
gRPC Client metrics
Enabling metrics collection
gRPC client metrics are automatically enabled when the application also uses
the quarkus-micrometer
extension.
Micrometer collects the metrics of all the gRPC clients used by the
application.
As an example, if you export the metrics to Prometheus, you will get:
# 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
The service name, method and type can be found in the tags.
Custom exception handling
If any of the gRPC services or server interceptors throw an (custom) exception, you can add your own ExceptionHandlerProvider as a CDI bean in your application, to provide a custom handling of those exceptions.
e.g.
@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);
}
}
}
Dev Mode
By default, when starting the application in dev mode, a gRPC server is started, even if no services are configured. You can configure the gRPC extension’s dev mode behavior using the following properties.
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Type |
Default |
|
---|---|---|
Start gRPC server in dev mode even if no gRPC services are implemented. By default set to Environment variable: Show more |
boolean |
|
Inject mock clients
In your @QuarkusTest
, you can use @InjectMock
to inject the Mutiny
client of a gRPC service:
@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");
}
}
Only the Mutiny client can be mocked, channels, and other stubs cannot be mocked. |