GRPC Client

This section describes core concepts that Spring gRPC uses on the client side. 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.

Application Properties

You can inject a GrpcChannelFactory into your application configuration and use it to create a gRPC channel. The default GrpcChannelFactory implementation can 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.

Automatic Client Configuration

The automatic creation of beans for gRPC stubs is switched on by the @EnableGrpcClients annotation. A Spring Boot application will have one of these by default, but you can add your own safely if you want to change the defaults, for instance to change the base type of the stubs or to scan a different package. Spring gRPC will scan the application packages for gRPC stubs and automatically create bean definitions, as long as the default channel is configured via spring.grpc.client.default-channel.* (it needs at least a target URL to work).

Package Scanning

The @GrpcClient annotation can be used to control the scan for gRPC stub implementations. To scan a package you can specify the basePackages or basePackageClasses attribute. Then elsewhere in the application you can @Autowired the generated gRPC stubs (the blocking sub-type by default). You can change the factory used to create the stubs from BlockingStubFactory by setting the factory attribute. There are standard factories pre-registered for common stub types, and if you want to register additional factories you can use a GrpcClientRegistryCustomizer (see below for details).

The default behaviour in a Spring Boot application is equivalent to the following configuration on your @SpringBootApplication class:

// This is the default behaviour, so not necessary to add this annotation unless you change its attributes
@EnableGrpcClients(@GrpcClient(basePackageClasses = MyApplication.class))
@SpringBootApplication
class MyApplication {
	// ...
}

You can enhance and modify the configuration by providing spring.grpc.client.* application properties or by defining your own GrpcClientRegistryCustomizer beans. The customizer has full control over the scanning and registration of the gRPC clients, including for example the ability to change the base type of the stubs that are registered.

Register Individual Stub Types

The @GrpcClient has a types attribute if you want to register specific stub types instead of scanning a package. A GrpcClientRegistryCustomizer can also be used to control the registration of the gRPC clients in the application context, and the API is flexible enough to allow you to add your own behaviour that would not be possible with just the @GrpcClient annotation. For example, to add just one client stub using the default channel:

@Bean
GrpcClientRegistryCustomizer stubs() {
	return registry -> registry
		.register(SimpleGrpc.SimpleBlockingStub.class);
}

More Complex Examples

A GrpcClientRegistryCustomizer can also control the creation of the channels and add custom behaviour to stubs (individually or via a scan). For example, to add a custom security interceptor to only clients:

@Bean
GrpcClientRegistryCustomizer stubs(ObjectProvider<ClientRegistrationRepository> context) {
	return registry -> registry
		.channel("stub",
				ChannelBuilderOptions.defaults()
					.withInterceptors(List.of(new BearerTokenAuthenticationInterceptor(() -> token(context)))))
		.prefix("secure")
		.register(SimpleGrpc.SimpleBlockingStub.class);
}

In this example, instead of scanning for all stubs, we register a specific stub class SimpleGrpc.SimpleBlockingStub with the channel named stub. The prefix secure is used as a bean definition name prefix, so the resulting bean definition in this case is "secureSimpleBlockingStub". This feature is useful when you want to have multiple instances of the same stub class with different configurations.

N.B. The ClientRegistrationRepository in the example is injected via an ObjectProvider which is not called directly to avoid early instantiation. The customizer has to run very early in the application lifecycle, so you always want to follow this pattern if you need to inject depdendencies to build the channel options.

Create a Client Manually

Instead of using the @GrpcClient or GrpcClientRegistry features, we can create a client @Bean manually. The most common usage of a channel is to create a client that binds to a service. For example:

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

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}

You can’t use @LocalGrpcPort in a GrpcClientRegistryCustomizer because it is not available until the server starts. You can lazily resolve local.grpc.port in the customizer by using the Environment when the channel is created, either directly via its API or through placeholders like in the properties file example above.

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.

OAuth2 Clients

Spring gRPC provides an autoconfigured OAuth2 client that can be used to provide authentication to your gRPC clients. It works the same as in any Spring Boot application, in that if you configure properties in spring.security.oauth2.authorizationserver.client.* you will be able to inject an ClientRegistrationRepository and use it to create an OAuth2AuthorizedClient for a given client registration. Here’s an example showing how to plug the client registration into a BearerTokenAuthenticationInterceptor in the gRPC client:

@Bean
@Lazy
SimpleGrpc.SimpleBlockingStub basic(GrpcChannelFactory channels, ClientRegistrationRepository registry) {
	ClientRegistration reg = registry.findByRegistrationId("spring");
	return SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:9090", ChannelBuilderOptions.defaults()
		.withInterceptors(List.of(new BearerTokenAuthenticationInterceptor(() -> token(reg))))));
}

private String token(ClientRegistration reg) {
	RestClientClientCredentialsTokenResponseClient creds = new RestClientClientCredentialsTokenResponseClient();
	String token = creds.getTokenResponse(new OAuth2ClientCredentialsGrantRequest(reg))
		.getAccessToken()
		.getTokenValue();
	return token;
}