タケハタのブログ

プログラマの生き方、働き方、技術について雑多に書いていくブログです。

サーバーサイドKotlin×gRPCコトハジメ 〜①プロジェクト作成から起動確認まで〜

今回からサーバーサイドKotlinでのgRPCの使用方法について、何回かに分けて書いていこうと思います。
9月に行われるCEDEC 2019に登壇することもあり、復習がてら。
(実際に登壇で話す内容とは異なります)

cedec.cesa.or.jp

「ちょっと触ってみよう」という人の手助けにもなれば。

gRPCとは?

gRPCはGoogle製のRPCフレームワークで、通信プロトコルとしてHTTP/2、IDLとしてProtocol Buffersを使用し、ハイパフォーマンスな通信を実現しています。

grpc.io

まだまだ国内での事例は少ないですが、GraphQLとともにRESTの次の通信の技術として注目されています。

参考資料

この記事の中でも出てくる、grpc-spring-boot-starter、protobuf-gradle-pluginのドキュメントを参考に作っています。
サンプルコードも、主にgrpc-spring-boot-starterの内容をほぼそのまま使っています。

github.com github.com

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

generateProtoTasksprotoc の実行時に 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 配下に GreeterOuterClasssrc/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を実行するためのクライアントツールです。

github.com

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ややこしいなあ・・・