Micronaut gRPC

Integration between Micronaut and gRPC

Version: 5.2.1-SNAPSHOT

1 Introduction

This project allows building gRPC servers and clients with Micronaut.

Micronaut adds the following features to the gRPC experience:

  • Compile Time Dependency Injection (DI) and Aspect Oriented Programming (AOP)

  • Service Discovery and Registrations

  • Distributed Tracing

  • Cloud Native Configuration

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Getting Started

To get started you need first create a Micronaut project. The easiest way to do this is with the Micronaut Launch:

  • Go to Micronaut Launch

  • Select "gRPC Application" under "Application Type"

  • Choose a Language / Build System etc.

  • Click Generate

Replace java with kotlin or groovy to change language and the build flag with maven to use Maven instead.

Or alternatively you can create a project with curl:

curl --location --request GET 'https://launch.micronaut.io/create/grpc/demo?lang=JAVA&build=GRADLE' --output demo.zip
unzip demo.zip -d demo
cd demo

To manually setup gRPC you can create an application:

$ mn create-app helloworld

Then follow the below steps depending on the build system chosen.

Configuring Gradle

To configure Gradle, first apply the com.google.protobuf plugin:

plugins {
    ...
    alias(libs.plugins.protobuf)
}

Then configure the gRPC and protobuf plugins:

ext {
    grpcVersion = libs.versions.managed.grpc.asProvider().get()
    protobufVersion = libs.versions.managed.protobuf.asProvider().get()
}


sourceSets {
    main {
        java {
            srcDirs 'build/generated/source/proto/main/grpc'
            srcDirs 'build/generated/source/proto/main/java'
        }
    }
}
protobuf {
    protoc { artifact = "com.google.protobuf:protoc:$protobufVersion" }
    plugins {
        grpc { artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion" }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
        }
    }
}
If you are using JDK9 or above, in order to avoid issues javax packages, provide the following compiler option to the grpc plugin to skip these stubs: option '@generated=omit' (added on grpc-kava v1.64.0) See discussion here: issue 9179
    ...
    generateProtoTasks {
        all()*.plugins {
            grpc {
                option '@generated=omit'
            } }
    }

Use this configuration for Kotlin projects:

ext {
    grpcVersion = libs.versions.managed.grpc.asProvider().get()
    grpcKotlinVersion = libs.versions.managed.grpc.kotlin.get()
    protobufVersion = libs.versions.managed.protobuf.asProvider().get()
}

dependencies {
...
    implementation libs.managed.grpc.kotlin.stub
    implementation libs.managed.grpc.services
    compileOnly libs.managed.grpc.stub
    compileOnly libs.managed.protobuf.java
    compileOnly libs.javax.annotation.api
}


sourceSets {
    main {
        java {
            srcDirs 'build/generated/source/proto/main/grpc'
            srcDirs 'build/generated/source/proto/main/grpckt'
            srcDirs 'build/generated/source/proto/main/java'
            srcDirs 'build/generated/source/proto/main/reactor'
        }
    }
}

protobuf {
    protoc { artifact = "com.google.protobuf:protoc:$protobufVersion" }
    plugins {
        grpc { artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion" }
        grpckt { artifact = "io.grpc:protoc-gen-grpc-kotlin:${grpcKotlinVersion}:jdk8@jar" }
        reactor { artifact = "com.salesforce.servicelibs:reactor-grpc:$reactiveGrpcVersion" }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
            grpckt {}
            reactor {}
        }
    }
}

If you want to generate Reactor-based stubs with reactive-grpc, add the generator plugin and stub dependency alongside the standard gRPC code generation:

ext {
    reactiveGrpcVersion = libs.versions.reactive.grpc.get()
}

dependencies {
...
    implementation platform(libs.micronaut.reactor)
    implementation libs.reactive.grpc.stub
    implementation "io.projectreactor:reactor-core"
}

sourceSets {
    main {
        java {
            srcDirs 'build/generated/source/proto/main/reactor'
        }
    }
}

protobuf {
...
        reactor { artifact = "com.salesforce.servicelibs:reactor-grpc:$reactiveGrpcVersion" }
...
            reactor {}
}

Finally, add the following dependencies to your build:

For gRPC servers:

implementation("io.micronaut.grpc:micronaut-grpc-server-runtime")
<dependency>
    <groupId>io.micronaut.grpc</groupId>
    <artifactId>micronaut-grpc-server-runtime</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.grpc:micronaut-grpc-server-runtime",
]

For gRPC clients:

implementation("io.micronaut.grpc:micronaut-grpc-client-runtime")
<dependency>
    <groupId>io.micronaut.grpc</groupId>
    <artifactId>micronaut-grpc-client-runtime</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.grpc:micronaut-grpc-client-runtime",
]

If you wish to use gRPC standalone without the Micronaut HTTP server you should comment out the micronaut-http-server-netty dependency.

You can then run:

$ ./gradlew generateProto

To generate the Java sources from protobuf definitions in src/main/proto.

Configuring Maven

For Maven create a maven project first:

$ mn create-app helloworld --build

Then configure the Protobuf plugin appropriately:

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

You can then run:

$ ./mvnw generate-sources

To generate the Java sources from protobuf definitions in src/main/proto.

Defining a Protobuf File

Once you have the build setup you can define a Protobuf file for your gRPC service. For example:

src/main/proto/helloworld.proto
// Copyright 2015 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";

option java_multiple_files = true;
option java_package = "helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}
With the Micronaut 1.1 or above CLI you can generate a service with mn create-grpc-service helloworld which will create the proto file and class that implements the stub.

4 gRPC Server

Writing a Simple gRPC Server

To implement a gRPC server for the previously defined helloworld.proto definition you first need to generate the Java stubs using Gradle or Maven then create a class that extends from GreeterGrpc.GreeterImplBase.

For example:

import io.grpc.stub.StreamObserver;

import jakarta.inject.Singleton;

@Singleton
public class GreetingEndpoint extends GreeterGrpc.GreeterImplBase { // (1)

    private final GreetingService greetingService;

    // (2)
    public GreetingEndpoint(GreetingService greetingService) {
        this.greetingService = greetingService;
    }

    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
        // (3)
        final String message = greetingService.sayHello(request.getName());
        HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
        responseObserver.onNext(reply);
        responseObserver.onCompleted();
    }
}
import java

from io.grpc.stub import StreamObserver
from jakarta.inject import Singleton

from .GreetingService import GreetingService

# `helloworld` is both the Java package of the generated gRPC classes and the package of these
# Python sources, so the generated types are looked up by name instead of imported.
GreeterGrpc = java.type("helloworld.GreeterGrpc")
HelloReply = java.type("helloworld.HelloReply")
HelloRequest = java.type("helloworld.HelloRequest")

