Skip to main content

MicronautとgRPCでサーバ・クライアント間通信を構築してみる

· 7 min read

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