テクノロジー 開発 非公開: ゼロからの Java gRPC

ゼロからの Java gRPC

Java で gRPC を実装する方法を見てみましょう。

gRPC (Google Remote Procedure Call): gRPC は、マイクロサービス間の高速通信を可能にするために Google によって開発されたオープンソース RPC アーキテクチャです。 gRPC を使用すると、開発者はさまざまな言語で記述されたサービスを統合できます。 gRPC は、構造化データをシリアル化するための高効率で高度にパックされたメッセージング形式である Protobuf メッセージング形式 (プロトコル バッファー) を使用します。

ユースケースによっては、gRPC API の方が REST API よりも効率的である場合があります。

gRPC 上にサーバーを書いてみましょう。まず、サービスとモデル (DTO) を記述するいくつかの .proto ファイルを作成する必要があります。単純なサーバーの場合は、ProfileService と ProfileDescriptor を使用します。

ProfileService は次のようになります。

 syntax = "proto3";
package com.deft.grpc;
import "google/protobuf/empty.proto";
import "profile_descriptor.proto";
service ProfileService {
  rpc GetCurrentProfile (google.protobuf.Empty) returns (ProfileDescriptor) {}
  rpc clientStream (stream ProfileDescriptor) returns (google.protobuf.Empty) {}
  rpc serverStream (google.protobuf.Empty) returns (stream ProfileDescriptor) {}
  rpc biDirectionalStream (stream ProfileDescriptor) returns (stream 	ProfileDescriptor) {}
}

gRPC は、さまざまなクライアント/サーバー通信オプションをサポートしています。それらをすべて分解してみましょう。

  • 通常のサーバー呼び出し – リクエスト/レスポンス。
  • クライアントからサーバーへのストリーミング。
  • サーバーからクライアントへのストリーミング。
  • そしてもちろん、双方向ストリーム。

ProfileService サービスは、インポート セクションで指定された ProfileDescriptor を使用します。

 syntax = "proto3";
package com.deft.grpc;
message ProfileDescriptor {
  int64 profile_id = 1;
  string name = 2;
}
  • int64 は Java の Long です。プロフィールIDを所属させます。
  • 文字列 – Java と同様、これは文字列変数です。

Gradle または Maven を使用してプロジェクトをビルドできます。私にとっては maven を使用する方が便利です。さらにMavenを使用したコードになります。 Gradle の場合、将来の世代の .proto は若干異なるため、ビルド ファイルを別の方法で構成する必要があるため、これは非常に重要です。単純な gRPC サーバーを作成するには、依存関係が 1 つだけ必要です。

 <dependency>
    <groupId>io.github.lognet</groupId>
    <artifactId>grpc-spring-boot-starter</artifactId>
    <version>4.5.4</version>
</dependency>

それは本当に信じられないことです。このスターターは私たちにとって非常に多くの仕事をしてくれます。

作成するプロジェクトは次のようになります。

Spring Boot アプリケーションを開始するには、GrpcServerApplication が必要です。 GrpcProfileService は、 .proto サービスからのメソッドを実装します。 protoc を使用し、書かれた .proto ファイルからクラスを生成するには、protobuf-maven-plugin を pom.xml に追加します。ビルドセクションは次のようになります。

 <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.6.2</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
                    <outputDirectory>${basedir}/target/generated-sources/grpc-java</outputDirectory>
                    <protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.38.0:exe:${os.detected.classifier}</pluginArtifact>
                    <clearOutputDirectory>false</clearOutputDirectory>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
  • protoSourceRoot – .proto ファイルが配置されているディレクトリを指定します。
  • OutputDirectory – ファイルが生成されるディレクトリを選択します。
  • clearOutputDirectory – 生成されたファイルをクリアしないことを示すフラグ。

この段階で、プロジェクトを構築できます。次に、出力ディレクトリで指定したフォルダーに移動する必要があります。生成されたファイルはそこにあります。これで、 GrpcProfileService を 段階的に実装できるようになります。

クラス宣言は次のようになります。

 @GRpcService