@Singleton
class GreetingEndpoint(GreeterGrpc.GreeterImplBase):  # (1)

    # (2)
    def __init__(self, greeting_service: GreetingService):
        self.greeting_service = greeting_service

    def sayHello(self, request: HelloRequest, response_observer: StreamObserver[HelloReply]) -> None:
        # (3)
        message = self.greeting_service.say_hello(request.getName())
        reply = HelloReply.newBuilder().setMessage(message).build()
        response_observer.onNext(reply)
        response_observer.onCompleted()
import jakarta.inject.Singleton

@Singleton // (1)
class GreetingEndpoint(val greetingService: GreetingService) : GreeterGrpcKt.GreeterCoroutineImplBase() { // (2)
    override suspend fun sayHello(request: HelloRequest): HelloReply {
        // (3)
        val message = greetingService.sayHello(request.name)
        val reply = HelloReply.newBuilder().setMessage(message).build()
        return reply
    }
}
import groovy.transform.CompileStatic
import io.grpc.stub.StreamObserver
import jakarta.inject.Singleton


@CompileStatic
@Singleton
class GreetingEndpoint extends GreeterGrpc.GreeterImplBase { // (1)

    final GreetingService greetingService

    // (2)
    GreetingEndpoint(GreetingService greetingService) {
        this.greetingService = greetingService
    }

    @Override
    void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
        // (3)
        HelloReply.newBuilder().with {
            message = greetingService.sayHello(request.name)
            responseObserver.onNext(build())
            responseObserver.onCompleted()
        }
    }
}
1 The class extends from GreeterGrpc.GreeterImplBase and is annotated with jakarta.inject.Singleton
2 You can dependency inject other beans into the implementation. In this case GreetingService is dependency injected.
3 The StreamObserver is used to send a response to the client.

If you generate Reactor bindings with reactive-grpc, Micronaut can register the generated BindableService implementation in exactly the same way. Implement the generated Reactor base class as a singleton bean:

@Singleton
public class ReactiveGreetingEndpoint extends ReactorReactiveGreeterGrpc.ReactiveGreeterImplBase {

    private final GreetingService greetingService;

    public ReactiveGreetingEndpoint(GreetingService greetingService) {
        this.greetingService = greetingService;
    }

    @Override
    public Mono<HelloReply> sayHello(Mono<HelloRequest> request) {
        return request.map(helloRequest ->
            HelloReply.newBuilder()
                .setMessage(greetingService.sayHello(helloRequest.getName()))
                .build()
        );
    }
}
@Singleton
class ReactiveGreetingEndpoint(ReactorReactiveGreeterGrpc.ReactiveGreeterImplBase):

    def __init__(self, greeting_service: GreetingService):
        self.greeting_service = greeting_service

    def sayHello(self, request: Mono[HelloRequest]) -> Mono[HelloReply]:
        return request.map(
            lambda hello_request: HelloReply.newBuilder()
                .setMessage(self.greeting_service.say_hello(hello_request.getName()))
                .build()
        )
@Singleton
class ReactiveGreetingEndpoint(private val greetingService: GreetingService) : ReactorReactiveGreeterGrpc.ReactiveGreeterImplBase() {

    override fun sayHello(request: Mono<HelloRequest>): Mono<HelloReply> =
        request.map { helloRequest ->
            HelloReply.newBuilder()
                .setMessage(greetingService.sayHello(helloRequest.name))
                .build()
        }
}
@CompileStatic
@Singleton
class ReactiveGreetingEndpoint extends ReactorReactiveGreeterGrpc.ReactiveGreeterImplBase {

    private final GreetingService greetingService

    ReactiveGreetingEndpoint(GreetingService greetingService) {
        this.greetingService = greetingService
    }

    @Override
    Mono<HelloReply> sayHello(Mono<HelloRequest> request) {
        request.map(helloRequest ->
            HelloReply.newBuilder()
                .setMessage(greetingService.sayHello(helloRequest.name))
                .build()
        )
    }
}

Running the gRPC Server

To run the server use the Application class or run ./gradlew run for Gradle or ./mvnw compile exec:exec for Maven.

The server by default runs on port 50051, however you can configure which port the server runs on by setting grpc.server.port to whichever value you wish (a value of ${random.port} will use a random port).

Configuring the gRPC Server

The server can be configured in a number of different ways. By default, Micronaut configures gRPC’s NettyServerBuilder via application.yml.

For example:

Configuring the gRPC server
grpc.server.port=8080
grpc.server.keep-alive-time=3h
grpc.server.max-inbound-message-size=1024
grpc.server.ssl.cert-chain=file://path/to/my.cert
grpc.server.ssl.private-key=classpath:my.key
grpc:
    server:
        port: 8080
        keep-alive-time: 3h
        max-inbound-message-size: 1024
        ssl:
            cert-chain: 'file://path/to/my.cert' # (1)
            private-key: 'classpath:my.key' # (2)
[grpc.server]
port = 8080
keep-alive-time = "3h"
max-inbound-message-size = 1024

[grpc.server.ssl]
cert-chain = "file://path/to/my.cert"
private-key = "classpath:my.key"
grpc {
  server {
    port = 8080
    keepAliveTime = "3h"
    maxInboundMessageSize = 1024
    ssl {
      certChain = "file://path/to/my.cert"
      privateKey = "classpath:my.key"
    }
  }
}
{
  grpc {
    server {
      port = 8080
      keep-alive-time = "3h"
      max-inbound-message-size = 1024
      ssl {
        cert-chain = "file://path/to/my.cert"
        private-key = "classpath:my.key"
      }
    }
  }
}
{
  "grpc": {
    "server": {
      "port": 8080,
      "keep-alive-time": "3h",
      "max-inbound-message-size": 1024,
      "ssl": {
        "cert-chain": "file://path/to/my.cert",
        "private-key": "classpath:my.key"
      }
    }
  }
}
1 Load the certificate from /path/to/my.cert file.
2 Load the private key from the classpath. The file should be in src/main/resources/my.key.

By default, the gRPC server will be enabled. To disable the gRPC server, set grpc.server.enabled to false.

For tests, add the io.micronaut.grpc:micronaut-grpc-inprocess module and set grpc.server.in-process-name to switch the embedded server and the injected @GrpcChannel("grpc-server") test channel to gRPC’s in-process transport instead of opening a TCP port.

Using the in-process server for tests
grpc.server.in-process-name=test-grpc
grpc:
    server:
        in-process-name: test-grpc
[grpc.server]
in-process-name = "test-grpc"
grpc {
  server {
    inProcessName = "test-grpc"
  }
}
{
  grpc {
    server {
      in-process-name = "test-grpc"
    }
  }
}
{
  "grpc": {
    "server": {
      "in-process-name": "test-grpc"
    }
  }
}

