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
Micronaut gRPC
Integration between Micronaut and gRPC
Version: 4.8.0
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
:
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:
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 {} }
}
}
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'
}
}
}
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" }
}
generateProtoTasks {
all()*.plugins {
grpc {}
grpckt {}
}
}
}
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>
For gRPC clients:
implementation("io.micronaut.grpc:micronaut-grpc-client-runtime")
<dependency>
<groupId>io.micronaut.grpc</groupId>
<artifactId>micronaut-grpc-client-runtime</artifactId>
</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>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:
// 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 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()
}
}
}
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
}
}
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. |
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:
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:
/*
* 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 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;
}
}
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:
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 |
io.micronaut.grpc.server.interceptor.OrderedServerInterceptor
@Factory (1)
public class ServerInterceptorFactory {
@Bean (2)
@Singleton
public ServerInterceptor customServerInterceptor() {
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.
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 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)
}
}
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)
}
}
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 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:
@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:
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:
/*
* 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 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;
}
}
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("https://${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 set up 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:
runtimeOnly("io.micronaut.discovery:micronaut-discovery-client")
<dependency>
<groupId>io.micronaut.discovery</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.tracing:micronaut-tracing-brave-http")
<dependency>
<groupId>io.micronaut.tracing</groupId>
<artifactId>micronaut-tracing-brave-http</artifactId>
</dependency>
runtimeOnly("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 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>
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: