MicronautとgRPCでサーバ・クライアント間通信を構築してみる
gRPC を Micronaut で触ってみる。 Micronauto は Java の軽量フレームワークで、起動がとにかく早い。 gRPC はサービス間通信の技術。
マイクロサービス向けのこの 2 つの技術でサクッと環境を整えて通信させてみる。
今回 gRPC、Micronauto のメリット・デメリットについては言及しない。またこれらの技術について、どういう仕組でというのも説明しない。ここではあくまで gRPC を用いた通信ができることを確認できるのみに留める。
深堀りするのはまた別で行う。
構築する環境
gRPC サーバとクライアントを、それぞれ異なる Micronaut アプリケーションで構築する。 サーバ・クライアントを同一アプリケーションに構築することもできるが、あまりおもしろくないので、実践ぽく分けてみる
- gRPC サーバ
- こいつが様々なメソッドを提供する
- 適当なポートで起動して、クライアントが接続できる環境にしておく
- gRPC クライアント
- http アプリケーションを起動して、特定のエンドポイントにアクセスした時に、gRPC サーバに接続する処理を記述する
gRPC サーバの構築
Micronauto のアプリケーションは IntelliJ より作成した。以下のサイトでも作成できる https://micronaut.io/launch/
java のバージョンを 17 にしたくらい。dependency は追加せず、以下のように直接 build.gradle に記述した。
ほとんど初期状態で、gRPC/Protocol Buffers の設定だけ加えた
./build.gradle
plugins {
id("com.github.johnrengelman.shadow") version "7.1.2"
id("io.micronaut.application") version "3.6.2"
id("com.google.protobuf") version "0.8.15"
}
version = "0.1"
group = "com.example"
repositories {
mavenCentral()
}
dependencies {
implementation("io.micronaut:micronaut-jackson-databind")
implementation("io.micronaut:micronaut-context")
implementation("jakarta.annotation:jakarta.annotation-api")
runtimeOnly("ch.qos.logback:logback-classic")
implementation("io.micronaut:micronaut-validation")
implementation("io.micronaut.grpc:micronaut-grpc-server-runtime")
}
test {
useJUnitPlatform()
}
application {
mainClass.set("com.example.Application")
}
java {
sourceCompatibility = JavaVersion.toVersion("17")
targetCompatibility = JavaVersion.toVersion("17")
}
graalvmNative.toolchainDetection = false
micronaut {
runtime("netty")
testRuntime("junit5")
processing {
incremental(true)
annotations("com.example.*")
}
}
sourceSets {
main {
java {
srcDirs("build/generated/source/proto/main/grpc")
srcDirs("build/generated/source/proto/main/java")
}
}
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:3.20.1" }
plugins {
grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.46.0" }
}
generateProtoTasks {
all()*.plugins { grpc {} }
}
}
io.micronaut.grpc:micronaut-grpc-server-runtime
で gRPC サーバが起動できる。
gRPC のサービスとメソッド・メッセージ作成
公式サンプルと同様だが、以下のように gRPC のサービスとメソッド・メッセージの作成を行った
./src/main/proto/helloworld.proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
proto ファイルができれば、コードを自動生成できるので以下コマンドで生成する。
./gradlew generateProt
これで、ファイルが生成されて、これないの GreeterImplBase という abstract class を実装することで、gRPC でメソッドをクライントに提供する事ができる。 ./build/generated/source/proto/main/grpc/helloworld/GreeterGrpc.java
gRPC メソッドの実装を行う
generate された GreeterImplBase を実装していく。
./src/main/java/com/example/GreetingEndpoint.java
package com.example;
import helloworld.GreeterGrpc;
import helloworld.HelloReply;
import helloworld.HelloRequest;
import io.grpc.stub.StreamObserver;
import io.micronaut.grpc.annotation.GrpcService;
@GrpcService
public class GreetingEndpoint extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> replyObserver) {
String value = request.getName();
HelloReply build = HelloReply.newBuilder()
.setMessage("Greeting: " + value)
.build();
replyObserver.onNext(build);
replyObserver.onCompleted();
}
}
簡単に説明すると、クライントから受け取ったメッセージの先頭にGreeting:
とつけて返すだけ。
起動周りの設定度、gRPC サーバ起動
port とか、メッセージサイズ数をちょっと変えてみたかったので、設定変更した ./src/main/resources/application.yml
micronaut:
server:
port: 8081
application:
name: grpc-server-sample
netty:
default:
allocator:
max-order: 3
grpc:
server:
port: 9001
max-inbound-message-size: 10
port: 8081
はクライアントのポートかぶるので変更した。使用することはない
起動は以下コマンド
./gradlew run
これでログに以下のように表示されれば OK
__ __ _ _
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
Micronaut (v3.7.1)
01:11:07.306 [main] INFO i.m.g.s.GrpcEmbeddedServerListener - GRPC started on port 9001
自分の環境だと http も起動していたが、使わないので放置
gRPC クライアントの構築
クライアント側はもちろん gRPC サーバを起動する必要がないので
io.micronaut.grpc:micronaut-grpc-server-runtime
が不要。ただ gRPC のライブラリは必要なので、以下を使う
io.micronaut.grpc:micronaut-grpc-client-runtime
./build.gradle
plugins {
id("com.github.johnrengelman.shadow") version "7.1.2"
id("io.micronaut.application") version "3.6.2"
id("com.google.protobuf") version "0.8.15"
}
version = "0.1"
group = "com.example"
repositories {
mavenCentral()
}
dependencies {
implementation("io.micronaut:micronaut-jackson-databind")
implementation("jakarta.annotation:jakarta.annotation-api")
runtimeOnly("ch.qos.logback:logback-classic")
implementation("io.micronaut:micronaut-validation")
implementation("io.micronaut.grpc:micronaut-grpc-client-runtime")
// lombok 便利なので追加した
annotationProcessor("org.projectlombok:lombok:1.18.24")
compileOnly("org.projectlombok:lombok:1.18.24")
}
application {
mainClass.set("com.example.Application")
}
java {
sourceCompatibility = JavaVersion.toVersion("17")
targetCompatibility = JavaVersion.toVersion("17")
}
graalvmNative.toolchainDetection = false
micronaut {
runtime("netty")
testRuntime("junit5")
processing {
incremental(true)
annotations("com.example.*")
}
}
sourceSets {
main {
java {
srcDirs("build/generated/source/proto/main/grpc")
srcDirs("build/generated/source/proto/main/java")
}
}
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:3.20.1" }
plugins {
grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.46.0" }
}
generateProtoTasks {
all()*.plugins { grpc {} }
}
}
#### クライアントに gRPC のサービスとメソッド・メッセージ作成の proto ファイルをコピーする
サーバで記述したものと同じファイルをクライアント側にも置く。
./src/main/proto/helloworld.proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
で、コードを generate しておく。
./gradlew generateProt
クライアントの Bean を作成する。
Bean を作らなくても、都度接続コードを記述すれば接続できるが面倒出し無駄な処理なので Bean 定義して、DI することにした。
./src/main/java/com/example/Clients.java
package com.example;
import helloworld.GreeterGrpc;
import io.grpc.ManagedChannel;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.grpc.annotation.GrpcChannel;
@Factory
public class Clients {
@Bean
GreeterGrpc.GreeterBlockingStub blockingStub(@GrpcChannel("greeter") ManagedChannel channel) {
return GreeterGrpc.newBlockingStub(channel);
}
}
Micronaut はio.micronaut:micronaut-inject
というのがあり、@Bean の代わりに@Inject というのが使えるが、Lombok を入れた際に@AllArgsConstructor が動かなくなったので、これは使わなかった。
gRPC サーバの接続情報定義
Clients.java に直接記述しても良いが、yaml でも定義できる。
./src/main/resources/application.yml
micronaut:
application:
name: grpc-client-sample
netty:
default:
allocator:
max-order: 3
grpc:
channels:
greeter:
address: "localhost:9001"
# TLS接続しないので、以下trueにしておく
plaintext: true
max-retry-attempts: 10
リモートコールする
特定のパスにアクセスしたら、リモートコールするよなコントローラを作成する
./src/main/java/com/example/HelloController.java
package com.example;
import helloworld.GreeterGrpc;
import helloworld.HelloReply;
import helloworld.HelloRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import lombok.AllArgsConstructor;
@AllArgsConstructor
@Controller("/hello")
public class HelloController {
private final GreeterGrpc.GreeterBlockingStub stub;
@Get("/world")
public String sayHello() {
HelloRequest request = HelloRequest.newBuilder()
.setName("Hello!!!") // 文字数制限してるので、長いとエラーとなるよ
.build();
return stub.sayHello(request).getMessage();
}
}
クライアントのアプリケーションも起動してみる
./gradlew run
以下のように localhost:8080 が起動すれば OK。gRPC は起動しない
__ __ _ _
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
Micronaut (v3.7.1)
01:30:34.404 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 398ms. Server Running: http://localhost:8080
http://localhost:8080/hello/world2
にアクセスしてみると、
gRPC Server: Hello!!!
となっていた。
gRPC Server:
というのが gRPC サーバ側、Hello!!!
がクライアントから送ったメッセージ。
ちゃんとサービス間通信ができていることが確認できた。
所感
- Micronaut の起動はとても高速で、開発する際にストレスが今の所少ない
- gRPC を用いることで、API 定義などクライアントが気にすることなく方に従って、内部のコード呼ぶかのようにサービス間の通信ができるので気にすることが少なくて良い
また別の機会に gRPC を使うメリットを深堀りしていく。データ圧縮による通信の高速化、効率化、型による恩恵など色々ある。
参考
https://micronaut-projects.github.io/micronaut-grpc/snapshot/guide/index.html