The in-process transport avoids port management and is managed by the Micronaut application lifecycle. SSL is not supported in this mode, and the support lives in the optional micronaut-grpc-inprocess module so the Netty-based server runtime does not require the extra dependency.

Alternatively if you prefer programmatic configuration you can write a BeanCreatedEventListener for example:

Configuring the ServerBuilder
import io.grpc.ServerBuilder;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import jakarta.inject.Singleton;

@Singleton
public class ServerBuilderListener implements BeanCreatedEventListener<ServerBuilder<?>> {

    @Override
    public ServerBuilder<?> onCreated(BeanCreatedEvent<ServerBuilder<?>> event) {
        final ServerBuilder<?> builder = event.getBean();
        builder.maxInboundMessageSize(1024);
        return builder;
    }
}
Configuring the ServerBuilder
from io.grpc import ServerBuilder
from jakarta.inject import Singleton
from micronaut.context.event import BeanCreatedEvent, BeanCreatedEventListener

@Singleton
class ServerBuilderListener(BeanCreatedEventListener[ServerBuilder]):

    def onCreated(self, event: BeanCreatedEvent[ServerBuilder]) -> ServerBuilder:
        builder = event.getBean()
        builder.maxInboundMessageSize(1024)
        return builder
Configuring the ServerBuilder
import io.grpc.ServerBuilder
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import jakarta.inject.Singleton

@Singleton
class ServerBuilderListener : BeanCreatedEventListener<ServerBuilder<*>> {

    override fun onCreated(event: BeanCreatedEvent<ServerBuilder<*>>): ServerBuilder<*> {
        val builder = event.bean
        builder.maxInboundMessageSize(1024)
        return builder
    }
}
Configuring the ServerBuilder
import groovy.transform.CompileStatic
import io.grpc.ServerBuilder
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import jakarta.inject.Singleton

@CompileStatic
@Singleton
class ServerBuilderListener implements BeanCreatedEventListener<ServerBuilder<?>> {

    @Override
    ServerBuilder<?> onCreated(BeanCreatedEvent<ServerBuilder<?>> event) {
        final ServerBuilder<?> builder = event.bean
        builder.maxInboundMessageSize(1024)
        builder
    }
}

Auto Injected Types

By default, the server will automatically be dependency injected with beans of the following types:

  • io.grpc.BindableService - Any services declared as beans

  • io.grpc.ServerInterceptor - Any interceptors declared as beans

  • io.grpc.ServerTransportFilter - Any transport filters declared as beans

In addition, by default the server will be setup to use Micronaut’s I/O executor service. Set grpc.server.executor to the name of an Executor bean to override that default from configuration.

Server Interceptor Ordering

To produce a consistent and predictable order of execution for server interceptors, it is required for the io.grpc.ServerInterceptor implementation to do one of the following:

Implement the io.micronaut.core.order.Ordered interface
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.micronaut.core.order.Ordered;
import jakarta.inject.Singleton;

@Singleton // (1)
public class CustomInterceptor implements ServerInterceptor, Ordered { // (2)
    @Override
    public <T, R> ServerCall.Listener<T> interceptCall(ServerCall<T, R> call,
                                                       Metadata headers,
                                                       ServerCallHandler<T, R> next) {
        return next.startCall(call, headers);
    }

    @Override
    public int getOrder() {
        return 10; // (3)
    }
}
Implement the io.micronaut.core.order.Ordered interface
from io.grpc import Metadata, ServerCall, ServerCallHandler, ServerInterceptor
from jakarta.inject import Singleton
from micronaut.core.order import Ordered

@Singleton  # (1)
class CustomInterceptor(ServerInterceptor, Ordered):  # (2)

    def interceptCall[T, R](
        self,
        call: ServerCall[T, R],
        headers: Metadata,
        next: ServerCallHandler[T, R],
    ) -> ServerCall.Listener[T]:
        return next.startCall(call, headers)

    def getOrder(self) -> int:
        return 10  # (3)
Implement the io.micronaut.core.order.Ordered interface
import io.grpc.Metadata
import io.grpc.ServerCall
import io.grpc.ServerCallHandler
import io.grpc.ServerInterceptor
import io.micronaut.core.order.Ordered
import jakarta.inject.Singleton

@Singleton // (1)
class CustomInterceptor : ServerInterceptor, Ordered { // (2)
    override fun <T, R> interceptCall(
        call: ServerCall<T, R>,
        headers: Metadata,
        next: ServerCallHandler<T, R>,
    ): ServerCall.Listener<T> {
        return next.startCall(call, headers)
    }

    override fun getOrder(): Int {
        return 10 // (3)
    }
}
Implement the io.micronaut.core.order.Ordered interface
import groovy.transform.CompileStatic
import io.grpc.Metadata
import io.grpc.ServerCall
import io.grpc.ServerCallHandler
import io.grpc.ServerInterceptor
import io.micronaut.core.order.Ordered
import jakarta.inject.Singleton

@CompileStatic
@Singleton // (1)
class CustomInterceptor implements ServerInterceptor, Ordered { // (2)
    @Override
    <T, R> ServerCall.Listener<T> interceptCall(ServerCall<T, R> call,
                                                Metadata headers,
                                                ServerCallHandler<T, R> next) {
        next.startCall(call, headers)
    }

    @Override
    int getOrder() {
        10 // (3)
    }
}
1 Declare the server interceptor as a bean to have it registered automatically
2 Implement Ordered in addition to ServerInterceptor
3 Provide the specified order of execution in the server interceptor chain
Wrap with io.micronaut.grpc.server.interceptor.OrderedServerInterceptor
import io.grpc.ServerInterceptor;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.grpc.server.interceptor.OrderedServerInterceptor;
import jakarta.inject.Singleton;

@Factory // (1)
public class ServerInterceptorFactory {

    @Bean // (2)
    @Singleton
    public ServerInterceptor customServerInterceptor() {
        return new OrderedServerInterceptor(new CustomInterceptor(), 10); // (3)
    }
}
Wrap with io.micronaut.grpc.server.interceptor.OrderedServerInterceptor
from io.grpc import ServerInterceptor
from jakarta.inject import Singleton
from micronaut.context.annotation import Bean, Factory
from micronaut.grpc.server.interceptor import OrderedServerInterceptor

from .CustomInterceptor import CustomInterceptor

@Factory  # (1)
class ServerInterceptorFactory:

    @Bean  # (2)
    @Singleton
    def custom_server_interceptor(self) -> ServerInterceptor:
        return OrderedServerInterceptor(CustomInterceptor(), 10)  # (3)
Wrap with io.micronaut.grpc.server.interceptor.OrderedServerInterceptor
import io.grpc.ServerInterceptor
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import io.micronaut.grpc.server.interceptor.OrderedServerInterceptor
import jakarta.inject.Singleton

