タケハタのブログ

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

サーバーサイドKotlin×gRPCコトハジメ 〜②Protocol Buffersの色々なオプション〜

前回の記事から始めた「サーバーサイドKotlin×gRPCコトハジメ」、今回はコードジェネレータの設定について設定や、protoファイルのオプションなどについて紹介します。
色々あるので全部を知りたい方は下記の公式ドキュメントを見ていただくと良いと思います。

developers.google.com github.com

ここでは基本的なよく使うものを説明していきます。

コードジェネレータの設定

build.gradleで記述していた、Protocol Buffersのコードジェネレータに関する設定についてです。
前回はほとんどデフォルトの設定で作成しましたが、いくつかオプションを紹介します。

protoファイルの配置ディレクトリ変更

自動生成の対象とするprotoファイルの配置ディレクトリを変更します。
デフォルトではprotoディレクトリが対象となっていますが、もし変えたい場合はsourceSetsへ下記のように変更してください。

sourceSets {
    main {
        // protoファイルの配置ディレクトリ
        proto {
            srcDir 'src/main/protobuf'
        }

        java {
            srcDirs 'src/main/grpc'
        }
    }
}

Javaのビルド設定と同じように、 proto ブロックに srcDir で対象にしたいディレクトリのパスを記述します。
ここでは src/main/protobuf を対象としています。

出力先ディレクトリの変更

生成するファイルの出力先ディレクトリを変更します。
前回の記事のサンプルでは分かりやすくするために、デフォルトの java grpc ディレクトリに出力していました。
しかし、実際は別のディレクトリに分かれているのが面倒だったり、自動生成のファイルは同じ場所にまとめたいなどあると思います。

build.gradleの generateProtoTasks ブロックの中を、下記のように変更してください。

generateProtoTasks {
    all().each { task ->
        // Protocol Buffers関連のファイル出力先
        task.builtins {
            java {
                outputSubDir = "generated"
            }
        }
        // gRPC関連のファイル出力先
        task.plugins {
            grpc {
                outputSubDir = "generated"
            }
        }
    }
}

task.builtinsjava で指定しているのがProtocol Buffers関連のファイル(xxxOuterClassなど)の出力先、 task.pluginsgrpc で指定しているのがgRPCプラグインで出力されるファイル(xxxGrpc)の出力先になります。

いずれも outputSubDir というパラメータをすることで、 generatedFilesBaseDir で指定したベースディレクトリ配下のこのディレクトリに出力されます。
今回はベースディレクトリである $projectDir/src/ の下の main/generated というディレクトリにいずれのファイルも出力されるように設定しました。

また、出力先の変更に伴い、 sourceSets の設定も変更してください。

sourceSets {
    main {
        proto {
            srcDir 'src/main/protobuf'
        }

        java {
            // ビルド対象のディレクトリを変更
            srcDirs 'src/main/generated'
        }
    }
}

cleanタスク

ついでに出力したファイルを削除するための clean タスクも作っておきましょう。
トップレベルの階層に下記の記述を追加します。

clean {
    delete "$protobuf.generatedFilesBaseDir/main/generated"
}

delete に出力先ディレクトリのパスを指定しています。
そして下記のコマンドの実行、もしくはIDEで clean タスクを実行すると、generatedディレクトリが削除されます。

 ./gradlew clean

変更後のbuild.gradle全体

ここまで紹介した変更を加えたbuild.gradleを、参考として載せておきます。

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"
    id( "java")
}

sourceSets {
    main {
        proto {
            srcDir 'src/main/protobuf'
        }

        java {
            srcDirs 'src/main/generated'
        }
    }
}

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")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

compileKotlin {
    kotlinOptions {
        freeCompilerArgs = ['-Xjsr305=strict']
        jvmTarget = '1.8'
    }
}

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.builtins {
                java {
                    outputSubDir = "generated"
                }
            }
            task.plugins {
                grpc {
                    outputSubDir = "generated"
                }
            }
        }
    }

    generatedFilesBaseDir = "$projectDir/src/"
}

clean {
    delete "$protobuf.generatedFilesBaseDir/main/generated"
}

protoファイルのオプション

protoファイルで設定できるいくつかのオプションを紹介します。
まず、前回の記事で使用した「Greeter.proto」を下記のように変更します。

syntax = "proto3";
option java_package = "com.example.grpc.kotlingrpcexample.proto"; // ①出力先パッケージ名の指定
option java_outer_classname = "GreeterProtobuf"; // ②出力ファイル名の指定
option java_multiple_files = true; // ③messageを別ファイル出力

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

