はじめに
- 今回は、SpringBootアプリケーションでgRPCを用いたアプリケーションを開発する方法について記事化しようと思います
gRPCとは?
- そもそも、gRPCとは?
- gRPC(gRPC Remote Procedure Call)は、Googleが開発したオープンソースのRPCフレームワークです。HTTP/2をベースとした通信プロトコルで、Protocol Buffers (protobuf) を使用したデータのシリアライズを行います
- REST APIと比較した時のメリットは?
- 高速な通信
- バイナリ形式のデータ転送でJSONより軽量
- HTTP/2の仕組みにより、1つの接続で複数のリクエストを並行処理
- 型安全性
- .protoファイルでスキーマを定義し、コードを自動生成
- コンパイル時に型チェックが行われ、実行時エラーを削減
- 双方向ストリーミング
- 4種類の通信パターンをサポート:
- Unary(単一リクエスト/レスポンス)
- Server streaming(サーバーからのストリーム)
- Client streaming(クライアントからのストリーム)
- Bidirectional streaming(双方向ストリーム)
- 4種類の通信パターンをサポート:
- マイクロサービスに最適
- サービス間の効率的な通信
- 言語に依存しない設計(例:JavaのサービスがGoのサービスを呼び出せる)
- デッドライン、キャンセル、タイムアウトなどの機能を標準サポート
- 高速な通信
- gRPCを採用する要件についてはどうか?
- 超高頻度・大量のサービス間通信があるケース
- 秒間数千〜数万リクエストレベルがありネットワーク帯域やレイテンシがボトルネックになっているケース
- リアルタイムの双方向ストリーミングが必須なケース
- チャット、リアルタイムダッシュボード、ゲームサーバーなどで、かつWebSocketでは実現が難しいケース
- 言語間の強い型安全性が必須なケース
- 超高頻度・大量のサービス間通信があるケース
- 結論、一般的なWebアプリケーションではREST APIで十分ではありそうです...
SpringBootアプリケーションでgRPCを利用する方法
アーキテクチャ
- 今回作成するアプリケーションの概要は下記になります
- 今回はWebFluxアプリケーションとgRPCサーバーの部分を作成します
- Spring MVCを用いても実装可能ですが、今回はSpring WebFluxを使用します
[HTTPクライアント]
↓ HTTP (8080)
[WebFluxアプリケーション](gRPCクライアント)
↓ gRPC (9090)
[gRPCサーバー] ※Springアプリケーション自体は8081portで起動
- また、プロジェクト構成(抜粋)は以下のようになります
spring-webflux-demo/
├── build.gradle # ビルド設定(gRPC依存関係、protobuf設定)
├── src/main/
│ ├── proto/
│ │ └── user.proto # Protocol Buffers定義ファイル
│ ├── resources/
│ │ ├── application.yml # WebFluxアプリ設定(ポート8080)
│ │ └── application-grpcserver.yml # gRPCサーバー設定(ポート8081)
│ └── java/com/example/
│ ├── springwebfluxdemo/ # WebFluxアプリケーション(HTTPサーバー)
│ │ ├── SpringWebfluxDemoApplication.java # メインクラス
│ │ ├── config/
│ │ │ ├── GrpcClientConfig.java # gRPCクライアント設定
│ │ └── service/
│ │ └── RemoteUserService.java # gRPCクライアント呼び出し
│ │ ※※RemoteUserServiceの呼び出し部分(Controller等)の実装は割愛
│ └── grpcserver/ # gRPCサーバーアプリケーション
│ ├── GrpcServerApplication.java # メインクラス
│ ├── config/
│ │ └── GrpcServerConfig.java # gRPCサーバー設定
│ ├── grpc/
│ │ └── UserGrpcService.java # gRPCサービス実装
│ ├── service/
│ │ └── UserService.java # ビジネスロジック
│ └── model/
│ └── User.java
└── build/generated/source/proto/main/ # Protocol Buffersから自動生成されるコード
├── java/ # protobufメッセージクラス
├── grpc/ # 標準gRPCスタブ
└── reactor/ # Reactor対応gRPCスタブ
依存関係の追加
-
https://github.com/grpc/grpc-java の記載に従い、
build.gradleに以下の依存関係を追加します
implementation "io.grpc:grpc-netty-shaded:${grpcVersion}"
implementation "io.grpc:grpc-protobuf:${grpcVersion}"
implementation "io.grpc:grpc-stub:${grpcVersion}"
- Protocol Bufferのユーティリティ機能を提供するライブラリ。Spring WebFluxでREST APIとgRPCを併用する等の要件がある場合に追加します
implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}"
- Spring WebFluxを用いる場合は以下の依存関係も追加します(参考:https://github.com/salesforce/reactive-grpc)
- ※ストリーミングや並列処理を行いたい場合は、Spring WebFluxを用いたほうがいいですが、そのような要件がないアプリケーションであればSpring MVCで十分です
implementation "com.salesforce.servicelibs:reactor-grpc-stub:${reactorGrpcVersion}"
- また、Protocol Buffers(protobuf)形式の定義からgRPC通信を行うコードを生成するため、以下の記載を追加します
- ※こちらも上述のgrpc-javaのGitHubページに記載があります
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protobufVersion}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
// Reactor対応スタブ(ReactorUserServiceGrpc)を追加で生成するための記載
reactor {
artifact = "com.salesforce.servicelibs:reactor-grpc:${reactorGrpcVersion}"
}
}
generateProtoTasks {
all().each { task ->
task.plugins {
grpc {}
reactor {}
}
}
}
}
protoの実装
- Protocol Buffersのスキーマ定義ファイルである.protoファイルを作成します
- serviceでgRPCのAPI仕様を作成し、messageでデータ構造(型)を定義します
- Protocol Buffersにおけるid = 1の1という数字は「フィールド番号(field number)」と呼ばれ、Protocol Buffersはフィールド名ではなくこのフィールド番号を使用してデータをシリアライズ/デシリアライズします。これにより、JSONなどのテキストベースのフォーマットと比べて、より効率的なデータ転送を可能にしています
// .protoファイルはクライアントとサーバーで共有する
syntax = "proto3";
// パッケージ名の定義
package com.example.grpc.service;
option java_multiple_files = true; // 生成されるJavaファイルを分割(推奨)
option java_package = "com.example.grpc.service"; // 生成されるJavaコードのパッケージ名(推奨)
// ユーザーサービス定義
service UserService {
// ユーザー情報を取得するRPC
rpc GetUser (GetUserRequest) returns (GetUserResponse);
// 新しいユーザーを作成するRPC
rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
// 全ユーザー情報をストリーミング形式で取得するRPC
rpc GetAllUsers (GetAllUsersRequest) returns (stream GetAllUsersResponse);
}
// ユーザー情報の基本構造
message User {
string id = 1; // ユーザーID
string name = 2; // ユーザー名
int32 age = 3; // 年齢
}
message GetUserRequest {
string id = 1;
}
message GetUserResponse {
User user = 1;
}
message CreateUserRequest {
string name = 1;
int32 age = 2;
}
message CreateUserResponse {
User user = 1;
}
message GetAllUsersRequest {
int32 limit = 1;
}
message GetAllUsersResponse {
User user = 1;
}
gRPCサーバーの実装
- GrpcServerConfig
- gRPCサーバーの設定および起動を行います
- REST APIでは意識しない部分(Spring Bootが自動でやってくれる)ですが、gRPCでは明示的に記述する必要があります
package com.example.grpcserver.config;
import java.io.IOException;
import java.net.InetSocketAddress;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.grpcserver.grpc.UserGrpcService;
import io.grpc.Server;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
@Configuration
public class GrpcServerConfig {
@Value("${grpc.server.port:9090}")
private int grpcPort;
@Bean(destroyMethod = "shutdown")
public Server grpcServer(UserGrpcService userGrpcService) throws IOException {
Server server = NettyServerBuilder.forAddress(new InetSocketAddress("0.0.0.0", grpcPort))
.addService(userGrpcService) // UserGrpcServiceに実装されたRPCメソッドをサーバーに登録
.build();
server.start(); // gRPCサーバーを起動
return server;
}
}
- UserGrpcService.java
- protoでファイルから自動生成されたserviceクラスの抽象クラスを継承し、各serviceメソッドを実装します
- レスポンスデータを、protoファイルから自動生成されたgRPCのレスポンス形式に変換します
- ※使用しているリクエスト/レスポンスクラスは全てprotoから自動生成されたクラス
- ここでは、UserGrpcService内でUserServiceをDIして、UserService内でビジネスロジックを実装しています。つまりUserGrpcServiceは、REST APIにおけるControllerと同様のレイヤー(プレゼンテーション層)に位置すると言えるでしょう
package com.example.grpcserver.grpc;
import com.example.grpc.service.*;
import com.example.grpcserver.model.User;
import com.example.grpcserver.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
@Slf4j
@Component
@RequiredArgsConstructor
public class UserGrpcService extends ReactorUserServiceGrpc.UserServiceImplBase {
private final UserService userService;
private static final long TIMEOUT_MILLIS = 5000L;
@Override
public Mono<GetUserResponse> getUser(Mono<GetUserRequest> request) {
return request.flatMap(req -> userService.getUserById(req.getId())
.map(this::mapToGrpcResponse))
.timeout(Duration.ofMillis(TIMEOUT_MILLIS))
.doOnError(error -> log.error("ユーザー取得エラー: {}", error.getMessage()))
.doOnSuccess(response -> log.info("ユーザー取得成功: {}", response.getUser().getId()));
}
@Override
public Mono<CreateUserResponse> createUser(Mono<CreateUserRequest> request) {
return request.flatMap(req -> {
User newUser = new User();
newUser.setName(req.getName());
newUser.setAge(req.getAge());
return userService.createUser(newUser)
.map(this::mapToGrpcCreateResponse);
})
.timeout(Duration.ofMillis(TIMEOUT_MILLIS))
.doOnError(error -> log.error("ユーザー作成エラー: {}", error.getMessage()))
.doOnSuccess(response -> log.info("ユーザー作成成功: {}", response.getUser().getId()));
}
@Override
public Flux<GetAllUsersResponse> getAllUsers(Mono<GetAllUsersRequest> request) {
return request.flatMapMany(req -> userService.getAllUsers(req.getLimit())
.map(this::mapToGrpcStreamResponse))
.timeout(Duration.ofMillis(TIMEOUT_MILLIS))
.doOnError(error -> log.error("全ユーザー取得エラー: {}", error.getMessage()));
}
private GetUserResponse mapToGrpcResponse(User user) {
return GetUserResponse.newBuilder()
.setUser(mapUserToGrpc(user))
.build();
}
private CreateUserResponse mapToGrpcCreateResponse(User user) {
return CreateUserResponse.newBuilder()
.setUser(mapUserToGrpc(user))
.build();
}
private GetAllUsersResponse mapToGrpcStreamResponse(User user) {
return GetAllUsersResponse.newBuilder()
.setUser(mapUserToGrpc(user))
.build();
}
private com.example.grpc.service.User mapUserToGrpc(User user) {
return com.example.grpc.service.User.newBuilder()
.setId(user.getId())
.setName(user.getName())
.setAge(user.getAge())
.build();
}
}
gRPCクライアントの実装
- GrpcClientConfig.java
- userServiceChannelのBean定義を行い、gRPCサーバーへの接続チャネルを定義しています。今回はデモのため通信方式は平文を指定しています
- userServiceStubのBean定義を行い、gRPCサーバーのメソッドを呼び出すクライアントを定義しています
- 今回はWebfluxを用いているため
ReactorUserServiceGrpc.ReactorUserServiceStubを用いていますが、Spring MVCの場合は、protoファイルから自動生成されるUserServiceGrpc.UserServiceBlockingStub(同期処理・ブロッキング用),UserServiceGrpc.UserServiceStub(非同期処理・コールバック用)のいずれかを用いれば良いです
- 今回はWebfluxを用いているため
package com.example.springwebfluxdemo.config;
import com.example.grpc.service.ReactorUserServiceGrpc;
import io.grpc.ManagedChannel;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GrpcClientConfig {
@Value("${grpc.client.user-service.host:localhost}")
private String userServiceHost;
@Value("${grpc.client.user-service.port:9090}")
private int userServicePort;
@Bean
public ManagedChannel userServiceChannel() {
return NettyChannelBuilder
.forAddress(userServiceHost, userServicePort)
.usePlaintext() // 本番環境ではTLSを使用すること
.build();
}
@Bean
public ReactorUserServiceGrpc.ReactorUserServiceStub userServiceStub(ManagedChannel userServiceChannel) {
return ReactorUserServiceGrpc.newReactorStub(userServiceChannel);
}
}
- RemoteUserService.java
- protoでファイルから自動生成されたserviceクラスのStub(
ReactorUserServiceGrpc.ReactorUserServiceStub)を利用し、各serviceメソッドを呼び出します - リクエストデータを、protoファイルから自動生成されたgRPCのリクエスト形式に変換します
- protoでファイルから自動生成されたserviceクラスのStub(
package com.example.springwebfluxdemo.service;
import com.example.grpc.service.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
@Service
@RequiredArgsConstructor
@Slf4j
public class RemoteUserService {
private final ReactorUserServiceGrpc.ReactorUserServiceStub userServiceStub;
private static final Duration TIMEOUT = Duration.ofSeconds(5);
public Mono<User> getUserById(String userId) {
return userServiceStub.getUser(
GetUserRequest.newBuilder()
.setId(userId)
.build())
.map(GetUserResponse::getUser)
.timeout(TIMEOUT)
.doOnError(error -> log.error("リモートユーザー取得エラー: {}", error.getMessage()))
.onErrorResume(error -> {
log.info("ユーザー取得エラーを処理: {}", error.getMessage());
// 空のユーザーを返す代わりにエラーを伝播
return Mono.error(error);
})
.doOnSuccess(user -> log.info("リモートユーザー取得成功: {}", user.getId()));
}
public Mono<User> createUser(String name, int age) {
return userServiceStub.createUser(
CreateUserRequest.newBuilder()
.setName(name)
.setAge(age)
.build())
.map(CreateUserResponse::getUser)
.timeout(TIMEOUT)
.doOnError(error -> log.error("リモートユーザー作成エラー: {}", error.getMessage()))
.doOnSuccess(user -> log.info("リモートユーザー作成成功: {}", user.getId()));
}
public Flux<User> getAllUsers(int limit) {
return userServiceStub.getAllUsers(
GetAllUsersRequest.newBuilder()
.setLimit(limit)
.build())
.map(GetAllUsersResponse::getUser)
.timeout(TIMEOUT)
.doOnError(error -> log.error("リモートユーザー一覧取得エラー: {}", error.getMessage()));
}
}