@Factory // (1)
class ServerInterceptorFactory {

    @Bean // (2)
    @Singleton
    fun customServerInterceptor(): ServerInterceptor {
        return OrderedServerInterceptor(CustomInterceptor(), 10) // (3)
    }
}
Wrap with io.micronaut.grpc.server.interceptor.OrderedServerInterceptor
import groovy.transform.CompileStatic
import io.grpc.ServerInterceptor
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import io.micronaut.grpc.server.interceptor.OrderedServerInterceptor
import jakarta.inject.Singleton

@CompileStatic
@Factory // (1)
class ServerInterceptorFactory {

    @Bean // (2)
    @Singleton
    ServerInterceptor customServerInterceptor() {
        new OrderedServerInterceptor(new CustomInterceptor(), 10) // (3)
    }
}
1 Use a @Factory to create an instance of ServerInterceptor bean
2 Register the created sever interceptor as a bean
3 Wrap the CustomInterceptor with OrderedServerInterceptor and provide the order of execution

The order which is provided will dictate the order of execution of the interceptors when receiving the request message, and then the order will be reversed when sending the response message.

3 interceptors, with respective orders, 1, 2, and 3:
Request -> 1 -> 2 -> 3 -> business logic -> 3 -> 2 -> 1 -> Response

Health checks

gRPC Health checks

If the gRPC services dependency (io.grpc:grpc-services) is added, then gRPC health checks will be enabled.

To modify the status of a service, call the setStatus method on an instance of HealthServiceManager, for example:

import io.grpc.health.v1.HealthCheckResponse;
import io.grpc.protobuf.services.HealthStatusManager;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

import jakarta.inject.Singleton;

@Singleton
public class HealthService {

    private final HealthStatusManager healthStatusManager;

    public HealthService(@Nullable HealthStatusManager healthStatusManager) {
        this.healthStatusManager = healthStatusManager;
    }

    public void setStatus(@NonNull String serviceName, HealthCheckResponse.@NonNull ServingStatus status) {
        if (healthStatusManager != null) {
            healthStatusManager.setStatus(serviceName, status);
        }
    }
}
from io.grpc.health.v1 import HealthCheckResponse
from io.grpc.protobuf.services import HealthStatusManager
from jakarta.inject import Singleton

@Singleton
class HealthService:

    def __init__(self, health_status_manager: HealthStatusManager | None):
        self.health_status_manager = health_status_manager

    def set_status(self, service_name: str, status: HealthCheckResponse.ServingStatus) -> None:
        if self.health_status_manager is not None:
            self.health_status_manager.setStatus(service_name, status)
import io.grpc.health.v1.HealthCheckResponse.ServingStatus
import io.grpc.protobuf.services.HealthStatusManager
import jakarta.inject.Singleton

@Singleton
class HealthService(private val healthStatusManager: HealthStatusManager?) {

    fun setStatus(serviceName: String, status: ServingStatus) {
        healthStatusManager?.setStatus(serviceName, status)
    }
}
import io.grpc.health.v1.HealthCheckResponse
import io.grpc.protobuf.services.HealthStatusManager
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.annotation.Nullable
import jakarta.inject.Singleton


@Singleton
class HealthService {

    private final HealthStatusManager healthStatusManager

    HealthService(@Nullable HealthStatusManager healthStatusManager) {
        this.healthStatusManager = healthStatusManager
    }

    void setStatus(@NonNull String serviceName, @NonNull HealthCheckResponse.ServingStatus status) {
        healthStatusManager?.setStatus(serviceName, status)
    }
}

If you wish to disable the gRPC health check while still using the services dependency you can set the property grpc.server.health.enabled to false in your application configuration.

Management Health checks

If the management dependency (io.micronaut:micronaut-management) is added, then Micronaut’s Health Endpoint can be used to expose the health status of the gRPC server.

For example, if gRPC is running then the /health endpoint will return:

{
  "status": "UP",
  "details": {
     "grpc-server": {
       "name": "your-project-name",
       "status": "UP",
       "details": {
         "host": "localhost",
         "port": 5050
       }
     }
  },
 ...
}

If you wish to disable the Micronaut gRPC server health check while still using the management dependency you can set the property grpc.server.health.enabled to false in your application configuration.

Testing the Server

To test the server it is recommended that you use Micronaut Test.

For detailed instructions on how to set up Micronaut Test for Spock, JUnit 5, or Kotest see the documentation on the subject.

You can then define a blocking stub bean in your test sources. For example:

Defining Test Clients
@Factory
class Clients {

    @Bean
    GreeterGrpc.GreeterBlockingStub blockingStub(
        @GrpcChannel(GrpcServerChannel.NAME) ManagedChannel channel) { // (1)
        return GreeterGrpc.newBlockingStub( // (2)
            channel
        );
    }
}
Defining Test Clients
@Factory
class Clients:

    @Bean
    def blocking_stub(self, channel: Annotated[ManagedChannel, GrpcChannel(GrpcServerChannel.NAME)]) -> GreeterGrpc.GreeterBlockingStub:  # (1)
        return GreeterGrpc.newBlockingStub(channel)  # (2)
Defining Test Clients
@Factory
class Clients {

    @Bean
    fun greetingClient(@GrpcChannel(GrpcServerChannel.NAME) channel: ManagedChannel): GreeterGrpcKt.GreeterCoroutineStub = // (1)
        GreeterGrpcKt.GreeterCoroutineStub(channel) // (2)
}
Defining Test Clients
@Factory
class Clients {

    @Bean
    GreeterGrpc.GreeterBlockingStub blockingStub(
            @GrpcChannel(GrpcServerChannel.NAME) ManagedChannel channel) { // (1)
        GreeterGrpc.newBlockingStub( // (2)
                channel
        )
    }
}
1 A ManagedChannel is injected that can communicate with the server.
2 The generated gRPC client blocking stub is created.

The above example uses the @GrpcChannel annotation to inject a gRPC ManagedChannel that can communicate with the running server. This channel will be automatically be shutdown when the application shuts down.

Now that you have a test client, writing the test becomes trivial:

import io.grpc.ManagedChannel;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.grpc.annotation.GrpcChannel;
import io.micronaut.grpc.server.GrpcServerChannel;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;

import org.junit.jupiter.api.Test;

import jakarta.inject.Inject;

import static org.junit.jupiter.api.Assertions.assertEquals;

@MicronautTest // (1)
class GreetingEndpointTest {

    @Inject
    GreeterGrpc.GreeterBlockingStub blockingStub; // (2)

    @Test
    void testHelloWorld() {
        final HelloRequest request = HelloRequest.newBuilder() // (3)
            .setName("Fred")
            .build();
        assertEquals(
            "Hello Fred",
            blockingStub.sayHello(request)
                .getMessage()
        );
    }

}
import java
from typing import Annotated