// ④Enumの定義
enum MessageType {
    NONE = 0;
    NORMAL = 1;
    SPECIAL = 2;
}

このファイルのコメントに沿って、順番に説明していきます。

①出力先パッケージの指定

これは前回も紹介していますが一応。
option java_package で生成するクラスのパッケージを指定できます。

option java_package = "com.example.grpc.kotlingrpcexample.proto";

ちなみに省略するとデフォルトパッケージになります。

②出力ファイル名の指定

ジェネレータで出力されるファイル名は、デフォルトで ${ファイル名}OuterClass になっていました。
(サンプルではGreeterOuterClass)

このファイル名を変えたい場合は、 option java_outer_classname で指定します。

option java_outer_classname = "GreeterProtobuf";

上記の例では、「GreeterProtobuf」という名前で出力されるようになります。

③messageを別ファイル出力

ジェネレータで出力されたファイル(デフォルトでGreeterOuterClass)には、 service message の定義に紐付いたクラス、メソッドが含まれています。
messageで定義しているRequest、Responseのクラスもstaticなインナークラスとして定義されています。

この形では扱いづらいこともあるため、出力するファイル、クラスを分離することができます。
option java_multiple_files を使用します。

option java_multiple_files = true;

このオプションをtrueにすることで(デフォルトはfalse)、OuterClassからはmessageに関する定義はなくなり、下記のBuilderインターフェース、実装クラスがそれぞれ別ファイルで出力されます。

  • HelloReply
  • HelloReplyOrBuilder
  • HelloRequest
  • HelloRequestOrBuilder

④Enumの定義

protoファイルでは、Enumを定義することもできます。
下記のように各要素に数値を指定して記述します。

enum MessageType {
    NONE = 0;
    NORMAL = 1;
    SPECIAL = 2;
}

proto3のバージョンでは、 0 を指定しないとエラーになってしまうので、必ず指定するようにしてください。
0で定義している要素が、このEnumのデフォルト値として扱われます。

そして、定義したEnumをmessageのプロパティの型に指定することができます。

message HelloReply {
    string message = 1;
    MessageType type = 2;
}

ジェネレータを実行すると MessageType というEnumファイルも生成される(java_multiple_filesがfalseの場合はOuterClassの中に定義される)ため、あとは通常のJavaのEnumと同じように使用できます。

val replyBuilder = HelloReply.newBuilder()
        .setMessage("Hello " + request.name)
        .setType(MessageType.NORMAL)

おまけ:messageを型に指定

先ほど定義したEnumをプロパティの型に指定しましたが、同じようにmesageを型にすることもできます。

message HelloRequest {
    string name = 1;
    // messageのUserを型に指定
    User user = 2;
}

message User {
    int32 id = 1;
}

上記の例では「User」というmessageを定義し、それをHelloRequestの2番目のプロパティの型に指定しています。

protoファイルのパッケージ構成パターン

ここまでprotoファイルは「Greeter.proto」という1つのファイルに全て記述してきましたが、中のserviceやmessageをファイル自体で分けることもできます。
protoファイルの配置場所として決められたディレクトリ内にさえ置かれていれば、その中のディレクトリ階層やファイル構成は関係なく、全て生成してくれます。

最後に、いくつか想定されるファイル構成のパターンを参考として紹介します。

serviceとその中で使うmessage、Enumを全て入れる

まずは1つのファイルに全部を入れるパターン。
service単位でファイルは分けて、その中で使うmessage、Enumの定義も同梱する構成。
今回のGreeter.protoと同じですね。

service、message、Enumを完全分離

service、message、Enumそれぞれでファイルを作るパターン。
今回の例で言うと、下記の4ファイルを作るイメージです。

  • GreeterService.proto
  • HelloRequest.proto
  • HelloReply.proto
  • MessageType.proto

Javaの出力ファイルと近い構成になりますね。

messageを一つにまとめる

Request、Responseのmessage、その中で参照するオブジェクトがセットのファイルをるパターン。

  • GreeterService.proto
  • HelloMessages.proto

のようなファイルを作り、「HelloMessages.proto」にHelloRequest、HelloReply、MessageTypeが定義されているイメージです。

今回はここまで

今回はProtocol Buffersのコードジェネレータや、protoファイルの記述について紹介しました。
これで基本的な構成ができあがりました。

次回はgRPCでのインタセプタやエラーハンドリングなどの実装方法について、紹介する予定です。