public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

GRpcService アノテーション – クラスを grpc-service Bean としてマークします。

サービスを ProfileServiceGrpc ProfileServiceImplBase から継承しているため、親クラスのメソッドをオーバーライドできます。オーバーライドする最初のメソッドは getCurrentProfile です。

    @Override
    public void getCurrentProfile(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {
        System.out.println("getCurrentProfile");
        responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
                .newBuilder()
                .setProfileId(1)
                .setName("test")
                .build());
        responseObserver.onCompleted();
    }

クライアントに応答するには、渡された StreamObserver の onNext メソッドを呼び出す必要があります。応答を送信した後、サーバーが 完了 したことを示すシグナルをクライアントに送信します。 getCurrentProfile サーバーにリクエストを送信すると、応答は次のようになります。

 {
  "profile_id": "1",
  "name": "test"
}

次に、サーバー ストリームを見てみましょう。このメッセージング手法では、クライアントがサーバーにリクエストを送信し、サーバーがメッセージのストリームでクライアントに応答します。たとえば、ループで 5 つのリクエストを送信します。送信が完了すると、サーバーはストリームが正常に完了したことを示すメッセージをクライアントに送信します。

オーバーライドされたサーバー ストリーム メソッドは次のようになります。

 @Override
    public void serverStream(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {
        for (int i = 0; i < 5; i++) {
            responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
                    .newBuilder()
                    .setProfileId(i)
                    .build());
        }
        responseObserver.onCompleted();
    }

したがって、クライアントは、応答番号と同じ ProfileId を持つ 5 つのメッセージを受信します。

 {
  "profile_id": "0",
  "name": ""
}
{
  "profile_id": "1",
  "name": ""
}
…
{
  "profile_id": "4",
  "name": ""
}

クライアント ストリームはサーバー ストリームと非常によく似ています。ここでのみ、クライアントがメッセージのストリームを送信し、サーバーがそれらを処理します。サーバーはメッセージをすぐに処理することも、クライアントからのすべてのリクエストを待ってからメッセージを処理することもできます。

    @Override
    public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> clientStream(StreamObserver<Empty> responseObserver) {
        return new StreamObserver<>() {

            @Override
            public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) {
                log.info("ProfileDescriptor from client. Profile id: {}", profileDescriptor.getProfileId());
            }

            @Override
            public void onError(Throwable throwable) {

            }

            @Override
            public void onCompleted() {
                responseObserver.onCompleted();
            }
        };
    }

クライアント ストリームでは、サーバーがメッセージを受信するクライアントに StreamObserver を返す必要があります。ストリームでエラーが発生した場合、onError メソッドが呼び出されます。たとえば、異常終了した場合などです。

双方向ストリームを実装するには、サーバーとクライアントからのストリームの作成を組み合わせる必要があります。

 @Override
    public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> biDirectionalStream(
            StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {

        return new StreamObserver<>() {
            int pointCount = 0;
            @Override
            public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) {
                log.info("biDirectionalStream, pointCount {}", pointCount);
                responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
                        .newBuilder()
                        .setProfileId(pointCount++)
                        .build());
            }

            @Override
            public void onError(Throwable throwable) {

            }

            @Override
            public void onCompleted() {
                responseObserver.onCompleted();
            }
        };
    } 

この例では、クライアントのメッセージに応答して、サーバーは pointCount を増加させたプロファイルを返します。

結論

gRPC を使用したクライアントとサーバー間のメッセージングの基本オプション (実装されたサーバー ストリーム、クライアント ストリーム、双方向ストリーム) について説明しました。

この記事はセルゲイ・ゴリツィンによって書かれました

ゼロからの Java gRPC
ゼロからの Java gRPC

「ゼロからの Java gRPC」についてわかりやすく解説!絶対に観るべきベスト2動画

Java超入門コース 合併版【Javaの超基本的な部分をたった1時間で学べます】【プログラミング初心者向け入門講座】
【Java入門講座】ゼロから本気で学ぶプログラミング講座。この動画でJavaの基礎全て学べます。

