MicronautでgRPC

このエントリは2025/01/09現在の情報に基づいています。将来の機能追加や変更に伴い、記載事項からの乖離が発生する可能性があります。

gRPC Server/ClientをMicronautで作成するにあたっての自分用のメモ。公式ドキュメントは以下。執筆時点のMicronautのバージョンは4.7.3。

Micronaut gRPC
https://micronaut-projects.github.io/micronaut-grpc/snapshot/guide/index.html

目次

  1. gRPC Server
  2. gRPC Client
  3. 動作確認
  4. その他

gRPC Server

プロジェクト作成

Micronautの場合、gRPC Applicationの作成を明示的に指定するためのcreate-grpc-appというサブコマンドがあるので、それを利用する。

mn create-grpc-app --build=maven --jdk=21 --lang=java --test=junit dev.logicojp.micronaut.grpc.cat.grpc-cat-api

依存関係の追加

gRPC-servicesを追加しておく。執筆時点の最新は1.69.0。

<dependency>
  <groupId>io.grpc</groupId>
  <artifactId>grpc-services</artifactId>
</dependency>

protoファイルを基にしてコードを生成するにあたって、以下のいずれかの構成を追加しておく。

1. com.github.os72:protoc-jar-maven-plugin を使う場合

コード生成のための設定を追加しておく。

<plugin>
  <groupId>com.github.os72</groupId>
  <artifactId>protoc-jar-maven-plugin</artifactId>
  <executions>
      <execution>
          <goals>
          <goal>run</goal>
          </goals>
        <configuration>
          <addProtoSources>all</addProtoSources>
          <includeMavenTypes>direct</includeMavenTypes>
          <inputDirectories>
            <include>src/main/proto</include>
          </inputDirectories>
          <outputTargets>
            <outputTarget>
              <type>java</type>
            </outputTarget>
            <outputTarget>
              <type>grpc-java</type>
              <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}</pluginArtifact>
              </outputTarget>
          </outputTargets>
        </configuration>
      </execution>
  </executions>
</plugin>

2. org.xolstice.maven.plugins:protobuf-maven-plugin を使う場合

以下の設定を追加しておく。なお、mnコマンドで生成しているpom.xmlの場合、 com.github.os72:protoc-jar-maven-plugin が追加されていることがあるので、その場合には当該箇所を削除しておく。

<plugin>
  <groupId>org.xolstice.maven.plugins</groupId>
  <artifactId>protobuf-maven-plugin</artifactId>
  <version>${protobuf-plugin.version}</version>
  <configuration>
    <!--suppress UnresolvedMavenProperty -->
    <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
    <pluginId>grpc-java</pluginId>
    <!--suppress UnresolvedMavenProperty -->
    <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
  </configuration>
  <executions>
    <execution>
      <goals>
        <goal>compile</goal>
        <goal>compile-custom</goal>
      </goals>
    </execution>
  </executions>
</plugin>

${os.detected.classifier} を自動取得したいのであれば、以下のextensionを追加しておく。2025/01/09現在の最新バージョンは1.7.1

<extensions>
    <extension>
        <groupId>kr.motd.maven</groupId>
        <artifactId>os-maven-plugin</artifactId>
        <version>1.7.1</version>
    </extension>
</extensions>

Protoファイル

Protoファイルとして以下を利用する。

syntax = "proto3";
option java_multiple_files = true;
option java_package = "CatAPI";
option java_outer_classname = "GrpcCatApi";
option objc_class_prefix = "GrpcCatApi";
package CatAPI;
service CatService {
  rpc GetCat (CatRequest) returns (Cat) {}
}
message CatRequest {
  string chip_id = 1;
}
message Cat {
  string name = 1;
  int32 age = 2;
}

ここまでできると、protoファイルを基にコードを自動生成できる。

$ mvn generate-codes

自動生成されたCatServiceGrpc.javaで定義されているCatServiceImplBaseという抽象クラスを実装していく(org.xolstice.maven.plugins:protobuf-maven-plugincom.github.os72:protoc-jar-maven-pluginでは生成されるディレクトリが異なるが、中身は同じ)。

実装

protoファイルで定義したサービスのCatServiceにて、GetCatというメソッドを定義したので、このメソッドを実装(override)する。動作確認目的なので、NameとAgeを固定値にしている。

