Help us understand the problem. What is going on with this article?

gRPC-WebをKotlinバックエンドで試した時のメモ - 2. Service編

About

このテーマの連載、Service編です。

https://qiita.com/hosopy/items/d7afd9cf85bd6e155815

はじめに

まず、自分はSpring Boot未経験な状態で露頭に迷いそうな状態でしたが、下記ブログ記事の内容が非常に参考になりました。筆者様に感謝です。

なお、筆者のスペックはこんな感じなので、Java,Kotlin,Gradleについては触れずにドライにスキップしています。

  • Java : 業務での開発経験あり
  • Kotlin : 趣味でライブラリ開発経験あり
  • Gradle : Androidアプリの開発で経験あり (詳しくはない)
  • Spring Boot : 未経験

サービス構成

おさらいになりますが、バックエンドのサービスはフロントのサービスであるGreeterServiceと、内部サービスであるTextAnalyzerServiceの2つから構成されています。

sequence.png

実装上はどちらも変わりないので、GreeterServiceを見ていきます。

GreeterService

GreeterServiceの実装は、サンプルトのserviceディレクトリに入っています。

以下、どんな流れで実装していったかのメモです。

プロジェクトの生成

例えばRailsであればrails gでプロジェクトを生成したりしますが、Spring Bootではどうするのか。

一般的にどうするのが主流なのか分かりませんが、Spring InitializerというWebサービスを使って、下記の条件でプロジェクトを生成しました。

  • Project: Gradle Project
  • Language: Kotlin
  • Spring Boot: 2.0.0 (SNAPSHOT)
  • Dependencies: Spring Web

build.gradle

Spring Initializerが生成するプロジェクトでは、デフォルトではbuild.gradle.kts (Kotlin Script形式)でbuild.gradleが生成されます。

ここは、参考サイト様の情報に従って、Groovy形式のbuild.gradleに書き換えました。
(Kotlin形式での設定に調整しましたが、同様にハマってしまったので...)

内容はリポジトリのbuild.gradleを参照して下さい。

GreeterServiceの定義

公開するGreeterServiceの定義(.proto)を作成します。

service/src/main/proto/Greeter.proto
syntax = "proto3";
option java_package = "com.hosopy.kotlingrpcsample.greeter.proto";
option java_outer_classname = "GreeterProtobuf";

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

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
    int32 nameLength = 2;
}

この状態で、generateProtoタスクが成功すればOKです。

./gradlew generateProto

GreeterServiceの実装

generateProtoタスクで生成されたスタブクラスを元にして、GreeterServiceの実装を行います。

なお、依存する内部サービスのClientであるTextAnalyzerClientの呼び出し実装に含まれていますが、混乱するので後述します。

grpc-spring-boot-starterのドキュメントを読めば、すんなりと実装できると思います。

service/src/main/kotlin/com/hosopy/kotlingrpcsample/grpc/service/GreeterGRpcService.kt
package com.hosopy.kotlingrpcsample.grpc.service

/*
 * GreeterServiceの関連クラス.
 * generateProtoタスクで自動生成されたもの.
 */
import com.hosopy.kotlingrpcsample.greeter.proto.GreeterGrpc
import com.hosopy.kotlingrpcsample.greeter.proto.GreeterProtobuf
/*
 * TextAnalyzerService(依存する別サービス)を呼び出すためのClient(後述).
 */
import com.hosopy.kotlingrpcsample.grpc.client.TextAnalyzerClient

import io.grpc.stub.StreamObserver
import org.lognet.springboot.grpc.GRpcService

@GRpcService
class GreeterGRpcService(
    // Spring Bootの仕組みでTextAnalyzerClientをDI
    private val textAnalyzerClient: TextAnalyzerClient
): GreeterGrpc.GreeterImplBase() {
    /**
     * rpc SayHelloの実装.
     */
    override fun sayHello(
        request: GreeterProtobuf.HelloRequest,
        responseObserver: StreamObserver<GreeterProtobuf.HelloReply>
    ) {
        val replyBuilder = GreeterProtobuf.HelloReply.newBuilder()
        replyBuilder.message = "Hello " + request.name
        // TextAnalyzerServiceの呼び出し結果をnameLengthに設定
        replyBuilder.nameLength = textAnalyzerClient.analyzeText(request.name).length
        responseObserver.onNext(replyBuilder.build())
        responseObserver.onCompleted()
    }
}

TextAnalyzerServiceの呼び出し

GreeterServiceが依存する別サービスでTextAnalyzerServiceを呼び出すためのClientを、TextAnalyzerClientとして実装しました。

実装にあたり、別サービスであるTextAnalyzerServiceのIDL(.proto)から、関連するスタブクラスを生成する必要があります。

TextAnalyzerServiceの実装は、サンプルのinternal-serviceディレクトリに入っているので、次のようなコマンドで生成しました。

$ protoc -I=internal-service/src/main/proto \
         --java_out=out \
         src/main/proto/TextAnalyzer.proto

自動生成されたスタブクラスを利用して、gRPCのJava公式チュートリアルを参考に次のようなClient実装になりました。