Java で gRPC を実装する方法を見てみましょう。

gRPC (Google Remote Procedure Call): gRPC は、マイクロサービス間の高速通信を可能にするために Google によって開発されたオープンソース RPC アーキテクチャです。 gRPC を使用すると、開発者はさまざまな言語で記述されたサービスを統合できます。 gRPC は、構造化データをシリアル化するための高効率で高度にパックされたメッセージング形式である Protobuf メッセージング形式 (プロトコル バッファー) を使用します。

ユースケースによっては、gRPC API の方が REST API よりも効率的である場合があります。

gRPC 上にサーバーを書いてみましょう。まず、サービスとモデル (DTO) を記述するいくつかの .proto ファイルを作成する必要があります。単純なサーバーの場合は、ProfileService と ProfileDescriptor を使用します。

ProfileService は次のようになります。

 syntax = "proto3";
package com.deft.grpc;
import "google/protobuf/empty.proto";
import "profile_descriptor.proto";
service ProfileService {
  rpc GetCurrentProfile (google.protobuf.Empty) returns (ProfileDescriptor) {}
  rpc clientStream (stream ProfileDescriptor) returns (google.protobuf.Empty) {}
  rpc serverStream (google.protobuf.Empty) returns (stream ProfileDescriptor) {}
  rpc biDirectionalStream (stream ProfileDescriptor) returns (stream 	ProfileDescriptor) {}
}

gRPC は、さまざまなクライアント/サーバー通信オプションをサポートしています。それらをすべて分解してみましょう。

  • 通常のサーバー呼び出し – リクエスト/レスポンス。
  • クライアントからサーバーへのストリーミング。
  • サーバーからクライアントへのストリーミング。
  • そしてもちろん、双方向ストリーム。

ProfileService サービスは、インポート セクションで指定された ProfileDescriptor を使用します。

 syntax = "proto3";
package com.deft.grpc;
message ProfileDescriptor {
  int64 profile_id = 1;
  string name = 2;
}
  • int64 は Java の Long です。プロフィールIDを所属させます。
  • 文字列 – Java と同様、これは文字列変数です。

Gradle または Maven を使用してプロジェクトをビルドできます。私にとっては maven を使用する方が便利です。さらにMavenを使用したコードになります。 Gradle の場合、将来の世代の .proto は若干異なるため、ビルド ファイルを別の方法で構成する必要があるため、これは非常に重要です。単純な gRPC サーバーを作成するには、依存関係が 1 つだけ必要です。

 <dependency>
    <groupId>io.github.lognet</groupId>
    <artifactId>grpc-spring-boot-starter</artifactId>
    <version>4.5.4</version>
</dependency>

それは本当に信じられないことです。このスターターは私たちにとって非常に多くの仕事をしてくれます。

作成するプロジェクトは次のようになります。

Spring Boot アプリケーションを開始するには、GrpcServerApplication が必要です。 GrpcProfileService は、 .proto サービスからのメソッドを実装します。 protoc を使用し、書かれた .proto ファイルからクラスを生成するには、protobuf-maven-plugin を pom.xml に追加します。ビルドセクションは次のようになります。

 <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.6.2</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
                    <outputDirectory>${basedir}/target/generated-sources/grpc-java</outputDirectory>
                    <protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.38.0:exe:${os.detected.classifier}</pluginArtifact>
                    <clearOutputDirectory>false</clearOutputDirectory>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
  • protoSourceRoot – .proto ファイルが配置されているディレクトリを指定します。
  • OutputDirectory – ファイルが生成されるディレクトリを選択します。
  • clearOutputDirectory – 生成されたファイルをクリアしないことを示すフラグ。

この段階で、プロジェクトを構築できます。次に、出力ディレクトリで指定したフォルダーに移動する必要があります。生成されたファイルはそこにあります。これで、 GrpcProfileService を 段階的に実装できるようになります。

クラス宣言は次のようになります。

 @GRpcService
public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