from io.grpc import ManagedChannel
from jakarta.inject import Inject
from micronaut.context.annotation import Bean, Factory
from micronaut.grpc.annotation import GrpcChannel
from micronaut.grpc.server import GrpcServerChannel
from micronaut.test.extensions.junit5.annotation import MicronautTest
from org.junit.jupiter.api import Test

# `helloworld` is both the Java package of the generated gRPC classes and the package of these
# Python sources, so the generated types are looked up by name instead of imported.
GreeterGrpc = java.type("helloworld.GreeterGrpc")
HelloRequest = java.type("helloworld.HelloRequest")

@MicronautTest  # (1)
class GreetingEndpointTest:

    blocking_stub: Annotated[GreeterGrpc.GreeterBlockingStub, Inject]  # (2)

    @Test
    def test_hello_world(self):
        request = HelloRequest.newBuilder().setName("Fred").build()  # (3)
        assert self.blocking_stub.sayHello(request).getMessage() == "Hello Fred"
import io.grpc.ManagedChannel
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import io.micronaut.grpc.annotation.GrpcChannel
import io.micronaut.grpc.server.GrpcServerChannel
import io.micronaut.test.extensions.kotest5.annotation.MicronautTest

@MicronautTest // (1)
class GreetingEndpointTest(
    private val greetingClient: GreeterGrpcKt.GreeterCoroutineStub, // (2)
) : StringSpec({
    "returns a greeting response" {
        greetingClient.sayHello( // (3)
            HelloRequest.newBuilder().setName("Fred").build(),
        ).message shouldBe "Hello Fred"
    }
})
import io.grpc.ManagedChannel
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import io.micronaut.grpc.annotation.GrpcChannel
import io.micronaut.grpc.server.GrpcServerChannel
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification

@MicronautTest // (1)
class GreetingEndpointTest extends Specification {

    @Inject
    GreeterGrpc.GreeterBlockingStub blockingStub // (2)

    void "test greeting endpoint"() {
        given:
        HelloRequest request = HelloRequest.newBuilder().with { // (3)
            name = "Fred"
            build()
        }

        expect:
        blockingStub.sayHello(
                request
        ).message == 'Hello Fred'
    }
}
1 The test is annotated with @MicronautTest
2 The client stub is injected into the test
3 A request is sent and the response asserted.

5 gRPC Clients

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.

Configuring ManagedChannel Instances

The channel can be configured using properties defined under grpc.client by default.

For example, if you wish to disable secure communication:

grpc.client.plaintext=true
grpc.client.max-retry-attempts=10
grpc:
    client:
        plaintext: true
        max-retry-attempts: 10
[grpc.client]
plaintext = true
max-retry-attempts = 10
grpc {
  client {
    plaintext = true
    maxRetryAttempts = 10
  }
}
{
  grpc {
    client {
      plaintext = true
      max-retry-attempts = 10
    }
  }
}
{
  "grpc": {
    "client": {
      "plaintext": true,
      "max-retry-attempts": 10
    }
  }
}

Properties under grpc.client are global properties and are the defaults used unless named configuration exists under grpc.channels.[NAME].

Any property of the io.grpc.netty.NettyChannelBuilder type can be configured.

You can also disable all gRPC client channel injection, or disable a specific named client:

grpc.client.enabled=false
grpc:
    client:
        enabled: false
[grpc.client]
enabled = false
grpc {
  client {
    enabled = false
  }
}
{
  grpc {
    client {
      enabled = false
    }
  }
}
{
  "grpc": {
    "client": {
      "enabled": false
    }
  }
}
grpc.client.greeter.enabled=false
grpc:
    client:
        greeter:
            enabled: false
[grpc.client.greeter]
enabled = false
grpc {
  client {
    greeter {
      enabled = false
    }
  }
}
{
  grpc {
    client {
      greeter {
        enabled = false
      }
    }
  }
}
{
  "grpc": {
    "client": {
      "greeter": {
        "enabled": false
      }
    }
  }
}

When a client is disabled, optional or nullable @GrpcChannel injection points resolve to an empty value, while required injection points fail with a dependency injection error.

Alternatively if you prefer programmatic configuration you can write a BeanCreatedEventListener for example:

Configuring the NettyChannelBuilder
import io.grpc.ManagedChannelBuilder;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import jakarta.inject.Singleton;

@Singleton
public class ManagedChannelBuilderListener implements BeanCreatedEventListener<ManagedChannelBuilder<?>> {

    @Override
    public ManagedChannelBuilder<?> onCreated(BeanCreatedEvent<ManagedChannelBuilder<?>> event) {
        final ManagedChannelBuilder<?> channelBuilder = event.getBean();
        channelBuilder.maxInboundMessageSize(1024);
        return channelBuilder;
    }
}
Configuring the NettyChannelBuilder
from io.grpc import ManagedChannelBuilder
from jakarta.inject import Singleton
from micronaut.context.event import BeanCreatedEvent, BeanCreatedEventListener

@Singleton
class ManagedChannelBuilderListener(BeanCreatedEventListener[ManagedChannelBuilder]):

    def onCreated(self, event: BeanCreatedEvent[ManagedChannelBuilder]) -> ManagedChannelBuilder:
        channel_builder = event.getBean()
        channel_builder.maxInboundMessageSize(1024)
        return channel_builder
Configuring the NettyChannelBuilder
import io.grpc.ManagedChannelBuilder
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import jakarta.inject.Singleton

@Singleton
class ManagedChannelBuilderListener : BeanCreatedEventListener<ManagedChannelBuilder<*>> {

    override fun onCreated(event: BeanCreatedEvent<ManagedChannelBuilder<*>>): ManagedChannelBuilder<*> {
        val channelBuilder = event.bean
        channelBuilder.maxInboundMessageSize(1024)
        return channelBuilder
    }
}
Configuring the NettyChannelBuilder
import groovy.transform.CompileStatic
import io.grpc.ManagedChannelBuilder
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import jakarta.inject.Singleton

@CompileStatic
@Singleton
class ManagedChannelBuilderListener implements BeanCreatedEventListener<ManagedChannelBuilder<?>> {

    @Override
    ManagedChannelBuilder<?> onCreated(BeanCreatedEvent<ManagedChannelBuilder<?>> event) {
        final ManagedChannelBuilder<?> channelBuilder = event.bean
        channelBuilder.maxInboundMessageSize(1024)
        channelBuilder
    }
}

Auto Injected Types

By default, each channel will automatically be dependency injected with beans of the following types:

  • io.grpc.ClientInterceptor - Any client interceptors declared as beans

  • io.grpc.NameResolver - The configured name resolver

Creating Client Stub Beans

The value of the @GrpcChannel annotation can be used to specify the target server, the configuration for which can also be externalized:

