Java
spring-boot
protobuf
gRPC
pom.xml

gRPC on Spring Boot with grpc-spring-boot-starter

この記事はBrainPad Advent Calendar 2018 3日目の記事になります。

こんにちは@nissy0409240です。
BrainPadでエンジニアをしています。
2018年もあと一ヶ月を切りましたが、
皆様いかがお過ごしでしょうか。

私事ではございますが
今年はPyCon2018で登壇させて頂いた
image.png
W杯でC・ロナウドのハットトリックを現地で観戦するなど
image.png
公私に渡り貴重な経験をさせて頂きました。

そんな2018年の最後に、上で書いた内容と全く関係無いgRPCをSpring Bootで使うにあたり、とんでもなくはまった箇所と(それだけでは文章量が少なすぎるので)gRPCってそもそも何かを添えて、本エントリーをお送りさせて頂きます。

gRPCとは

まず、gRPCとは何かについて説明します。
gRPCとはGoogleが作成したRPCフレームワークです。
これだけでは「なんのこっちゃ」な状態なのでRPCフレームワークについても説明します。

RPCフレームワークのRPCの正式名称は"Remote Procedure Call"と言います。
翻訳すると「遠隔手続き呼び出し」となります。
違うサーバ上で動いているアプリケーションに記載されているメソッドを手元のサーバで動いているものであるかのように呼び出せるようにするものです。

文字だけではイメージつきにくいので、gPRC本家のドキュメントに記載されている図と一緒に見ていきましょう。

image.png

上の図ではC++で作られたサービスをRubyのクライアントとJavaのクライアントが呼び出している状況を示しています。

gRPCを使うメリット

gRPCではサービス側が提供したい実装内容を.protoファイルという定義ファイルに記述することで、クライアント側からサービスの実装を呼び出すスタブという名前の連携モジュールを自動生成してくれます。
これにより、API連携で見られるインターフェース仕様定義ファイルが実装に必ず反映されるため、以下のような利点を享受することが出来ます。

  • 呼び出し元・呼び出し先の双方でどのような型のデータをやりとりするかを共有することが出来る。
  • http2 + protocol buffersを用いているためAPI連携よりパフォーマンスを向上させることも可能
  • 呼び出し元・呼び出し先で使用されている言語に関係無くマイクロサービス化を進めていくことも出来る
    • gRPCでは様々な言語に対応しており、Web開発で用いられている主要な言語はほぼほぼ対応済 スクリーンショット 2018-12-03 1.57.28.png

gRPCの使い方

pom.xmlへのモジュール追加

gRPCの使い方についてご紹介させて頂きます。
とはいえ、文字だけでは伝わりづらいですし、各言語ごとに異なるものですので、
今回はJava8 + Spring Bootを使い、実際に手を動かしながら進めていこうと思います。

まず、雛形となるプロダクトをSpring Initializerを使って作成します。
作成前の雛形の構成はこちらです。

demo_protobuf
├── pom.xml
├── main
│   ├── java
│   │   └── com
│   │       └── example
│   │           └── demo_protobuf
│   │               └── DemoProtobufApplication.java
│   └── resources
│       └── application.properties
└── test
    └── java
        └── com
            └── example
                └── demo_protobuf
                    └── DemoProtobufApplicationTests.java

続いてpom.xmlにモジュールを追加しますが、ここがとんでもなくはまった箇所でした。
モジュールの追加については後述するとして、先に定義ファイルについての説明に移ります。

定義ファイルの作成方法

実行が完了したら定義ファイルを作成します。
今回はsrc/main/proto/demo.protoファイルに定義を書いていきます。
この時点でのプロダクトの構成は以下の通りとなります。

demo_protobuf
.
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── demo_protobuf
    │   │               └── DemoProtobufApplication.java
    │   │           
    │   ├── proto
    │   │   └── demo.proto
    │   └── resources
    │       └── application.properties
    └── test
        └── java
            └── com
                └── example
                    └── demo_protobuf
                        └── DemoProtobufApplicationTests.java

作成したdemo.protoには以下のように記述していきます。

demo.proto
syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.example.fizzbuzz";
option java_outer_classname = "FizzBuzzProto";

package calc;

// Mathというサービスの戻り値と引数の組み合わせを定義します
service Math {
    rpc Math (MathRequest) returns (IntResponse);
}

// IntResponseを選択した時の戻り値の型と個数を定義します
message IntResponse {
    int32 value = 1;
}

// MathRequestを定義した時の引数の型と個数を定義します
message MathRequest {
    int32 x = 1;
    int32 y = 2;
}

ピンときた方もいらっしゃると思いますが、.protoファイルには
「どのサービスがどのようなリクエストを受け、どのような戻り値を返すか」が書かれています。
まさに.protoファイルがどのような型のデータをやりとりするかが書かれているのです。

記述後はmvn clean installを実行します。
実行後、ソースコードが以下のように生成されます。
(実際にはもっと沢山のコードが生成されますがこの場では省略させて頂きます)

