Document gRPC support

Closes gh-49291
This commit is contained in:
Phillip Webb
2026-04-21 20:03:28 -07:00
parent e62a0409c9
commit 3bafd4994e
51 changed files with 1843 additions and 7 deletions
@@ -161,6 +161,9 @@ public class AntoraAsciidocAttributes {
});
attributes.put("version-native-build-tools", (String) this.projectProperties.get("nativeBuildToolsVersion"));
attributes.put("version-graal", (String) this.projectProperties.get("graalVersion"));
attributes.put("version-protobuf-gradle-plugin",
(String) this.projectProperties.get("protobufGradlePluginVersion"));
addDependencyVersion(attributes, "grpc-api", "io.grpc:grpc-api");
addDependencyVersion(attributes, "jackson-annotations", "com.fasterxml.jackson.core:jackson-annotations");
addDependencyVersion(attributes, "jackson-core", "tools.jackson.core:jackson-core");
addDependencyVersion(attributes, "jackson-databind", "tools.jackson.core:jackson-databind");
@@ -26,6 +26,7 @@ url-gradle-docs-java-plugin={url-gradle-docs}/java_plugin.html
url-gradle-docs-war-plugin={url-gradle-docs}/war_plugin.html
url-gradle-dsl=https://docs.gradle.org/current/dsl
url-gradle-javadoc=https://docs.gradle.org/current/javadoc
url-grpc-api-javadoc=https://javadoc.io/doc/io.grpc/grpc-api/{version-grpc-api}
url-kotlin-docs-kotlin-plugin={url-kotlin-docs}/using-gradle.html
url-micrometer-docs-concepts={url-micrometer-docs}/concepts
url-micrometer-docs-implementations={url-micrometer-docs}/implementations
@@ -82,6 +83,8 @@ url-jackson-databind-javadoc=https://javadoc.io/doc/tools.jackson.core/jackson-d
url-jackson-dataformat-xml-javadoc=https://javadoc.io/doc/tools.jackson.dataformat/jackson-dataformat-xml/{version-jackson-dataformat-xml}
url-jackson2-databind-javadoc=https://javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/{version-jackson2-databind}
https://javadoc.io/doc/io.grpc/grpc-api/latest/io/grpc/package-summary.html
# === Javadoc Locations ===
javadoc-location-com-fasterxml-jackson-annotation={url-jackson-annotations-javadoc}
@@ -104,6 +107,7 @@ javadoc-location-org-springframework-data-rest={url-spring-data-rest-javadoc}
javadoc-location-tools-jackson-core={url-jackson-core-javadoc}
javadoc-location-tools-jackson-databind={url-jackson-databind-javadoc}
javadoc-location-tools-jackson-dataformat-xml={url-jackson-dataformat-xml-javadoc}
javadoc-location-io-grpc={url-grpc-api-javadoc}
# === API References ===
@@ -286,6 +286,7 @@ class AntoraAsciidocAttributesTests {
addMockJacksonCoreVersion(versions, "jackson-core", version);
addMockJacksonCoreVersion(versions, "jackson-databind", version);
addMockJacksonCoreVersion(versions, "jackson-databind", version);
versions.put("io.grpc:grpc-api", version);
versions.put("org.apache.pulsar:pulsar-client-api", version);
versions.put("tools.jackson.dataformat:jackson-dataformat-xml", version);
return versions;
@@ -149,6 +149,8 @@ dependencies {
implementation("ch.qos.logback:logback-classic")
implementation("com.redis:testcontainers-redis")
implementation("com.zaxxer:HikariCP")
implementation("io.grpc:grpc-stub")
implementation("io.grpc:grpc-netty")
implementation("io.micrometer:micrometer-jakarta9")
implementation("io.micrometer:micrometer-tracing")
implementation("io.micrometer:micrometer-registry-graphite")
@@ -0,0 +1,743 @@
[[io.grpc]]
= gRPC
Google Remote Procedure Call (gRPC) is a high-performance RPC framework that enables client-server communication using binary messages.
Spring Boot include support for developing and testing both client and server gRPC applications.
The underling message format used by gRPC is Protocol Buffers which allow messages to be created and consumed by a wide variety of programming languages.
[[io.grpc.servicedefinitions]]
== Service Definitions
To develop a gRPC application you first need a Protocol Buffers service definition file.
A `.proto` file defines the services and messages that your application can consume or provide.
Here's an example of a typical `.proto` file that uses https://protobuf.dev/programming-guides/proto3/[the `proto3` revision] of the protocol buffers language:
[,protobuf]
----
syntax = "proto3";
option java_package = "com.example.grpc.proto";
option java_multiple_files = true;
service HelloWorld {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
----
This file defines a `HelloWorld` service with a single method that accepts a `HelloReqest` message and return a `HelloReply` message.
The `HelloReqest` message contains a `name` string field.
The `HelloReply` message contains a `message` string field.
With the exception of a the `java_package` and `java_multiple_files` options, there is nothing in the `.proto` file that is specific to the Java programming langage.
[[io.grpc.servicedefinitions.generatingjavacode]]
=== Generating Java Code
Since `.proto` files are language agnostic, we need a process to convert them into usable Java code.
We can then use the generated code to either make a remote procedure call to running service, or implement the service ourselves so that others may call it.
The exact process you use to generate code will depend on your build system.
Spring Boot supports for both Maven and Gradle protobuf plugins, but you are free to use whatever solution works best for you.
[[io.grpc.servicedefinitions.generatingjavacode.maven]]
==== Using the Maven Plugin
Spring Boot include dependency management for the `io.github.ascopes:protobuf-maven-plugin` Maven plugin.
If you are using the the `spring-boot-starter-parent` POM, you'll also get sensible out-of-the-box configuration.
The following shows a typical Maven POM file that uses the plugin:
[source,xml,subs="verbatim,attributes"]
----
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>{version-spring-boot}</version>
</parent>
<groupId>com.example</groupId>
<artifactId>myproject</artifactId>
<version>0.0.1-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>io.github.ascopes</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
ifeval::["{build-and-artifact-release-type}" == "opensource-snapshot"]
<!-- you don't need this if you are using a release version -->
<repositories>
<repository>
<id>spring-snapshots</id>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
endif::[]
</project>
----
Since POM above extends `spring-boot-starter-parent`, you'll get the following:
* Configuration of the `protoc` version.
* Configuration of the `binary-maven` plugin.
* Execution configuration for the `generate` goal.
The `.proto` files should be added to `src/main/proto`.
TIP: If you don't use `spring-boot-starter-parent`, or you want to configure the plugin directly, refer to the https://ascopes.github.io/protobuf-maven-plugin[protobuf-maven-plugin documentation].
If you use Spring Boot's dependency management the `${protobuf-java.version}` and `${grpc-java.version}` properties will be useful.
[[io.grpc.servicedefinitions.generatingjavacode.gradle]]
==== Using the Gradle Plugin
Spring Boot include dependency management for the `com.google.protobuf:protobuf-gradle-plugin` Gradle plugin.
In addition, the `spring-boot-gradle-plugin` will react to the presence of the protobuf plugin and configure it appropriately.
The following shows a typical Gradle file that uses the plugin:
[source,gradle,subs="verbatim,attributes"]
----
plugins {
id 'java'
id 'org.springframework.boot' version '{version-spring-boot}'
id 'io.spring.dependency-management' version '{version-dependency-management-plugin}'
id 'com.google.protobuf' version '{version-protobuf-gradle-plugin}'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
ifeval::["{build-and-artifact-release-type}" == "opensource-snapshot"]
// you don't need this if you are using a release version
maven { url 'https://repo.spring.io/snapshot' }
endif::[]
}
----
Since this gradle file uses both the `org.springframework.boot` and `com.google.protobuf` plugins, you'll get the following:
* Configuration of the `protoc` version.
* Configuration of the `protoc-gen-grpc-java` version.
The `.proto` files should be added to `src/main/proto`.
TIP: If you don't use `org.springframework.boot` plugin, or you want to configure the plugin directly, refer to the https://github.com/google/protobuf-gradle-plugin[protobuf-gradle-plugin documentation].
[[io.grpc.server]]
== Writing a gRPC Server Application
Spring Boot provides a `spring-boot-grpc-server` module and a `spring-boot-starter-grpc-server` starter POM that you can use for server applications.
In order to write the actual server code, you'll need to extended one or more of base classes generated from your `.proto` file and expose them as Spring beans.
Spring gRPC will automatically expose any bean that implements javadoc:io.grpc.BindableService[] as a gRPC server.
Since all `.proto` generated classes implement javadoc:io.grpc.BindableService[], adding them as beans is enough to expose them over gRPC.
TIP: For more details see {url-spring-grpc-docs}/server.html#_create_a_grpc_service[the Spring gRPC documentation].
The following example shows how the `HelloWorld` service from the `.proto` file above could be implemented.
In this example, we're using the javadoc:org.springframework.grpc.server.service.GrpcService[format=annotation] annotation and assuming that the code is in a package that will be picked up by component scanning:
include-code::MyHelloWorldService[]
If the application makes used of `spring-boot-starter-grpc-server`, then Netty will be used as the server implementation listening on port `9090`.
You can test your application using https://github.com/fullstorydev/grpcurl[grpcurl]:
[source,shell]
----
$ grpcurl -d '{"name":"Spring"}' -plaintext localhost:9090 HelloWorld.SayHello
----
[source,json]
----
{
"message": "Hello 'Spring'"
}
----
[[io.grpc.server.netty-shaded]]
=== Switching to a Netty Shaded Server
If you find that the version of Netty provided by the `spring-boot-starter-grpc-server` starter POM isn't compatible with other libraries you use, you can switch to a "`shaded`" version.
To switch, you can excluded `io.grpc:grpc-netty` and include `io.grpc:grpc-netty-shaded`.
For example:
[tabs]
======
Maven::
+
[source,xml]
----
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-grpc-server</artifactId>
<exclusions>
<!-- Exclude the gRPC Netty dependency -->
<exclusion>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Use gRPC Netty Shaded instead -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
</dependency>
----
+
Gradle::
+
[source,gradle]
----
dependencies {
implementation('org.springframework.boot:spring-boot-starter-grpc-server') {
// Exclude the gRPC Netty dependency
exclude group: 'io.grpc', module: 'grpc-netty'
}
// Use gRPC Netty Shaded instead
implementation "io.grpc:grpc-netty-shaded"
}
----
+
======
[[io.grpc.server.servlet]]
=== Switching to a Servlet Container
It's possible to expose gRPC services using a regular Servlet Container such as Tomcat rather than using Netty.
To do so, your Servlet Container must be configured to support HTTP/2.
To switch to the Servlet gRPC implementation, you can exclude `io.grpc:grpc-netty` and include `io.grpc:grpc-servlet-jakarta`.
For example:
[tabs]
======
Maven::
+
[source,xml]
----
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-grpc-server</artifactId>
<exclusions>
<!-- Exclude the gRPC Netty dependency -->
<exclusion>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Use gRPC Servlet Jakarta instead -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-servlet-jakarta</artifactId>
</dependency>
----
+
Gradle::
+
[source,gradle]
----
dependencies {
implementation('org.springframework.boot:spring-boot-starter-webmvc') {
implementation('org.springframework.boot:spring-boot-starter-grpc-server') {
// Exclude the gRPC Netty dependency
exclude group: 'io.grpc', module: 'grpc-netty'
}
// Use gRPC Servlet Jakarta instead
implementation "io.grpc:grpc-servlet-jakarta"
}
----
+
======
TIP: Remember to include a Servlet Container dependency, for example using `spring-boot-starter-tomcat`, and to set `server.http2.enabled` to `true`.
NOTE: When using a servlet container, certain gRPC server configuration properties are not relevant and will be ignored.
For example, configprop:spring.grpc.server.port[] is ignored since configprop:server.port[] used used to set a web server port.
[[io.grpc.server.ssl]]
=== SSL Support
SSL can be configured for both `grpc-netty` and `grpc-netty-shaded` servers using SSL bundles.
See the xref:features/ssl.adoc[SSL core documentation] for details on how to declare an SSL bundle.
Once your bundle has been defined, you can use the following properties in your gRPC server application to use it:
[configprops,yaml]
----
spring:
grpc:
server:
ssl:
bundle: mysslbundle
----
Client authentication can also be configured by setting configprop:spring.grpc.server.ssl.client-auth[] to `optional` or `require`.
TIP: To temporarily disable server SSL support, for example to aid with testing, you can set configprop:spring.grpc.server.ssl.enabled[] to `false`.
[[io.grpc.server.in-process]]
=== Using an In-Process Server
You can run an in-process server by including the `io.grpc:grpc-inprocess` dependency on your classpath and defining a configprop:spring.grpc.server.inprocess.name[] property.
In this mode, the in-process server factory is auto-configured in addition to the regular server factory.
The name you provide can be used as a client channel target using the form `in-process:<name>`.
[[io.grpc.server.reflection]]
=== Reflection
When it's available, Spring Boot will auto-configure the https://grpc.io/docs/guides/reflection/[gRPC Reflection service].
This allows clients to browse the metadata of your services and download their `.proto` files.
The reflection service resides in the `io.grpc:grpc-services` library, which is an optional dependency.
You will need to add the dependency to your project in order for auto-configuration to apply.
TIP: If you have the `io.grpc:grpc-services` library but prefer that reflection isn't auto-configured, you can set configprop:spring.grpc.server.reflection.enabled[] to `false`.
[[io.grpc.server.health]]
=== Server Health
A gRPC server can provide health information using a standard service API (https://github.com/grpc/grpc-proto/blob/master/grpc/health/v1/health.proto[health/v1]).
This allows clients to check on the health of your server services a route traffic appropriately.
Spring Boot provides a bridge between its own `spring-boot-health` module and the standard gRPC health service.
Health information is provided whenever the `io.grpc:grpc-services` and `org.springframework.boot:spring-boot-health` modules are on your classpath.
TIP: If you don't want health indicators to be exposed, you can set configprop:spring.grpc.server.health.enabled[] to `false`.
[[io.grpc.server.health.service-mappings]]
==== Service Specific Health Mappings
By default, health information is provided for the overall server status (`""`) using all available health indicators.
It is also possible to provide fine-grained health information for specific services by including only a sub-set of health indicators.
Custom mapping and ordering rules can also be defined on a per-service basis.
For example, the following configuration will provide health for "`myservice`" using only the `db` and `redis` indicators.
[configprops,yaml]
----
spring:
grpc:
server:
health:
service:
myservice:
include:
- db
- redis
----
TIP: You can set configprop:spring.grpc.server.health.include-overall-health[] to `false` to disable the overall server status health if you only want to provide service-specific health.
[[io.grpc.server.health.push]]
==== Push Configuration
Unlike web-based health checks, gRPC health information is periodically pushed rather than pulled.
By default, the first health push happens 5 seconds after the application starts and then every subsequent 5 seconds.
To fine-tune this, you can use the following properties:
[configprops,yaml]
----
spring:
grpc:
server:
health:
schedule:
period: 5m
delay: 2s
----
TIP: You can also set configprop:spring.grpc.server.health.schedule.enabled[] to `false` if want to send health updates in some other way.
[[io.grpc.server.security]]
=== Securing gRPC Server Applications
[[io.grpc.server.security.netty]]
==== Netty Based Servers
Spring gRPC includes features that allow you to secure your Netty based server applications declaratively using Spring Security.
This follows similar patterns to those you would use to secure a regular web application.
Spring Boot provides auto-configuration for both javadoc:org.springframework.grpc.server.security.GrpcSecurity[] and javadoc:org.springframework.grpc.server.security.SecurityGrpcExceptionHandler[] beans.
Typically gRPC application are then secured using javadoc:org.springframework.security.access.prepost.PreAuthorize[format=annotation] annotations on your gRPC service beans, or a javadoc:org.springframework.grpc.server.security.AuthenticationProcessInterceptor[] bean.
For more details, please see {url-spring-grpc-docs}/server.html#_declarative_security_with_spring_security[the Spring gRPC documentation].
[[io.grpc.server.security.servlet]]
==== Servlet Container Based Servers
If your gRPC server is running xref:web/servlet.adoc[within a standard Servlet Container], you can use typical xref:web/spring-security.adoc#web.security.spring-mvc[web security configuration] to secure your application.
Spring Boot will auto-configre javadoc:org.springframework.boot.grpc.server.autoconfigure.GrpcServerExecutorProvider[] and javadoc:org.springframework.grpc.server.security.SecurityContextServerInterceptor[] beans to ensure that Spring Security works correctly.
Cross-Site Request Forgery (CSRF) protection is incompatible with the gRPC protocol and will be disabled by default for all gRPC requests.
If you prefer to configure your own CSRF protection, you can switch this off by setting configprop:spring.grpc.server.security.csrf.enabled[] to `false`.
To help with manual Security configuration, Spring Boot provides request matchers for gRPC services.
Matches are available for both servlet and reactive stacks.
For example, the following will include all gRPC services with the exception of "`special`".
include-code::MySecurityConfiguration[]
NOTE: For reactive matches use javadoc:org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest[format=full].
[[io.grpc.server.security.oauth-resource-server]]
==== OAuth2 Resource Server
https://oauth.net/2/[OAuth2] is a widely used authorization framework.
Spring Boot's OAuth2 Resource Server support in compatible with gRPC and may be configured in the usual way.
For details of how to configure an OAuth2 Resource Server to use with your gRPC server application, see the xref:security/oauth2.adoc#security.oauth2.server["`OAuth2`" section] of under "`Security`".
[[io.grpc.client]]
== Writing a gRPC Client Application
Spring Boot provides a `spring-boot-grpc-client` module and a `spring-boot-starter-grpc-client` starter POM that you can use for client applications.
Clients can call remote gRPC services by importing one or more of the "`stub`" classes generated from their `.proto` file.
You can use the javadoc:org.springframework.grpc.client.ImportGrpcClients[format=annotation] annotation to import the stub classes you want to use.
Each import includes a `target` which can either be a logical channel name, or the base URL of the remote server.
We typically recommend using channel names rather than hard-coding targets.
Here's a typical example:
include-code::MyApplication[]
NOTE: If you don't specify a `target` then "`default`" is used.
TIP: You can use the `basePackageClasses` or `basePackages` attribute of javadoc:org.springframework.grpc.client.ImportGrpcClients[format=annotation] to import all stubs in given package.
[[io.grpc.client.channel-properties]]
=== Channel Properties
When the `target` attribute of javadoc:org.springframework.grpc.client.ImportGrpcClients[format=annotation] uses a logical channel name, you'll need to provide some properties so that Spring Boot can find the actual gRPC server to call.
To do that, you can add an entry using `spring.grpc.channel.<name>.*` properties.
Typically, you'll configure the actual real `target`, along with any other settings that are unique to the channel.
For example, the following will configure `myservice` to use the real target of `static://grpc.example.com:9090`.
It also changes the keep-alive timeout and the maximum message size permitted:
[configprops,yaml]
----
spring:
grpc:
client:
channel:
myservice:
target: static://grpc.example.com:9090
inbound:
keepalive:
timeout: 40s
message:
max-size: 8MB
----
[[io.grpc.client.stub-beans]]
=== Using Stub Beans
With your javadoc:org.springframework.grpc.client.ImportGrpcClients[format=annotation] annotations in place, and your properties written, you can use stubs as you would any other bean.
For example, here's the `HelloWorldStub` being injected into a `ApplicationRunner` bean:
include-code::MyApplicationRunner[]
[[io.grpc.client.netty-shaded]]
=== Switching to Netty Shaded Client Transport
Under the hood, remote gRPC network calls are made using Netty.
If you find that the version of Netty provided by the `spring-boot-starter-grpc-client` starter POM isn't compatible with other libraries you use, you can switch to a "`shaded`" version.
To switch, you can excluded `io.grpc:grpc-netty` and include `io.grpc:grpc-netty-shaded`.
For example:
[tabs]
======
Maven::
+
[source,xml]
----
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-grpc-client</artifactId>
<exclusions>
<!-- Exclude the gRPC Netty dependency -->
<exclusion>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Use gRPC Netty Shaded instead -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
</dependency>
----
+
Gradle::
+
[source,gradle]
----
dependencies {
implementation('org.springframework.boot:spring-boot-starter-grpc-client') {
// Exclude the gRPC Netty dependency
exclude group: 'io.grpc', module: 'grpc-netty'
}
// Use gRPC Netty Shaded instead
implementation "io.grpc:grpc-netty-shaded"
}
----
+
======
[[io.grpc.client.ssl]]
=== SSL Support
Client gRPC applications can connect to gRPC services using SSL/TSL encrypted connections.
You can configure gRPC connections to use standard one-way-TLS, or mutual TLS
[[io.grpc.client.ssl.one-way]]
==== Standard one-way TLS
To use standard one-way TLS, you can set the `ssl.enabled` property to `true` in your channel properties.
For example, the following will enabled an SSL/TLS connection for the `myservice` channel:
[configprops,yaml]
----
spring:
grpc:
client:
channel:
myservice:
target: static://grpc.example.com:9090
ssl:
enabled: true
----
[[io.grpc.client.ssl.mutual]]
==== Mutual TLS
Mutual TLS (mTLS) is a security protocol that requires both the client and the server to present certificates to each other.
To use mutual TLS, you can set the `ssl.bundle` property in your channel properties.
See the xref:features/ssl.adoc[SSL core features documentation] for details on how to declare an SSL bundle.
Here is an example the configures the `myservice` channel to use the `mybundle` bundle for mutual TLS:
[configprops,yaml]
----
spring:
grpc:
client:
channel:
myservice:
target: static://grpc.example.com:9090
ssl:
bundle: mybundle
----
[TIP]
====
To temporarily disable client SSL support, for example to aid with testing, you can set `bypass-certificate-validation` to `true` on your channel config:
[configprops,yaml]
----
spring:
grpc:
client:
channel:
myservice:
bypass-certificate-validation: true
----
====
[[io.grpc.client.in-process]]
=== Using In-Process Channels
You can communicate with an xref:io/grpc.adoc#io.grpc.server.in-process[in-process server] (i.e. not listening on a network port) by including the `io.grpc.grpc-inprocess` dependency on your classpath.
In this mode, the in-process channel factory is auto-configured in addition to the regular channel factories (e.g. Netty).
To prevent users from having to deal with multiple channel factories, a composite channel factory is configured as the primary channel factory bean.
The composite consults its composed factories to find the first one that supports the channel target.
To use the in-process server the channel target must be set to `in-process:<name>`
TIP: To disable the in-process channel factory, you can set the configprop:spring.grpc.client.inprocess.enabled[] property to `false`.
[[io.grpc.client.observability]]
=== Observability
Spring Boot provides auto-configuration of the javadoc:io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor[] whenever Micrometer is available.
This interceptor provides observability into your gRPC client applications.
TIP: If you use Micrometer, but prefer to not to use it for gRPC, you can set configprop:spring.grpc.client.observation.enabled[] to `false`.
[[io.grpc.client.channel-customization]]
=== Channel Customization
If you need to customize your gRPC channel beyond the basic properties, you can use a javadoc:org.springframework.grpc.client.GrpcChannelBuilderCustomizer[].
Each customizer is called with the logical target name and the javadoc:io.grpc.ManagedChannelBuilder[] that will build the channel.
There's also a convenient `matching(String pattern)` factory method that will limit customizations to targets that match the given regex pattern.
A common customizer use-case is to add {url-spring-grpc-docs}/client.html#_http_headers[security interceptors] to the builder.
For example, here we're adding the javadoc:org.springframework.grpc.client.interceptor.security.BearerTokenAuthenticationInterceptor[] to the target matching "`hello`":
include-code::MyGrpcConfiguration[]
[[io.grpc.testing]]
== Testing gRPC Applications
To help test your gRPC client and server applications you can use the `spring-boot-grpc-test` module or the `spring-boot-starter-grpc-client-test` / `spring-boot-starter-grpc-server-test` starter POMs.
[[io.grpc.testing.test-transport]]
=== Using In-Process Test Transport
The javadoc:org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport[format=annotation] annotation allows you to quickly replace gRPC communication channels with in-process channels specifically designed for testing.
Unlike regular in-process channels, these test channels to not require any configuration.
Using test gRPC transport means that you don't need to actually listen on a network port to start your application.
This allows your tests to run quickly, whilst still ensuring that your application works as expected.
By default, using javadoc:org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport[format=annotation] will:
* Configure test javadoc:org.springframework.grpc.server.GrpcServerFactory[] / javadoc:org.springframework.grpc.client.GrpcChannelFactory[] beans
* Disable any gRPC servelt registration.
* Disable javadoc:org.springframework.grpc.server.GrpcServerFactory[] bean auto-configuration.
* Disable javadoc:org.springframework.grpc.client.GrpcChannelFactory[] bean auto-configuration.
The following example shows how you can use javadoc:org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport[format=annotation] to test a gRPC server application:
include-code::MyGrpcTests[]
[[io.grpc.testing.local-server-port]]
=== Testing With a Running Server
If you prefer to test your gRPC application by starting the real server and using the actual network connection, we recommend that you use random ports.
This will ensure that you can run your tests in any environment, and that you won't accidentally call real services.
To start a gRPC server using a random port, set configprop:spring.grpc.server.port[] to `0`.
You can use the javadoc:org.springframework.boot.grpc.test.autoconfigure.LocalGrpcServerPort[format=annotation] annotation to obtain the actual port that the server started on.
Here's an example test:
include-code::MyGrpcIntegrationTests[]
// FIXME https://github.com/spring-projects/spring-grpc/issues/397
@@ -40,6 +40,7 @@
** xref:reference:io/index.adoc[]
*** xref:reference:io/caching.adoc[]
*** xref:reference:io/spring-batch.adoc[]
*** xref:reference:io/grpc.adoc[]
*** xref:reference:io/hazelcast.adoc[]
*** xref:reference:io/quartz.adoc[]
*** xref:reference:io/email.adoc[]
@@ -155,7 +155,7 @@ Open your favorite text editor and add the following:
plugins {
id 'java'
id 'org.springframework.boot' version '{version-spring-boot}'
id 'io.spring.dependency-management' version '{version-dependency-management-plugin}'
id 'io.spring.dependency-management' version '{version-dependency-management-plugin}'
}
group = 'com.example'
@@ -170,7 +170,7 @@ java {
repositories {
mavenCentral()
ifeval::["{build-and-artifact-release-type}" == "opensource-snapshot"]
// you don't need this if you are using a release version
// you don't need this if you are using a release version
maven { url 'https://repo.spring.io/snapshot' }
endif::[]
}
@@ -0,0 +1,25 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.client;
class HelloWorldGrpc {
interface HelloWorldBlockingStub {
}
}
@@ -0,0 +1,31 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.grpc.client.ImportGrpcClients;
@SpringBootApplication(proxyBeanMethods = false)
@ImportGrpcClients(target = "hello", types = HelloWorldGrpc.HelloWorldBlockingStub.class)
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.client.channelcustomization;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
import org.springframework.grpc.client.interceptor.security.BasicAuthenticationInterceptor;
@Configuration(proxyBeanMethods = false)
public class MyGrpcConfiguration {
@Bean
GrpcChannelBuilderCustomizer<?> helloChannelCustomizer() {
return GrpcChannelBuilderCustomizer.matching("hello",
(builder) -> builder.intercept(new BasicAuthenticationInterceptor("user", "password")));
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.client.stubbeans;
interface HelloReply {
String getMessage();
static Builder newBuilder() {
throw new IllegalStateException();
}
interface Builder {
Builder setMessage(String message);
HelloReply build();
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.client.stubbeans;
interface HelloRequest {
String getName();
static Builder newBuilder() {
throw new IllegalStateException();
}
interface Builder {
Builder setName(String name);
HelloRequest build();
}
}
@@ -0,0 +1,27 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.client.stubbeans;
class HelloWorldGrpc {
interface HelloWorldBlockingStub {
HelloReply sayHello(HelloRequest request);
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.client.stubbeans;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.docs.io.grpc.client.stubbeans.HelloWorldGrpc.HelloWorldBlockingStub;
import org.springframework.stereotype.Component;
@Component
class MyApplicationRunner implements ApplicationRunner {
private final HelloWorldBlockingStub helloStub;
MyApplicationRunner(HelloWorldGrpc.HelloWorldBlockingStub helloStub) {
this.helloStub = helloStub;
}
@Override
public void run(ApplicationArguments args) throws Exception {
HelloRequest request = HelloRequest.newBuilder().setName("Spring").build();
HelloReply reply = this.helloStub.sayHello(request);
System.out.println(reply.getMessage());
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.server;
interface HelloReply {
String getMessage();
static Builder newBuilder() {
throw new IllegalStateException();
}
interface Builder {
Builder setMessage(String message);
HelloReply build();
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.server;
interface HelloRequest {
String getName();
static Builder newBuilder() {
throw new IllegalStateException();
}
interface Builder {
Builder setName(String name);
HelloRequest build();
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.server;
import io.grpc.stub.StreamObserver;
class HelloWorldGrpc {
abstract static class HelloWorldImplBase {
abstract void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver);
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.server;
import io.grpc.stub.StreamObserver;
import org.springframework.grpc.server.service.GrpcService;
@GrpcService
public class MyHelloWorldService extends HelloWorldGrpc.HelloWorldImplBase {
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String message = "Hello '%s'".formatted(request.getName());
HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
@@ -0,0 +1,38 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.server.security.servlet;
import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration(proxyBeanMethods = false)
public class MySecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http.securityMatcher(GrpcRequest.toAnyService().excluding("special"));
http.authorizeHttpRequests((requests) -> requests.anyRequest().hasRole("GRPC_ADMIN"));
http.httpBasic(withDefaults());
return http.build();
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.testing.localserverport;
interface HelloReply {
String getMessage();
static Builder newBuilder() {
throw new IllegalStateException();
}
interface Builder {
Builder setMessage(String message);
HelloReply build();
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.testing.localserverport;
interface HelloRequest {
String getName();
static Builder newBuilder() {
throw new IllegalStateException();
}
interface Builder {
Builder setName(String name);
HelloRequest build();
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.testing.localserverport;
import io.grpc.ManagedChannel;
interface HelloWorldGrpc {
static HelloWorldBlockingStub newBlockingStub(ManagedChannel channel) {
throw new IllegalStateException();
}
interface HelloWorldBlockingStub {
HelloReply sayHello(HelloRequest request);
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.testing.localserverport;
import io.grpc.ManagedChannel;
import io.grpc.netty.NettyChannelBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.boot.docs.io.grpc.testing.localserverport.HelloWorldGrpc.HelloWorldBlockingStub;
import org.springframework.boot.grpc.test.autoconfigure.LocalGrpcServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(properties = "spring.grpc.server.port=0")
class MyGrpcIntegrationTests {
@LocalGrpcServerPort
private int port;
@Test
void sayHello() {
String target = "localhost:%s".formatted(this.port);
ManagedChannel channel = NettyChannelBuilder.forTarget(target).usePlaintext().build();
try {
HelloWorldBlockingStub hello = HelloWorldGrpc.newBlockingStub(channel);
HelloRequest request = HelloRequest.newBuilder().setName("Spring").build();
assertThat(hello.sayHello(request).getMessage()).isEqualTo("Hello 'Spring'");
}
finally {
channel.shutdown();
}
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.testing.testtransport;
interface HelloReply {
String getMessage();
static Builder newBuilder() {
throw new IllegalStateException();
}
interface Builder {
Builder setMessage(String message);
HelloReply build();
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.testing.testtransport;
interface HelloRequest {
String getName();
static Builder newBuilder() {
throw new IllegalStateException();
}
interface Builder {
Builder setName(String name);
HelloRequest build();
}
}
@@ -0,0 +1,27 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.testing.testtransport;
class HelloWorldGrpc {
interface HelloWorldBlockingStub {
HelloReply sayHello(HelloRequest request);
}
}
@@ -0,0 +1,43 @@
/*
* Copyright 2012-present the original author or 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 org.springframework.boot.docs.io.grpc.testing.testtransport;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.grpc.client.ImportGrpcClients;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@AutoConfigureTestGrpcTransport
@ImportGrpcClients(types = HelloWorldGrpc.HelloWorldBlockingStub.class)
class MyGrpcTests {
@Autowired
private HelloWorldGrpc.HelloWorldBlockingStub helloStub;
@Test
void sayHello() {
HelloRequest request = HelloRequest.newBuilder().setName("Spring").build();
HelloReply reply = this.helloStub.sayHello(request);
assertThat(reply.getMessage()).isEqualTo("Hello 'Spring'");
}
}
@@ -0,0 +1,9 @@
package org.springframework.boot.docs.io.grpc.client
class HelloWorldGrpc {
interface HelloWorldBlockingStub {
}
}
@@ -0,0 +1,14 @@
package org.springframework.boot.docs.io.grpc.client
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.docs.features.springapplication.MyApplication
import org.springframework.boot.runApplication
import org.springframework.grpc.client.ImportGrpcClients
@SpringBootApplication(proxyBeanMethods = false)
@ImportGrpcClients(target = "hello", types = [HelloWorldGrpc.HelloWorldBlockingStub::class])
class MyApplication
fun main(args: Array<String>) {
runApplication<MyApplication>(*args)
}
@@ -0,0 +1,20 @@
package org.springframework.boot.docs.io.grpc.client.channelcustomization
import io.grpc.ManagedChannelBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer
import org.springframework.grpc.client.interceptor.security.BasicAuthenticationInterceptor
import java.util.function.Consumer
@Configuration(proxyBeanMethods = false)
class MyGrpcConfiguration {
@Bean
fun helloChannelCustomizer(): GrpcChannelBuilderCustomizer<*> {
return GrpcChannelBuilderCustomizer.matching("hello", { builder ->
builder.intercept(BasicAuthenticationInterceptor("user", "password"))
})
}
}
@@ -0,0 +1,23 @@
package org.springframework.boot.docs.io.grpc.client.stubbeans
interface HelloReply {
fun getMessage(): String
companion object {
fun newBuilder(): Builder {
throw IllegalStateException()
}
}
interface Builder {
fun setMessage(message: String): Builder
fun build(): HelloReply
}
}
@@ -0,0 +1,23 @@
package org.springframework.boot.docs.io.grpc.client.stubbeans
interface HelloRequest {
fun getName(): String
companion object {
fun newBuilder(): Builder {
throw IllegalStateException()
}
}
interface Builder {
fun setName(name: String): Builder
fun build(): HelloRequest
}
}
@@ -0,0 +1,11 @@
package org.springframework.boot.docs.io.grpc.client.stubbeans
class HelloWorldGrpc {
interface HelloWorldBlockingStub {
fun sayHello(request: HelloRequest): HelloReply
}
}
@@ -0,0 +1,15 @@
package org.springframework.boot.docs.io.grpc.client.stubbeans
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
class MyApplicationRunner(val helloStub: HelloWorldGrpc.HelloWorldBlockingStub) : ApplicationRunner {
override fun run(args: ApplicationArguments) {
val request = HelloRequest.newBuilder().setName("Spring").build()
val reply: HelloReply = helloStub.sayHello(request)
println(reply.getMessage())
}
}
@@ -0,0 +1,23 @@
package org.springframework.boot.docs.io.grpc.server
interface HelloReply {
fun getMessage(): String
companion object {
fun newBuilder(): Builder {
throw IllegalStateException()
}
}
interface Builder {
fun setMessage(message: String): Builder
fun build(): HelloReply
}
}
@@ -0,0 +1,23 @@
package org.springframework.boot.docs.io.grpc.server
interface HelloRequest {
fun getName(): String
companion object {
fun newBuilder(): Builder {
throw IllegalStateException()
}
}
interface Builder {
fun setName(name: String): Builder
fun build(): HelloRequest
}
}
@@ -0,0 +1,14 @@
package org.springframework.boot.docs.io.grpc.server
import io.grpc.stub.StreamObserver
class HelloWorldGrpc {
abstract class HelloWorldImplBase {
abstract fun sayHello(
request: HelloRequest,
responseObserver: StreamObserver<HelloReply>
)
}
}
@@ -0,0 +1,16 @@
package org.springframework.boot.docs.io.grpc.server
import io.grpc.stub.StreamObserver
import org.springframework.grpc.server.service.GrpcService
@GrpcService
class MyHelloWorldService : HelloWorldGrpc.HelloWorldImplBase() {
override fun sayHello(request: HelloRequest, responseObserver: StreamObserver<HelloReply>) {
val message = "Hello '${request.getName()}'"
val reply = HelloReply.newBuilder().setMessage(message).build()
responseObserver.onNext(reply)
responseObserver.onCompleted()
}
}
@@ -0,0 +1,21 @@
package org.springframework.boot.docs.io.grpc.server.security.servlet
import org.springframework.boot.grpc.server.autoconfigure.security.web.servlet.GrpcRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.Customizer.withDefaults
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.SecurityFilterChain
@Configuration(proxyBeanMethods = false)
class MySecurityConfiguration {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http.securityMatcher(GrpcRequest.toAnyService().excluding("special"))
http.authorizeHttpRequests { requests -> requests.anyRequest().hasRole("GRPC_ADMIN") }
http.httpBasic(withDefaults())
return http.build()
}
}
@@ -0,0 +1,23 @@
package org.springframework.boot.docs.io.grpc.testing.localserverport
interface HelloReply {
fun getMessage(): String
companion object {
fun newBuilder(): Builder {
throw IllegalStateException()
}
}
interface Builder {
fun setMessage(message: String): Builder
fun build(): HelloReply
}
}
@@ -0,0 +1,23 @@
package org.springframework.boot.docs.io.grpc.testing.localserverport
interface HelloRequest {
fun getName(): String
companion object {
fun newBuilder(): Builder {
throw IllegalStateException()
}
}
interface Builder {
fun setName(name: String): Builder
fun build(): HelloRequest
}
}
@@ -0,0 +1,19 @@
package org.springframework.boot.docs.io.grpc.testing.localserverport
import io.grpc.ManagedChannel
class HelloWorldGrpc {
interface HelloWorldBlockingStub {
fun sayHello(request: HelloRequest): HelloReply
}
companion object {
fun newBlockingStub(channel: ManagedChannel): HelloWorldBlockingStub {
throw IllegalStateException()
}
}
}
@@ -0,0 +1,28 @@
package org.springframework.boot.docs.io.grpc.testing.localserverport
import io.grpc.ManagedChannel
import io.grpc.netty.NettyChannelBuilder
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.boot.grpc.test.autoconfigure.LocalGrpcServerPort
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest(properties = ["spring.grpc.server.port=0"])
class MyGrpcIntegrationTests {
@LocalGrpcServerPort
var port = 0
@Test
fun sayHello() {
val target = "localhost:${port}"
val channel: ManagedChannel = NettyChannelBuilder.forTarget(target).usePlaintext().build()
try {
val hello: HelloWorldGrpc.HelloWorldBlockingStub = HelloWorldGrpc.newBlockingStub(channel)
val request = HelloRequest.newBuilder().setName("Spring").build()
assertThat(hello.sayHello(request).getMessage()).isEqualTo("Hello 'Spring'")
} finally {
channel.shutdown()
}
}
}
@@ -0,0 +1,23 @@
package org.springframework.boot.docs.io.grpc.testing.testtransport
interface HelloReply {
fun getMessage(): String
companion object {
fun newBuilder(): Builder {
throw IllegalStateException()
}
}
interface Builder {
fun setMessage(message: String): Builder
fun build(): HelloReply
}
}
@@ -0,0 +1,23 @@
package org.springframework.boot.docs.io.grpc.testing.testtransport
interface HelloRequest {
fun getName(): String
companion object {
fun newBuilder(): Builder {
throw IllegalStateException()
}
}
interface Builder {
fun setName(name: String): Builder
fun build(): HelloRequest
}
}
@@ -0,0 +1,19 @@
package org.springframework.boot.docs.io.grpc.testing.testtransport
import io.grpc.ManagedChannel
class HelloWorldGrpc {
interface HelloWorldBlockingStub {
fun sayHello(request: HelloRequest): HelloReply
}
companion object {
fun newBlockingStub(channel: ManagedChannel): HelloWorldBlockingStub {
throw IllegalStateException()
}
}
}
@@ -0,0 +1,23 @@
package org.springframework.boot.docs.io.grpc.testing.testtransport
import org.assertj.core.api.Assertions.assertThat
import org.jooq.DSLContext
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.grpc.test.autoconfigure.AutoConfigureTestGrpcTransport
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.grpc.client.ImportGrpcClients
@SpringBootTest
@AutoConfigureTestGrpcTransport
@ImportGrpcClients(types = [HelloWorldGrpc.HelloWorldBlockingStub::class])
class MyGrpcTests(@Autowired val helloStub: HelloWorldGrpc.HelloWorldBlockingStub) {
@Test
fun sayHello() {
val request = HelloRequest.newBuilder().setName("Spring").build()
val reply = helloStub.sayHello(request)
assertThat(reply.getMessage()).isEqualTo("Hello 'Spring'")
}
}
@@ -2597,7 +2597,7 @@ bom {
github("https://github.com/spring-projects/spring-grpc")
javadoc(version -> "https://docs.spring.io/spring-grpc/docs/%s/api"
.formatted(version.forMajorMinorGeneration()), "org.springframework.grpc")
docs(version -> "https://docs.spring.io/spring-grpc/docs/%s/reference"
docs(version -> "https://docs.spring.io/spring-grpc/reference/%s"
.formatted(version.forMajorMinorGeneration()))
releaseNotes("https://github.com/spring-projects/spring-grpc/releases/tag/v{version}")
}
@@ -24,7 +24,7 @@ description = "Spring Boot gRPC server netty shaded smoke test"
dependencies {
implementation(project(":starter:spring-boot-starter-grpc-server")) {
exclude(group: "io.grpc", module: "grpc-netty-shaded")
exclude(group: "io.grpc", module: "grpc-netty")
}
implementation("io.grpc:grpc-netty-shaded")
@@ -25,7 +25,7 @@ description = "Spring Boot gRPC server servlet smoke test"
dependencies {
implementation(project(":starter:spring-boot-starter-grpc-server")) {
exclude(group: "io.grpc", module: "grpc-netty-shaded")
exclude(group: "io.grpc", module: "grpc-netty")
}
implementation(project(":starter:spring-boot-starter-tomcat"))
implementation("io.grpc:grpc-servlet-jakarta")
@@ -23,10 +23,10 @@ import smoketest.grpcserver.proto.HelloReply;
import smoketest.grpcserver.proto.HelloRequest;
import smoketest.grpcserver.proto.HelloWorldGrpc;
import org.springframework.stereotype.Service;
import org.springframework.grpc.server.service.GrpcService;
import org.springframework.util.Assert;
@Service
@GrpcService
public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase {
private static Log logger = LogFactory.getLog(HelloWorldService.class);