GRpcService アノテーション – クラスを grpc-service Bean としてマークします。

サービスを ProfileServiceGrpc ProfileServiceImplBase から継承しているため、親クラスのメソッドをオーバーライドできます。オーバーライドする最初のメソッドは getCurrentProfile です。

    @Override
    public void getCurrentProfile(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {
        System.out.println("getCurrentProfile");
        responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
                .newBuilder()
                .setProfileId(1)
                .setName("test")
                .build());
        responseObserver.onCompleted();
    }

クライアントに応答するには、渡された StreamObserver の onNext メソッドを呼び出す必要があります。応答を送信した後、サーバーが 完了 したことを示すシグナルをクライアントに送信します。 getCurrentProfile サーバーにリクエストを送信すると、応答は次のようになります。

 {
  "profile_id": "1",
  "name": "test"
}

次に、サーバー ストリームを見てみましょう。このメッセージング手法では、クライアントがサーバーにリクエストを送信し、サーバーがメッセージのストリームでクライアントに応答します。たとえば、ループで 5 つのリクエストを送信します。送信が完了すると、サーバーはストリームが正常に完了したことを示すメッセージをクライアントに送信します。

オーバーライドされたサーバー ストリーム メソッドは次のようになります。

 @Override
    public void serverStream(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {
        for (int i = 0; i < 5; i++) {
            responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
                    .newBuilder()
                    .setProfileId(i)
                    .build());
        }
        responseObserver.onCompleted();
    }

したがって、クライアントは、応答番号と同じ ProfileId を持つ 5 つのメッセージを受信します。

 {
  "profile_id": "0",
  "name": ""
}
{
  "profile_id": "1",
  "name": ""
}
…
{
  "profile_id": "4",
  "name": ""
}

クライアント ストリームはサーバー ストリームと非常によく似ています。ここでのみ、クライアントがメッセージのストリームを送信し、サーバーがそれらを処理します。サーバーはメッセージをすぐに処理することも、クライアントからのすべてのリクエストを待ってからメッセージを処理することもできます。

    @Override
    public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> clientStream(StreamObserver<Empty> responseObserver) {
        return new StreamObserver<>() {

            @Override
            public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) {
                log.info("ProfileDescriptor from client. Profile id: {}", profileDescriptor.getProfileId());
            }

            @Override
            public void onError(Throwable throwable) {

            }

            @Override
            public void onCompleted() {
                responseObserver.onCompleted();
            }
        };
    }

クライアント ストリームでは、サーバーがメッセージを受信するクライアントに StreamObserver を返す必要があります。ストリームでエラーが発生した場合、onError メソッドが呼び出されます。たとえば、異常終了した場合などです。

双方向ストリームを実装するには、サーバーとクライアントからのストリームの作成を組み合わせる必要があります。

 @Override
    public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> biDirectionalStream(
            StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {

        return new StreamObserver<>() {
            int pointCount = 0;
            @Override
            public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) {
                log.info("biDirectionalStream, pointCount {}", pointCount);
                responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
                        .newBuilder()
                        .setProfileId(pointCount++)
                        .build());
            }

            @Override
            public void onError(Throwable throwable) {

            }

            @Override
            public void onCompleted() {
                responseObserver.onCompleted();
            }
        };
    } 

この例では、クライアントのメッセージに応答して、サーバーは pointCount を増加させたプロファイルを返します。

結論

gRPC を使用したクライアントとサーバー間のメッセージングの基本オプション (実装されたサーバー ストリーム、クライアント ストリーム、双方向ストリーム) について説明しました。

この記事はセルゲイ・ゴリツィンによって書かれました

ゼロからの Java gRPC
ゼロからの Java gRPC

「ゼロからの Java gRPC」についてわかりやすく解説!絶対に観るべきベスト2動画

Java超入門コース 合併版【Javaの超基本的な部分をたった1時間で学べます】【プログラミング初心者向け入門講座】
【Java入門講座】ゼロから本気で学ぶプログラミング講座。この動画でJavaの基礎全て学べます。