.
├── mvnw
├── mvnw.cmd
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── example
│   │   │           └── demo_protobuf
│   │   │               └── DemoProtobufApplication.java
│   │   ├── proto
│   │   │   └── demo.proto
│   │   └── resources
│   │       └── application.properties
│   └── test
│       └── java
│           └── com
│               └── example
│                   └── demo_protobuf
│                       └── DemoProtobufApplicationTests.java
└── target
    └── generated-sources
        ├── annotations
        └── protobuf
            ├── grpc-java
            │   └── com
            │       └── example
            │           └── fizzbuzz
            │               └── MathGrpc.java
            └── java
                └── com
                    └── example
                        └── fizzbuzz
                            ├── FizzBuzzProto.java
                            ├── IntResponse.java
                            ├── IntResponseOrBuilder.java
                            ├── MathRequest.java
                            └── MathRequestOrBuilder.java

コードの再生成

しかし、ここで私はミスをしてしまったことに気付きました。
最初はFizzBuzzにしようとしたのを止めてMathに変更したのにも関わらず、
一部FizzBuzzのままコードを生成してしまったのです。
このままリポジトリを破棄して作り直しでしょうか。
そんなことはありません。

作成したdemo.protoを以下のように書き換え

demo.proto
syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.example.math";
option java_outer_classname = "MathProto";

package calc;

// Mathというサービスの戻り値と引数の組み合わせを定義します
service Math {
    rpc Math (MathRequest) returns (IntResponse);
}

// IntResponseを選択した時の戻り値の型と個数を定義します
message IntResponse {
    int32 value = 1;
}

// MathRequestを定義した時の引数の型と個数を定義します
message MathRequest {
    int32 x = 1;
    int32 y = 2;
}

mvn clean installを実行しましょう。

.
├── mvnw
├── mvnw.cmd
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── example
│   │   │           └── demo_protobuf
│   │   │               └── DemoProtobufApplication.java
│   │   ├── proto
│   │   │   └── demo.proto
│   │   └── resources
│   │       └── application.properties
│   └── test
│       └── java
│           └── com
│               └── example
│                   └── demo_protobuf
│                       └── DemoProtobufApplicationTests.java
└── target
    └── generated-sources
        ├── annotations
        └── protobuf
            ├── grpc-java
            │   └── com
            │       └── example
            │           └── math
            │               └── MathGrpc.java
            └── java
                └── com
                    └── example
                        └── math
                            ├── MathProto.java
                            ├── IntResponse.java
                            ├── IntResponseOrBuilder.java
                            ├── MathRequest.java
                            └── MathRequestOrBuilder.java

target下のコードが作り直されています。
また、demo.protoを更新したい時はこの方法で良いこともわかりますね。

後は生成されたコードを継承してサービスとクライアントを作成すれば完成です。

pom.xmlにモジュール追加時にはまったこと

一連の流れについて触れましたのでここではまったことについてご紹介させて頂きます。
端的に言ってしまうと「モジュールのgroupIdがMaven Repositoryで出てきた内容と異なるので注意が必要」ということでした。

そもそもMaven Repositoryとは何か。ご存知の方も多いと思われますが、ミドルウェアをインポートする際にどのような記述をするかを教えてくれるなくてはならないサイトです。
他言語の例で言えば、PythonのPyPiが近いものにあたります。

そして、Spring Bootというフレームワークに対応した全部入りツールいわゆるスターターと呼ばれるものも数多く存在しています。
例えば、認証モジュールを作る際にはSpring Boot Security Starterを使うといった具合です。

いままで、Spring BootでgRPCを使う際には以下のような書き方をしていました。

pom.xml
<!-- https://mvnrepository.com/artifact/org.lognet/grpc-spring-boot-starter -->
<dependency>
    <groupId>org.lognet</groupId>
    <artifactId>grpc-spring-boot-starter</artifactId>
    <version>2.3.2</version>
</dependency>

しかし、このモジュールの最新バージョンが3系へアップデートされた際、groupIdが"org.lognet"から"org.github.lognet"へと変わってしまったため、バージョンだけでなくgroupIdも変えなくてはならなくなりました。

grpc-spring-boot-starterのリポジトリにも上記と同様の情報が書かれています。
スクリーンショット 2018-12-04 2.25.34.png

そのため、今後は以下の通りに記述する必要があります。

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

こちらgroupIdを未修正のままですとmvn install時には下記のようなエラーが出てしまうので注意が必要です。

[ERROR] Failed to execute goal on project demo_protobuf: Could not resolve dependencies for project com.example:demo_protobuf:jar:1.16.1: Failure to find org.lognet:grpc-spring-boot-starter:jar:3.0.0 in https://repo.maven.apache.org/maven2 was cached in the local repository, resolution will not be reattempted until the update interval of central has elapsed or updates are forced -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/DependencyResolutionException

結び

構成としては少しおかしくはなりましたが日本語のエントリーが少ない情報をエラーメッセージとともに掲載することでハマっている方の助けになればと考え、本エントリーの結びと代えさせて頂きます。
まとまりのない文章となってしまい、申し訳ございませんでしたが最後までお付き合い頂きありがとうございました。

参考

https://qiita.com/nozaq/items/9cd9bf7ee6118779bda9
http://redj.hatenablog.com/entry/2017/10/13/084423
https://github.com/LogNet/grpc-spring-boot-starter
https://kengotoda.gitbooks.io/what-is-maven/primer/maven-repository.html