Micronaut gRPC

Integration between Micronaut and gRPC

Version:

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.

See also the Examples in the repository that provides examples in Java, Kotlin and Groovy.

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 {
    ...
    id 'com.google.protobuf' version '0.8.14'
}

Then configure the gRPC and protobuf plugins:

ext {
    protocVersion="3.12.2"
    grpcVersion="1.30.0"
}

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

protobuf {
    protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" }
    plugins {
        grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" }
    }
    generateProtoTasks {
        all()*.plugins { grpc {} }
    }
}

Use this configuration for Kotlin projects:

ext {
    protocVersion="3.12.2"
    grpcVersion="1.29.0"
    grpcKotlinVersion="0.1.2"
}

dependencies {
...
    implementation("io.grpc:grpc-kotlin-stub:${grpcKotlinVersion}")
}


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

protobuf {
    protoc { artifact = "com.google.protobuf:protoc:${protocVersion}" }
    plugins {
        grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" }
        grpckt { artifact = "io.grpc:protoc-gen-grpc-kotlin:${grpcKotlinVersion}" }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
            grpckt {}
        }
    }
}

Finally add the following dependencies to your build:

For gRPC servers:

implementation("io.micronaut.grpc:micronaut-grpc-server-runtime:2.3.0")
<dependency>
    <groupId>io.micronaut.grpc</groupId>
    <artifactId>micronaut-grpc-server-runtime</artifactId>
    <version>2.3.0</version>
</dependency>

For gRPC clients:

implementation("io.micronaut.grpc:micronaut-grpc-client-runtime:2.3.0")
<dependency>
    <groupId>io.micronaut.grpc</groupId>
    <artifactId>micronaut-grpc-client-runtime</artifactId>
    <version>2.3.0</version>
</dependency>

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>com.github.os72</groupId>
  <artifactId>protoc-jar-maven-plugin</artifactId>
  <version>3.6.0.2</version>
  <executions>
    <execution>
      <phase>generate-sources</phase>
      <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:1.17.1</pluginArtifact>
              </outputTarget>
          </outputTargets>
        </configuration>
    </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 javax.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 groovy.transform.CompileStatic
import io.grpc.stub.StreamObserver
import javax.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()
        }
    }
}
import javax.inject.Singleton

@Singleton (1)
@Suppress("unused")
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
    }
}
1 The class extends from GreeterGrpc.GreeterImplBase and is annotated with javax.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.

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 be configured in a number of different ways. You can use the io.micronaut.grpc.server.GrpcServerConfiguration type to configure any property of gRPC’s NettyServerBuilder class via application.yml.

For example:

Configuring the gRPC server
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)
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.

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

Configuring the ServerBuilder
/*
 * Copyright 2017-2019 original 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
 *
 * https://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.
 */
package io.micronaut.grpc;

import io.grpc.ServerBuilder;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;

import javax.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;
    }
}

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.

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 io.micronaut.core.order.Ordered interface
@Singleton (1)
public class CustomInterceptor implements ServerInterceptor, Ordered { (2)
    ...

    @Override
    public int getOrder() {
        return 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
@Factory (1)
public class ServerInterceptorFactory {

    @Bean (2)
    @Singleton
    public ServerInterceptor customSeverInterceptor() {
        return 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

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 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 setup Micronaut Test for either Spock or JUnit 5 see the documentation on the subject.

You can then define a blocking stub bean in src/test/java. For example:

Defining Test Clients
@Factory
class Clients {
    @Bean
    GreeterGrpc.GreeterBlockingStub blockingStub(
            @GrpcChannel(GrpcServerChannel.NAME) ManagedChannel channel) { (1)
        return 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.*;
import io.micronaut.grpc.annotation.GrpcChannel;
import io.micronaut.grpc.server.GrpcServerChannel;
import io.micronaut.test.annotation.MicronautTest;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

import javax.inject.Inject;

@MicronautTest (1)
public 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()
        );
    }

}
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
        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.

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

Configuring the NettyChannelBuilder
/*
 * Copyright 2017-2019 original 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
 *
 * https://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.
 */
package io.micronaut.grpc;

import io.grpc.ManagedChannelBuilder;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;

import javax.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;
    }
}

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 Clients {
    @Singleton
    GreeterGrpc.GreeterStub reactiveStub(
        @GrpcChannel("http://${my.server}:${my.port}")
        ManagedChannel channel) {
        return 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}'
            plaintext: true
            max-retry-attempts: 10

You can then define the @GrpcChannel annotation as follows:

@Singleton
GreeterGrpc.GreeterStub reactiveStub(
    @GrpcChannel("greeter")
    ManagedChannel channel) {
    return 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 setup gRPC clients, because it works nicely with Service Discovery (see the next section).

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:

@GrpcChannel("dns://greeter")

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:

runtime("io.micronaut:micronaut-discovery-client")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-discovery-client</artifactId>
    <scope>runtime</scope>
</dependency>

Then setup Consul correctly:

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 (1)
grpc:
    server:
        instance-id: 'hello-grpc' (2)
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}"

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

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

7 Distributed Tracing

gRPC includes tracing based on OpenCensus, however if you wish to use Micronaut’s integration with Jaeger or Zipkin you can do so by adding the following dependencies:

implementation("io.micronaut:micronaut-tracing")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-tracing</artifactId>
</dependency>

runtime("io.opentracing.contrib:opentracing-grpc:0.2.1")
<dependency>
    <groupId>io.opentracing.contrib</groupId>
    <artifactId>opentracing-grpc</artifactId>
    <version>0.2.1</version>
    <scope>runtime</scope>
</dependency>

You then need to configure either Jaeger or Zipkin appropriately.

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 add the micronaut-protobuff-support dependency:

implementation("io.micronaut.grpc:micronaut-protobuff-support:2.3.0")
<dependency>
    <groupId>io.micronaut.grpc</groupId>
    <artifactId>micronaut-protobuff-support</artifactId>
    <version>2.3.0</version>
</dependency>

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

9 Repository

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