@Factory
class ExternalizedClients {

    @Singleton
    GreeterGrpc.GreeterStub greeterStub(
        @GrpcChannel("https://${my.server}:${my.port}")
        ManagedChannel channel) {
        return GreeterGrpc.newStub(
            channel
        );
    }
}
@Factory
class ExternalizedClients:

    @Singleton
    def greeter_stub(
        self,
        channel: Annotated[ManagedChannel, GrpcChannel("https://${my.server}:${my.port}")]
    ) -> GreeterGrpc.GreeterStub:
        return GreeterGrpc.newStub(
            channel
        )
@Factory
class ExternalizedClients {

    @Singleton
    fun greeterStub(
        @GrpcChannel("https://\${my.server}:\${my.port}")
        channel: ManagedChannel,
    ): GreeterGrpc.GreeterStub {
        return GreeterGrpc.newStub(
            channel
        )
    }
}
@CompileStatic
@Factory
class ExternalizedClients {

    @Singleton
    GreeterGrpc.GreeterStub greeterStub(
            @GrpcChannel('https://${my.server}:${my.port}')
            ManagedChannel channel) {
        GreeterGrpc.newStub(
                channel
        )
    }
}

The above example requires that my.server and my.port are specified in application.yml (or via environment variables MY_SERVER and MY_PORT). You can also externalize this further into configuration and provide channel specific configuration.

For example given the following configuration:

grpc.channels.greeter.address=${my.server}:${my.port}
grpc.channels.greeter.plaintext=true
grpc.channels.greeter.max-retry-attempts=10
grpc:
    channels:
        greeter:
            address: '${my.server}:${my.port}'
            plaintext: true
            max-retry-attempts: 10
[grpc.channels.greeter]
address = "${my.server}:${my.port}"
plaintext = true
max-retry-attempts = 10
grpc {
  channels {
    greeter {
      address = "${my.server}:${my.port}"
      plaintext = true
      maxRetryAttempts = 10
    }
  }
}
{
  grpc {
    channels {
      greeter {
        address = "${my.server}:${my.port}"
        plaintext = true
        max-retry-attempts = 10
      }
    }
  }
}
{
  "grpc": {
    "channels": {
      "greeter": {
        "address": "${my.server}:${my.port}",
        "plaintext": true,
        "max-retry-attempts": 10
      }
    }
  }
}

You can then define the @GrpcChannel annotation as follows:

@Factory
class NamedChannelClients {

    @Singleton
    GreeterGrpc.GreeterStub greeterStub(
        @GrpcChannel("greeter")
        ManagedChannel channel) {
        return GreeterGrpc.newStub(
            channel
        );
    }
}
@Factory
class NamedChannelClients:

    @Singleton
    def greeter_stub(
        self,
        channel: Annotated[ManagedChannel, GrpcChannel("greeter")]
    ) -> GreeterGrpc.GreeterStub:
        return GreeterGrpc.newStub(
            channel
        )
@Factory
class NamedChannelClients {

    @Singleton
    fun greeterStub(
        @GrpcChannel("greeter")
        channel: ManagedChannel,
    ): GreeterGrpc.GreeterStub {
        return GreeterGrpc.newStub(
            channel
        )
    }
}
@CompileStatic
@Factory
class NamedChannelClients {

    @Singleton
    GreeterGrpc.GreeterStub greeterStub(
            @GrpcChannel("greeter")
            ManagedChannel channel) {
        GreeterGrpc.newStub(
                channel
        )
    }
}

The ID greeter is used to reference the configuration for grpc.channels.greeter.

Using service IDs in this way is the preferred way to set up gRPC clients, because it works nicely with Service Discovery (see the next section).

If you generate Reactor bindings with reactive-grpc, define the generated Reactor stub in the same kind of factory and inject the ManagedChannel with @GrpcChannel:

@Bean
ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub reactiveStub(
    @GrpcChannel(GrpcServerChannel.NAME) ManagedChannel channel) {
    return ReactorReactiveGreeterGrpc.newReactorStub(channel);
}
@Bean
def reactive_stub(self, channel: Annotated[ManagedChannel, GrpcChannel(GrpcServerChannel.NAME)]) -> ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub:
    return ReactorReactiveGreeterGrpc.newReactorStub(channel)
@Bean
fun reactiveStub(@GrpcChannel(GrpcServerChannel.NAME) channel: ManagedChannel): ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub =
    ReactorReactiveGreeterGrpc.newReactorStub(channel)
@Bean
ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub reactiveStub(
        @GrpcChannel(GrpcServerChannel.NAME) ManagedChannel channel) {
    ReactorReactiveGreeterGrpc.newReactorStub(channel)
}

The resulting stub can then be injected into your application or tests and used directly with Reactor operators:

@MicronautTest
class ReactiveGreetingEndpointTest {

    @Inject
    ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub reactiveStub;

    @Test
    void testReactiveHelloWorld() {
        String message = Mono.just(HelloRequest.newBuilder().setName("Fred").build())
            .transform(reactiveStub::sayHello)
            .map(HelloReply::getMessage)
            .block(Duration.ofSeconds(5));

        assertEquals("Hello Fred", message);
    }
}
@MicronautTest
class ReactiveGreetingEndpointTest:

    reactive_stub: Annotated[ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub, Inject]

    @Test
    def test_reactive_hello_world(self):
        message = self.reactive_stub.sayHello(Mono.just(HelloRequest.newBuilder().setName("Fred").build())) \
            .map(lambda reply: reply.getMessage()) \
            .block(Duration.ofSeconds(5))

        assert message == "Hello Fred"
@MicronautTest
class ReactiveGreetingEndpointTest {

    @Inject
    lateinit var reactiveStub: ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub

    @Test
    fun testReactiveHelloWorld() {
        val message = Mono.just(HelloRequest.newBuilder().setName("Fred").build())
            .transform(reactiveStub::sayHello)
            .map(HelloReply::getMessage)
            .block(Duration.ofSeconds(5))

        assertEquals("Hello Fred", message)
    }
}
@MicronautTest
class ReactiveGreetingEndpointTest extends Specification {

    @Inject
    ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub reactiveStub

    void "test reactive greeting endpoint"() {
        when:
        String message = Mono.just(HelloRequest.newBuilder().setName("Fred").build())
            .transform(reactiveStub::sayHello)
            .map(HelloReply::getMessage)
            .block(Duration.ofSeconds(5))

        then:
        message == "Hello Fred"
    }
}

6 Service Discovery

When using @GrpcChannel with a service ID without explicitly configuring the address of the service will trigger gRPC’s NameResolver and attempt to do service discovery.

The default strategy for this is to use DNS based discovery. So for example you can do:

@Factory
class DnsClients {

