About
このテーマの連載、Service編です。
はじめに
まず、自分はSpring Boot未経験な状態で露頭に迷いそうな状態でしたが、下記ブログ記事の内容が非常に参考になりました。筆者様に感謝です。
なお、筆者のスペックはこんな感じなので、Java,Kotlin,Gradleについては触れずにドライにスキップしています。
- Java : 業務での開発経験あり
- Kotlin : 趣味でライブラリ開発経験あり
- Gradle : Androidアプリの開発で経験あり (詳しくはない)
- Spring Boot : 未経験
サービス構成
おさらいになりますが、バックエンドのサービスはフロントのサービスであるGreeterService
と、内部サービスであるTextAnalyzerService
の2つから構成されています。
実装上はどちらも変わりないので、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
)を作成します。
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のドキュメントを読めば、すんなりと実装できると思います。
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実装になりました。
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)
}
internal-service.text-analyzer.host=internal-service
internal-service.text-analyzer.port=6565
hostがinternal-service
なのは、docker-compose.yamlで定義しているサービス名に起因します。
補足: Logging
最低限のログ出力を行うために、サービスグローバルなLogInterceptorを実装しました (参考)
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に対応したバイナリが含まれていないためのようです。
protobufを個別にインストールしてpathを指定することで解決出来るという情報もありましたが、ビルド設定としてのポータビリティに欠けるような気がして、今回は大人しくopenjdk:8
のイメージをベースにしました。
TextAnalyzerService
サンプルのinternal-serviceディレクトリ配下に実装がありますが、GreeterService
の簡易版でほぼ同じ構成なので説明は省略します。
次回
次回はReverseProxy編です。