GRPC Client

This section describes core concepts that Spring gRPC uses on the client side.

Create a Channel

You can inject a GrpcChannelFactory into your application configuration and use it to create a gRPC channel. The most common usage of a channel is to create a client that binds to a service. The Protobuf-generated sources in your project will contain the stub classes, and they just need to be bound to a channel. The Protobuf files will be provided by the service you are connecting to. For example, consider the SimpleGrpc service generated by the Protobuf tooling in your build. To bind to this service on a local server:

@Bean
SimpleGrpc.SimpleBlockingStub stub(GrpcChannelFactory channels) {
	return SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:9090"));
}

Shaded Netty Client

The default client implementation uses the Netty client. You can switch to a shaded Netty implementation provided by the gRPC team by adding the grpc-netty-shaded dependency and excluding the grpc-netty dependency.

<dependency>
	<groupId>org.springframework.grpc</groupId>
	<artifactId>spring-grpc-spring-boot-starter</artifactId>
	<exclusions>
		<exclusion>
			<groupId>io.grpc</groupId>
			<artifactId>grpc-netty</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
	<groupId>io.grpc</groupId>
	<artifactId>grpc-netty-shaded</artifactId>
</dependency>

For Gradle users

dependencies {
	implementation "org.springframework.grpc:spring-grpc-spring-boot-starter"
	implementation 'io.grpc:grpc-netty-shaded'
	modules {
		module("io.grpc:grpc-netty") {
			replacedBy("io.grpc:grpc-netty-shaded", "Use Netty shaded instead of regular Netty")
		}
	}
}

Channel Configuration

The channel factory provides an API to create channels. The channel creation process can be configured as follows.

Channel Builder Customizer

The ManagedChannelBuilder used by the factory to create the channel can be customized prior to channel creation.

Global

To customize the builder used for all created channels you can register one more GrpcChannelBuilderCustomizer beans. The customizers are applied to the auto-configured GrpcChannelFactory in order according to their bean natural ordering (i.e. @Order).

@Bean
@Order(100)
GrpcChannelBuilderCustomizer<NettyChannelBuilder> flowControlCustomizer() {
    return (name, builder) -> builder.flowControlWindow(1024 * 1024);
}

@Bean
@Order(200)
<T extends ManagedChannelBuilder<T>> GrpcChannelBuilderCustomizer<T> retryChannelCustomizer() {
	return (name, builder) -> builder.enableRetry().maxRetryAttempts(5);
}

In the preceding example, the flowControlCustomizer customizer is applied prior to the retryChannelCustomizer. Furthermore, the flowControlCustomizer is only applied if the auto-configured channel factory is a NettyGrpcChannelFactory.

Per-channel

To customize an individual channel you can specify a GrpcChannelBuilderCustomizer on the options passed to the factory during channel creation. The per-channel customizer will be applied after any global customizers.

@Bean
SimpleGrpc.SimpleBlockingStub stub(GrpcChannelFactory channelFactory) {
    ChannelBuilderOptions options = ChannelBuilderOptions.defaults()
            .withCustomizer((__, b) -> b.disableRetry());
    ManagedChannel channel = channelFactory.createChannel("localhost", options);
    return SimpleGrpc.newBlockingStub(channel);
}

The above example disables retries for the single created channel only.

While the channel builder customizer gives you full access to the native channel builder, you should not call build on the customized builder as the channel factory handles the build call for you and doing so will create orphaned channels.

Application Properties

The default GrpcChannelFactory implementation can also create a "named" channel, which you can then use to extract the configuration to connect to the server. For example:

@Bean
SimpleGrpc.SimpleBlockingStub stub(GrpcChannelFactory channels) {
	return SimpleGrpc.newBlockingStub(channels.createChannel("local"));
}

then in application.properties:

spring.grpc.client.channels.local.address=0.0.0.0:9090

There is a default named channel that you can configure as spring.grpc.client.default-channel.*, and then it will be used by default if there is no channel with the name specified in the channel creation.

The Local Server Port

If you are running a gRPC server locally as part of your application, you will often want to connect to it in an integration test. It can be convenient in that case to use an ephemeral port for the server (spring.grpc.server.port=0) and then use the port that is allocated to connect to it. You can discover the port that the server is running on by injecting the @LocalGrpcPort bean into your test. The @Bean has to be marked as @Lazy to ensure that the port is available when the bean is created (it is only known when the server starts which is part of the startup process).