    @Singleton
    GreeterGrpc.GreeterStub greeterStub(
        @GrpcChannel("dns:///greeter")
        ManagedChannel channel) {
        return GreeterGrpc.newStub(
            channel
        );
    }
}
@Factory
class DnsClients:

    @Singleton
    def greeter_stub(
        self,
        channel: Annotated[ManagedChannel, GrpcChannel("dns:///greeter")]
    ) -> GreeterGrpc.GreeterStub:
        return GreeterGrpc.newStub(
            channel
        )
@Factory
class DnsClients {

    @Singleton
    fun greeterStub(
        @GrpcChannel("dns:///greeter")
        channel: ManagedChannel,
    ): GreeterGrpc.GreeterStub {
        return GreeterGrpc.newStub(
            channel
        )
    }
}
@CompileStatic
@Factory
class DnsClients {

    @Singleton
    GreeterGrpc.GreeterStub greeterStub(
            @GrpcChannel("dns:///greeter")
            ManagedChannel channel) {
        GreeterGrpc.newStub(
                channel
        )
    }
}

Where DNS has been configured to know the address of the greeter service.

Alternatively, if you prefer to use a service discovery server then you can use integration with Micronaut service discovery.

Service Discovery with Consul

You can use Micronaut’s built-in service discovery features with any supported server (Consul and Eureka currently).

The way in which this is done is the same as a regular Micronaut service.

Registering a gRPC Service with Consul

To register a gRPC service with Consul first add the micronaut-discovery-client dependency:

runtimeOnly("io.micronaut.discovery:micronaut-discovery-client")
<dependency>
    <groupId>io.micronaut.discovery</groupId>
    <artifactId>micronaut-discovery-client</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.discovery:micronaut-discovery-client",
]

Then setup Consul correctly:

micronaut.application.name=greeter
consul.client.registration.enabled=true
consul.client.defaultZone=${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}
micronaut:
    application:
        name: greeter
consul:
    client:
        registration:
            enabled: true
        defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
[micronaut.application]
name = "greeter"

[consul.client]
defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"

[consul.client.registration]
enabled = true
micronaut {
  application {
    name = "greeter"
  }
}
consul {
  client {
    registration {
      enabled = true
    }
    defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
  }
}
{
  micronaut {
    application {
      name = "greeter"
    }
  }
  consul {
    client {
      registration {
        enabled = true
      }
      defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "greeter"
    }
  },
  "consul": {
    "client": {
      "registration": {
        "enabled": true
      },
      "defaultZone": "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
    }
  }
}

When using Service Discovery, Micronaut will register the service in Consul using the name defined in micronaut.application.name. If the application also uses an HTTP Server (Netty, Tomcat,…​), Micronaut will register the application with the same name and a different port in Consul. In case you want to use a different name for the gRPC service in Consul:

micronaut.application.name=greeter
grpc.server.instance-id=hello-grpc
micronaut:
    application:
        name: greeter # (1)
grpc:
    server:
        instance-id: 'hello-grpc' # (2)
[micronaut.application]
name = "greeter"

[grpc.server]
instance-id = "hello-grpc"
micronaut {
  application {
    name = "greeter"
  }
}
grpc {
  server {
    instanceId = "hello-grpc"
  }
}
{
  micronaut {
    application {
      name = "greeter"
    }
  }
  grpc {
    server {
      instance-id = "hello-grpc"
    }
  }
}
{
  "micronaut": {
    "application": {
      "name": "greeter"
    }
  },
  "grpc": {
    "server": {
      "instance-id": "hello-grpc"
    }
  }
}
1 The HTTP port will be registered in Consul with the name greeter
2 The gRPC port will be registered in Consul with the name hello-grpc

Discoverying Services via Consul

To discovery services via Consul and the Micronaut DiscoveryClient abstraction enable Consul and gRPC service discovery:

grpc.client.discovery.enabled=true
consul.client.defaultZone=${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}
grpc:
    client:
        discovery:
            enabled: true
consul:
    client:
        defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
[grpc.client.discovery]
enabled = true

[consul.client]
defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
grpc {
  client {
    discovery {
      enabled = true
    }
  }
}
consul {
  client {
    defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
  }
}
{
  grpc {
    client {
      discovery {
        enabled = true
      }
    }
  }
  consul {
    client {
      defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
    }
  }
}
{
  "grpc": {
    "client": {
      "discovery": {
        "enabled": true
      }
    }
  },
  "consul": {
    "client": {
      "defaultZone": "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
    }
  }
}

Then use the value greeter to discover the service when injecting the channel:

@Factory
class DiscoveryClients {

    @Singleton
    @Bean
    GreeterGrpc.GreeterStub greeterStub(
        @GrpcChannel("greeter")
        ManagedChannel channel) {
        return GreeterGrpc.newStub(
            channel
        );
    }
}
@Factory
class DiscoveryClients:

    @Singleton
    @Bean
    def greeter_stub(
        self,
        channel: Annotated[ManagedChannel, GrpcChannel("greeter")]
    ) -> GreeterGrpc.GreeterStub:
        return GreeterGrpc.newStub(
            channel
        )
@Factory
class DiscoveryClients {

    @Singleton
    @Bean
    fun greeterStub(
        @GrpcChannel("greeter")
        channel: ManagedChannel,
    ): GreeterGrpc.GreeterStub {
        return GreeterGrpc.newStub(
            channel
        )
    }
}
@CompileStatic
@Factory
class DiscoveryClients {

    @Singleton
    @Bean
    GreeterGrpc.GreeterStub greeterStub(
            @GrpcChannel("greeter")
            ManagedChannel channel) {
        GreeterGrpc.newStub(
                channel
        )
    }
}

7 Distributed Tracing

Micronaut gRPC includes legacy OpenTracing-based tracing support. For new applications using Micronaut Tracing, add the OpenTelemetry gRPC integration:

implementation("io.micronaut.tracing:micronaut-tracing-opentelemetry-grpc")
<dependency>
    <groupId>io.micronaut.tracing</groupId>
    <artifactId>micronaut-tracing-opentelemetry-grpc</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.tracing:micronaut-tracing-opentelemetry-grpc",
]

Micronaut Tracing will then create spans for gRPC client requests, server requests, client responses, and server responses.

To export traces to Google Cloud Trace, add the Google Cloud OpenTelemetry exporter:

runtimeOnly("com.google.cloud.opentelemetry:exporter-auto")
<dependency>
    <groupId>com.google.cloud.opentelemetry</groupId>
    <artifactId>exporter-auto</artifactId>
    <scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "com.google.cloud.opentelemetry:exporter-auto",
]

Then configure OpenTelemetry to use the google_cloud_trace exporter:

otel.traces.exporter=google_cloud_trace
otel:
  traces:
    exporter: google_cloud_trace
