今回からサーバーサイドKotlinでのgRPCの使用方法について、何回かに分けて書いていこうと思います。
9月に行われるCEDEC 2019に登壇することもあり、復習がてら。
(実際に登壇で話す内容とは異なります)
「ちょっと触ってみよう」という人の手助けにもなれば。
gRPCとは?
gRPCはGoogle製のRPCフレームワークで、通信プロトコルとしてHTTP/2、IDLとしてProtocol Buffersを使用し、ハイパフォーマンスな通信を実現しています。
まだまだ国内での事例は少ないですが、GraphQLとともにRESTの次の通信の技術として注目されています。
参考資料
この記事の中でも出てくる、grpc-spring-boot-starter、protobuf-gradle-pluginのドキュメントを参考に作っています。
サンプルコードも、主にgrpc-spring-boot-starterの内容をほぼそのまま使っています。
Kotlin、Spring Boot、gRPCを使ったサーバーアプリケーションの作成
早速ですが、実際にサーバーアプリケーションを作っていきます。
いくつか手順があるので、順に説明します。
Spring Bootのプロジェクト作成
まず、Spring Initializrなどを使いSpring Bootのプロジェクトを作成してください。
作成時にDependenciesを追加する場合は、「Spring Web Starter」を追加しておいてください。
protoファイルの作成
gRPCでは、IDLとして「Protocol Buffers」を使用します。 Protocol Buffersは、「protoファイル」と呼ばれる定義ファイルに通信のインターフェースを記述します。
先程作成したプロジェクトを展開し、 src/main
配下に proto
というディレクトリを作り「Greeter.proto」という名前で下記のファイルを作成してください。
syntax = "proto3"; option java_package = "com.example.grpc.kotlin.proto"; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
このprotoファイルを元に通信に必要なコード(データのシリアライズ、デシリアライズなど)を、C++、C#、Dart、Go、Java、Pythonといった様々な言語で生成することができます。
それぞれの記述の意味は下記になります。
message
通信時のデータのやり取りで使用するデータの定義です。
REST通信でもよくあるリクエスト、レスポンスのインターフェース、またその中でプロパティとして使用するオブジェクトなどが定義できます。
このサンプルではシンプルに、リクエストのオブジェクト(HelloRequest)に「name」、レスポンスのオブジェクト(HelloReply)に「message」をいずれもstring型で定義しているだけになります。
プロパティ名の後ろに = 1
と書いていますが、protoファイルにはフィールドの順序を指定する必要があり、その数値になります。
今回は1つずつしかないためいずれも「1」が指定されていますが、2つ以上存在する場合は下記のように連番で数値を指定します。
message Sample { string name = 1; string profile = 2; }
service
Spring BootでのREST通信でいうControllerのような部分です。
関数名とともに、受け取るリクエスト、レスポンスの型を記述します。
ここでは「Greeter」という名前で定義し、リクエスト、レスポンスにそれぞれ message
で定義した型を設定します。
syntax、option
syntax
で指定しているのは、Protocol Buffersのバージョンです。
ここでは3系を指定していますが、なにも記述しないとデフォルトでは2系と判断され、この構文ではエラーになってしまいます。
また、 option java_package
で指定しているのは、自動生成クラスの出力先パッケージです。
option
では他にも様々な指定できる項目があるので、また別の記事で紹介したいと思います。
build.gradleの変更
次に、build.gradleの設定を変更します。
Spring Initializrで作成した場合、現在はbuild.gradleがKotlin DSLになっていますが、今回のサンプルではGroovyで記述していますので、拡張子 .kts
を外し、いくつかの記述を変更してGroovyへ変換して使用してください。
(Kotlin DSLでやるといくつかの記述が上手く動かなかったため・・・)
plugins { id("org.springframework.boot") version "2.1.6.RELEASE" id("org.jetbrains.kotlin.jvm") version "1.3.21" id("org.jetbrains.kotlin.plugin.spring") version "1.3.21" id("io.spring.dependency-management") version "1.0.7.RELEASE" id("com.google.protobuf") version "0.8.9" // ①Protocol Buffersを扱うためのプラグイン id( "java") } // ④sourceSetsに自動生成クラスのディレクトリを追加 sourceSets { main { java { srcDirs 'src/main/grpc' } } } group = "com.example.grpc.kotlin" version = "0.0.1-SNAPSHOT" sourceCompatibility = JavaVersion.VERSION_1_8 def grpcVersion = "1.10.0" repositories { mavenCentral() } dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") compile("io.github.lognet:grpc-spring-boot-starter:3.3.0") // ②gRPCをSpring Bootで扱うためのStarter testImplementation("org.springframework.boot:spring-boot-starter-test") } compileKotlin { kotlinOptions { freeCompilerArgs = ['-Xjsr305=strict'] jvmTarget = '1.8' } } // ③Protocol Buffersのコードジェネレータに関する設定 protobuf { protoc { artifact = "com.google.protobuf:protoc:3.5.1-1" } plugins { grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } } generateProtoTasks { all().each { task -> task.plugins { grpc } } } generatedFilesBaseDir = "$projectDir/src/" }
いくつかポイントがあるので、コード上に記述しているコメントに合わせて紹介します。
①Protocol Buffersを扱うためのプラグイン
GradleでProtocol Buffersを扱うためのプラグインの追加です。
前述のprotoファイルからコードの自動生成などを、Gradleタスクとして実行するために必要です。
id("com.google.protobuf") version "0.8.9"
②gRPCをSpring Bootで扱うためのStarter
grpc-spring-boot-starter
入れることで、Spring Bootで簡単にgRPCを使うことができるようになります。
下記の記述で依存関係を追加しています。
compile("io.github.lognet:grpc-spring-boot-starter:3.3.0")
使い方、実装方法に関しては後述します。
③Protocol Buffersのコードジェネレータに関する設定
前述のGradleプラグインを使い、protoファイルからコードを自動生成するための設定です。
protobuf
というブロックの中に記述します。
ちなみにProtocol BuffersのコードジェネレータはKotlinに対応していないため、Javaで生成します。
自動生成のコードは手動でいじらないため、Javaのままでも(本当はKotlinにしたいけど)一旦は使えるということで。
protobuf { protoc { artifact = "com.google.protobuf:protoc:3.5.1-1" } plugins { grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" } } generateProtoTasks { all().each { task -> task.plugins { grpc } } } generatedFilesBaseDir = "$projectDir/src/" }
まず、 protoc
は、コードの自動生成実行するタスクになります。
artifact
で、使用するライブラリ、バージョンを指定しています。
ここではMaven Centralから取得したprotocを使用していますが、下記のようにローカルに配置したprotocのパスを指定することもできます。
protoc {
path = '/usr/local/bin/protoc'
}
次に、 plugins
でコード生成に使用するプラグインを定義します。
ここでは grpc
というプラグインに、 generateProtoTasks
を指定しています。
これは簡単に言うと、protocでの自動生成のコードを、gRPCで使うためのコードを一緒に生成してくれるようなものと思ってください。 具体的には、後ほど実際に生成されたコードを説明する際に、一緒に紹介します。
そして、このプラグインを使うために、下記の設定が必要です。
generateProtoTasks { all().each { task -> task.plugins { grpc } } }
generateProtoTasks
で protoc
の実行時に grpc
プラグインを呼び出すように設定しています。
最後に、自動生成の出力先の設定として、下記を記述しています。
generatedFilesBaseDir = "$projectDir/src/"
generatedFilesBaseDir
に指定されたディレクトリの配下で、protocのデフォルトで作られるコードは main/java
、grpcプラグインによって作られるコードは main/grpc
のディレクトリに出力されます。
今回はプロジェクトの src
ディレクトリを指定いるので、 src/main/java
src/main/grpc
になります。
(これだけだとよく分からないと思うので、こちらも後ほど生成したファイルとともに改めて説明します)
④sourceSetsに自動生成クラスのディレクトリを追加
③で書いた通り自動生成のクラスで src/main/grpc
に出力されるファイルがあるため、 sourceSets
のjavaプラグインの参照先に追加します。
この記述で src/main/java
src/main/grpc
両方にあるJavaファイルをコンパイルしてくれるようになります。
sourceSets {
main {
java {
srcDirs 'src/main/grpc'
}
}
}
コード生成
プロジェクトのルートディレクトリで下記コマンドの実行、もしくはIDEからGradleの generateProto
タスクを実行してください。
./gradlew generateProto
src/main/java
配下に GreeterOuterClass
、 src/main/grpc
配下に GreeterGrpc
というファイルが作成されます。
いずれもprotoファイルで指定した com.example.grpc.kotlin.proto
パッケージに作られています。
プロジェクトの
src
ディレクトリを指定することで、protocのデフォルトで作られるコードはsrc/main/java
、grpcプラグインによって作られるコードはsrc/main/grpc
のディレクトリに出力されます。
と前述しましたが、GreeterOuterClassがこの「protocのデフォルトで作られるコード」になります。
中身の紹介は省きますが、ここで先程protoファイルで定義したservice、messageを実装したコードが含まれます。
GreeterGrpcは「grpcプラグインによって作られるコード」になり、GreeterOuterClassを使用してgRPCの通信部分を実装するためのクラスになります。
使い方は後述します。
protocがデフォルトで生成するのはあくまでも「Protocol Buffers」に関する部分で、それを「gRPC」で使うためのコードを生成するのがgRPCプラグインだと思ってください。
Serviceの実装、起動
では、いよいよ自動生成したコードを使って実装します。
(やっとKotlinが出てきます)
実装は下記。
import com.example.grpc.kotlin.proto.GreeterGrpc import com.example.grpc.kotlin.proto.GreeterOuterClass import org.lognet.springboot.grpc.GRpcService import io.grpc.stub.StreamObserver @GRpcService // ①gRPCのServiceと認識させるためのアノテーション class GreeterService: GreeterGrpc.GreeterImplBase() { // ②自動生成コードのクラスを継承 override fun sayHello(request: GreeterOuterClass.HelloRequest, responseObserver: StreamObserver<GreeterOuterClass.HelloReply>) { ③レスポンスの作成、返却処理 val replyBuilder = GreeterOuterClass.HelloReply.newBuilder().setMessage("Hello " + request.name) responseObserver.onNext(replyBuilder.build()) responseObserver.onCompleted() } }
これだけです。
①gRPCのServiceと認識させるためのアノテーション
まず @GRpcService
というアノテーションをつけると、SpringがそのクラスをgRPCのServiceとして認識し、起動してくれます。
これは前述の grpc-spring-boot-starter
に含まれるものですね。
これがあることでgRPCの導入がすごく簡単になっています。
②自動生成コードのクラスを継承
ここで先ほどプラグインで自動生成したコード、 GreeterGrpc
が出てきます。
このGreeterGrpcにはprotoファイルで定義した Greeter
を実装するためのベースとなる抽象クラスの GreeterImplBase
が定義されているため、こちらを継承します。
③レスポンスの作成、返却処理
ここでは HelloRequest
の型で受け取ったリクエストの値を、 HelloReply
型のインスタンスに設定し、返却しているだけの処理になります。
自動生成されるmessageのクラスの構造上、Builderパターンで生成する必要があります。
また、 StreamObserver
というクラスを使ってレスポンスを返しているのも通常のREST通信とは違うところです。
gRPCはHTTP/2を標準で使用しているため、ストリーミングRPCという双方向通信が可能で、その関係でこの作りになっているのですが・・・こちらに関してはまた別の記事で書きます。
ひとまず今は onNext
メソッドでレスポンスの値を設定し、 onCompleted
で通信の完了を通知するためのObserverと思ってください。
onCompleted
を実行しないと終了が検知されず、レスポンスは返却されません。
起動
作成したServiceを起動します。
通常のSpring Bootのアプリケーションの起動と同じように、gradlewコマンドかIDEでGradleの bootRun
タスクを実行してください。
./gradlew bootRun
下記のように、GreeteServiceを登録し、6565ポート(gRPCのデフォルト)でgRPCサーバーが立ち上がったメッセージが表示されれば成功です。
[ main] o.l.springboot.grpc.GRpcServerRunner : 'com.example.grpc.kotlin.kotlingrpcexample.grpcservice.GreeterService' service has been registered. [ main] o.l.springboot.grpc.GRpcServerRunner : gRPC Server started, listening on port 6565.
動作確認
実行して動作確認します。
gRPCはRESTのようにcurlコマンドやPostmanで実行することはできないため、一手間必要です。
実行用のクライアントプログラムを用意するなどしても良いのですが、今回は一旦手軽に動作確認してみるために、 grpcc
というツールを使います。
grpccのインストール
grpccはgRPCを実行するためのクライアントツールです。
READMEにも載っていますが、npmで簡単にインストールできます。
npm install -g grpcc
sayHelloの実行
そしてプロジェクトのルートディレクトリで、下記のコマンドを実行します。
grpcc --proto ./src/main/proto/Greeter.proto --address 127.0.0.1:6565 -i
動作確認したいServiceのprotoファイル、接続先アドレス(今回はlocalhostの6565ポート)を指定し、 -i
オプションを付けて実行すると対話モードになります。
そしてリクエストパラメータをJSON形式で記載しEnter。
Greeter@127.0.0.1:6565> client.sayHello({name:"takehata"}, printReply)
printReply
はgrpccで用意されている、レスポンスを表示するためのコールバックです。
EventEmitter {} Greeter@127.0.0.1:6565> { "message": "Hello takehata" }
レスポンスが表示されます。
ここまでできれば一旦Kotlin × Spring BootでのgRPC起動は完了です。
今回はここまで
今回は触りで、一旦起動するところまでをやってみました。
gRPCの機能とか設定の仕方とかまだまだ色々あるので、これから何回かに分けて書いていければなと思っています。
それにしてもGradleややこしいなあ・・・