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"
        }
    }
}

2. Input

.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