[otel.traces]
exporter = "google_cloud_trace"
otel {
  traces {
    exporter = "google_cloud_trace"
  }
}
{
  otel {
    traces {
      exporter = "google_cloud_trace"
    }
  }
}
{
  "otel": {
    "traces": {
      "exporter": "google_cloud_trace"
    }
  }
}

When running outside Google Cloud, provide Application Default Credentials with the GOOGLE_APPLICATION_CREDENTIALS environment variable (or gcloud auth application-default login) and set GOOGLE_CLOUD_PROJECT if the project cannot be detected automatically.

For additional exporter options and authentication details, see the Micronaut Tracing OpenTelemetry exporter documentation.

8 Protocol Buffers Support

This project also includes a module that adds the ability to encode and decode Protocol buffers messages with the Micronaut HTTP server.

To use this adds the micronaut-protobuff-support dependency:

implementation("io.micronaut.grpc:micronaut-protobuff-support")
<dependency>
    <groupId>io.micronaut.grpc</groupId>
    <artifactId>micronaut-protobuff-support</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.grpc:micronaut-protobuff-support",
]

Micronaut will now support the encoding and decoding requests / responses of type application/x-protobuf.

9 Protocol Buffers Json Support (Experimental)

This project also includes a module that adds the ability to send json-serialized messages via a POST HTTP 1.1 call with the Micronaut HTTP server.

To use this add the micronaut-protobuff-json-support dependency:

implementation("io.micronaut.grpc:micronaut-protobuff-json-support")
<dependency>
    <groupId>io.micronaut.grpc</groupId>
    <artifactId>micronaut-protobuff-json-support</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
    "io.micronaut.grpc:micronaut-protobuff-json-support",
]

grpc.rest.json.exposed=true

And then include the @GrpcRestJsonExposed annotation on your gRPC service definition:

import io.grpc.stub.StreamObserver;
import io.micronaut.grpc.annotation.GrpcRestJsonExposed;
import jakarta.inject.Singleton;
import org.example.grpc.GreeterGrpc;
import org.example.grpc.HelloRequest;
import org.example.grpc.HelloResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Singleton
public class GreeterService extends GreeterGrpc.GreeterImplBase {
    private static final Logger LOG = LoggerFactory.getLogger(GreeterService.class);

    @GrpcRestJsonExposed
    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
        LOG.debug("Received request: {}", request);
        String name = request.getName();
        String greeting = "Hello, " + name;

        HelloResponse reply = HelloResponse.newBuilder()
            .setGreeting(greeting)
            .build();

        responseObserver.onNext(reply);
        responseObserver.onCompleted();
    }
}
import logging

from io.grpc.stub import StreamObserver
from jakarta.inject import Singleton
from micronaut.grpc.annotation import GrpcRestJsonExposed
from org.example.grpc import GreeterGrpc, HelloRequest, HelloResponse

LOG = logging.getLogger(__name__)

@Singleton
class GreeterService(GreeterGrpc.GreeterImplBase):

    @GrpcRestJsonExposed
    def sayHello(self, request: HelloRequest, response_observer: StreamObserver[HelloResponse]) -> None:
        LOG.debug("Received request: %s", request)
        name = request.getName()
        greeting = "Hello, " + name

        reply = HelloResponse.newBuilder().setGreeting(greeting).build()

        response_observer.onNext(reply)
        response_observer.onCompleted()
import io.grpc.stub.StreamObserver
import io.micronaut.grpc.annotation.GrpcRestJsonExposed
import jakarta.inject.Singleton
import org.example.grpc.GreeterGrpc
import org.example.grpc.HelloRequest
import org.example.grpc.HelloResponse
import org.slf4j.LoggerFactory

@Singleton
class GreeterService : GreeterGrpc.GreeterImplBase() {

    @GrpcRestJsonExposed
    override fun sayHello(request: HelloRequest, responseObserver: StreamObserver<HelloResponse>) {
        LOG.debug("Received request: {}", request)
        val name = request.name
        val greeting = "Hello, $name"

        val reply = HelloResponse.newBuilder()
            .setGreeting(greeting)
            .build()

        responseObserver.onNext(reply)
        responseObserver.onCompleted()
    }

    companion object {
        private val LOG = LoggerFactory.getLogger(GreeterService::class.java)
    }
}
import groovy.transform.CompileStatic
import io.grpc.stub.StreamObserver
import io.micronaut.grpc.annotation.GrpcRestJsonExposed
import jakarta.inject.Singleton
import org.example.grpc.GreeterGrpc
import org.example.grpc.HelloRequest
import org.example.grpc.HelloResponse
import org.slf4j.Logger
import org.slf4j.LoggerFactory

@CompileStatic
@Singleton
class GreeterService extends GreeterGrpc.GreeterImplBase {
    private static final Logger LOG = LoggerFactory.getLogger(GreeterService)

    @GrpcRestJsonExposed
    @Override
    void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
        LOG.debug("Received request: {}", request)
        String name = request.name
        String greeting = "Hello, " + name

        HelloResponse reply = HelloResponse.newBuilder()
            .setGreeting(greeting)
            .build()

        responseObserver.onNext(reply)
        responseObserver.onCompleted()
    }
}

Micronaut will now support the encoding and decoding requests / responses of type application/json via http 1.1.

How this works

Controller Creation

  1. After the controller is created, the post creation annotation will traverse the beans created with the @GrpcRestJsonExposed annotation on the service method.

  2. For each bean, peek into the gRPC allocated methods created.

  3. Register the bean in a cache

The controller endpoint will use google’s built-in JSON/ProtocolBuffer serialization. Unfortunately, there is not yet a way to print a full API schema for a request. However, to create a json request output one can take any gRPC request protocol buffer and convert it to json using the io.micronaut.protobuf.json.ProtobufJsonTranscoder class.

Usage

Request comes in and matches as:

/grpc-json/{serviceName}/{method}

where the serviceName is the name of the gRPC service and the method is the name of the method.

Here’s an example of a gRPC service definition.

For example:

syntax = "proto3";
package greeter;

option java_package = "org.example.grpc";
option java_outer_classname = "GreeterProto";
option java_multiple_files = true;

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string greeting = 1;
}

service Greeter {
  rpc sayHello (HelloRequest) returns (HelloResponse);
}

Mapping this to a grpc service, the request would look like this:

Where GreeterService is the service name and sayHello is the method name.

The payload would look like this:

{"name": "YourName"}

Take in a POST as application/json and the request should cleanly map as a grpc response.

Example client usage with cURL

If you run the test-suite-protobuff-json-java project and issue the following cURL call:

curl -X POST http://localhost:8080/grpc-json/GreeterService/sayHello \
-H "Content-Type: application/json" \
-d '{"name": "YourName"}'

10 Repository

You can find the source code of this project in this repository: