KotlinでSpringBootでgRPCサーバーを立てた時の設定メモ


概要

趣味で作るアプリでgRPCを使った通信をしてみようと思い設定した時のメモです。
build.gradleで何を設定する必要があるかについて主に書いています。

Kotlin、SpringBoot、gRPCについては、細かく説明しません。
こちらの設定についてのメモです。

技術選択の気持ち

なぜKotlin?

Java8のOptionalは手に馴染まない。isPresentとか書くの面倒くさい。
最近周りで流行している。
かわいい。

サイバーエージェントでは、Androidだけでなく、サーバーサイドでもKotlin使っているところがある。(※1 関連資料)

なぜSpringBoot?

Javaでアプリケーション書く時のデファクトスタンダード(だと思ってる)。

なぜgRPC?

触ってみたいから。
趣味で作るアプリなので使ったことがないものを使いたい。

メルカリのバックエンドでも利用されていて(※2 関連資料)、今後マイクロサービス化とともに広まっている可能性がある。

build.gradleの設定

buildscript

buildscript {
    ext.kotlin_version = '1.1.2'

    repositories {
        mavenCentral()
        jcenter()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-allopen:$kotlin_version" // -- (1)
        classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.3.RELEASE"
        classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.1" // -- (2)
    }
}

(1) kotlin-allopen

KotlinでSpringBootを使うために入れています。Kotlinでは、デフォルトでfinal classになるため、SpringFrameworkの@Serviceなどのアノテーションを使ったAutowiredができません。

kotlin-allopenを使うことで、特定のアノテーションが付いているクラスをopenにすることができます。

また、apply plugin: 'kotlin-spring'によって、SpringFrameworkで使われるアノテーションがついているクラスをopenにしてくれます。

参考:
Compiler Plugins - Kotlin Programming Language

(2) protobuf-gradle-plugin

Protocol Buffersのビルドをするためのプラグイン。

apply plugin

先ほど出てきましたが、SpringFrameworkを使うためkotlin-springを入れておきます。

apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'org.springframework.boot'
apply plugin: 'com.google.protobuf'
apply plugin: 'application'

repositories

grpc-spring-boot-starterjcenter()にあるので追加しています。

repositories {
    mavenCentral()
    jcenter()
}

sourceSets

Protocol Buffersが生成するソースのディレクトリを追加しておきます。

sourceSets {
    main.kotlin.srcDirs += 'src/main/kotlin'
    main.java.srcDirs += 'src/main/java'
    main.java.srcDirs += 'src/main/generated-proto'
}

dependencies

def grpcVersion = '1.5.0'

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"

    // grpc
    compile "io.grpc:grpc-netty:${grpcVersion}" // -- (1)
    compile "io.grpc:grpc-protobuf:${grpcVersion}" // -- (2)
    compile "io.grpc:grpc-stub:${grpcVersion}" // -- (3)

    // spring-boot
    compile "org.springframework.boot:spring-boot-starter-web:1.5.3.RELEASE"
    compile "org.springframework.boot:spring-boot-actuator:1.5.3.RELEASE"
    compile "org.lognet:grpc-spring-boot-starter:2.0.4" // -- (4)
}

(1) grpc-netty

書かなくてもorg.lognet:grpc-spring-boot-starterの依存で入ります。
ただ、バージョン1.4.0が入って通信に失敗するので、1.5.0を明示的に書いてます。

(2) grpc-protobuf

Protocol Buffersが生成したクラスの中で使っているので必須。

(3) grpc-stub

Protocol Buffersが生成したクラスの中で使っているので必須。

(4) grpc-spring-boot-starter

@GRpcServiceをつけるだけで、SpringBootがgRPCのサービスとして起動してくれるようになるOSS。
Eurekaも一緒に使えるようなので、ロードバランシングもできそう。(キタコレ!)

Apache License 2.0。

protobuf

Protocol Buffersのコンパイル設定を書きます。

参考: protobuf-gradle-plugin

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.3.0' // -- (1)
    }
    plugins {
        grpc {
            artifact = "io.grpc:protoc-gen-grpc-java:1.5.0" // -- (2)
        }
    }
    generateProtoTasks { // -- (3)
        all().each { task ->
            task.builtins {
                java {
                    outputSubDir = 'generated-proto'
                }
            }
            task.plugins {
                grpc {
                    outputSubDir = 'generated-proto'
                }
            }
        }
    }
    generatedFilesBaseDir = "$projectDir/src/" // -- (4)
}

(1) protoc

