grpc-ecosystem/grpc-spring を使ってみる

# Java と gRPC 界隈のこれまでの現状

Java, Kotlin ユーザーの皆様、gRPC は使っているでしょうか？

JVM 言語の上で gRPC を使う際、[grpc-java](https://github.com/grpc/grpc-java) が提供されていますが、これは Java 上で netty を使って単体で動くもので、Spring Boot との親和性が皆無でした。

https://github.com/grpc/grpc-java

Google 社内では Spring Boot を使っていないであろうから良いのでしょうが、我々一般の Java 開発者にとっては不自由なものです。

これまでは、有志からいくつかの Spring Boot の上で grpc-java を動かすライブラリが提供されていましたが、どれも個人が開発しているもの ([LogNet/grpc-spring-boot-starter](https://github.com/LogNet/grpc-spring-boot-starter) など) で、プロジェクトが放置気味だったり品質が微妙だったり、良い選択肢があまりありませんでした。

# grpc-ecosystem/grpc-spring 爆誕

そんな中、少し前に [grpc-ecosystem organization](https://github.com/grpc-ecosystem) に grpc-spring が追加されました。

これからは、この Spring Boot 実装が gRPC 公認のコミュニティプロジェクトとしてメンテされることになる様です。

https://github.com/grpc-ecosystem/grpc-spring

この実装は元々、[yidongnan/grpc-spring-boot-starter](https://github.com/yidongnan/grpc-spring-boot-starter) として開発されていたもので、中々対応されていなかった Spring Boot 3 対応を機に grpc-ecosystem へ移管された模様。

きっとこれからは、Java + Spring Boot で gRPC というと grpc-spring が標準的になっていくのでしょう。

英語のドキュメントも整備されています: [gRPC-Spring-Boot-Starter Documentation](https://grpc-ecosystem.github.io/grpc-spring/en/)

# サンプルコードを動かしてみる

早速、grpc-spring がどんな感じで使えるか見ていきましょう。

[サンプルコード](https://github.com/grpc-ecosystem/grpc-spring/tree/master/examples)に沿って、以下のようなシンプルな gRPC サービス `MyService` を実装していきます。

```proto
syntax = "proto3";

package net.devh.boot.grpc.example;

option java_multiple_files = true;
option java_package = "net.devh.boot.grpc.examples.lib";
option java_outer_classname = "HelloWorldProto";

// gRPC サービスの定義
service MyService {
    rpc SayHello (HelloRequest) returns (HelloReply) {
    }
}

// リクエストメッセージの定義
message HelloRequest {
    string name = 1;
}

// レスポンスメッセージの定義
message HelloReply {
    string message = 1;
}
```

## gRPC Server

まずサーバー側のコードですが、Gradle の設定や protoc plugin などは、公式ドキュメントの [Getting started](https://grpc-ecosystem.github.io/grpc-spring/en/server/getting-started.html) に詳しく書かれているので割愛して、肝となる部分を見ていきましょう。

https://grpc-ecosystem.github.io/grpc-spring/en/server/getting-started.html

実際のサンプルコードは、[grpc-spring/examples/local-grpc-server](https://github.com/grpc-ecosystem/grpc-spring/tree/master/examples/local-grpc-server) にあります。

基本的にやることは4つで

1. protoc が生成した `MyServiceGrpc.MyServiceImplBase` を継承したクラス `MyServiceImpl` を定義する
1. `@GrpcService` アノテーションを `MyServiceImpl` に追加する
1. `MyServiceImpl` が、Spring に Bean としてスキャンされる様にする
1. 実際の gRPC のサービスメソッドを実装する

つまり、proto をコンパイルして生成されたクラスを継承したクラスを書いて、そこに `@GrpcService` アノテーションをつけるだけです。


```Java
import example.HelloReply;
import example.HelloRequest;
import example.MyServiceGrpc;

import io.grpc.stub.StreamObserver;

import net.devh.boot.grpc.server.service.GrpcService;

@GrpcService
public class MyServiceImpl extends MyServiceGrpc.MyServiceImplBase {
    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
        HelloReply reply = HelloReply.newBuilder()
                .setMessage("Hello ==> " + request.getName())
                .build();
        responseObserver.onNext(reply);
        responseObserver.onCompleted();
    }
}
```

この状態で Spring Boot を立ち上げると、デフォルトでは 9090 ポートに gRPC サーバーが立ち上がっています。

ポートを変更したい場合には、Spring Boot のプロパティで以下の様に設定を変更できます。
ただし、他の Spring が利用しているポートと同じポートを利用することはできません。同じアプリケーションで、Spring MVC や Webflux を利用している場合でも、gRPC は別のポートで立ち上げる必要があります。


```yaml:application.yml
grpc:
  server:
    port: 9898

```

実際に、[gRPCurl](https://github.com/fullstorydev/grpcurl) などの gRPC デバッグツールを利用して API を叩いてみましょう。

```shell
$ grpcurl --plaintext -d '{"name": "test"}' localhost:9090 net.devh.boot.grpc.example.MyService/sayHello
{
  "message": "Hello ==> test"
}
```

### Exception handler

`@GrpcAdvice` アノテーションを付与したクラスを定義して、その中に `@GrpcExceptionHandler` アノテーションを付与したメソッドを定義すると、引数の型に対応した例外ハンドラが書ける様です。

gRPC は、[独自のステータスコード](https://grpc.github.io/grpc/core/md_doc_statuscodes.html)を元にエラーを返すので、アプリケーションが定義する例外から `Status` または `StatusException` への変換を書くことになります。


```java
@GrpcAdvice
public class GrpcExceptionAdvice {
    @GrpcExceptionHandler
    public Status handleInvalidArgument(IllegalArgumentException e) {
        return Status.INVALID_ARGUMENT.withDescription("Your description").withCause(e);
    }

    @GrpcExceptionHandler(ResourceNotFoundException.class)
    public StatusException handleResourceNotFoundException(ResourceNotFoundException e) {
        Status status = Status.NOT_FOUND.withDescription("Your description").withCause(e);
        Metadata metadata = ...
        return status.asException(metadata);
    }
}
```



### その他

Spring Security にも対応している様です。詳しくは、[Server Security](https://grpc-ecosystem.github.io/grpc-spring/en/server/security.html) あたりを参照。

また、grpc-java にはいくつかの Flavor が用意されていて、ここではブロッキングする通常のクラスを使っていますが、Reactor, RxJava, Kotlin Coroutines に対応した非同期の Flavor も用意されています。
grpc-spring はこれらにも対応してくれているので、非同期の gRPC サーバーも簡単に書く事ができます。詳しくは [gRPC-Java Flavors](https://grpc-ecosystem.github.io/grpc-spring/en/flavors.html) を参照してください。


## gRPC Client

では、クライアント側の実装も、[Getting started](https://grpc-ecosystem.github.io/grpc-spring/en/client/getting-started.html) に書かれている手順を元に抜粋して見ていきましょう。

https://grpc-ecosystem.github.io/grpc-spring/en/client/getting-started.html

実際のサンプルコードは、[grpc-spring/examples/local-grpc-client](https://github.com/grpc-ecosystem/grpc-spring/blob/master/examples/local-grpc-client) にあります。

クライアントも手順はとても簡単で、以下の様な手順になるでしょう

1. protoc が生成した gRPC サービスのクライアントスタブ、ここでは `MyServiceBlockingStub` を、利用するサービスクラスにフィールドとして定義する
1. 定義したスタブのフィールドに `@GrpcClient` アノテーションを付与する
1. アノテーションで定義した名前を元に、Spring Boot のプロパティで接続先等を設定する

他にも、[`@GrpcClientBean` を利用してクライアントスタブを注入](https://grpc-ecosystem.github.io/grpc-spring/en/client/configuration.html#grpcclientbean)する方法がある様ですが、ここでは割愛します。

利用するサービスクラスは以下の様な形で、クライアントスタブを利用します。


```java
import example.HelloRequest;
import example.MyServiceGrpc.MyServiceBlockingStub;

import net.devh.boot.grpc.client.inject.GrpcClient;

import org.springframework.stereotype.Service;

@Service
public class FoobarService {
    @GrpcClient("myService")
    private MyServiceBlockingStub myServiceStub;

    public String receiveGreeting(String name) {
        HelloRequest request = HelloRequest.newBuilder()
                .setName(name)
                .build();
        return myServiceStub.sayHello(request).getMessage();
    }
}

```

さらに、以下のように、接続先等の Spring Boot のプロパティを設定します。

`myService` の部分が、`@GrpcClient` のアノテーションで定義した名前と対応することになります。


```yaml:application.yml
grpc:
  client:
    myService:
      address: 'static://127.0.0.1:9898'
      enableKeepAlive: true
      keepAliveWithoutCalls: true
      negotiationType: plaintext
```


address の `static://` の部分は、DNS や様々なサービスディスカバリなど、他の様々なターゲットを利用できる様になっているためのようです。詳しくは、[Choosing the Target](https://grpc-ecosystem.github.io/grpc-spring/en/client/configuration.html#choosing-the-target) を参照。

### その他

クライアントも、非同期のクライアントスタブに一部標準で対応している様ですが、Reactive 対応や Coroutines 対応のクライアントスタブを利用するためは、`StubFactory` の実装が必要の様です。

https://grpc-ecosystem.github.io/grpc-spring/en/client/configuration.html#stubfactory


# まとめ

Java で gRPC 公認の Spring Boot 実装が出てきて、gRPC を実装するライブラリに迷わなくて済む時代になりました。実装も簡単です。

今後メンテされなくなるという点も、公式コミュニティプロジェクト化に伴いある程度は払拭されたので、今後は機会があれば grpc-spring も使っていきたい所です。

---

### ここから宣伝

じゃあ、お前はお仕事で gRPC を実装するためにどんなライブラリ使ってるの？と言われると、LINE が開発している [Armeria](https://armeria.dev/) を使っています。

https://armeria.dev/docs/server-grpc

だって、gRPC だって Swagger のように Web UI でドキュメントを生成してデバッグしたいじゃん？REST API と gRPC を同じポートで共存させたいじゃん？堅牢なマイクロサービスを構築するためには多機能なデコレーターが必要じゃん？といった要望には、既存の grpc-java をベースとしたライブラリではいろいろ不足しているからです。

grpc-spring より高機能なので、こちらも使ってみてくださいね。