add grpc server kotlin tests

Signed-off-by: Thomas McKernan <tmeaglei@gmail.com>
This commit is contained in:
Thomas McKernan
2025-05-14 11:10:21 -05:00
committed by Dave Syer
parent a59a0961cc
commit 943c72abf5
17 changed files with 1254 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
# Getting Started
### Reference Documentation
For further reference, please consider the following sections:
* [Official Gradle documentation](https://docs.gradle.org)
* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/3.4.5/gradle-plugin)
* [Create an OCI image](https://docs.spring.io/spring-boot/3.4.5/gradle-plugin/packaging-oci-image.html)
* [GraalVM Native Image Support](https://docs.spring.io/spring-boot/3.4.5/reference/packaging/native-image/introducing-graalvm-native-images.html)
* [Spring gRPC [Experimental]](https://docs.spring.io/spring-grpc/reference/index.html)
### Additional Links
These additional references should also help you:
* [Gradle Build Scans insights for your project's build](https://scans.gradle.com#gradle)
* [Configure AOT settings in Build Plugin](https://docs.spring.io/spring-boot/3.4.4/how-to/aot.html)
* [Various sample apps using Spring gRPC](https://github.com/spring-projects/spring-grpc/tree/main/samples)
## GraalVM Native Support
This project has been configured to let you generate either a lightweight container or a native executable.
It is also possible to run your tests in a native image.
### Lightweight Container with Cloud Native Buildpacks
If you're already familiar with Spring Boot container images support, this is the easiest way to get started.
Docker should be installed and configured on your machine prior to creating the image.
To create the image, run the following goal:
```
$ ./gradlew bootBuildImage
```
Then, you can run the app like any other container:
```
$ docker run --rm grpc-server:0.5.0-SNAPSHOT
```
### Executable with Native Build Tools
Use this option if you want to explore more options such as running your tests in a native image.
The GraalVM `native-image` compiler should be installed and configured on your machine.
NOTE: GraalVM 22.3+ is required.
To create the executable, run the following goal:
```
$ ./gradlew nativeCompile
```
Then, you can run the app as follows:
```
$ build/native/nativeCompile/grpc-server
```
You can also run your existing tests suite in a native image.
This is an efficient way to validate the compatibility of your application.
To run your existing tests in a native image, run the following goal:
```
$ ./gradlew nativeTest
```
### Gradle Toolchain support
There are some limitations regarding Native Build Tools and Gradle toolchains.
Native Build Tools disable toolchain support by default.
Effectively, native image compilation is done with the JDK used to execute Gradle.
You can read more about [toolchain support in the Native Build Tools here](https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html#configuration-toolchains).

View File

@@ -0,0 +1,71 @@
# Getting Started
### Reference Documentation
For further reference, please consider the following sections:
* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.4.5/maven-plugin)
* [Create an OCI image](https://docs.spring.io/spring-boot/3.4.5/maven-plugin/build-image.html)
* [GraalVM Native Image Support](https://docs.spring.io/spring-boot/3.4.5/reference/packaging/native-image/introducing-graalvm-native-images.html)
* [Spring gRPC [Experimental]](https://docs.spring.io/spring-grpc/reference/index.html)
### Additional Links
These additional references should also help you:
* [Configure AOT settings in Build Plugin](https://docs.spring.io/spring-boot/3.4.4/how-to/aot.html)
* [Various sample apps using Spring gRPC](https://github.com/spring-projects/spring-grpc/tree/main/samples)
## GraalVM Native Support
This project has been configured to let you generate either a lightweight container or a native executable.
It is also possible to run your tests in a native image.
### Lightweight Container with Cloud Native Buildpacks
If you're already familiar with Spring Boot container images support, this is the easiest way to get started.
Docker should be installed and configured on your machine prior to creating the image.
To create the image, run the following goal:
```
$ ./mvnw spring-boot:build-image -Pnative
```
Then, you can run the app like any other container:
```
$ docker run --rm grpc-server-sample:0.5.0-SNAPSHOT
```
### Executable with Native Build Tools
Use this option if you want to explore more options such as running your tests in a native image.
The GraalVM `native-image` compiler should be installed and configured on your machine.
NOTE: GraalVM 22.3+ is required.
To create the executable, run the following goal:
```
$ ./mvnw native:compile -Pnative
```
Then, you can run the app as follows:
```
$ target/grpc-server-sample
```
You can also run your existing tests suite in a native image.
This is an efficient way to validate the compatibility of your application.
To run your existing tests in a native image, run the following goal:
```
$ ./mvnw test -PnativeTest
```
### Maven Parent overrides
Due to Maven's design, elements are inherited from the parent POM to the project POM.
While most of the inheritance is fine, it also inherits unwanted elements like `<license>` and `<developers>` from the parent.
To prevent this, the project POM contains empty overrides for these elements.
If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides.

View File

@@ -0,0 +1,40 @@
# Spring Boot gRPC Sample
This project is a copy one of the samples from the [gRPC Spring Boot Starter](https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/examples/local-grpc-server/build.gradle). Build and run any way you like to run Spring Boot. E.g:
```
$ ./mvnw spring-boot:run
...
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.4.5)
2022-12-08T05:32:24.934-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 17.0.5 with PID 551632 (/home/dsyer/dev/scratch/demo/target/classes started by dsyer in /home/dsyer/dev/scratch/demo)
2022-12-08T05:32:24.938-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
2022-12-08T05:32:25.377-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcHealthServiceAutoConfiguration
2022-12-08T05:32:25.416-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration
2022-12-08T05:32:25.425-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration
2022-12-08T05:32:25.427-08:00 INFO 551632 --- [ main] g.s.a.GrpcServerFactoryAutoConfiguration : Detected grpc-netty: Creating NettyGrpcServerFactory
2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: Simple, bean: grpcServerService, class: com.example.demo.GrpcServerService
2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.health.v1.Health, bean: grpcHealthService, class: io.grpc.protobuf.services.HealthServiceImpl
2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.reflection.v1alpha.ServerReflection, bean: protoReflectionService, class: io.grpc.protobuf.services.ProtoReflectionService
2022-12-08T05:32:25.820-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.GrpcServerLifecycle : gRPC Server started, listening on address: *, port: 9090
2022-12-08T05:32:25.831-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.264 seconds (process running for 1.623)
```
The server starts by default on port 9090. Test with [gRPCurl](https://github.com/fullstorydev/grpcurl):
```
$ grpcurl -d '{"name":"Hi"}' -plaintext localhost:9090 Simple.SayHello
{
"message": "Hello ==\u003e Hi"
}
```
## Native Image
[Native Image with Gradle](./HELP-gradle.md)
[Native Image with Maven](./HELP-maven.md)

View File

@@ -0,0 +1,83 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.5'
id 'io.spring.dependency-management' version '1.1.6'
// id 'org.graalvm.buildtools.native' version '0.10.3'
id 'com.google.protobuf' version '0.9.4'
id 'org.jetbrains.kotlin.jvm' version '2.1.20'
id 'org.jetbrains.kotlin.plugin.spring' version '2.1.20'
}
group = 'com.example'
version = '0.9.0-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}
dependencyManagement {
imports {
mavenBom 'org.springframework.grpc:spring-grpc-dependencies:0.9.0-SNAPSHOT'
}
}
def kotlinStubVersion = "1.4.3"
dependencies {
implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.grpc:grpc-services'
implementation "io.grpc:grpc-kotlin-stub:${kotlinStubVersion}"
implementation 'com.google.protobuf:protobuf-kotlin'
implementation "org.jetbrains.kotlin:kotlin-reflect"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.grpc:spring-grpc-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testRuntimeOnly "io.netty:netty-transport-native-epoll::linux-x86_64"
}
test {
useJUnitPlatform()
testLogging.showStandardStreams = true
outputs.upToDateWhen { false }
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${dependencyManagement.importedProperties['protobuf-java.version']}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${dependencyManagement.importedProperties['grpc.version']}"
}
grpckt {
artifact = "io.grpc:protoc-gen-grpc-kotlin:${kotlinStubVersion}:jdk8@jar"
}
}
generateProtoTasks {
all()*.plugins {
grpc {
option 'jakarta_omit'
option '@generated=omit'
}
grpckt {
outputSubDir = "kotlin"
}
}
}
}

View File

@@ -0,0 +1,264 @@
<?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>3.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.springframework.grpc</groupId>
<artifactId>grpc-server-kotlin-sample</artifactId>
<version>0.9.0-SNAPSHOT</version>
<name>Spring gRPC Server Sample</name>
<description>Demo project for Spring gRPC</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
<spring-javaformat-maven-plugin.version>0.0.43</spring-javaformat-maven-plugin.version>
<protobuf-java.version>4.30.2</protobuf-java.version>
<grpc.version>1.72.0</grpc.version>
<grpc.kotlin.version>1.4.3</grpc.kotlin.version>
<kotlin.version>1.9.22</kotlin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.grpc</groupId>
<artifactId>spring-grpc-dependencies</artifactId>
<version>0.9.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.grpc</groupId>
<artifactId>spring-grpc-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>1.10.2</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-kotlin-stub</artifactId>
<version>${grpc.kotlin.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-kotlin</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-services</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<!-- for testing unix domain sockets -->
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<classifier>linux-x86_64</classifier>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.grpc</groupId>
<artifactId>spring-grpc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.0.1</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
<buildArg>--verbose</buildArg>
</buildArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>io.spring.javaformat</groupId>
<artifactId>spring-javaformat-maven-plugin</artifactId>
<version>${spring-javaformat-maven-plugin.version}</version>
<executions>
<execution>
<?m2e ignore?>
<phase>validate</phase>
<inherited>true</inherited>
<goals>
<goal>validate</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>
com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>
io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
</pluginArtifact>
<protocPlugins>
<protocPlugin>
<id>grpc-kotlin</id>
<groupId>io.grpc</groupId>
<artifactId>protoc-gen-grpc-kotlin</artifactId>
<version>${grpc.kotlin.version}</version>
<classifier>jdk8</classifier>
<mainClass>org.springframework.grpc.sample.GrpcServerApplicationKt</mainClass>
</protocPlugin>
</protocPlugins>
</configuration>
<executions>
<execution>
<configuration>
<pluginParameter>
jakarta_omit,@generated=omit
</pluginParameter>
</configuration>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
<execution>
<id>grpc-kotlin</id>
<goals>
<goal>compile-custom</goal>
</goals>
<configuration>
<pluginId>grpc-kotlin</pluginId>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
</project>

View File

@@ -0,0 +1,25 @@
package org.springframework.grpc.sample
import io.grpc.Status
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.grpc.server.exception.GrpcExceptionHandler
@SpringBootApplication
open class GrpcServerApplication {
@Bean
open fun globalInterceptor(): GrpcExceptionHandler = GrpcExceptionHandler { exception ->
when (exception) {
is IllegalArgumentException -> Status.INVALID_ARGUMENT.withDescription(exception.message).asException();
else -> null
}
}
}
fun main(args: Array<String>) {
runApplication<GrpcServerApplication>(*args)
}

View File

@@ -0,0 +1,33 @@
package org.springframework.grpc.sample
import org.springframework.grpc.sample.proto.HelloReply
import org.springframework.grpc.sample.proto.HelloRequest
import org.springframework.grpc.sample.proto.SimpleGrpcKt
import org.springframework.stereotype.Service
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
@Service
class GrpcServerService : SimpleGrpcKt.SimpleCoroutineImplBase() {
override suspend fun sayHello(request: HelloRequest): HelloReply {
if (request.name.startsWith("error")) {
throw IllegalArgumentException("Bad name: ${request.name}")
}
if (request.name.startsWith("internal")) {
throw RuntimeException()
}
return HelloReply.newBuilder()
.setMessage("Hello ==> ${request.name}")
.build()
}
override fun streamHello(request: HelloRequest): Flow<HelloReply> {
return (1..10)
.map { HelloReply.newBuilder().setMessage("Hello ($it) ==> ${request.name}").build() }
.asFlow()
}
}

View File

@@ -0,0 +1,23 @@
syntax = "proto3";
option java_multiple_files = true;
option java_package = "org.springframework.grpc.sample.proto";
option java_outer_classname = "HelloWorldProto";
// The greeting service definition.
service Simple {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {
}
rpc StreamHello(HelloRequest) returns (stream 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;
}

View File

@@ -0,0 +1 @@
spring.application.name=grpc-server-kotlin

View File

@@ -0,0 +1,84 @@
package org.springframework.grpc.sample
import io.grpc.stub.AbstractStub
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.ApplicationContext
import org.springframework.grpc.client.FutureStubFactory
import org.springframework.grpc.client.ImportGrpcClients
import org.springframework.grpc.sample.proto.SimpleGrpc
import org.springframework.grpc.sample.proto.SimpleGrpc.SimpleBlockingStub
import org.springframework.grpc.sample.proto.SimpleGrpc.SimpleFutureStub
import org.springframework.grpc.test.AutoConfigureInProcessTransport
@SpringBootTest
@AutoConfigureInProcessTransport
internal class NoAutowiredClients {
@Autowired
private lateinit var context: ApplicationContext
@Test
fun noStubIsCreated() {
assertThat(context.containsBeanDefinition("simpleBlockingStub")).isFalse()
assertThat(context.containsBeanDefinition("simpleStub")).isFalse()
assertThat(context.containsBeanDefinition("simpleFutureStub")).isFalse()
assertThat(context.getBeanNamesForType(AbstractStub::class.java))
.isEmpty()
}
}
@SpringBootTest(properties = ["spring.grpc.client.default-channel.address=0.0.0.0:9090"])
@AutoConfigureInProcessTransport
internal class DefaultAutowiredClients {
@Autowired
private lateinit var context: ApplicationContext
@Test
fun onlyDefaultStubIsCreated() {
Assertions.assertThat(context.containsBeanDefinition("simpleBlockingStub")).isTrue()
assertThat(
context.getBean(
SimpleBlockingStub::class.java
)
).isNotNull()
assertThat(context.containsBeanDefinition("simpleStub")).isFalse()
assertThat(context.containsBeanDefinition("simpleFutureStub")).isFalse()
assertThat(context.getBeanNamesForType(AbstractStub::class.java))
.hasSize(1)
}
}
@SpringBootTest(
properties = ["spring.grpc.client.default-channel.address=0.0.0.0:9090"]
)
@AutoConfigureInProcessTransport
internal class SpecificAutowiredClients {
@Autowired
private lateinit var context: ApplicationContext
@Test
fun stubOfCorrectTypeIsCreated() {
assertThat(context.containsBeanDefinition("simpleFutureStub")).isTrue()
assertThat(
context.getBean(
SimpleFutureStub::class.java
)
).isNotNull()
assertThat(context.containsBeanDefinition("simpleStub")).isFalse()
assertThat(context.containsBeanDefinition("simpleBlockingStub")).isFalse()
assertThat(context.getBeanNamesForType(AbstractStub::class.java))
.hasSize(1)
}
@TestConfiguration
@ImportGrpcClients(basePackageClasses = [SimpleGrpc::class], factory = FutureStubFactory::class)
internal open class TestConfig
}

View File

@@ -0,0 +1,51 @@
package org.springframework.grpc.sample
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod
import org.springframework.grpc.sample.proto.HelloRequest
import org.springframework.grpc.sample.proto.SimpleGrpc.SimpleBlockingStub
import org.springframework.test.annotation.DirtiesContext
@SpringBootTest(
properties = [
"spring.grpc.server.port=0",
"spring.grpc.client.default-channel.address=0.0.0.0:\${local.grpc.port}"
],
useMainMethod = UseMainMethod.ALWAYS
)
@DirtiesContext
class GrpcServerApplicationTests {
@Autowired
private lateinit var stub: SimpleBlockingStub
@Test
fun contextLoads() {
}
@Test
fun serverResponds() {
log.info("Testing")
val response = stub.sayHello(
HelloRequest.newBuilder()
.setName("Alien")
.build()
)
Assertions.assertEquals("Hello ==> Alien", response.getMessage())
}
companion object {
private val log: Log = LogFactory.getLog(GrpcServerApplicationTests::class.java)
@JvmStatic
fun main(args: Array<String>) {
SpringApplicationBuilder(GrpcServerApplication::class.java).run()
}
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2024-2024 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.grpc.sample
import io.grpc.StatusRuntimeException
import io.grpc.health.v1.HealthCheckRequest
import io.grpc.health.v1.HealthCheckResponse.ServingStatus
import io.grpc.health.v1.HealthGrpc
import io.grpc.health.v1.HealthGrpc.HealthBlockingStub
import io.grpc.protobuf.services.HealthStatusManager
import org.assertj.core.api.Assertions
import org.assertj.core.api.ThrowableAssert
import org.awaitility.Awaitility
import org.awaitility.core.ThrowingRunnable
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator
import org.springframework.boot.actuate.health.Health
import org.springframework.boot.actuate.health.HealthIndicator
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.grpc.client.GrpcChannelFactory
import org.springframework.grpc.sample.proto.HelloRequest
import org.springframework.grpc.sample.proto.SimpleGrpc
import org.springframework.grpc.sample.proto.SimpleGrpc.SimpleBlockingStub
import org.springframework.grpc.test.AutoConfigureInProcessTransport
import org.springframework.test.annotation.DirtiesContext
import java.time.Duration
/**
* Integration tests for gRPC server health feature.
*/
@SpringBootTest(
properties = ["spring.grpc.server.port=0", "spring.grpc.client.channels.health-test.address=static://0.0.0.0:\${local.grpc.port}", "spring.grpc.client.channels.health-test.health.enabled=true", "spring.grpc.client.channels.health-test.health.service-name=my-service"]
)
@DirtiesContext
internal class WithClientHealthEnabled {
@Test
fun loadBalancerRespectsServerHealth(
@Autowired channels: GrpcChannelFactory,
@Autowired healthStatusManager: HealthStatusManager
) {
val channel = channels.createChannel("health-test")
val client = SimpleGrpc.newBlockingStub(channel)
// put the service up (SERVING) and give load balancer time to update
updateHealthStatusAndWait("my-service", ServingStatus.SERVING, healthStatusManager)
// initially the status should be SERVING
assertThatResponseIsServedToChannel(client)
// put the service down (NOT_SERVING) and give load balancer time to update
updateHealthStatusAndWait("my-service", ServingStatus.NOT_SERVING, healthStatusManager)
// now the request should fail
assertThatResponseIsNotServedToChannel(client)
// put the service up (SERVING) and give load balancer time to update
updateHealthStatusAndWait("my-service", ServingStatus.SERVING, healthStatusManager)
// now the request should pass
assertThatResponseIsServedToChannel(client)
}
private fun updateHealthStatusAndWait(
serviceName: String?, healthStatus: ServingStatus,
healthStatusManager: HealthStatusManager
) {
healthStatusManager.setStatus(serviceName, healthStatus)
try {
Thread.sleep(2000L)
} catch (e: InterruptedException) {
throw RuntimeException(e)
}
}
private fun assertThatResponseIsServedToChannel(client: SimpleBlockingStub) {
val response = client.sayHello(HelloRequest.newBuilder().setName("Alien").build())
Assertions.assertThat(response.getMessage()).isEqualTo("Hello ==> Alien")
}
private fun assertThatResponseIsNotServedToChannel(client: SimpleBlockingStub) {
Assertions.assertThatExceptionOfType<StatusRuntimeException?>(StatusRuntimeException::class.java)
.isThrownBy(ThrowableAssert.ThrowingCallable {
client.sayHello(
HelloRequest.newBuilder().setName("Alien").build()
)
})
.withMessageContaining("UNAVAILABLE: Health-check service responded NOT_SERVING for 'my-service'")
}
}
@SpringBootTest(
properties = ["spring.grpc.server.health.actuator.health-indicator-paths=custom", "spring.grpc.server.health.actuator.update-initial-delay=3s", "spring.grpc.server.health.actuator.update-rate=3s", "management.health.defaults.enabled=true"]
)
@AutoConfigureInProcessTransport
@DirtiesContext
internal class WithActuatorHealthAdapter {
@Test
fun healthIndicatorsAdaptedToGrpcHealthStatus(
@Autowired channels: GrpcChannelFactory,
@Autowired customHealthIndicator: CustomHealthIndicator
) {
val channel = channels.createChannel("0.0.0.0:0")
val healthStub = HealthGrpc.newBlockingStub(channel)
val serviceName = "custom"
// initially the status should be SERVING
assertThatGrpcHealthStatusIs(healthStub, serviceName, ServingStatus.SERVING, Duration.ofSeconds(4))
// put the service down and the status should then be NOT_SERVING
customHealthIndicator.SERVICE_IS_UP = false
assertThatGrpcHealthStatusIs(healthStub, serviceName, ServingStatus.NOT_SERVING, Duration.ofSeconds(4))
// put the service up and the status should be SERVING
customHealthIndicator.SERVICE_IS_UP = true
assertThatGrpcHealthStatusIs(healthStub, serviceName, ServingStatus.SERVING, Duration.ofSeconds(4))
}
private fun assertThatGrpcHealthStatusIs(
healthBlockingStub: HealthBlockingStub, service: String,
expectedStatus: ServingStatus?, maxWaitTime: Duration?
) {
Awaitility.await().atMost(maxWaitTime).ignoreException(StatusRuntimeException::class.java).untilAsserted {
val healthRequest = HealthCheckRequest.newBuilder().setService(service).build()
val healthResponse = healthBlockingStub.check(healthRequest)
Assertions.assertThat(healthResponse.getStatus()).isEqualTo(expectedStatus)
// verify the overall status as well
val overallHealthRequest = HealthCheckRequest.newBuilder().setService("").build()
val overallHealthResponse = healthBlockingStub.check(overallHealthRequest)
Assertions.assertThat(overallHealthResponse.getStatus()).isEqualTo(expectedStatus)
}
}
@TestConfiguration
internal open class MyHealthIndicatorsConfig {
@ConditionalOnEnabledHealthIndicator("custom")
@Bean
open fun customHealthIndicator(): CustomHealthIndicator {
return CustomHealthIndicator()
}
}
internal class CustomHealthIndicator : HealthIndicator {
override fun health(): Health? {
return if (SERVICE_IS_UP) Health.up().build() else Health.down().build()
}
var SERVICE_IS_UP: Boolean = true
}
}

View File

@@ -0,0 +1,332 @@
/*
* Copyright 2024-2024 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.grpc.sample
import io.grpc.ForwardingServerCallListener
import io.grpc.ManagedChannel
import io.grpc.Metadata
import io.grpc.ServerCall
import io.grpc.ServerCallHandler
import io.grpc.ServerInterceptor
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.grpc.netty.NettyChannelBuilder
import org.assertj.core.api.Assertions
import org.junit.Assert
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledOnOs
import org.junit.jupiter.api.condition.OS
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.grpc.client.ChannelBuilderOptions
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer
import org.springframework.grpc.client.GrpcChannelFactory
import org.springframework.grpc.sample.proto.HelloRequest
import org.springframework.grpc.sample.proto.SimpleGrpc
import org.springframework.grpc.server.GlobalServerInterceptor
import org.springframework.grpc.test.AutoConfigureInProcessTransport
import org.springframework.grpc.test.LocalGrpcPort
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ActiveProfiles
import java.util.concurrent.atomic.AtomicInteger
/**
* More detailed integration tests for [gRPC server factories][GrpcServerFactory] and
* various [GrpcServerProperties].
*/
@SpringBootTest
@AutoConfigureInProcessTransport
internal class ServerWithInProcessChannel {
@Test
fun servesResponseToClient(@Autowired channels: GrpcChannelFactory) {
assertThatResponseIsServedToChannel(channels.createChannel("0.0.0.0:0"))
}
}
@SpringBootTest
@AutoConfigureInProcessTransport
internal class ServerWithException {
@Test
fun specificErrorResponse(@Autowired channels: GrpcChannelFactory) {
val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0"))
Assertions.assertThat(
Assert.assertThrows(StatusRuntimeException::class.java) {
client.sayHello(
HelloRequest.newBuilder().setName("error").build()
)
}
.status
.code
).isEqualTo(Status.Code.INVALID_ARGUMENT)
}
@Test
fun defaultErrorResponseIsUnknown(@Autowired channels: GrpcChannelFactory) {
val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0"))
Assertions.assertThat<Status.Code?>(
Assert.assertThrows(
StatusRuntimeException::class.java
) { client.sayHello(HelloRequest.newBuilder().setName("internal").build()) }
.status
.code
).isEqualTo(Status.Code.UNKNOWN)
}
}
@SpringBootTest
@AutoConfigureInProcessTransport
internal class ServerWithExceptionInInterceptorCall {
@Test
fun specificErrorResponse(@Autowired channels: GrpcChannelFactory) {
val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0"))
Assertions.assertThat(
Assert.assertThrows(
StatusRuntimeException::class.java
) { client.sayHello(HelloRequest.newBuilder().setName("foo").build()) }
.status
.code
).isEqualTo(Status.Code.INVALID_ARGUMENT)
}
@TestConfiguration
internal open class TestConfig {
@Bean
@GlobalServerInterceptor
open fun exceptionInterceptor(): ServerInterceptor {
return CustomInterceptor()
}
internal class CustomInterceptor : ServerInterceptor {
override fun <ReqT, RespT> interceptCall(
call: ServerCall<ReqT?, RespT?>?, headers: Metadata?,
next: ServerCallHandler<ReqT?, RespT?>?
): ServerCall.Listener<ReqT?>? {
throw IllegalArgumentException("test")
}
}
}
}
@SpringBootTest
@AutoConfigureInProcessTransport
internal class ServerWithExceptionInInterceptorListener {
@Test
fun specificErrorResponse(
@Autowired channels: GrpcChannelFactory,
@Autowired testConfig: TestConfig
) {
testConfig.reset()
val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0"))
Assertions.assertThat(
Assert.assertThrows(
StatusRuntimeException::class.java
) { client.sayHello(HelloRequest.newBuilder().setName("foo").build()) }
.status
.code
).isEqualTo(Status.Code.INVALID_ARGUMENT)
Assertions.assertThat(TestConfig.readyCount.get()).isEqualTo(1)
Assertions.assertThat(TestConfig.callCount.get()).isEqualTo(0)
Assertions.assertThat(TestConfig.messageCount.get()).isEqualTo(0)
}
@TestConfiguration
internal open class TestConfig {
companion object {
var callCount: AtomicInteger = AtomicInteger()
var messageCount: AtomicInteger = AtomicInteger()
var readyCount: AtomicInteger = AtomicInteger()
}
fun reset() {
callCount.set(0)
messageCount.set(0)
readyCount.set(0)
}
@Bean
@GlobalServerInterceptor
open fun exceptionInterceptor(): ServerInterceptor {
return CustomInterceptor()
}
internal class CustomInterceptor : ServerInterceptor {
override fun <ReqT, RespT> interceptCall(
call: ServerCall<ReqT?, RespT?>?, headers: Metadata?,
next: ServerCallHandler<ReqT?, RespT?>
): ServerCall.Listener<ReqT?> {
return CustomListener(next.startCall(call, headers))
}
}
internal class CustomListener<ReqT>(private val delegate: ServerCall.Listener<ReqT?>?) :
ForwardingServerCallListener<ReqT?>() {
override fun onReady() {
readyCount.incrementAndGet()
throw IllegalArgumentException("test")
}
override fun onHalfClose() {
callCount.incrementAndGet()
super.onHalfClose()
}
override fun onMessage(message: ReqT?) {
messageCount.incrementAndGet()
super.onMessage(message)
}
override fun delegate(): ServerCall.Listener<ReqT?>? {
return this.delegate
}
}
}
}
@SpringBootTest("spring.grpc.server.exception-handler.enabled=false")
@AutoConfigureInProcessTransport
internal class ServerWithUnhandledException {
@Test
fun specificErrorResponse(@Autowired channels: GrpcChannelFactory) {
val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0"))
Assertions.assertThat<Status.Code?>(
Assert.assertThrows(StatusRuntimeException::class.java) {
client.sayHello(
HelloRequest.newBuilder().setName("error").build()
)
}
.status
.code
).isEqualTo(Status.Code.UNKNOWN)
}
@Test
fun defaultErrorResponseIsUnknown(@Autowired channels: GrpcChannelFactory) {
val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0"))
Assertions.assertThat(
Assert.assertThrows(
StatusRuntimeException::class.java
) { client.sayHello(HelloRequest.newBuilder().setName("internal").build()) }
.status
.code
).isEqualTo(Status.Code.UNKNOWN)
}
}
@SpringBootTest(properties = ["spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0"])
internal class ServerWithAnyIPv4AddressAndRandomPort {
@Test
fun servesResponseToClientWithAnyIPv4AddressAndRandomPort(
@Autowired channels: GrpcChannelFactory,
@LocalGrpcPort port: Int
) {
assertThatResponseIsServedToChannel(channels.createChannel("0.0.0.0:" + port))
}
}
@SpringBootTest(properties = ["spring.grpc.server.host=::", "spring.grpc.server.port=0"])
internal class ServerWithAnyIPv6AddressAndRandomPort {
@Test
fun servesResponseToClientWithAnyIPv4AddressAndRandomPort(
@Autowired channels: GrpcChannelFactory,
@LocalGrpcPort port: Int
) {
assertThatResponseIsServedToChannel(channels.createChannel("0.0.0.0:" + port))
}
}
@SpringBootTest(properties = ["spring.grpc.server.host=127.0.0.1", "spring.grpc.server.port=0"])
internal class ServerWithLocalhostAndRandomPort {
@Test
fun servesResponseToClientWithLocalhostAndRandomPort(
@Autowired channels: GrpcChannelFactory,
@LocalGrpcPort port: Int
) {
assertThatResponseIsServedToChannel(channels.createChannel("127.0.0.1:" + port))
}
}
@SpringBootTest(
properties = ["spring.grpc.server.port=0", "spring.grpc.client.channels.test-channel.address=static://0.0.0.0:\${local.grpc.port}"]
)
@DirtiesContext
internal class ServerConfiguredWithStaticClientChannel {
@Test
fun servesResponseToClientWithConfiguredChannel(@Autowired channels: GrpcChannelFactory) {
assertThatResponseIsServedToChannel(channels.createChannel("test-channel"))
}
}
@SpringBootTest(properties = ["spring.grpc.server.address=unix:unix-test-channel"])
@EnabledOnOs(OS.LINUX)
internal class ServerWithUnixDomain {
@Test
fun clientChannelWithUnixDomain(@Autowired channels: GrpcChannelFactory) {
assertThatResponseIsServedToChannel(
channels.createChannel(
"unix:unix-test-channel",
ChannelBuilderOptions.defaults()
.withCustomizer<NettyChannelBuilder?>(GrpcChannelBuilderCustomizer { `__`: String?, b: NettyChannelBuilder? -> b!!.usePlaintext() })
)
)
}
}
@SpringBootTest(
properties = ["spring.grpc.server.port=0", "spring.grpc.client.channels.test-channel.address=static://0.0.0.0:\${local.grpc.port}", "spring.grpc.client.channels.test-channel.negotiation-type=TLS", "spring.grpc.client.channels.test-channel.secure=false"]
)
@ActiveProfiles("ssl")
@DirtiesContext
internal class ServerWithSsl {
@Test
fun clientChannelWithSsl(@Autowired channels: GrpcChannelFactory) {
assertThatResponseIsServedToChannel(channels.createChannel("test-channel"))
}
}
@SpringBootTest(
properties = ["spring.grpc.server.port=0", "spring.grpc.server.ssl.client-auth=REQUIRE", "spring.grpc.server.ssl.secure=false", "spring.grpc.client.channels.test-channel.address=static://0.0.0.0:\${local.grpc.port}", "spring.grpc.client.channels.test-channel.ssl.bundle=ssltest", "spring.grpc.client.channels.test-channel.negotiation-type=TLS", "spring.grpc.client.channels.test-channel.secure=false"]
)
@ActiveProfiles("ssl")
@DirtiesContext
internal class ServerWithClientAuth {
@Test
fun clientChannelWithSsl(@Autowired channels: GrpcChannelFactory) {
assertThatResponseIsServedToChannel(channels.createChannel("test-channel"))
}
}
private fun assertThatResponseIsServedToChannel(clientChannel: ManagedChannel?) {
val client = SimpleGrpc.newBlockingStub(clientChannel)
val response = client.sayHello(HelloRequest.newBuilder().setName("Alien").build())
Assertions.assertThat(response.getMessage()).isEqualTo("Hello ==> Alien")
}

View File

@@ -0,0 +1,5 @@
spring.grpc.server.ssl.bundle=ssltest
spring.ssl.bundle.jks.ssltest.keystore.location=classpath:test.jks
spring.ssl.bundle.jks.ssltest.keystore.password=secret
spring.ssl.bundle.jks.ssltest.keystore.type=JKS
spring.ssl.bundle.jks.ssltest.key.password=password

Binary file not shown.

View File

@@ -21,6 +21,7 @@
<module>grpc-secure</module>
<module>grpc-oauth2</module>
<module>grpc-reactive</module>
<module>grpc-server-kotlin</module>
<module>grpc-server-netty-shaded</module>
<module>grpc-tomcat</module>
<module>grpc-tomcat-secure</module>

View File

@@ -10,6 +10,7 @@ include 'grpc-reactive'
include 'grpc-oauth2'
include 'grpc-secure'
include 'grpc-server'
include 'grpc-server-kotlin'
include 'grpc-server-netty-shaded'
include 'grpc-tomcat'
include 'grpc-tomcat-secure'