Micronaut GRPC

Integration between Micronaut and GRPC

Version: 1.0.0.BUILD-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

Release History

1.0.0.RC1

  • Upgrade to GRPC 1.19.0

  • Upgrade to Protobuf 3.7.0

  • Upgrade to Open Tracing GRPC 0.0.14

1.0.0.M1

  • Initial Release

2 Getting Started

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

With Micronaut 1.1 or above you can use the GRPC profile:

$ mn create-app helloworld --profile grpc --lang java --build gradle
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.5'
}

Then configure the GRPC and protobuf plugins:

ext {
    protocVersion="3.5.1"
    grpcVersion="1.17.1"
}

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

Finally add the following dependencies to your build:

compile 'io.micronaut.grpc:micronaut-grpc-runtime:1.0.0.BUILD-SNAPSHOT'
<dependency>
    <groupId>io.micronaut.grpc</groupId>
    <artifactId>micronaut-grpc-runtime</artifactId>
    <version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>

If you wish to use GRPC standalone without the Micronaut HTTP server you should comment ouf 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.

3 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 io.grpc.stub.StreamObserver
import javax.inject.Singleton

@Singleton (1)
class GreetingEndpoint(val greetingService : GreetingService) : GreeterGrpc.GreeterImplBase() { (2)
    override fun sayHello(request: HelloRequest, responseObserver: StreamObserver<HelloReply>) {
    	(3)
        val message = greetingService.sayHello(request.name)
        val reply = HelloReply.newBuilder().setMessage(message).build()
        responseObserver.onNext(reply)
        responseObserver.onCompleted()
    }
}
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 -1 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
        cert-chain: '/path/to/my.cert'
        private-key: '/path/to/my.key'

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

Configuring the ServerBuilder
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.

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.

4 GRPC Clients

Micronaut for GRPC does not create client beans automatically for you. Instead you must expose which client stubs you 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
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: 'http://${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).

5 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}"

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
    );
}

6 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:

compile 'io.micronaut:micronaut-tracing'
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-tracing</artifactId>
</dependency>

runtime 'io.opentracing.contrib:opentracing-grpc:0.0.10'
<dependency>
    <groupId>io.opentracing.contrib</groupId>
    <artifactId>opentracing-grpc</artifactId>
    <version>0.0.10</version>
    <scope>runtime</scope>
</dependency>

You then need to configure either Jaeger or Zipkin appropriately.

7 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:

compile 'io.micronaut.grpc:micronaut-protobuff-support:1.0.0.BUILD-SNAPSHOT'
<dependency>
    <groupId>io.micronaut.grpc</groupId>
    <artifactId>micronaut-protobuff-support</artifactId>
    <version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>

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