Abstract
半年ほど前に Spring boot starter for gRPC framework を使う機会があったので、思い出しながらまとめておきます。
まず、gRPCとは、Googleが開発した RPC (Remote Procedure Call)を実現するためのプロトコルの1つです。
以下の特徴があります。
- Protocol Buffers を使ってデータをシリアライズし、高速な通信を実現できる。
- メソッドを呼び出すのと同じ手順で、ネットワーク越しに別のコンピュータ上のプログラムを呼び出せる。
- スキーマ(
.proto
)を定義することで、サーバー側/クライアント側のソースコードを自動生成できる。
備考:前回の記事(OpenAPI Generatorを使ってみた)で出てきたOpen APIと同じようなものです。
gRPCがサポートしている言語は、公式サイトに載っている通りです。
今回は、SpringBoot(Java)を使って、gRPCを使ってみました。
各言語でのクイックスタートは、Java – gRPCなどから参照できます。
1. Setting
SpringBootの場合、プラグインを入れるだけで設定完了です。
今回は、generatedFilesBaseDir
で、ソースコードの出力フォルダを指定し、そのフォルダを読み込み対象とするために、sourceSets
で指定しています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
plugins {
id "com.google.protobuf" version "0.8.11"
}
bootJar.enabled = false
jar.enabled = true
dependencies {
implementation 'io.github.lognet:grpc-spring-boot-starter:3.5.1'
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.11.1'
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.25.0'
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
doc {}
}
}
generatedFilesBaseDir = "$projectDir/gen-src"
}
sourceSets {
main {
java {
srcDirs "$projectDir/gen-src/main/grpc"
srcDirs "$projectDir/gen-src/main/java"
}
}
}
|
.proto
ファイルが、Inputです。
.proto
ファイルの記載方法は、Googleのドキュメント「Language Guide (proto3)」に記載されています。
備考:3年前でかなり古いですが、proto3なのでセーフ? Googleのドキュメントの日本語訳が、Qiitaの記事「Proto3 Language Guide(和訳)」に載っていました。
helloworld.proto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {
}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
|
3. Output
3.1. Auto Generated Source Code
.proto
から自動生成されるソースコードはこのような感じです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public final class HelloReply extends
com.google.protobuf.GeneratedMessageV3 implements
// @@protoc_insertion_point(message_implements:helloworld.HelloReply)
HelloReplyOrBuilder {
private static final long serialVersionUID = 0L;
// Use HelloReply.newBuilder() to construct.
private HelloReply(com.google.protobuf.GeneratedMessageV3.Builder<?> builder) {
super(builder);
}
private HelloReply() {
message_ = "";
}
@java.lang.Override
@SuppressWarnings({"unused"})
protected java.lang.Object newInstance(
UnusedPrivateParameter unused) {
return new HelloReply();
}
// 省略
}
|
4. Manual Source Code
自動生成されたものを簡単にテストしました。
テストコードは書いておかないと、@t_wada さんに怒られますよね。。。
参考:組織にテストを書く文化を根付かせる戦略と戦術
4.1. Server Source Code
SampleGrpcSpringbootApplication.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
/* There is not any LISENSE yet. */
package poc.grpc.server;
import io.grpc.examples.helloworld.GreeterGrpc;
import io.grpc.examples.helloworld.HelloReply;
import io.grpc.examples.helloworld.HelloRequest;
import io.grpc.stub.StreamObserver;
import org.lognet.springboot.grpc.GRpcService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* gRPCサンプルアプリケーションのサンプルクラス
*/
@SpringBootApplication
public class SampleGrpcSpringbootApplication {
/**
* SpringApplication起動用
*
* @param args 引数
*/
public static void main(String[] args) {
SpringApplication.run(SampleGrpcSpringbootApplication.class, args);
}
/**
* gRPCのサンプルサービスクラス
*/
@GRpcService
public static class GreeterService extends GreeterGrpc.GreeterImplBase {
/**
* gRPCのサンプルサービス
*
* @param request リクエスト
* @param responseObserver {@link StreamObserver}
*/
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
if ("error data".equals(request.getName())) {
throw new IllegalArgumentException();
}
var replyBuilder = HelloReply.newBuilder().setMessage("Hello " + request.getName());
responseObserver.onNext(replyBuilder.build());
responseObserver.onCompleted();
}
}
}
|
4.2. Client Source Code
SampleGrpcClient.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
/* There is not any LISENSE yet. */
package poc.grpc.client;
import io.grpc.ManagedChannelBuilder;
import io.grpc.examples.helloworld.GreeterGrpc;
import io.grpc.examples.helloworld.HelloReply;
import io.grpc.examples.helloworld.HelloRequest;
import lombok.AllArgsConstructor;
/**
* gRPCのサンプルクライアント
*/
@AllArgsConstructor
public class SampleGrpcClient {
/**
* gRPCで利用するホスト
*/
private String grpcHost;
/**
* gRPCで利用するポート番号
*/
private int grpcPort;
/**
* Sampleクライアント
*
* @return {@link HelloReply}
*/
public HelloReply access(String name) {
var channel = ManagedChannelBuilder.forAddress(grpcHost, grpcPort).usePlaintext().build();
var stub = GreeterGrpc.newBlockingStub(channel);
var request = HelloRequest.newBuilder().setName(name).build();
return stub.sayHello(request);
}
}
|
4.3. Test Code
SampleGrpcClientTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
/* There is not any LISENSE yet. */
package poc.grpc.client;
import poc.grpc.config.GrpcConfiguration;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
/**
* {@link SampleGrpcClient} のテストクラス
*/
@SpringBootTest(classes = {GrpcConfiguration.class})
public class SampleGrpcClientTest {
/**
* 正常系
*/
@Test
void test_正常系() {
var sampleGrpcClient = new SampleGrpcClient("localhost", 6565);
var actualReply = sampleGrpcClient.access("Tom").getMessage();
var expectedReply = "Hello Tom";
assertThat(actualReply).isEqualTo(expectedReply);
}
/**
* 異常系_送信データがnullの場合
*/
@Test
void test_異常系_送信データnull() {
var sampleGrpcClient = new SampleGrpcClient("localhost", 6565);
try {
sampleGrpcClient.access(null).getMessage();
fail();
} catch (NullPointerException ex) {
// 成功
}
}
/**
* 異常系_クライアントからサーバーに接続できない場合
*/
@Test
void test_異常系_クライアントから接続不可() {
var sampleGrpcClient = new SampleGrpcClient("99.99.99.99", 0);
try {
sampleGrpcClient.access("Tom").getMessage();
fail();
} catch (RuntimeException ex) {
// 成功
}
}
/**
* 異常系_サーバー側でエラーが発生した場合
*/
@Test
void test_異常系_サーバー側エラー() {
var sampleGrpcClient = new SampleGrpcClient("localhost", 6565);
try {
sampleGrpcClient.access("error data").getMessage();
fail();
} catch (RuntimeException ex) {
// 成功
ex.printStackTrace();
}
}
}
|
GrpcConfiguration.java
1
2
3
4
5
6
7
8
9
|
package poc.grpc.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(value = {"poc.grpc.server"})
public class GrpcConfiguration {
}
|
5. Refference