service/src/main/kotlin/com/hosopy/kotlingrpcsample/grpc/client/TextAnalyzerClient.kt
package com.hosopy.kotlingrpcsample.grpc.client

/*
 * 依存する内部サービス(TextAnalyzerService)の関連クラス.
 * protocで自動生成されたもの.
 */
import com.hosopy.kotlingrpcsample.textanalyzer.proto.TextAnalyzerGrpc
import com.hosopy.kotlingrpcsample.textanalyzer.proto.TextAnalyzerProtobuf

import io.grpc.ManagedChannelBuilder
import org.springframework.stereotype.Component

@Component
class TextAnalyzerClient(
    // Clientの設定情報(ホスト/ポート)をDI
    private val configuration: TextAnalyzerClientConfiguration
) {

    fun analyzeText(text: String): TextAnalysisResult {
        // NOTE: usePlaintext only for development/test
        val channel =
            ManagedChannelBuilder.forAddress(
                configuration.host, configuration.portInt
            ).usePlaintext(true).build()
        val blockingStub = TextAnalyzerGrpc.newBlockingStub(channel)
        val requestBuilder = TextAnalyzerProtobuf.TextAnalysisRequest.newBuilder()
        requestBuilder.text = text
        val response = blockingStub.analizeText(requestBuilder.build())
        return TextAnalysisResult(response.length)
    }
}

data class TextAnalysisResult(val length: Int)

なお、接続先のホスト/ポートの情報はTextAnalyzerClientConfigurationとして別クラスで実装し、Spring Bootの仕組み?で注入されるようになっています。

このあたりの仕組みはSpring Boot初心者の自分にとってブラックボックスすぎるので、そのうち勉強したいところです。

package com.hosopy.kotlingrpcsample.grpc.client

import org.springframework.boot.context.properties.ConfigurationProperties

// src/main/resources/application.propertiesの内容が読み込まれる.
@ConfigurationProperties(prefix="internal-service.text-analyzer")
class TextAnalyzerClientConfiguration {
    lateinit var host: String
    // NOTE: primitiveが使えないので仕方なくStringで読む
    lateinit var port: String

    val portInt: Int
      get() = Integer.parseInt(port)
}
src/main/resources/application.properties
internal-service.text-analyzer.host=internal-service
internal-service.text-analyzer.port=6565

hostがinternal-serviceなのは、docker-compose.yamlで定義しているサービス名に起因します。

補足: Logging

最低限のログ出力を行うために、サービスグローバルなLogInterceptorを実装しました (参考)

service/src/main/kotlin/com/hosopy/kotlingrpcsample/grpc/service/interceptor/LogInterceptor.kt
package com.hosopy.kotlingrpcsample.grpc.service.interceptor

import io.grpc.Metadata
import io.grpc.ServerCall
import io.grpc.ServerCallHandler
import io.grpc.ServerInterceptor
import org.lognet.springboot.grpc.GRpcGlobalInterceptor
import org.slf4j.LoggerFactory

@GRpcGlobalInterceptor
class LogInterceptor: ServerInterceptor {
    companion object {
        private val logger = LoggerFactory.getLogger(LogInterceptor::class.java)
    }

    override fun <ReqT : Any, RespT : Any> interceptCall(
        call: ServerCall<ReqT, RespT>,
        headers: Metadata,
        next: ServerCallHandler<ReqT, RespT>
    ): ServerCall.Listener<ReqT> {
        logger.info(call.methodDescriptor.fullMethodName)
        return next.startCall(call, headers)
    }
}

Dockerfile

最後に、GreeterServiceをコンテナ化するためのDockerfileを準備しました。

# TODO: https://github.com/google/protobuf-gradle-plugin/issues/265
# FROM openjdk:8-jdk-alpine
FROM openjdk:8

EXPOSE 6565

VOLUME /tmp
RUN mkdir /work
COPY . /work
WORKDIR /work
RUN /work/gradlew build
RUN mv /work/build/libs/*.jar /work/app.jar

ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/work/app.jar"]

1点ハマったのは、最初はopenjdk:8-jdk-alpineのイメージをベースにしていたのですが、イメージのビルド中にgenerateProtoタスクがエラーになってしまいました。

Execution failed for task ':generateProto'.
> java.io.IOException: Cannot run program "/root/.gradle/caches/modules-2/files-2.1/com.google.protobuf/protoc/3.5.1-1/647d360fdbf38e4f92f8cb4854e3d92b62599766/protoc-3.5.1-1-linux-x86_64.exe": error=2, No such file or directory

protobuf-gradle-pluginのIssueにもあったのですが、protobuf-gradle-pluginのMeavenリポジトリに同包されているprotocのバイナリに、alpineに対応したバイナリが含まれていないためのようです。

https://github.com/google/protobuf-gradle-plugin/issues/265

protobufを個別にインストールしてpathを指定することで解決出来るという情報もありましたが、ビルド設定としてのポータビリティに欠けるような気がして、今回は大人しくopenjdk:8のイメージをベースにしました。

TextAnalyzerService

サンプルのinternal-serviceディレクトリ配下に実装がありますが、GreeterServiceの簡易版でほぼ同じ構成なので説明は省略します。

次回

次回はReverseProxy編です。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away