Protocol Buffersのコンパイラを指定。
前述した、grpc-protobufやgrpc-stubのバージョンとprotocのバージョンには依存関係があり、バージョンがズレていると、Protocol Buffersが生成したjavaのコンパイルに失敗することがあります。(依存関係があるのに依存関係チェックしてくれない。。)

どちらも最新版にしておけば、動くと思います。

(2) protoc-gen-grpc-java

protocでJavaを生成するためにPluginを入れます。
Protocol Buffersは、GoやPHPなどの様々な言語への出力ができるようになっています。残念ながらまだKotlinを生成できないので、Javaにしておきます。

(3) generateProtoTasks

生成したJavaファイルの配置先を設定します。
task.builtinsのファイルは、デフォルトで"${generatedFilesBaseDir}/main/java"の下に置かれるようです。
task.pluginsのファイルは、設定を入れないとどこにも出力されませんでした。

ここでは、Protocol Buffersが出力したことが分かりやすいように、generated-protoとしています。

(4) generatedFilesBaseDir

生成したJavaファイルの配置先を設定します。
(3)で設定したものと合わせて、"${generatedFilesBaseDir}/main/${outputSubDir}"に生成されます。

clean

gradle clean時に生成ファイルが消えるように設定しておきます。

clean {
    delete "$projectDir/src/main/generated-proto"
}

bootRepackage

fat jarを作るためにbootRepackageいれておきます。

bootRepackage  {
    executable = true
}

build.gradle設定まとめ

build.gradleの設定はここまでです。ソースはこちら

その他つらつらと

.gitignore

Protocol Buffersのコンパイルで生成されるファイルがgitに入らないようにignoreしましょう。

generated-proto

Dockerfile

javaが入っている環境で作ったfat jarを実行するだけのDockerイメージを作ると、runするだけでサーバー上で動かせます。
ありがとうDocker。

Dockerfile
FROM java:8-jre

ADD sample-1.0.0.jar /app/

CMD ["java", "-jar", "/app/sample-1.0.0.jar"]

EXPOSE 6565

サーバーの動作確認

polyglotを利用すると簡単な動作確認ができます。
protoファイル、通信先、実行するメソッドを指定すると、通信結果をjsonで吐き出してくれます。

コマンド
echo "{'name': 'world'}" | java -jar ~/polyglot.jar \
--command=call --endpoint=localhost:6565 \
--full_method=helloworld.Greeter/SayHello \
--proto_discovery_root=src/main/proto/ \
--use_tls=false
実行結果
[main] INFO me.dinowernli.grpc.polyglot.Main - Polyglot version: 1.2.0
[main] INFO me.dinowernli.grpc.polyglot.Main - Loaded configuration: 
[main] INFO me.dinowernli.grpc.polyglot.command.ServiceCall - Creating dynamic grpc client
[main] INFO io.grpc.internal.ManagedChannelImpl - [ManagedChannelImpl@25af5db5] Created with target localhost:6565
[main] INFO me.dinowernli.grpc.polyglot.command.ServiceCall - Making rpc with 1 request(s) to endpoint [localhost:6565]
[main] INFO me.dinowernli.grpc.polyglot.grpc.DynamicGrpcClient - Making unary call
[grpc-default-executor-1] INFO me.dinowernli.grpc.polyglot.io.LoggingStatsWriter - Got response message
{
  "message": "Hello world"
}

[grpc-default-executor-1] INFO me.dinowernli.grpc.polyglot.io.LoggingStatsWriter - Completed rpc with 1 response(s)

curlの方が簡単だけど、gRPCだとcurlできないので仕方ない。

KotlinでのSpringFrameworkのコンストラクタインジェクションの書き方

kotlin
@Service
class SampleService @Autowired constructor(val repository: SampleRepository) {
}

Javaにデコンパイルすると以下のようになって、確かに動きそう。(Kotlinなのでgetterが勝手に生えるw)

Java.decompiled
@Service
public class SampleService {
   @NotNull
   private final SampleRepository repository;

   @NotNull
   public SampleRepository getRepository() {
      return this.repository;
   }

   @Autowired
   public SampleService(@NotNull SampleRepository repository) {
      Intrinsics.checkParameterIsNotNull(repository, "repository");
      super();
      this.repository = repository;
   }
}

関連資料

※1 GRPCの実践と現状での利点欠点 / Go Conference 2016 Spring
※2 サイバーエージェントのKotlin勉強会「CA.kt」がスタートーーベータ版から開発で利用している主催者に話を聞きました