@GrpcService
public class CatEndpoint extends CatServiceGrpc.CatServiceImplBase {
    private final Logger log = Logger.getLogger(this.getClass().getName());
    @Override
    public void getCat(CatRequest request,
                       StreamObserver<Cat> responseObserver) {
        String chipId = request.getChipId();
        log.log(java.util.logging.Level.INFO, "Chip ID: " + chipId);
        Cat response = Cat.newBuilder()
                .setName("Tama")
                .setAge(3)
                .build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

あとはビルドすればOK。application.propertiesapplication.ymlでリスニングポートを指定していない場合、デフォルトではtcp/50051をリスニングポートとして利用する。

gRPC Client

プロジェクト作成

ClientはREST APIとして公開し、裏で先ほど作成したgRPC Serverと対話する構成にする。ただし、ClientからのResponseは、gRPC Serverの応答に加えて、Chip IDを折り返すようにしておく。

mnでプロジェクトを作成する場合、gRPC Serverではないので(create-grpc-appではなく)create-appというサブコマンド(一般的なREST API作成のためのサブコマンド)で作成する。

mn create-app --build=maven --jdk=21 --lang=java --test=junit dev.logicojp.micronaut.grpc.cat.grpc-cat-client

依存関係の追加

gRPC Clientとして動作するため、以下の依存関係を追加しておく必要がある。

<dependency>
  <groupId>io.micronaut.grpc</groupId>
  <artifactId>micronaut-grpc-client-runtime</artifactId>
</dependency>
<dependency>
  <groupId>io.grpc</groupId>
  <artifactId>grpc-services</artifactId>
</dependency>

protoファイルから自動生成するためのしくみはgRPC Serverと同じものを利用する。

Protoファイル

Severと同じprotoファイルを使う(protoファイルでインターフェースを定義しているので当たり前ではある)。

実装

以下のドキュメントに記載の通り、Client beanは自動作成されないので、@Factoryを使って明示的にStubを公開する必要がある。

Micronaut for gRPC does not create client beans automatically for you. Instead, you must expose which client stubs your application needs using a @Factory.
You can dependency inject a io.grpc.ManagedChannel into the factory. Each injected io.grpc.ManagedChannel will automatically be shutdown when the application shuts down.

https://micronaut-projects.github.io/micronaut-grpc/snapshot/guide/index.html#client

同期型、非同期型いずれのStubも作成できるが、今回は同期型 (Blockingあり) のStubを作る。gRPC Channelは、今回application.propertiesもしくはapplication.ymlに指定したアドレスを使っている。実運用するならEureka ServerやConsulあたりを使うことになるはず。

@Factory
public class CatServiceClientFactory {
    @Singleton
    CatServiceGrpc.CatServiceBlockingStub catServiceStub(
            @GrpcChannel("${grpc.channels.grpc-cat-api.address}")
            ManagedChannel channel ) {
        return CatServiceGrpc.newBlockingStub(channel);
    }
}

gRPC clientの入口は通常のREST APIなので、以下のような感じ。

  • Chip IDをQuery Parameterで渡す。
  • Chip IDがnullならgRPC Serverへの問い合わせをせずに返す。
@Controller("/api")
public class CatServiceClientController {
    @Inject
    private final CatServiceGrpc.CatServiceBlockingStub catServiceStub;
    private final Logger logger = Logger.getLogger(this.getClass().getName());
    public CatServiceClientController(CatServiceGrpc.CatServiceBlockingStub catServiceStub) {
        this.catServiceStub = catServiceStub;
    }
    @Get("/cat")
    @Produces(MediaType.APPLICATION_JSON)
    public CatResponse send(@Nullable @QueryValue(value = "chip") Optional<String> _chipId) {
        if(_chipId.isEmpty()) {
            logger.warning("No chipId provided");
            return new CatResponse("NO_CHIPID_PROVIDED", 0, "NO NAME");
        }
        String chipId = _chipId.get();
        logger.info("chipId: " + chipId);
        CatRequest request = CatRequest.newBuilder().setChipId(chipId).build();
        logger.info("request: " + request);
        Cat cat = catServiceStub.getCat(request);
        logger.info("response: " + cat);
        return new CatResponse(chipId, cat.getAge(), cat.getName());
    }
}

CatResponseクラスはrecordクラスであり、以下のような定義をしている。

@Serdeable
@Introspected
public record CatResponse(String chipId, int age, String name){}

あとはビルドしておしまい。

動作確認

gRPC Server単体

gRPCのテストクライアントを使って動作確認。Postmanでも何でもよいが、今回はApidogを使っている(実際のところ、chip_idとして文字列を指定していなくても動作するようにしている)。

gRPC Client – gRPC Server

作成したgRPC Clientを実行しておく。REST APIとして作成したので、ポートを明示的に指定していない限り、tcp/8080をリスニングポートとして利用する。こちらもApidogから呼びだしてみると、問題なく動作していることがわかる。

もしchipを指定していなければ以下のようにgRPC Serverまで到達しないで応答していることがわかる。

その他

Health Check

以下の依存関係を追加していればデフォルトで有効化されている。無効化するのであれば、application.propertiesもしくはapplication.ymlgrpc.server.health.enabledfalseに指定する。

<dependency>
  <groupId>io.grpc</groupId>
  <artifactId>grpc-services</artifactId>
</dependency>

詳細は以下のドキュメントを参照。

Health Checks
https://micronaut-projects.github.io/micronaut-grpc/snapshot/guide/index.html#_health_checks

Health Checkのprotoファイルは以下のURLにある。

GRPC Health Checking Protocol
https://github.com/grpc/grpc/blob/master/doc/health-checking.md

実際にgRPCテストクライアントで実行したら以下のような感じの応答が返る。

Server reflection

Micronautの場合、Server reflectionは2種類の方法がある。

1) 以下のIssueに記載の通りで、@Factoryで登録する方法。

gRPC api discovery by reflection #79
https://github.com/micronaut-projects/micronaut-grpc/issues/79

@Factory
class ReflectionFactory {
      @Singleton
       ProtoReflectionService reflectionService() {
              return ProtoReflectionService.newInstance();
       }
}

2) Micronaut.start() で指定してしまう方法。