@Bean
@Lazy
SimpleGrpc.SimpleBlockingStub stub(GrpcChannelFactory channels, @LocalGrpcPort int port) {
	return SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:" + port));
}

The channel can be configured via application.properties as well, by using the `${local.grpc.port} property placeholder. The @Bean where you create the stub must still be @Lazy for the same reason as above. For example:

spring.grpc.client.channels.local.address=0.0.0.0:${local.grpc.port}

Client Interceptors

Global

To add a client interceptor to be applied to all created channels you can simply register a client interceptor bean and then annotate it with @GlobalClientInterceptor. When you register multiple interceptor beans they are ordered according to their bean natural ordering (i.e. @Order).

@Bean
@Order(100)
@GlobalClientInterceptor
ClientInterceptor globalLoggingInterceptor() {
    return new LoggingInterceptor();
}

@Bean
@Order(200)
@GlobalClientInterceptor
ClientInterceptor globalExtraThingsInterceptor() {
    return new ExtraThingsInterceptor();
}

In the preceding example, the globalLoggingInterceptor customizer is applied prior to the globalExtraThingsInterceptor.

Per-Channel

To add one or more client interceptors to be applied to a single client channel you can simply set the interceptor instance(s) on the options passed to the channel factory when creating the channel.

@Bean
SimpleGrpc.SimpleBlockingStub stub(GrpcChannelFactory channelFactory) {
    ClientInterceptor interceptor1 = getChannelInterceptor1();
    ClientInterceptor interceptor2 = getChannelInterceptor2();
    ChannelBuilderOptions options = ChannelBuilderOptions.defaults()
            .withInterceptors(List.of(interceptor1, interceptor2));
    ManagedChannel channel = channelFactory.createChannel("localhost", options);
    return SimpleGrpc.newBlockingStub(channel);
}

The above example applies interceptor1 then interceptor2 to the single created channel.

While the channel builder customizer gives you full access to the native channel builder, we recommend not calling intercept on the customized builder but rather set the per-channel interceptors using the ChannelBuilderOptions as described above. If you do call intercept directly on the builder then those interceptors will be applied before the above described global and per-channel interceptors.

Blended

When a channel is constructed with both global and per-channel interceptors, the global interceptors are first applied in their sorted order followed by the per-channel interceptors in their sorted order.

However, by setting the withInterceptorsMerge parameter on the ChannelBuilderOptions passed to the channel factory to "true" you can change this behavior so that the interceptors are all combined and then sorted according to their bean natural ordering (i.e. @Order or Ordered interface).

You can use this option if you want to add a per-client interceptor between global interceptors.

The per-channel interceptors you pass in must either be bean instances marked with @Order or regular objects that implement the Ordered interface to be properly merged/ordered with the global interceptors.

Observability

Spring gRPC provides an autoconfigured interceptor that can be used to provide observability to your gRPC clients.

Security

If your remote gRPC server expects requests to be authenticated you will need to configure the client to provide authentication credentials.

Mutual TLS

Mutual TLS (mTLS) is a security protocol that requires both the client and the server to present certificates to each other. A Spring gRPC client can use mTLS by configuring the client in application.properties. The mechanism is through the use of SSL Bundles (from Spring Boot). Here’s an example:

spring.grpc.client.channels.my-channel.ssl.bundle=sslclient
spring.grpc.client.channels.my-channel.negotiation-type=TLS
spring.ssl.bundle.jks.sslclient.keystore.location=classpath:client.jks
spring.ssl.bundle.jks.sslclient.keystore.password=secret
spring.ssl.bundle.jks.sslclient.keystore.type=JKS
spring.ssl.bundle.jks.sslclient.key.password=password

The first two lines configure a channel named my-channel so that it has an SSL bundle named sslclient. The rest is the configuration of the SSL bundle itself, in this case using JKS encoding (other options are available).

HTTP Headers

Spring gRPC provides a couple of interceptor that can be used to provide security to your gRPC clients. There is one for Basic HHTP authentication and one for OAuth2 (bearer tokens). Here’s an example of creating a channel that uses Basic HTTP authentication:

@Bean
@Lazy
Channel basic(GrpcChannelFactory channels) {
	return channels.createChannel("my-channel", ChannelBuilderOptions.defaults()
		.withInterceptors(List.of(new BasicAuthenticationInterceptor("user", "password"))));
}

Usage of the bearer token interceptor is similar. You can look at the implementation of those interceptors to see how to create your own for custom headers.