gRPC サービスの使用
gRPCクライアントは、アプリケーションコードにインジェクトすることができます。
gRPC サービスを消費するには、gRPC クラスを生成する必要があります。 proto ファイルを src/main/proto に置き、 mvn compile を実行してください。
|
スタブとインジェクション
gRPCの生成には、いくつかのスタブが用意されており、gRPCサービスを使用するためのさまざまな方法を提供しています。下記をインジェクトすることができます。
-
Mutiny APIを使用したサービスインターフェース
-
gRPC APIを使用したブロッキングスタブ
-
Mutinyをベースにしたリアクティブ・スタブ
-
他のタイプのスタブを作成することができるgRPC
io.grpc.Channel
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 | gRPCクライアントのインジェクションポイントには、 @GrpcClient の修飾子を付ける必要があります。この修飾子は、基礎となるgRPCクライアントの設定に使用される名前を指定するために使用できます。たとえば、この修飾子を hello-service に設定すると、サービスのホストの設定は、quarkus.grpc.clients.hello-service.host を使用して行われるようになります。 |
2 | GrpcClient#value() で名前が指定されていない場合は、代わりにフィールド名が使用されます。例えば、この例では helloService です。 |
スタブクラス名は、 proto
ファイルで使用されているサービス名に由来します。例えば、 Greeter
をサービス名として使用している場合は以下のようになります。
option java_package = "hello";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
サービスインターフェース名は hello.Greeter
、Mutiny stub名は hello.MutinyGreeterGrpc.MutinyGreeterStub
、Blocking stub名は hello.GreeterGrpc.GreeterBlockingStub
となります。
例
サービスインターフェース
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 | サービス名はインジェクションポイントに由来する - フィールド名を使用しています。 quarkus.grpc.clients.hello.host プロパティの設定が必要です。 |
ブロッキングスタブ
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 | quarkus.grpc.clients.hello.host プロパティを設定する必要があります。 |
ストリームの取り扱い
gRPCでは、ストリームの送受信が可能です。
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
}
Mutinyのスタブを使用すると、以下のようにこれらとやりとりすることができます。
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);
}
}
クライアント設定
アプリケーションにインジェクトする各gRPCサービスに対して、以下の属性を設定することができます。
ビルド時に固定される構成プロパティ - 他のすべての構成プロパティは実行時にオーバーライド可能
Configuration property |
タイプ |
デフォルト |
---|---|---|
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 |
ブーリアン |
|
client-name
は、 @GrpcClient
で設定された名称、または明示的に定義されていない場合は、インジェクションポイントから派生した名称です。
以下の例では、クライアント名として _hello_を使用しています。 @GrpcClient
アノテーションで使用した名前に置き換えることを忘れないでください。
quarkus.grpc.clients."client-name".xds.enabled を有効にすると、上記の設定のほとんどを処理するのはxDSになります。
|
TLSを有効にする
TLS を有効にするには、以下の設定を使用します。構成内のすべてのパスは、クラスパス上のリソース (通常は src/main/resources
またはそのサブフォルダーから) または外部ファイルのいずれかを指定することに注意してください。
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
SSL/TLSを設定すると、 plain-text は自動的に無効になります。
|
相互認証付きTLS
相互認証付きのTLSを使用するには、以下の設定を使用します。
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
クライアント・スタブ・デッドライン
gRPCスタブにデッドラインを設定する必要がある場合、つまり、以後、スタブが常にステータスエラー DEADLINE_EXCEEDED
を返すようになる期間を指定する必要がある場合、デッドラインは、 quarkus.grpc.clients."service-name".deadline
の設定プロパティで指定することができます。例:
quarkus.grpc.clients.hello.host=localhost
quarkus.grpc.clients.hello.deadline=2s (1)
1 | すべての注入されたスタブのデッドラインを設定します。 |
この機能を使用して、RPC タイムアウトを実装しないでください。RPC タイムアウトを実装するには、Mutiny call.ifNoItem().after(…) または Fault Tolerance @Timeout のいずれかを使用してください。
|
gRPCヘッダー
HTTPと同様に、gRPCの呼び出しはメッセージと一緒にヘッダを運ぶことができます。 ヘッダは、例えば、認証のために有用です。
gRPCコールにヘッダを設定するには、ヘッダを付加したクライアントを作成し、このクライアントで呼び出しを実行します。
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 | extraHeaders を付加して呼び出しができるようにクライアントを変更します。 |
2 | 変更したクライアントで呼び出しを行います。オリジナルのクライアントは変更されないままです。 |
GrpcClientUtils
は、あらゆる種類のクライアントに対応しています。
クライアントインターセプター
gRPCクライアントインターセプターは、 io.grpc.ClientInterceptor
インターフェースも実装する CDI Bean で実装できます。インジェクトされたクライアントに @io.quarkus.grpc.RegisterClientInterceptor
のアノテーションを付加することで、特定のクライアントインスタンスに対して指定されたインターセプターを登録できます。 @RegisterClientInterceptor
のアノテーションは繰り返し使用できます。また、注入されたクライアントにインターセプターを適用したい場合は、インターセプターBeanに @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 | このインターセプターは、インジェクトされたすべてのgRPCクライアントに適用されます。 |
プロデューサー・メソッドをグローバル・インターセプターとしてアノテーションすることも可能です:
import io.quarkus.grpc.GlobalInterceptor;
import jakarta.enterprise.inject.Produces;
public class MyProducer {
@GlobalInterceptor
@Produces
public MyInterceptor myInterceptor() {
return new MyInterceptor();
}
}
ClientInterceptor JavaDoc をチェックして、インターセプターを適切に実装してください。 |
@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 | この特定のクライアントのために MySpecialInterceptor を登録します。 |
複数のクライアントインターセプターがある場合、 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;
}
}
最高の優先度を持つインターセプターが最初に呼び出されます。インターセプターが Prioritized
インターフェイスを実装していない場合に使用されるデフォルトの優先度は 0
です。
gRPCクライアントのメトリクス
メトリクス収集の有効化
gRPC クライアント・メトリクスは、アプリケーションもエクステンションを使用する場合に自動的に有効になります。 quarkus-micrometer
エクステンションを使用している場合、gRPC クライアント・メトリクスは自動的に有効になります。micrometerは、アプリケーションが使用するすべてのgRPCクライアントのメトリクスを収集します。
例えば、メトリクスをPrometheusにエクスポートすると、以下のように取得できます。
# 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
サービス名、メソッド、タイプは tags で確認できます。
カスタム例外処理
gRPCサービスやサーバーインターセプターが(カスタム)例外を投げる場合、アプリケーションのCDI Beanとして独自の ExceptionHandlerProvider を追加し、これらの例外のカスタムハンドリングを提供することができます。
例
@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モード
デフォルトでは、devモードでアプリケーションを起動すると、サービスが設定されていない場合でもgRPCサーバが起動します。 gRPCエクステンションのDevモードの動作は、以下のプロパティで設定することができます。
ビルド時に固定される設定プロパティ - その他の設定プロパティは実行時にオーバーライド可能です。
Configuration property |
タイプ |
デフォルト |
---|---|---|
Start gRPC server in dev mode even if no gRPC services are implemented. By default set to Environment variable: Show more |
ブーリアン |
|
モッククライアントの注入
@QuarkusTest
では、 @InjectMock
を使用して、gRPC サービスの Mutiny クライアントを注入できます:
@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");
}
}
モック できるのはMutinyクライアントのみで、チャンネルや、その他のスタブはモックできません。 |