public static void main(String[] args) {
    Micronaut.build(args)
            .banner(false)
            .singletons(ProtoReflectionServiceV1.newInstance())
            .start();
}

ポート番号の変更

Micronautの場合、gRPCのデフォルトポートはtcp/50051だが、もちろん変更可能。変更は grpc.server.port で実施する。

# application.propertiesの場合
grpc.server.port=(ポート番号)
# application.ymlの場合
grpc:
  server:
    port: (ポート番号)
# 環境変数での指定
GRPC_SERVER_PORT=(ポート番号)

時として、Micronaut HTTP Server(micronaut-http-server-netty)を依存関係に含めてしまっている場合があるが、Micronaut HTTP Serverの機能を使う必要がない場合、換言するとgRPCをスタンドアロンで使うのであれば、ドキュメントにも記載のある通り、依存関係 micronaut-http-server-netty を外すことが推奨される。

If you wish to use gRPC standalone without the Micronaut HTTP server you should comment out the micronaut-http-server-netty dependency.
Micronaut gRPC – https://micronaut-projects.github.io/micronaut-grpc/snapshot/guide/index.html#gettingStarted

Micronaut HTTP Serverが含まれている場合、Listeningするポートは、grpc.server.port(デフォルトはtcp/50051)とMicronaut HTTP Serverのポート(micronaut.server.port、デフォルトはtcp/8080)の2個。

GraalVM Native Image

これはいつも通りのPluginを設定してビルドすればOK。

  <profiles>
    <profile>
      <id>native</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.graalvm.buildtools</groupId>
            <artifactId>native-maven-plugin</artifactId>
            <version>${native.maven.plugin.version}</version>
            <extensions>true</extensions>
            <executions>
              <execution>
                <id>build-native</id>
                <goals>
                  <goal>compile-no-fork</goal>
                </goals>
                <phase>package</phase>
              </execution>
              <execution>
                <id>test-native</id>
                <goals>
                  <goal>test</goal>
                </goals>
                <phase>test</phase>
              </execution>
            </executions>
            <configuration>
              <imageName>${project.artifactId}</imageName>
              <skipNativeTests>true</skipNativeTests>
              <mainClass>${exec.mainClass}</mainClass>
              <buildArgs combine.children="append">
                <buildArg>--no-fallback</buildArg>
                <buildArg>-Ob</buildArg>
              </buildArgs>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください