From a491d438316612448aea3fcaf3e3ab9664aa80fe Mon Sep 17 00:00:00 2001 From: onobc Date: Sat, 20 Sep 2025 12:37:26 -0500 Subject: [PATCH 01/21] Add Spring gRPC support This commit introduces support for Spring gRPC by moving the autoconfiguration, test, and starter modules from Spring gRPC to Spring Boot (here). The relavant docs from Spring gRPC are not included in this commit and will be available in a subsequent commit. Signed-off-by: onobc --- .../DocumentConfigurationProperties.java | 5 + config/checkstyle/checkstyle-suppressions.xml | 1 + documentation/spring-boot-docs/build.gradle | 3 + module/spring-boot-grpc-client/build.gradle | 67 +++ .../ChannelBuilderCustomizers.java | 60 ++ .../ClientInterceptorsConfiguration.java | 40 ++ ...entPropertiesChannelBuilderCustomizer.java | 83 +++ .../ClientScanConfiguration.java | 88 +++ ...positeChannelFactoryAutoConfiguration.java | 67 +++ .../ConditionalOnGrpcClientEnabled.java | 45 ++ .../GrpcChannelFactoryConfigurations.java | 124 ++++ .../GrpcChannelFactoryCustomizer.java | 29 + .../GrpcClientAutoConfiguration.java | 91 +++ ...rpcClientObservationAutoConfiguration.java | 46 ++ .../autoconfigure/GrpcClientProperties.java | 478 ++++++++++++++++ .../NamedChannelCredentialsProvider.java | 79 +++ .../codec/GrpcCodecConfiguration.java | 57 ++ .../autoconfigure/codec/package-info.java | 23 + .../client/autoconfigure/package-info.java | 23 + ...itional-spring-configuration-metadata.json | 29 + ...ot.autoconfigure.AutoConfiguration.imports | 3 + .../ChannelBuilderCustomizersTests.java | 124 ++++ .../ClientScanConfigurationTests.java | 179 ++++++ ...eChannelFactoryAutoConfigurationTests.java | 141 +++++ .../GrpcClientAutoConfigurationTests.java | 435 ++++++++++++++ ...ientObservationAutoConfigurationTests.java | 118 ++++ .../GrpcClientPropertiesTests.java | 299 ++++++++++ .../codec/GrpcCodecConfigurationTests.java | 48 ++ .../src/test/resources/logback-test.xml | 4 + module/spring-boot-grpc-server/build.gradle | 67 +++ .../ConditionalOnGrpcNativeServer.java | 41 ++ .../ConditionalOnGrpcServerEnabled.java | 45 ++ .../ConditionalOnGrpcServletServer.java | 50 ++ .../DefaultServerFactoryPropertyMapper.java | 89 +++ .../GrpcServerAutoConfiguration.java | 130 +++++ .../GrpcServerExecutorProvider.java | 29 + .../GrpcServerFactoryAutoConfiguration.java | 114 ++++ .../GrpcServerFactoryConfigurations.java | 194 +++++++ .../GrpcServerFactoryCustomizer.java | 29 + ...rpcServerObservationAutoConfiguration.java | 71 +++ .../autoconfigure/GrpcServerProperties.java | 458 +++++++++++++++ ...GrpcServerReflectionAutoConfiguration.java | 54 ++ .../InProcessServerFactoryPropertyMapper.java | 41 ++ .../NettyServerFactoryPropertyMapper.java | 40 ++ .../OnGrpcNativeServerCondition.java | 69 +++ .../ServerBuilderCustomizers.java | 59 ++ .../ServletEnvironmentPostProcessor.java | 39 ++ ...hadedNettyServerFactoryPropertyMapper.java | 40 ++ .../codec/GrpcCodecConfiguration.java | 57 ++ .../autoconfigure/codec/package-info.java | 23 + ...GrpcExceptionHandlerAutoConfiguration.java | 61 ++ .../autoconfigure/exception/package-info.java | 23 + .../health/ActuatorHealthAdapter.java | 113 ++++ .../health/ActuatorHealthAdapterInvoker.java | 65 +++ .../GrpcServerHealthAutoConfiguration.java | 134 +++++ .../autoconfigure/health/package-info.java | 23 + .../server/autoconfigure/package-info.java | 24 + .../GrpcDisableCsrfHttpConfigurer.java | 70 +++ .../security/GrpcReactiveRequest.java | 131 +++++ .../GrpcSecurityAutoConfiguration.java | 113 ++++ .../security/GrpcServletRequest.java | 137 +++++ .../OAuth2ClientAutoConfiguration.java | 61 ++ ...OAuth2ResourceServerAutoConfiguration.java | 331 +++++++++++ .../autoconfigure/security/package-info.java | 23 + ...itional-spring-configuration-metadata.json | 51 ++ .../main/resources/META-INF/spring.factories | 5 + ...ot.autoconfigure.AutoConfiguration.imports | 9 + .../GrpcServerAutoConfigurationTests.java | 529 ++++++++++++++++++ ...rverObservationAutoConfigurationTests.java | 117 ++++ .../GrpcServerPropertiesTests.java | 190 +++++++ ...erverReflectionAutoConfigurationTests.java | 97 ++++ .../GrpcServletAutoConfigurationTests.java | 114 ++++ .../ServerBuilderCustomizersTests.java | 123 ++++ .../ServerFactoryPropertyMappersTests.java | 85 +++ .../codec/GrpcCodecConfigurationTests.java | 48 ++ ...xceptionHandlerAutoConfigurationTests.java | 128 +++++ .../ActuatorHealthAdapterInvokerTests.java | 52 ++ .../health/ActuatorHealthAdapterTests.java | 155 +++++ ...rpcServerHealthAutoConfigurationTests.java | 282 ++++++++++ .../security/GrpcReactiveRequestTests.java | 87 +++ .../GrpcSecurityAutoConfigurationTests.java | 118 ++++ .../security/GrpcServletRequestTests.java | 98 ++++ ...2ResourceServerAutoConfigurationTests.java | 175 ++++++ .../src/test/resources/logback-test.xml | 4 + .../boot/grpc/server/autoconfigure/test.jks | Bin 0 -> 1276 bytes .../grpc/AutoConfigureInProcessTransport.java | 53 ++ .../grpc/InProcessTestAutoConfiguration.java | 157 ++++++ ...cessTransportContextCustomizerFactory.java | 91 +++ .../autoconfigure/grpc/LocalGrpcPort.java | 42 ++ ...PortInfoApplicationContextInitializer.java | 87 +++ .../test/autoconfigure/grpc/package-info.java | 23 + .../InProcessTestAutoConfigurationTests.java | 99 ++++ .../spring-boot-dependencies/build.gradle | 63 +++ .../build.gradle | 5 - settings.gradle | 7 + .../spring-boot-smoke-test-grpc/build.gradle | 67 +++ .../smoketest/grpc/GrpcServerApplication.java | 29 + .../smoketest/grpc/GrpcServerService.java | 67 +++ .../java/smoketest/grpc/package-info.java | 20 + .../src/main/proto/hello.proto | 23 + .../src/main/resources/application.properties | 1 + .../GrpcServerApplicationHealthTests.java | 174 ++++++ .../grpc/GrpcServerApplicationTests.java | 99 ++++ .../test/resources/application-ssl.properties | 5 + .../src/test/resources/test.jks | Bin 0 -> 2264 bytes .../build.gradle | 29 + .../build.gradle | 27 + .../build.gradle | 29 + starter/spring-boot-starter-grpc/build.gradle | 28 + 109 files changed, 9524 insertions(+), 5 deletions(-) create mode 100644 module/spring-boot-grpc-client/build.gradle create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientInterceptorsConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientPropertiesChannelBuilderCustomizer.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientScanConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientEnabled.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/package-info.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java create mode 100644 module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizersTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ClientScanConfigurationTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfigurationTests.java create mode 100644 module/spring-boot-grpc-client/src/test/resources/logback-test.xml create mode 100644 module/spring-boot-grpc-server/build.gradle create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcNativeServer.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServletServer.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessServerFactoryPropertyMapper.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyServerFactoryPropertyMapper.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnGrpcNativeServerCondition.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyServerFactoryPropertyMapper.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/package-info.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/package-info.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapter.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvoker.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequest.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequest.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ClientAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java create mode 100644 module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 module/spring-boot-grpc-server/src/main/resources/META-INF/spring.factories create mode 100644 module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServletAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizersTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerFactoryPropertyMappersTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvokerTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequestTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequestTests.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfigurationTests.java create mode 100644 module/spring-boot-grpc-server/src/test/resources/logback-test.xml create mode 100644 module/spring-boot-grpc-server/src/test/resources/org/springframework/boot/grpc/server/autoconfigure/test.jks create mode 100644 module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/AutoConfigureInProcessTransport.java create mode 100644 module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfiguration.java create mode 100644 module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTransportContextCustomizerFactory.java create mode 100644 module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/LocalGrpcPort.java create mode 100644 module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/ServerPortInfoApplicationContextInitializer.java create mode 100644 module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/package-info.java create mode 100644 module/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfigurationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc/build.gradle create mode 100644 smoke-test/spring-boot-smoke-test-grpc/src/main/java/smoketest/grpc/GrpcServerApplication.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc/src/main/java/smoketest/grpc/GrpcServerService.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc/src/main/java/smoketest/grpc/package-info.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc/src/main/proto/hello.proto create mode 100644 smoke-test/spring-boot-smoke-test-grpc/src/main/resources/application.properties create mode 100644 smoke-test/spring-boot-smoke-test-grpc/src/test/java/smoketest/grpc/GrpcServerApplicationHealthTests.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc/src/test/java/smoketest/grpc/GrpcServerApplicationTests.java create mode 100644 smoke-test/spring-boot-smoke-test-grpc/src/test/resources/application-ssl.properties create mode 100644 smoke-test/spring-boot-smoke-test-grpc/src/test/resources/test.jks create mode 100644 starter/spring-boot-starter-grpc-client/build.gradle create mode 100644 starter/spring-boot-starter-grpc-server-web/build.gradle create mode 100644 starter/spring-boot-starter-grpc-server/build.gradle create mode 100644 starter/spring-boot-starter-grpc/build.gradle diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java index d6070017d25c..9e3a28955a36 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java @@ -58,6 +58,7 @@ void documentConfigurationProperties() throws IOException { Snippets snippets = new Snippets(this.configurationPropertyMetadata); snippets.add("application-properties.core", "Core Properties", this::corePrefixes); snippets.add("application-properties.cache", "Cache Properties", this::cachePrefixes); + snippets.add("application-properties.grpc", "gRPC Properties", this::grpcPrefixes); snippets.add("application-properties.mail", "Mail Properties", this::mailPrefixes); snippets.add("application-properties.json", "JSON Properties", this::jsonPrefixes); snippets.add("application-properties.data", "Data Properties", this::dataPrefixes); @@ -159,6 +160,10 @@ private void dataMigrationPrefixes(Config prefix) { prefix.accept("spring.sql.init"); } + private void grpcPrefixes(Config prefix) { + prefix.accept("spring.grpc"); + } + private void integrationPrefixes(Config prefix) { prefix.accept("spring.activemq"); prefix.accept("spring.artemis"); diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index eac6829276e7..085130a1c007 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -50,6 +50,7 @@ + diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index 5bf08fc75605..c205696ab8b7 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -102,6 +102,8 @@ dependencies { implementation(project(path: ":module:spring-boot-data-redis-test")) implementation(project(path: ":module:spring-boot-devtools")) implementation(project(path: ":module:spring-boot-graphql-test")) + implementation(project(path: ":module:spring-boot-grpc-client")) + implementation(project(path: ":module:spring-boot-grpc-server")) implementation(project(path: ":module:spring-boot-health")) implementation(project(path: ":module:spring-boot-hibernate")) implementation(project(path: ":module:spring-boot-http-converter")) @@ -195,6 +197,7 @@ dependencies { implementation("org.springframework.data:spring-data-r2dbc") implementation("org.springframework.graphql:spring-graphql") implementation("org.springframework.graphql:spring-graphql-test") + implementation("org.springframework.grpc:spring-grpc-core") implementation("org.springframework.kafka:spring-kafka") implementation("org.springframework.kafka:spring-kafka-test") implementation("org.springframework.pulsar:spring-pulsar") diff --git a/module/spring-boot-grpc-client/build.gradle b/module/spring-boot-grpc-client/build.gradle new file mode 100644 index 000000000000..1f00daa289fb --- /dev/null +++ b/module/spring-boot-grpc-client/build.gradle @@ -0,0 +1,67 @@ +/* + * 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. + */ + +plugins { + id "java-library" + id "org.springframework.boot.auto-configuration" + id "org.springframework.boot.configuration-properties" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot gRPC Client" + + +dependencies { + api(project(":core:spring-boot")) + api("org.springframework.grpc:spring-grpc-core") + + compileOnly("com.fasterxml.jackson.core:jackson-annotations") + + optional(project(":core:spring-boot-autoconfigure")) + optional(project(":module:spring-boot-actuator")) + optional(project(":module:spring-boot-actuator-autoconfigure")) + optional(project(":module:spring-boot-health")) + optional(project(":module:spring-boot-micrometer-observation")) + optional(project(":module:spring-boot-security")) + optional(project(":module:spring-boot-security-oauth2-client")) + optional(project(":module:spring-boot-security-oauth2-resource-server")) + optional("io.grpc:grpc-servlet-jakarta") + optional("io.grpc:grpc-stub") + optional("io.grpc:grpc-netty") + optional("io.grpc:grpc-netty-shaded") + optional("io.grpc:grpc-inprocess") + optional("io.grpc:grpc-kotlin-stub") { + exclude group: "javax.annotation", module: "javax.annotation-api" + } + optional("io.micrometer:micrometer-core") + optional("io.netty:netty-transport-native-epoll") + optional("io.projectreactor:reactor-core") + optional("jakarta.servlet:jakarta.servlet-api") + optional("org.springframework:spring-web") + optional("org.springframework.security:spring-security-config") + optional("org.springframework.security:spring-security-oauth2-client") + optional("org.springframework.security:spring-security-oauth2-resource-server") + optional("org.springframework.security:spring-security-oauth2-jose") + optional("org.springframework.security:spring-security-web") + + testCompileOnly("com.fasterxml.jackson.core:jackson-annotations") + + testImplementation(project(":core:spring-boot-test")) + testImplementation(project(":test-support:spring-boot-test-support")) + + testRuntimeOnly("ch.qos.logback:logback-classic") +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java new file mode 100644 index 000000000000..663b564cf50c --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java @@ -0,0 +1,60 @@ +/* + * 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.grpc.client.autoconfigure; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import io.grpc.ManagedChannelBuilder; + +import org.springframework.boot.util.LambdaSafe; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; + +/** + * Invokes the available {@link GrpcChannelBuilderCustomizer} instances for a given + * {@link ManagedChannelBuilder}. + * + * @author Chris Bono + * @since 4.0.0 + */ +public class ChannelBuilderCustomizers { + + private final List> customizers; + + ChannelBuilderCustomizers(List> customizers) { + this.customizers = (customizers != null) ? new ArrayList<>(customizers) : Collections.emptyList(); + } + + /** + * Customize the specified {@link ManagedChannelBuilder}. Locates all + * {@link GrpcChannelBuilderCustomizer} beans able to handle the specified instance + * and invoke {@link GrpcChannelBuilderCustomizer#customize} on them. + * @param the type of channel builder + * @param authority the target authority of the channel + * @param channelBuilder the builder to customize + * @return the customized builder + */ + @SuppressWarnings("unchecked") + > T customize(String authority, T channelBuilder) { + LambdaSafe.callbacks(GrpcChannelBuilderCustomizer.class, this.customizers, channelBuilder) + .withLogger(ChannelBuilderCustomizers.class) + .invoke((customizer) -> customizer.customize(authority, channelBuilder)); + return channelBuilder; + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientInterceptorsConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientInterceptorsConfiguration.java new file mode 100644 index 000000000000..2716b6e30f36 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientInterceptorsConfiguration.java @@ -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.grpc.client.autoconfigure; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.client.ClientInterceptorsConfigurer; + +/** + * Configuration for {@link ClientInterceptorsConfigurer}. + * + * @author Chris Bono + * @since 4.0.0 + */ +@Configuration(proxyBeanMethods = false) +public class ClientInterceptorsConfiguration { + + @Bean + @ConditionalOnMissingBean + ClientInterceptorsConfigurer clientInterceptorsConfigurer(ApplicationContext applicationContext) { + return new ClientInterceptorsConfigurer(applicationContext); + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientPropertiesChannelBuilderCustomizer.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientPropertiesChannelBuilderCustomizer.java new file mode 100644 index 000000000000..280eaeed79db --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientPropertiesChannelBuilderCustomizer.java @@ -0,0 +1,83 @@ +/* + * 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.grpc.client.autoconfigure; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import io.grpc.ManagedChannelBuilder; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.ChannelConfig; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; +import org.springframework.grpc.client.interceptor.DefaultDeadlineSetupClientInterceptor; +import org.springframework.util.unit.DataSize; + +/** + * A {@link GrpcChannelBuilderCustomizer} that maps {@link GrpcClientProperties client + * properties} to a channel builder. + * + * @param the type of the builder + * @author David Syer + * @author Chris Bono + */ +class ClientPropertiesChannelBuilderCustomizer> + implements GrpcChannelBuilderCustomizer { + + private final GrpcClientProperties properties; + + ClientPropertiesChannelBuilderCustomizer(GrpcClientProperties properties) { + this.properties = properties; + } + + @Override + public void customize(String authority, T builder) { + ChannelConfig channel = this.properties.getChannel(authority); + PropertyMapper mapper = PropertyMapper.get(); + mapper.from(channel.getUserAgent()).to(builder::userAgent); + if (!authority.startsWith("unix:") && !authority.startsWith("in-process:")) { + mapper.from(channel.getDefaultLoadBalancingPolicy()).to(builder::defaultLoadBalancingPolicy); + } + mapper.from(channel.getMaxInboundMessageSize()).asInt(DataSize::toBytes).to(builder::maxInboundMessageSize); + mapper.from(channel.getMaxInboundMetadataSize()).asInt(DataSize::toBytes).to(builder::maxInboundMetadataSize); + mapper.from(channel.getKeepAliveTime()).to(durationProperty(builder::keepAliveTime)); + mapper.from(channel.getKeepAliveTimeout()).to(durationProperty(builder::keepAliveTimeout)); + mapper.from(channel.getIdleTimeout()).to(durationProperty(builder::idleTimeout)); + mapper.from(channel.isKeepAliveWithoutCalls()).to(builder::keepAliveWithoutCalls); + Map defaultServiceConfig = new HashMap<>(channel.getServiceConfig()); + if (channel.getHealth().isEnabled()) { + String serviceNameToCheck = (channel.getHealth().getServiceName() != null) + ? channel.getHealth().getServiceName() : ""; + defaultServiceConfig.put("healthCheckConfig", Map.of("serviceName", serviceNameToCheck)); + } + if (!defaultServiceConfig.isEmpty()) { + builder.defaultServiceConfig(defaultServiceConfig); + } + if (channel.getDefaultDeadline() != null && channel.getDefaultDeadline().toMillis() > 0L) { + builder.intercept(new DefaultDeadlineSetupClientInterceptor(channel.getDefaultDeadline())); + } + } + + Consumer durationProperty(BiConsumer setter) { + return (duration) -> setter.accept(duration.toNanos(), TimeUnit.NANOSECONDS); + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientScanConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientScanConfiguration.java new file mode 100644 index 000000000000..e2aa4a566759 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientScanConfiguration.java @@ -0,0 +1,88 @@ +/* + * 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.grpc.client.autoconfigure; + +import java.util.ArrayList; +import java.util.List; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.boot.autoconfigure.AutoConfigurationPackages; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.grpc.client.autoconfigure.ClientScanConfiguration.DefaultGrpcClientRegistrations; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.ChannelConfig; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.grpc.client.AbstractGrpcClientRegistrar; +import org.springframework.grpc.client.GrpcClientFactory; +import org.springframework.grpc.client.GrpcClientFactory.GrpcClientRegistrationSpec; +import org.springframework.util.Assert; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(GrpcClientFactory.class) +@Import(DefaultGrpcClientRegistrations.class) +public class ClientScanConfiguration { + + static class DefaultGrpcClientRegistrations extends AbstractGrpcClientRegistrar + implements EnvironmentAware, BeanFactoryAware { + + private @Nullable Environment environment; + + private @Nullable BeanFactory beanFactory; + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + @Override + protected GrpcClientRegistrationSpec[] collect(AnnotationMetadata meta) { + Assert.notNull(this.environment, "Environment must not be null"); + Assert.notNull(this.beanFactory, "BeanFactory must not be null"); + Binder binder = Binder.get(this.environment); + boolean hasDefaultChannel = binder.bind("spring.grpc.client.default-channel", ChannelConfig.class) + .isBound(); + if (hasDefaultChannel) { + List packages = new ArrayList<>(); + if (AutoConfigurationPackages.has(this.beanFactory)) { + packages.addAll(AutoConfigurationPackages.get(this.beanFactory)); + } + GrpcClientProperties props = binder.bind("spring.grpc.client", GrpcClientProperties.class) + .orElseGet(GrpcClientProperties::new); + + return new GrpcClientRegistrationSpec[] { GrpcClientRegistrationSpec.of("default") + .factory(props.getDefaultStubFactory()) + .packages(packages.toArray(new String[0])) }; + } + return new GrpcClientRegistrationSpec[0]; + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java new file mode 100644 index 000000000000..563c388e7d4f --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java @@ -0,0 +1,67 @@ +/* + * 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.grpc.client.autoconfigure; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.condition.NoneNestedConditions; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Primary; +import org.springframework.grpc.client.CompositeGrpcChannelFactory; +import org.springframework.grpc.client.GrpcChannelFactory; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for a + * {@link CompositeGrpcChannelFactory}. + * + * @author Chris Bono + * @since 4.0.0 + */ +@AutoConfiguration +@ConditionalOnGrpcClientEnabled +@Conditional(CompositeChannelFactoryAutoConfiguration.MultipleNonPrimaryChannelFactoriesCondition.class) +public final class CompositeChannelFactoryAutoConfiguration { + + @Bean + @Primary + CompositeGrpcChannelFactory compositeChannelFactory(ObjectProvider channelFactoriesProvider) { + return new CompositeGrpcChannelFactory(channelFactoriesProvider.orderedStream().toList()); + } + + static class MultipleNonPrimaryChannelFactoriesCondition extends NoneNestedConditions { + + MultipleNonPrimaryChannelFactoriesCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnMissingBean(GrpcChannelFactory.class) + static class NoChannelFactoryCondition { + + } + + @ConditionalOnSingleCandidate(GrpcChannelFactory.class) + static class SingleInjectableChannelFactoryCondition { + + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientEnabled.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientEnabled.java new file mode 100644 index 000000000000..40658fff4c9a --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientEnabled.java @@ -0,0 +1,45 @@ +/* + * 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.grpc.client.autoconfigure; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.grpc.stub.AbstractStub; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that only matches when the {@code io.grpc:grpc-stub} + * module is in the classpath and the {@code spring.grpc.client.enabled} property is not + * explicitly set to {@code false}. + * + * @author Freeman Freeman + * @author Chris Bono + * @since 4.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@ConditionalOnClass(AbstractStub.class) +@ConditionalOnProperty(prefix = "spring.grpc.client", name = "enabled", matchIfMissing = true) +public @interface ConditionalOnGrpcClientEnabled { + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java new file mode 100644 index 000000000000..57a8af5ae711 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java @@ -0,0 +1,124 @@ +/* + * 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.grpc.client.autoconfigure; + +import java.util.List; + +import io.grpc.Channel; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.netty.NettyChannelBuilder; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.client.ChannelCredentialsProvider; +import org.springframework.grpc.client.ClientInterceptorFilter; +import org.springframework.grpc.client.ClientInterceptorsConfigurer; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; +import org.springframework.grpc.client.GrpcChannelFactory; +import org.springframework.grpc.client.InProcessGrpcChannelFactory; +import org.springframework.grpc.client.NettyGrpcChannelFactory; +import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory; + +/** + * Configurations for {@link GrpcChannelFactory gRPC channel factories}. + * + * @author Chris Bono + */ +class GrpcChannelFactoryConfigurations { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ io.grpc.netty.shaded.io.netty.channel.Channel.class, + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class }) + @ConditionalOnMissingBean(value = GrpcChannelFactory.class, ignored = InProcessGrpcChannelFactory.class) + @ConditionalOnProperty(prefix = "spring.grpc.client.inprocess.", name = "exclusive", havingValue = "false", + matchIfMissing = true) + @EnableConfigurationProperties(GrpcClientProperties.class) + static class ShadedNettyChannelFactoryConfiguration { + + @Bean + ShadedNettyGrpcChannelFactory shadedNettyGrpcChannelFactory(GrpcClientProperties properties, + ChannelBuilderCustomizers channelBuilderCustomizers, + ClientInterceptorsConfigurer interceptorsConfigurer, + ObjectProvider channelFactoryCustomizers, + ChannelCredentialsProvider credentials) { + List> builderCustomizers = List + .of(channelBuilderCustomizers::customize); + var factory = new ShadedNettyGrpcChannelFactory(builderCustomizers, interceptorsConfigurer); + factory.setCredentialsProvider(credentials); + factory.setVirtualTargets(properties); + channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); + return factory; + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ Channel.class, NettyChannelBuilder.class }) + @ConditionalOnMissingBean(value = GrpcChannelFactory.class, ignored = InProcessGrpcChannelFactory.class) + @ConditionalOnProperty(prefix = "spring.grpc.client.inprocess.", name = "exclusive", havingValue = "false", + matchIfMissing = true) + @EnableConfigurationProperties(GrpcClientProperties.class) + static class NettyChannelFactoryConfiguration { + + @Bean + NettyGrpcChannelFactory nettyGrpcChannelFactory(GrpcClientProperties properties, + ChannelBuilderCustomizers channelBuilderCustomizers, + ClientInterceptorsConfigurer interceptorsConfigurer, + ObjectProvider channelFactoryCustomizers, + ChannelCredentialsProvider credentials) { + List> builderCustomizers = List + .of(channelBuilderCustomizers::customize); + var factory = new NettyGrpcChannelFactory(builderCustomizers, interceptorsConfigurer); + factory.setCredentialsProvider(credentials); + factory.setVirtualTargets(properties); + channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); + return factory; + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(InProcessChannelBuilder.class) + @ConditionalOnMissingBean(InProcessGrpcChannelFactory.class) + @ConditionalOnProperty(prefix = "spring.grpc.client.inprocess", name = "enabled", havingValue = "true", + matchIfMissing = true) + static class InProcessChannelFactoryConfiguration { + + @Bean + InProcessGrpcChannelFactory inProcessGrpcChannelFactory(ChannelBuilderCustomizers channelBuilderCustomizers, + ClientInterceptorsConfigurer interceptorsConfigurer, + ObjectProvider interceptorFilter, + ObjectProvider channelFactoryCustomizers) { + List> inProcessBuilderCustomizers = List + .of(channelBuilderCustomizers::customize); + InProcessGrpcChannelFactory factory = new InProcessGrpcChannelFactory(inProcessBuilderCustomizers, + interceptorsConfigurer); + if (interceptorFilter != null) { + factory.setInterceptorFilter(interceptorFilter.getIfAvailable(() -> null)); + } + channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); + return factory; + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java new file mode 100644 index 000000000000..c3a665ca8b32 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java @@ -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.grpc.client.autoconfigure; + +import org.springframework.grpc.client.GrpcChannelFactory; + +public interface GrpcChannelFactoryCustomizer { + + /** + * Customize the given {@link GrpcChannelFactory}. + * @param factory the factory to customize + */ + void customize(GrpcChannelFactory factory); + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java new file mode 100644 index 000000000000..a14f0aa716d2 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java @@ -0,0 +1,91 @@ +/* + * 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.grpc.client.autoconfigure; + +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.ManagedChannelBuilder; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.grpc.client.autoconfigure.codec.GrpcCodecConfiguration; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.grpc.client.ChannelCredentialsProvider; +import org.springframework.grpc.client.CoroutineStubFactory; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; + +@AutoConfiguration(before = CompositeChannelFactoryAutoConfiguration.class) +@ConditionalOnGrpcClientEnabled +@EnableConfigurationProperties(GrpcClientProperties.class) +@Import({ GrpcCodecConfiguration.class, ClientInterceptorsConfiguration.class, + GrpcChannelFactoryConfigurations.ShadedNettyChannelFactoryConfiguration.class, + GrpcChannelFactoryConfigurations.NettyChannelFactoryConfiguration.class, + GrpcChannelFactoryConfigurations.InProcessChannelFactoryConfiguration.class, ClientScanConfiguration.class }) +public final class GrpcClientAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(ChannelCredentialsProvider.class) + NamedChannelCredentialsProvider channelCredentialsProvider(SslBundles bundles, GrpcClientProperties properties) { + return new NamedChannelCredentialsProvider(bundles, properties); + } + + @Bean + > GrpcChannelBuilderCustomizer clientPropertiesChannelCustomizer( + GrpcClientProperties properties) { + return new ClientPropertiesChannelBuilderCustomizer<>(properties); + } + + @ConditionalOnBean(CompressorRegistry.class) + @Bean + > GrpcChannelBuilderCustomizer compressionClientCustomizer( + CompressorRegistry registry) { + return (name, builder) -> builder.compressorRegistry(registry); + } + + @ConditionalOnBean(DecompressorRegistry.class) + @Bean + > GrpcChannelBuilderCustomizer decompressionClientCustomizer( + DecompressorRegistry registry) { + return (name, builder) -> builder.decompressorRegistry(registry); + } + + @ConditionalOnMissingBean + @Bean + ChannelBuilderCustomizers channelBuilderCustomizers(ObjectProvider> customizers) { + return new ChannelBuilderCustomizers(customizers.orderedStream().toList()); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = "io.grpc.kotlin.AbstractCoroutineStub") + static class GrpcClientCoroutineStubConfiguration { + + @Bean + @ConditionalOnMissingBean + CoroutineStubFactory coroutineStubFactory() { + return new CoroutineStubFactory(); + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java new file mode 100644 index 000000000000..a98a9e9af435 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java @@ -0,0 +1,46 @@ +/* + * 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.grpc.client.autoconfigure; + +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor; +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.grpc.client.GlobalClientInterceptor; + +@AutoConfiguration( + afterName = "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration") +@ConditionalOnGrpcClientEnabled +@ConditionalOnClass({ ObservationRegistry.class, ObservationGrpcClientInterceptor.class }) +@ConditionalOnBean(ObservationRegistry.class) +@ConditionalOnProperty(name = "spring.grpc.client.observation.enabled", havingValue = "true", matchIfMissing = true) + +public final class GrpcClientObservationAutoConfiguration { + + @Bean + @GlobalClientInterceptor + @ConditionalOnMissingBean + ObservationGrpcClientInterceptor observationGrpcClientInterceptor(ObservationRegistry observationRegistry) { + return new ObservationGrpcClientInterceptor(observationRegistry); + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java new file mode 100644 index 000000000000..0da9e0dcf4da --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java @@ -0,0 +1,478 @@ +/* + * 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.grpc.client.autoconfigure; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import io.grpc.ManagedChannel; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.convert.DurationUnit; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.grpc.client.BlockingStubFactory; +import org.springframework.grpc.client.NegotiationType; +import org.springframework.grpc.client.StubFactory; +import org.springframework.grpc.client.VirtualTargets; +import org.springframework.util.unit.DataSize; + +@ConfigurationProperties(prefix = "spring.grpc.client") +public class GrpcClientProperties implements EnvironmentAware, VirtualTargets { + + /** + * The default channel configuration to use for new channels. + */ + private final ChannelConfig defaultChannel = new ChannelConfig(); + + /** + * Map of channels configured by name. + */ + private final Map channels = new HashMap<>(); + + /** + * Default stub factory to use for all channels. + */ + private Class> defaultStubFactory = BlockingStubFactory.class; + + private Environment environment; + + GrpcClientProperties() { + this.defaultChannel.setAddress("static://localhost:9090"); + this.environment = new StandardEnvironment(); + } + + public ChannelConfig getDefaultChannel() { + return this.defaultChannel; + } + + public Map getChannels() { + return this.channels; + } + + public Class> getDefaultStubFactory() { + return this.defaultStubFactory; + } + + public void setDefaultStubFactory(Class> defaultStubFactory) { + this.defaultStubFactory = defaultStubFactory; + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + /** + * Gets the configured channel with the given name. If no channel is configured for + * the specified name then one is created using the default channel as a template. + * @param name the name of the channel + * @return the configured channel if found, or a newly created channel using the + * default channel as a template + */ + public ChannelConfig getChannel(String name) { + if ("default".equals(name)) { + return this.defaultChannel; + } + ChannelConfig channel = this.channels.get(name); + if (channel != null) { + return channel; + } + channel = this.defaultChannel.copy(); + String address = name; + if (!name.contains(":/") && !name.startsWith("unix:")) { + if (name.contains(":")) { + address = "static://" + name; + } + else { + address = this.defaultChannel.getAddress(); + if (!address.contains(":/")) { + address = "static://" + address; + } + } + } + channel.setAddress(address); + return channel; + } + + @Override + public String getTarget(String authority) { + ChannelConfig channel = this.getChannel(authority); + String address = channel.getAddress(); + if (address.startsWith("static:") || address.startsWith("tcp:")) { + address = address.substring(address.indexOf(":") + 1).replaceFirst("/*", ""); + } + return this.environment.resolvePlaceholders(address); + } + + /** + * Represents the configuration for a {@link ManagedChannel gRPC channel}. + */ + public static class ChannelConfig { + + /** + * The target address uri to connect to. + */ + private String address = "static://localhost:9090"; + + public String getAddress() { + return this.address; + } + + public void setAddress(final String address) { + this.address = address; + } + + // -------------------------------------------------- + // defaultLoadBalancingPolicy + // -------------------------------------------------- + + /** + * The default load balancing policy the channel should use. + */ + private String defaultLoadBalancingPolicy = "round_robin"; + + public String getDefaultLoadBalancingPolicy() { + return this.defaultLoadBalancingPolicy; + } + + public void setDefaultLoadBalancingPolicy(final String defaultLoadBalancingPolicy) { + this.defaultLoadBalancingPolicy = defaultLoadBalancingPolicy; + } + + // -------------------------------------------------- + + private final Health health = new Health(); + + public Health getHealth() { + return this.health; + } + + /** + * Map representation of the service config to use for the channel. + */ + private final Map serviceConfig = new HashMap<>(); + + public Map getServiceConfig() { + return this.serviceConfig; + } + + /** + * The negotiation type for the channel. + */ + private NegotiationType negotiationType = NegotiationType.PLAINTEXT; + + public NegotiationType getNegotiationType() { + return this.negotiationType; + } + + public void setNegotiationType(NegotiationType negotiationType) { + this.negotiationType = negotiationType; + } + + // -------------------------------------------------- + // KeepAlive + // -------------------------------------------------- + + /** + * Whether keep alive is enabled on the channel. + */ + private boolean enableKeepAlive = false; + + public boolean isEnableKeepAlive() { + return this.enableKeepAlive; + } + + public void setEnableKeepAlive(boolean enableKeepAlive) { + this.enableKeepAlive = enableKeepAlive; + } + + // -------------------------------------------------- + + /** + * The duration without ongoing RPCs before going to idle mode. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration idleTimeout = Duration.ofSeconds(20); + + public Duration getIdleTimeout() { + return this.idleTimeout; + } + + public void setIdleTimeout(Duration idleTimeout) { + this.idleTimeout = idleTimeout; + } + + // -------------------------------------------------- + + /** + * The delay before sending a keepAlive. Note that shorter intervals increase the + * network burden for the server and this value can not be lower than + * 'permitKeepAliveTime' on the server. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration keepAliveTime = Duration.ofMinutes(5); + + public Duration getKeepAliveTime() { + return this.keepAliveTime; + } + + public void setKeepAliveTime(Duration keepAliveTime) { + this.keepAliveTime = keepAliveTime; + } + + // -------------------------------------------------- + + /** + * The default timeout for a keepAlives ping request. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration keepAliveTimeout = Duration.ofSeconds(20); + + public Duration getKeepAliveTimeout() { + return this.keepAliveTimeout; + } + + public void setKeepAliveTimeout(Duration keepAliveTimeout) { + this.keepAliveTimeout = keepAliveTimeout; + } + + // -------------------------------------------------- + + /** + * Whether a keepAlive will be performed when there are no outstanding RPC on a + * connection. + */ + private boolean keepAliveWithoutCalls = false; + + public boolean isKeepAliveWithoutCalls() { + return this.keepAliveWithoutCalls; + } + + public void setKeepAliveWithoutCalls(boolean keepAliveWithoutCalls) { + this.keepAliveWithoutCalls = keepAliveWithoutCalls; + } + + // -------------------------------------------------- + // Message Transfer + // -------------------------------------------------- + + /** + * Maximum message size allowed to be received by the channel (default 4MiB). Set + * to '-1' to use the highest possible limit (not recommended). + */ + private DataSize maxInboundMessageSize = DataSize.ofBytes(4194304); + + /** + * Maximum metadata size allowed to be received by the channel (default 8KiB). Set + * to '-1' to use the highest possible limit (not recommended). + */ + private DataSize maxInboundMetadataSize = DataSize.ofBytes(8192); + + public DataSize getMaxInboundMessageSize() { + return this.maxInboundMessageSize; + } + + public void setMaxInboundMessageSize(final DataSize maxInboundMessageSize) { + this.setMaxInboundSize(maxInboundMessageSize, (s) -> this.maxInboundMessageSize = s, + "maxInboundMessageSize"); + } + + public DataSize getMaxInboundMetadataSize() { + return this.maxInboundMetadataSize; + } + + public void setMaxInboundMetadataSize(DataSize maxInboundMetadataSize) { + this.setMaxInboundSize(maxInboundMetadataSize, (s) -> this.maxInboundMetadataSize = s, + "maxInboundMetadataSize"); + } + + private void setMaxInboundSize(DataSize maxSize, Consumer setter, String propertyName) { + if (maxSize != null && maxSize.toBytes() >= 0) { + setter.accept(maxSize); + } + else if (maxSize != null && maxSize.toBytes() == -1) { + setter.accept(DataSize.ofBytes(Integer.MAX_VALUE)); + } + else { + throw new IllegalArgumentException("Unsupported %s: %s".formatted(propertyName, maxSize)); + } + } + + // -------------------------------------------------- + + /** + * The custom User-Agent for the channel. + */ + private @Nullable String userAgent = null; + + public @Nullable String getUserAgent() { + return this.userAgent; + } + + public void setUserAgent(@Nullable String userAgent) { + this.userAgent = userAgent; + } + + /** + * The default deadline for RPCs performed on this channel. + */ + private @Nullable Duration defaultDeadline = null; + + public @Nullable Duration getDefaultDeadline() { + return this.defaultDeadline; + } + + public void setDefaultDeadline(@Nullable Duration defaultDeadline) { + this.defaultDeadline = defaultDeadline; + } + + /** + * Provide a copy of the channel instance. + * @return a copy of the channel instance. + */ + public ChannelConfig copy() { + ChannelConfig copy = new ChannelConfig(); + copy.address = this.address; + copy.defaultLoadBalancingPolicy = this.defaultLoadBalancingPolicy; + copy.negotiationType = this.negotiationType; + copy.enableKeepAlive = this.enableKeepAlive; + copy.idleTimeout = this.idleTimeout; + copy.keepAliveTime = this.keepAliveTime; + copy.keepAliveTimeout = this.keepAliveTimeout; + copy.keepAliveWithoutCalls = this.keepAliveWithoutCalls; + copy.maxInboundMessageSize = this.maxInboundMessageSize; + copy.maxInboundMetadataSize = this.maxInboundMetadataSize; + copy.userAgent = this.userAgent; + copy.defaultDeadline = this.defaultDeadline; + copy.health.copyValuesFrom(this.getHealth()); + copy.ssl.copyValuesFrom(this.getSsl()); + copy.serviceConfig.putAll(this.serviceConfig); + return copy; + } + + // -------------------------------------------------- + + /** + * Flag to say that strict SSL checks are not enabled (so the remote certificate + * could be anonymous). + */ + private boolean secure = true; + + public boolean isSecure() { + return this.secure; + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + // -------------------------------------------------- + + private final Ssl ssl = new Ssl(); + + public Ssl getSsl() { + return this.ssl; + } + + public static class Ssl { + + /** + * Whether to enable SSL support. Enabled automatically if "bundle" is + * provided unless specified otherwise. + */ + private @Nullable Boolean enabled; + + /** + * SSL bundle name. + */ + private @Nullable String bundle; + + public boolean isEnabled() { + return (this.enabled != null) ? this.enabled : this.bundle != null; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public @Nullable String getBundle() { + return this.bundle; + } + + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; + } + + /** + * Copies the values from another instance. + * @param other instance to copy values from + */ + public void copyValuesFrom(Ssl other) { + this.enabled = other.enabled; + this.bundle = other.bundle; + } + + } + + public static class Health { + + /** + * Whether to enable client-side health check for the channel. + */ + private boolean enabled = false; + + /** + * Name of the service to check health on. + */ + private @Nullable String serviceName; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public @Nullable String getServiceName() { + return this.serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + /** + * Copies the values from another instance. + * @param other instance to copy values from + */ + public void copyValuesFrom(Health other) { + this.enabled = other.enabled; + this.serviceName = other.serviceName; + } + + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java new file mode 100644 index 000000000000..45778bcefe15 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java @@ -0,0 +1,79 @@ +/* + * 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.grpc.client.autoconfigure; + +import javax.net.ssl.TrustManagerFactory; + +import io.grpc.ChannelCredentials; +import io.grpc.InsecureChannelCredentials; +import io.grpc.TlsChannelCredentials; + +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.ChannelConfig; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.grpc.client.ChannelCredentialsProvider; +import org.springframework.grpc.client.NegotiationType; +import org.springframework.grpc.internal.InsecureTrustManagerFactory; +import org.springframework.util.Assert; + +/** + * Provides channel credentials using channel configuration and {@link SslBundles}. + * + * @author David Syer + * @since 4.0.0 + */ +public class NamedChannelCredentialsProvider implements ChannelCredentialsProvider { + + private final SslBundles bundles; + + private final GrpcClientProperties properties; + + public NamedChannelCredentialsProvider(SslBundles bundles, GrpcClientProperties properties) { + this.bundles = bundles; + this.properties = properties; + } + + @Override + public ChannelCredentials getChannelCredentials(String path) { + ChannelConfig channel = this.properties.getChannel(path); + if (!channel.getSsl().isEnabled() && channel.getNegotiationType() == NegotiationType.PLAINTEXT) { + return InsecureChannelCredentials.create(); + } + if (channel.getSsl().isEnabled()) { + String bundleName = channel.getSsl().getBundle(); + Assert.notNull(bundleName, "Bundle name must not be null when SSL is enabled"); + SslBundle bundle = this.bundles.getBundle(bundleName); + TrustManagerFactory trustManagers = channel.isSecure() ? bundle.getManagers().getTrustManagerFactory() + : InsecureTrustManagerFactory.INSTANCE; + return TlsChannelCredentials.newBuilder() + .keyManager(bundle.getManagers().getKeyManagerFactory().getKeyManagers()) + .trustManager(trustManagers.getTrustManagers()) + .build(); + } + else { + if (channel.isSecure()) { + return TlsChannelCredentials.create(); + } + else { + return TlsChannelCredentials.newBuilder() + .trustManager(InsecureTrustManagerFactory.INSTANCE.getTrustManagers()) + .build(); + } + } + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfiguration.java new file mode 100644 index 000000000000..ea01da35c325 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfiguration.java @@ -0,0 +1,57 @@ +/* + * 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.grpc.client.autoconfigure.codec; + +import io.grpc.Codec; +import io.grpc.Compressor; +import io.grpc.CompressorRegistry; +import io.grpc.Decompressor; +import io.grpc.DecompressorRegistry; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * The configuration that contains all codec related beans for clients. + * + * @author Andrei Lisa + * @since 4.0.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(Codec.class) +public class GrpcCodecConfiguration { + + @Bean + @ConditionalOnMissingBean + CompressorRegistry compressorRegistry(ObjectProvider compressors) { + CompressorRegistry registry = CompressorRegistry.getDefaultInstance(); + compressors.orderedStream().forEachOrdered(registry::register); + return registry; + } + + @Bean + @ConditionalOnMissingBean + DecompressorRegistry decompressorRegistry(ObjectProvider decompressors) { + DecompressorRegistry registry = DecompressorRegistry.getDefaultInstance(); + decompressors.orderedStream().forEachOrdered((decompressor) -> registry.with(decompressor, false)); + return registry; + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/package-info.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/package-info.java new file mode 100644 index 000000000000..82c09391fd1f --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * Auto-configuration for gRPC client codecs. + */ +@NullMarked +package org.springframework.boot.grpc.client.autoconfigure.codec; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java new file mode 100644 index 000000000000..efa3388c6505 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * Auto-configuration for gRPC client. + */ +@NullMarked +package org.springframework.boot.grpc.client.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 000000000000..52beeba738d9 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,29 @@ +{ + "groups": [], + "properties": [ + { + "name": "spring.grpc.client.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable client autoconfiguration.", + "defaultValue": true + }, + { + "name": "spring.grpc.client.inprocess.enabled", + "type": "java.lang.Boolean", + "description": "Whether to configure the in-process channel factory.", + "defaultValue": true + }, + { + "name": "spring.grpc.client.inprocess.exclusive", + "type": "java.lang.Boolean", + "description": "Whether the inprocess channel factory should be the only channel factory available. When the value is true, no other channel factory will be configured.", + "defaultValue": true + }, + { + "name": "spring.grpc.client.observations.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Observations on the client.", + "defaultValue": true + } + ] +} diff --git a/module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000000..7d3249ab92fc --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +org.springframework.boot.grpc.client.autoconfigure.CompositeChannelFactoryAutoConfiguration +org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration +org.springframework.boot.grpc.client.autoconfigure.GrpcClientObservationAutoConfiguration diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizersTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizersTests.java new file mode 100644 index 000000000000..dd089aefff49 --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizersTests.java @@ -0,0 +1,124 @@ +/* + * 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.grpc.client.autoconfigure; + +import java.util.ArrayList; +import java.util.List; + +import io.grpc.ManagedChannelBuilder; +import io.grpc.netty.NettyChannelBuilder; +import org.junit.jupiter.api.Test; + +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ChannelBuilderCustomizers}. + * + * @author Chris Bono + */ +class ChannelBuilderCustomizersTests { + + private static final String DEFAULT_TARGET = "localhost"; + + @Test + void customizeWithNullCustomizersShouldDoNothing() { + ManagedChannelBuilder channelBuilder = mock(ManagedChannelBuilder.class); + new ChannelBuilderCustomizers(null).customize(DEFAULT_TARGET, channelBuilder); + then(channelBuilder).shouldHaveNoInteractions(); + } + + @Test + void customizeSimpleChannelBuilder() { + ChannelBuilderCustomizers customizers = new ChannelBuilderCustomizers( + List.of(new SimpleChannelBuilderCustomizer())); + NettyChannelBuilder channelBuilder = mock(NettyChannelBuilder.class); + customizers.customize(DEFAULT_TARGET, channelBuilder); + then(channelBuilder).should().flowControlWindow(100); + } + + @Test + void customizeShouldCheckGeneric() { + List> list = new ArrayList<>(); + list.add(new TestCustomizer<>()); + list.add(new TestNettyChannelBuilderCustomizer()); + list.add(new TestShadedNettyChannelBuilderCustomizer()); + ChannelBuilderCustomizers customizers = new ChannelBuilderCustomizers(list); + + customizers.customize(DEFAULT_TARGET, mock(ManagedChannelBuilder.class)); + assertThat(list.get(0).getCount()).isOne(); + assertThat(list.get(1).getCount()).isZero(); + assertThat(list.get(2).getCount()).isZero(); + + customizers.customize(DEFAULT_TARGET, mock(NettyChannelBuilder.class)); + assertThat(list.get(0).getCount()).isEqualTo(2); + assertThat(list.get(1).getCount()).isOne(); + assertThat(list.get(2).getCount()).isZero(); + + customizers.customize(DEFAULT_TARGET, mock(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)); + assertThat(list.get(0).getCount()).isEqualTo(3); + assertThat(list.get(1).getCount()).isOne(); + assertThat(list.get(2).getCount()).isOne(); + } + + static class SimpleChannelBuilderCustomizer implements GrpcChannelBuilderCustomizer { + + @Override + public void customize(String target, NettyChannelBuilder channelBuilder) { + channelBuilder.flowControlWindow(100); + } + + } + + /** + * Test customizer that will match any {@link GrpcChannelBuilderCustomizer}. + */ + static class TestCustomizer> implements GrpcChannelBuilderCustomizer { + + private int count; + + @Override + public void customize(String target, T channelBuilder) { + this.count++; + } + + int getCount() { + return this.count; + } + + } + + /** + * Test customizer that will match only {@link NettyChannelBuilder}. + */ + static class TestNettyChannelBuilderCustomizer extends TestCustomizer { + + } + + /** + * Test customizer that will match only + * {@link io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder}. + */ + static class TestShadedNettyChannelBuilderCustomizer + extends TestCustomizer { + + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ClientScanConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ClientScanConfigurationTests.java new file mode 100644 index 000000000000..16cc33bca683 --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ClientScanConfigurationTests.java @@ -0,0 +1,179 @@ +/* + * 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.grpc.client.autoconfigure; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigurationPackages; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.grpc.client.BlockingStubFactory; +import org.springframework.grpc.client.CoroutineStubFactory; +import org.springframework.grpc.client.GrpcClientFactory.GrpcClientRegistrationSpec; +import org.springframework.grpc.client.ReactorStubFactory; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for the {@link ClientScanConfiguration}. + * + * @author CheolHwan Ihn + */ +class ClientScanConfigurationTests { + + @Test + void testReactorStubFactory() { + Map properties = new HashMap<>(); + properties.put("spring.grpc.client.default-stub-factory", ReactorStubFactory.class.getName()); + properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090"); + + MockEnvironment environment = new MockEnvironment(); + environment.getPropertySources().addFirst(new MapPropertySource("test", properties)); + + ClientScanConfiguration.DefaultGrpcClientRegistrations registrations = new ClientScanConfiguration.DefaultGrpcClientRegistrations(); + registrations.setEnvironment(environment); + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.setEnvironment(environment); + AutoConfigurationPackages.register(context, "org.springframework.grpc.autoconfigure.client"); + registrations.setBeanFactory(context.getBeanFactory()); + + GrpcClientRegistrationSpec[] specs = registrations.collect(null); + + assertThat(specs).hasSize(1); + assertThat(specs[0].factory()).isEqualTo(ReactorStubFactory.class); + } + + @Test + void testDefaultStubFactory() { + Map properties = new HashMap<>(); + properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090"); + + MockEnvironment environment = new MockEnvironment(); + environment.getPropertySources().addFirst(new MapPropertySource("test", properties)); + + ClientScanConfiguration.DefaultGrpcClientRegistrations registrations = new ClientScanConfiguration.DefaultGrpcClientRegistrations(); + registrations.setEnvironment(environment); + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.setEnvironment(environment); + AutoConfigurationPackages.register(context, "org.springframework.grpc.autoconfigure.client"); + registrations.setBeanFactory(context.getBeanFactory()); + + GrpcClientRegistrationSpec[] specs = registrations.collect(null); + + assertThat(specs).hasSize(1); + assertThat(specs[0].factory()).isEqualTo(BlockingStubFactory.class); + } + + @Test + void testCoroutineStubFactory() { + Map properties = new HashMap<>(); + properties.put("spring.grpc.client.default-stub-factory", CoroutineStubFactory.class.getName()); + properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090"); + + MockEnvironment env = new MockEnvironment(); + env.getPropertySources().addFirst(new MapPropertySource("test", properties)); + + var regs = new ClientScanConfiguration.DefaultGrpcClientRegistrations(); + regs.setEnvironment(env); + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.setEnvironment(env); + AutoConfigurationPackages.register(context, "org.springframework.grpc.autoconfigure.client"); + regs.setBeanFactory(context.getBeanFactory()); + + GrpcClientRegistrationSpec[] specs = regs.collect(null); + assertThat(specs).hasSize(1); + assertThat(specs[0].factory()).isEqualTo(CoroutineStubFactory.class); + } + } + + @Test + void testInvalidStubFactoryValueThrowsBindException() { + Map properties = new HashMap<>(); + properties.put("spring.grpc.client.default-stub-factory", "com.example.InvalidStubFactory"); + properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090"); + + MockEnvironment env = new MockEnvironment(); + env.getPropertySources().addFirst(new MapPropertySource("test", properties)); + + var regs = new ClientScanConfiguration.DefaultGrpcClientRegistrations(); + regs.setEnvironment(env); + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.setEnvironment(env); + AutoConfigurationPackages.register(context, "org.springframework.grpc.autoconfigure.client"); + regs.setBeanFactory(context.getBeanFactory()); + + assertThatExceptionOfType(BindException.class).isThrownBy(() -> regs.collect(null)) + .isInstanceOf(BindException.class) + .withMessageContaining("spring.grpc.client.default-stub-factory"); + } + } + + @Nested + @TestConfiguration(proxyBeanMethods = false) + @SpringBootTest(classes = { ClientScanConfiguration.class, ClientScanConfigurationSpringBootTest.TestConfig.class }, + properties = { "spring.grpc.client.default-channel.address=static://localhost:9090", + "spring.grpc.client.default-stub-factory=org.springframework.grpc.client.ReactorStubFactory" }) + class ClientScanConfigurationSpringBootTest { + + @Autowired + private ApplicationContext context; + + @Autowired + private GrpcClientProperties props; + + @Autowired + private Environment env; + + @Test + void propertyIsBoundAsBeanAndUsable() { + assertThat(this.props.getDefaultStubFactory()).isEqualTo(ReactorStubFactory.class); + + GrpcClientProperties rebound = Binder.get(this.env) + .bind("spring.grpc.client", GrpcClientProperties.class) + .get(); + + assertThat(rebound.getDefaultStubFactory()).isEqualTo(ReactorStubFactory.class); + } + + @Configuration + @EnableConfigurationProperties(GrpcClientProperties.class) + static class TestConfig { + + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java new file mode 100644 index 000000000000..5540405d15bc --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java @@ -0,0 +1,141 @@ +/* + * 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.grpc.client.autoconfigure; + +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.netty.NettyChannelBuilder; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.grpc.client.CompositeGrpcChannelFactory; +import org.springframework.grpc.client.GrpcChannelFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link CompositeChannelFactoryAutoConfiguration}. + * + * @author Chris Bono + */ +@SuppressWarnings({ "unchecked", "rawtypes" }) +class CompositeChannelFactoryAutoConfigurationTests { + + private ApplicationContextRunner contextRunnerWithoutChannelFactories() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class, SslAutoConfiguration.class, + CompositeChannelFactoryAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class, + NettyChannelBuilder.class, InProcessChannelBuilder.class)); + } + + @Test + void whenNoChannelFactoriesDoesNotAutoconfigureComposite() { + this.contextRunnerWithoutChannelFactories() + .run((context) -> assertThat(context).doesNotHaveBean(GrpcChannelFactory.class)); + } + + @Test + void whenSingleChannelFactoryDoesNotAutoconfigureComposite() { + GrpcChannelFactory channelFactory1 = mock(); + this.contextRunnerWithoutChannelFactories() + .withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1) + .run((context) -> assertThat(context).hasSingleBean(GrpcChannelFactory.class) + .getBean(GrpcChannelFactory.class) + .isNotInstanceOf(CompositeGrpcChannelFactory.class) + .isSameAs(channelFactory1)); + } + + @Test + void whenMultipleChannelFactoriesWithPrimaryDoesNotAutoconfigureComposite() { + GrpcChannelFactory channelFactory1 = mock(); + GrpcChannelFactory channelFactory2 = mock(); + this.contextRunnerWithoutChannelFactories() + .withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1) + .withBean("channelFactory2", GrpcChannelFactory.class, () -> channelFactory2, (bd) -> bd.setPrimary(true)) + .run((context) -> { + assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("channelFactory1", "channelFactory2"); + assertThat(context).getBean(GrpcChannelFactory.class) + .isNotInstanceOf(CompositeGrpcChannelFactory.class) + .isSameAs(channelFactory2); + }); + } + + @Test + void whenMultipleChannelFactoriesDoesAutoconfigureComposite() { + GrpcChannelFactory channelFactory1 = mock(); + GrpcChannelFactory channelFactory2 = mock(); + this.contextRunnerWithoutChannelFactories() + .withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1) + .withBean("channelFactory2", GrpcChannelFactory.class, () -> channelFactory2) + .run((context) -> { + assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("channelFactory1", "channelFactory2", "compositeChannelFactory"); + assertThat(context).getBean(GrpcChannelFactory.class).isInstanceOf(CompositeGrpcChannelFactory.class); + }); + } + + @Test + void compositeAutoconfiguredAsExpected() { + this.contextRunnerWithoutChannelFactories() + .withUserConfiguration(MultipleFactoriesTestConfig.class) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(CompositeGrpcChannelFactory.class) + .extracting("channelFactories") + .asInstanceOf(InstanceOfAssertFactories.list(GrpcChannelFactory.class)) + .containsExactly(MultipleFactoriesTestConfig.CHANNEL_FACTORY_BAR, + MultipleFactoriesTestConfig.CHANNEL_FACTORY_ZAA, + MultipleFactoriesTestConfig.CHANNEL_FACTORY_FOO)); + + } + + @Configuration(proxyBeanMethods = false) + static class MultipleFactoriesTestConfig { + + static GrpcChannelFactory CHANNEL_FACTORY_FOO = mock(); + static GrpcChannelFactory CHANNEL_FACTORY_BAR = mock(); + static GrpcChannelFactory CHANNEL_FACTORY_ZAA = mock(); + + @Bean + @Order(3) + GrpcChannelFactory channelFactoryFoo() { + return CHANNEL_FACTORY_FOO; + } + + @Bean + @Order(1) + GrpcChannelFactory channelFactoryBar() { + return CHANNEL_FACTORY_BAR; + } + + @Bean + @Order(2) + GrpcChannelFactory channelFactoryZaa() { + return CHANNEL_FACTORY_ZAA; + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java new file mode 100644 index 000000000000..175be92bf2cb --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java @@ -0,0 +1,435 @@ +/* + * 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.grpc.client.autoconfigure; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import io.grpc.Codec; +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.ManagedChannelBuilder; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.kotlin.AbstractCoroutineStub; +import io.grpc.netty.NettyChannelBuilder; +import io.grpc.stub.AbstractStub; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.grpc.client.ChannelCredentialsProvider; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; +import org.springframework.grpc.client.GrpcChannelFactory; +import org.springframework.grpc.client.InProcessGrpcChannelFactory; +import org.springframework.grpc.client.NettyGrpcChannelFactory; +import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link GrpcClientAutoConfiguration}. + * + * @author Chris Bono + */ +@SuppressWarnings({ "unchecked", "rawtypes" }) +class GrpcClientAutoConfigurationTests { + + private ApplicationContextRunner contextRunner() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class, SslAutoConfiguration.class)); + } + + private ApplicationContextRunner contextRunnerWithoutInProcessChannelFactory() { + return this.contextRunner().withPropertyValues("spring.grpc.client.inprocess.enabled=false"); + } + + @Test + void whenGrpcStubNotOnClasspathThenAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(AbstractStub.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientAutoConfiguration.class)); + } + + @Test + void whenGrpcKotlinIsNotOnClasspathThenAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(AbstractCoroutineStub.class)) + .run((context) -> assertThat(context) + .doesNotHaveBean(GrpcClientAutoConfiguration.GrpcClientCoroutineStubConfiguration.class)); + } + + @Test + void whenClientEnabledPropertySetFalseThenAutoConfigurationIsSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.client.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientAutoConfiguration.class)); + } + + @Test + void whenClientEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() { + this.contextRunner().run((context) -> assertThat(context).hasSingleBean(GrpcClientAutoConfiguration.class)); + } + + @Test + void whenClientEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.client.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcClientAutoConfiguration.class)); + } + + @Test + void whenHasUserDefinedCredentialsProviderDoesNotAutoConfigureBean() { + ChannelCredentialsProvider customCredentialsProvider = mock(ChannelCredentialsProvider.class); + this.contextRunner() + .withBean("customCredentialsProvider", ChannelCredentialsProvider.class, () -> customCredentialsProvider) + .run((context) -> assertThat(context).getBean(ChannelCredentialsProvider.class) + .isSameAs(customCredentialsProvider)); + } + + @Test + void credentialsProviderAutoConfiguredAsExpected() { + this.contextRunner() + .run((context) -> assertThat(context).getBean(NamedChannelCredentialsProvider.class) + .hasFieldOrPropertyWithValue("properties", context.getBean(GrpcClientProperties.class)) + .extracting("bundles") + .isInstanceOf(SslBundles.class)); + } + + @Test + void clientPropertiesAutoConfiguredResolvesPlaceholders() { + this.contextRunner() + .withPropertyValues("spring.grpc.client.channels.c1.address=my-server-${channelName}:8888", + "channelName=foo") + .run((context) -> assertThat(context).getBean(GrpcClientProperties.class) + .satisfies((properties) -> assertThat(properties.getTarget("c1")).isEqualTo("my-server-foo:8888"))); + } + + @Test + void clientPropertiesChannelCustomizerAutoConfiguredWithHealthAsExpected() { + this.contextRunner() + .withPropertyValues("spring.grpc.client.channels.test.health.enabled=true", + "spring.grpc.client.channels.test.health.service-name=my-service") + .run((context) -> { + assertThat(context).getBean("clientPropertiesChannelCustomizer", GrpcChannelBuilderCustomizer.class) + .isNotNull(); + var customizer = context.getBean("clientPropertiesChannelCustomizer", + GrpcChannelBuilderCustomizer.class); + ManagedChannelBuilder builder = Mockito.mock(); + customizer.customize("test", builder); + Map healthCheckConfig = Map.of("healthCheckConfig", Map.of("serviceName", "my-service")); + then(builder).should().defaultServiceConfig(healthCheckConfig); + }); + } + + @Test + void clientPropertiesChannelCustomizerAutoConfiguredWithoutHealthAsExpected() { + this.contextRunner().run((context) -> { + assertThat(context).getBean("clientPropertiesChannelCustomizer", GrpcChannelBuilderCustomizer.class) + .isNotNull(); + var customizer = context.getBean("clientPropertiesChannelCustomizer", GrpcChannelBuilderCustomizer.class); + ManagedChannelBuilder builder = Mockito.mock(); + customizer.customize("test", builder); + then(builder).should(never()).defaultServiceConfig(anyMap()); + }); + } + + @Test + void whenNoCompressorRegistryAutoConfigurationIsSkipped() { + // Codec class guards the imported GrpcCodecConfiguration which provides the + // registry + this.contextRunner() + .withClassLoader(new FilteredClassLoader(Codec.class)) + .run((context) -> assertThat(context) + .getBean("compressionClientCustomizer", GrpcChannelBuilderCustomizer.class) + .isNull()); + } + + @Test + void compressionCustomizerAutoConfiguredAsExpected() { + this.contextRunner().run((context) -> { + assertThat(context).getBean("compressionClientCustomizer", GrpcChannelBuilderCustomizer.class).isNotNull(); + var customizer = context.getBean("compressionClientCustomizer", GrpcChannelBuilderCustomizer.class); + var compressorRegistry = context.getBean(CompressorRegistry.class); + ManagedChannelBuilder builder = Mockito.mock(); + customizer.customize("testChannel", builder); + then(builder).should().compressorRegistry(compressorRegistry); + }); + } + + @Test + void whenNoDecompressorRegistryAutoConfigurationIsSkipped() { + // Codec class guards the imported GrpcCodecConfiguration which provides the + // registry + this.contextRunner() + .withClassLoader(new FilteredClassLoader(Codec.class)) + .run((context) -> assertThat(context) + .getBean("decompressionClientCustomizer", GrpcChannelBuilderCustomizer.class) + .isNull()); + } + + @Test + void decompressionCustomizerAutoConfiguredAsExpected() { + this.contextRunner().run((context) -> { + assertThat(context).getBean("decompressionClientCustomizer", GrpcChannelBuilderCustomizer.class) + .isNotNull(); + var customizer = context.getBean("decompressionClientCustomizer", GrpcChannelBuilderCustomizer.class); + var decompressorRegistry = context.getBean(DecompressorRegistry.class); + ManagedChannelBuilder builder = Mockito.mock(); + customizer.customize("testChannel", builder); + then(builder).should().decompressorRegistry(decompressorRegistry); + }); + } + + @Test + void whenHasUserDefinedChannelBuilderCustomizersDoesNotAutoConfigureBean() { + ChannelBuilderCustomizers customCustomizers = mock(ChannelBuilderCustomizers.class); + this.contextRunner() + .withBean("customCustomizers", ChannelBuilderCustomizers.class, () -> customCustomizers) + .run((context) -> assertThat(context).getBean(ChannelBuilderCustomizers.class).isSameAs(customCustomizers)); + } + + @Test + void channelBuilderCustomizersAutoConfiguredAsExpected() { + this.contextRunner() + .withUserConfiguration(ChannelBuilderCustomizersConfig.class) + .run((context) -> assertThat(context).getBean(ChannelBuilderCustomizers.class) + .extracting("customizers", InstanceOfAssertFactories.list(GrpcChannelBuilderCustomizer.class)) + .contains(ChannelBuilderCustomizersConfig.CUSTOMIZER_BAR, + ChannelBuilderCustomizersConfig.CUSTOMIZER_FOO)); + } + + @Test + void whenInProcessEnabledPropNotSetDoesAutoconfigureInProcess() { + this.contextRunner() + .run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .containsKey("inProcessGrpcChannelFactory")); + } + + @Test + void whenInProcessEnabledPropSetToTrueDoesAutoconfigureInProcess() { + this.contextRunner() + .withPropertyValues("spring.grpc.client.inprocess.enabled=true") + .run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .containsKey("inProcessGrpcChannelFactory")); + } + + @Test + void whenInProcessEnabledPropSetToFalseDoesNotAutoconfigureInProcess() { + this.contextRunner() + .withPropertyValues("spring.grpc.client.inprocess.enabled=false") + .run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .doesNotContainKey("inProcessGrpcChannelFactory")); + } + + @Test + void whenInProcessIsNotOnClasspathDoesNotAutoconfigureInProcess() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(InProcessChannelBuilder.class)) + .run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .doesNotContainKey("inProcessGrpcChannelFactory")); + } + + @Test + void whenHasUserDefinedInProcessChannelFactoryDoesNotAutoConfigureBean() { + InProcessGrpcChannelFactory customChannelFactory = mock(); + this.contextRunner() + .withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .withBean("customChannelFactory", InProcessGrpcChannelFactory.class, () -> customChannelFactory) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class).isSameAs(customChannelFactory)); + } + + @Test + void whenHasUserDefinedChannelFactoryDoesNotAutoConfigureNettyOrShadedNetty() { + GrpcChannelFactory customChannelFactory = mock(); + this.contextRunnerWithoutInProcessChannelFactory() + .withBean("customChannelFactory", GrpcChannelFactory.class, () -> customChannelFactory) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class).isSameAs(customChannelFactory)); + } + + @Test + void userDefinedChannelFactoryWithInProcessChannelFactory() { + GrpcChannelFactory customChannelFactory = mock(); + this.contextRunner() + .withBean("customChannelFactory", GrpcChannelFactory.class, () -> customChannelFactory) + .run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("customChannelFactory", "inProcessGrpcChannelFactory")); + } + + @Test + void whenShadedAndNonShadedNettyOnClasspathShadedNettyFactoryIsAutoConfigured() { + this.contextRunnerWithoutInProcessChannelFactory() + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(ShadedNettyGrpcChannelFactory.class)); + } + + @Test + void shadedNettyWithInProcessChannelFactory() { + this.contextRunner() + .run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("shadedNettyGrpcChannelFactory", "inProcessGrpcChannelFactory")); + } + + @Test + void whenOnlyNonShadedNettyOnClasspathNonShadedNettyFactoryIsAutoConfigured() { + this.contextRunnerWithoutInProcessChannelFactory() + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(NettyGrpcChannelFactory.class)); + } + + @Test + void nonShadedNettyWithInProcessChannelFactory() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("nettyGrpcChannelFactory", "inProcessGrpcChannelFactory")); + } + + @Test + void whenShadedNettyAndNettyNotOnClasspathNoChannelFactoryIsAutoConfigured() { + this.contextRunnerWithoutInProcessChannelFactory() + .withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcChannelFactory.class)); + } + + @Test + void noChannelFactoryWithInProcessChannelFactory() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(InProcessGrpcChannelFactory.class)); + } + + @Test + void shadedNettyChannelFactoryAutoConfiguredAsExpected() { + this.contextRunnerWithoutInProcessChannelFactory() + .withPropertyValues("spring.grpc.server.port=0") + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(ShadedNettyGrpcChannelFactory.class) + .hasFieldOrPropertyWithValue("credentials", context.getBean(NamedChannelCredentialsProvider.class)) + .extracting("targets") + .isInstanceOf(GrpcClientProperties.class)); + } + + @Test + void nettyChannelFactoryAutoConfiguredAsExpected() { + this.contextRunnerWithoutInProcessChannelFactory() + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .withPropertyValues("spring.grpc.server.port=0") + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(NettyGrpcChannelFactory.class) + .hasFieldOrPropertyWithValue("credentials", context.getBean(NamedChannelCredentialsProvider.class)) + .extracting("targets") + .isInstanceOf(GrpcClientProperties.class)); + } + + @Test + void inProcessChannelFactoryAutoConfiguredAsExpected() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(InProcessGrpcChannelFactory.class) + .extracting("credentials") + .isSameAs(ChannelCredentialsProvider.INSECURE)); + } + + @Test + void shadedNettyChannelFactoryAutoConfiguredWithCustomizers() { + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder builder = mock(); + channelFactoryAutoConfiguredWithCustomizers(this.contextRunnerWithoutInProcessChannelFactory(), builder, + ShadedNettyGrpcChannelFactory.class); + } + + @Test + void nettyChannelFactoryAutoConfiguredWithCustomizers() { + NettyChannelBuilder builder = mock(); + channelFactoryAutoConfiguredWithCustomizers(this.contextRunnerWithoutInProcessChannelFactory() + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)), + builder, NettyGrpcChannelFactory.class); + } + + @Test + void inProcessChannelFactoryAutoConfiguredWithCustomizers() { + InProcessChannelBuilder builder = mock(); + channelFactoryAutoConfiguredWithCustomizers( + this.contextRunner() + .withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)), + builder, InProcessGrpcChannelFactory.class); + } + + @SuppressWarnings("unchecked") + private > void channelFactoryAutoConfiguredWithCustomizers( + ApplicationContextRunner contextRunner, ManagedChannelBuilder mockChannelBuilder, + Class expectedChannelFactoryType) { + GrpcChannelBuilderCustomizer customizer1 = (__, b) -> b.keepAliveTime(40L, TimeUnit.SECONDS); + GrpcChannelBuilderCustomizer customizer2 = (__, b) -> b.keepAliveTime(50L, TimeUnit.SECONDS); + ChannelBuilderCustomizers customizers = new ChannelBuilderCustomizers(List.of(customizer1, customizer2)); + contextRunner.withPropertyValues("spring.grpc.server.port=0") + .withBean("channelBuilderCustomizers", ChannelBuilderCustomizers.class, () -> customizers) + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(expectedChannelFactoryType) + .extracting("globalCustomizers", InstanceOfAssertFactories.list(GrpcChannelBuilderCustomizer.class)) + .satisfies((allCustomizers) -> { + allCustomizers.forEach((c) -> c.customize("channel1", mockChannelBuilder)); + InOrder ordered = inOrder(mockChannelBuilder); + ordered.verify(mockChannelBuilder).keepAliveTime(40L, TimeUnit.SECONDS); + ordered.verify(mockChannelBuilder).keepAliveTime(50L, TimeUnit.SECONDS); + })); + } + + @Configuration(proxyBeanMethods = false) + static class ChannelBuilderCustomizersConfig { + + static GrpcChannelBuilderCustomizer CUSTOMIZER_FOO = mock(); + + static GrpcChannelBuilderCustomizer CUSTOMIZER_BAR = mock(); + + @Bean + @Order(200) + GrpcChannelBuilderCustomizer customizerFoo() { + return CUSTOMIZER_FOO; + } + + @Bean + @Order(100) + GrpcChannelBuilderCustomizer customizerBar() { + return CUSTOMIZER_BAR; + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java new file mode 100644 index 000000000000..47158ea5fda0 --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java @@ -0,0 +1,118 @@ +/* + * 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.grpc.client.autoconfigure; + +import io.grpc.stub.AbstractStub; +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor; +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.grpc.client.GlobalClientInterceptor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for the {@link GrpcClientObservationAutoConfiguration}. + */ +class GrpcClientObservationAutoConfigurationTests { + + private final ApplicationContextRunner baseContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcClientObservationAutoConfiguration.class)); + + private ApplicationContextRunner validContextRunner() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcClientObservationAutoConfiguration.class)) + .withBean("observationRegistry", ObservationRegistry.class, Mockito::mock); + } + + @Test + void whenObservationRegistryNotOnClasspathAutoConfigSkipped() { + this.validContextRunner() + .withClassLoader(new FilteredClassLoader(ObservationRegistry.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenObservationGrpcClientInterceptorNotOnClasspathAutoConfigSkipped() { + this.validContextRunner() + .withClassLoader(new FilteredClassLoader(ObservationGrpcClientInterceptor.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenObservationRegistryNotProvidedThenAutoConfigSkipped() { + this.baseContextRunner + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenObservationPropertyEnabledThenAutoConfigNotSkipped() { + this.validContextRunner() + .withPropertyValues("spring.grpc.client.observation.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenObservationPropertyDisabledThenAutoConfigIsSkipped() { + this.validContextRunner() + .withPropertyValues("spring.grpc.client.observation.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenClientEnabledPropertyNotSetThenAutoConfigNotSkipped() { + this.validContextRunner() + .run((context) -> assertThat(context).hasSingleBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenClientEnabledPropertySetTrueThenAutoConfigIsNotSkipped() { + this.validContextRunner() + .withPropertyValues("spring.grpc.client.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenClientEnabledPropertySetFalseThenAutoConfigIsSkipped() { + this.validContextRunner() + .withPropertyValues("spring.grpc.client.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenGrpcStubNotOnClasspathThenAutoConfigIsSkipped() { + this.validContextRunner() + .withClassLoader(new FilteredClassLoader(AbstractStub.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class)); + } + + @Test + void whenAllConditionsAreMetThenInterceptorConfiguredAsExpected() { + this.validContextRunner().run((context) -> { + assertThat(context).hasSingleBean(ObservationGrpcClientInterceptor.class); + assertThat(context.getBeansWithAnnotation(GlobalClientInterceptor.class)).hasEntrySatisfying( + "observationGrpcClientInterceptor", + (bean) -> assertThat(bean.getClass().isAssignableFrom(ObservationGrpcClientInterceptor.class)) + .isTrue()); + }); + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java new file mode 100644 index 000000000000..8f6d9452c20d --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java @@ -0,0 +1,299 @@ +/* + * 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.grpc.client.autoconfigure; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.ChannelConfig; +import org.springframework.grpc.client.NegotiationType; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.unit.DataSize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link GrpcClientProperties}. + * + * @author Chris Bono + */ +class GrpcClientPropertiesTests { + + private GrpcClientProperties bindProperties(Map map) { + return new Binder(new MapConfigurationPropertySource(map)) + .bind("spring.grpc.client", GrpcClientProperties.class) + .get(); + } + + private GrpcClientProperties newProperties(ChannelConfig defaultChannel, Map channels) { + var properties = new GrpcClientProperties(); + ReflectionTestUtils.setField(properties, "defaultChannel", defaultChannel); + ReflectionTestUtils.setField(properties, "channels", channels); + return properties; + } + + @Nested + class BindPropertiesAPI { + + @Test + void defaultChannelWithDefaultValues() { + this.withDefaultValues("default-channel", GrpcClientProperties::getDefaultChannel); + } + + @Test + void specificChannelWithDefaultValues() { + this.withDefaultValues("channels.c1", (p) -> p.getChannel("c1")); + } + + private void withDefaultValues(String channelName, + Function channelFromProperties) { + Map map = new HashMap<>(); + // we have to at least bind one property or bind() fails + map.put("spring.grpc.client.%s.enable-keep-alive".formatted(channelName), "false"); + GrpcClientProperties properties = bindProperties(map); + var channel = channelFromProperties.apply(properties); + assertThat(channel.getAddress()).isEqualTo("static://localhost:9090"); + assertThat(channel.getDefaultLoadBalancingPolicy()).isEqualTo("round_robin"); + assertThat(channel.getHealth().isEnabled()).isFalse(); + assertThat(channel.getHealth().getServiceName()).isNull(); + assertThat(channel.getNegotiationType()).isEqualTo(NegotiationType.PLAINTEXT); + assertThat(channel.isEnableKeepAlive()).isFalse(); + assertThat(channel.getIdleTimeout()).isEqualTo(Duration.ofSeconds(20)); + assertThat(channel.getKeepAliveTime()).isEqualTo(Duration.ofMinutes(5)); + assertThat(channel.getKeepAliveTimeout()).isEqualTo(Duration.ofSeconds(20)); + assertThat(channel.isEnableKeepAlive()).isFalse(); + assertThat(channel.isKeepAliveWithoutCalls()).isFalse(); + assertThat(channel.getMaxInboundMessageSize()).isEqualTo(DataSize.ofBytes(4194304)); + assertThat(channel.getMaxInboundMetadataSize()).isEqualTo(DataSize.ofBytes(8192)); + assertThat(channel.getUserAgent()).isNull(); + assertThat(channel.isSecure()).isTrue(); + assertThat(channel.getSsl().isEnabled()).isFalse(); + assertThat(channel.getSsl().getBundle()).isNull(); + } + + @Test + void defaultChannelWithSpecifiedValues() { + this.withSpecifiedValues("default-channel", GrpcClientProperties::getDefaultChannel); + } + + @Test + void specificChannelWithSpecifiedValues() { + this.withSpecifiedValues("channels.c1", (p) -> p.getChannel("c1")); + } + + private void withSpecifiedValues(String channelName, + Function channelFromProperties) { + Map map = new HashMap<>(); + var propPrefix = "spring.grpc.client.%s.".formatted(channelName); + map.put("%s.address".formatted(propPrefix), "static://my-server:8888"); + map.put("%s.default-load-balancing-policy".formatted(propPrefix), "pick_first"); + map.put("%s.health.enabled".formatted(propPrefix), "true"); + map.put("%s.health.service-name".formatted(propPrefix), "my-service"); + map.put("%s.negotiation-type".formatted(propPrefix), "plaintext_upgrade"); + map.put("%s.enable-keep-alive".formatted(propPrefix), "true"); + map.put("%s.idle-timeout".formatted(propPrefix), "1m"); + map.put("%s.keep-alive-time".formatted(propPrefix), "200s"); + map.put("%s.keep-alive-timeout".formatted(propPrefix), "60000ms"); + map.put("%s.keep-alive-without-calls".formatted(propPrefix), "true"); + map.put("%s.max-inbound-message-size".formatted(propPrefix), "200MB"); + map.put("%s.max-inbound-metadata-size".formatted(propPrefix), "1GB"); + map.put("%s.user-agent".formatted(propPrefix), "me"); + map.put("%s.secure".formatted(propPrefix), "false"); + map.put("%s.ssl.enabled".formatted(propPrefix), "true"); + map.put("%s.ssl.bundle".formatted(propPrefix), "my-bundle"); + GrpcClientProperties properties = bindProperties(map); + var channel = channelFromProperties.apply(properties); + assertThat(channel.getAddress()).isEqualTo("static://my-server:8888"); + assertThat(channel.getDefaultLoadBalancingPolicy()).isEqualTo("pick_first"); + assertThat(channel.getHealth().isEnabled()).isTrue(); + assertThat(channel.getHealth().getServiceName()).isEqualTo("my-service"); + assertThat(channel.getNegotiationType()).isEqualTo(NegotiationType.PLAINTEXT_UPGRADE); + assertThat(channel.isEnableKeepAlive()).isTrue(); + assertThat(channel.getIdleTimeout()).isEqualTo(Duration.ofMinutes(1)); + assertThat(channel.getKeepAliveTime()).isEqualTo(Duration.ofSeconds(200)); + assertThat(channel.getKeepAliveTimeout()).isEqualTo(Duration.ofMillis(60000)); + assertThat(channel.isEnableKeepAlive()).isTrue(); + assertThat(channel.isKeepAliveWithoutCalls()).isTrue(); + assertThat(channel.getMaxInboundMessageSize()).isEqualTo(DataSize.ofMegabytes(200)); + assertThat(channel.getMaxInboundMetadataSize()).isEqualTo(DataSize.ofGigabytes(1)); + assertThat(channel.getUserAgent()).isEqualTo("me"); + assertThat(channel.isSecure()).isFalse(); + assertThat(channel.getSsl().isEnabled()).isTrue(); + assertThat(channel.getSsl().getBundle()).isEqualTo("my-bundle"); + } + + @Test + void withoutKeepAliveUnitsSpecified() { + Map map = new HashMap<>(); + map.put("spring.grpc.client.default-channel.idle-timeout", "1"); + map.put("spring.grpc.client.default-channel.keep-alive-time", "60"); + map.put("spring.grpc.client.default-channel.keep-alive-timeout", "5"); + GrpcClientProperties properties = bindProperties(map); + var defaultChannel = properties.getDefaultChannel(); + assertThat(defaultChannel.getIdleTimeout()).isEqualTo(Duration.ofSeconds(1)); + assertThat(defaultChannel.getKeepAliveTime()).isEqualTo(Duration.ofSeconds(60)); + assertThat(defaultChannel.getKeepAliveTimeout()).isEqualTo(Duration.ofSeconds(5)); + } + + @Test + void withoutInboundSizeUnitsSpecified() { + Map map = new HashMap<>(); + map.put("spring.grpc.client.default-channel.max-inbound-message-size", "1000"); + map.put("spring.grpc.client.default-channel.max-inbound-metadata-size", "256"); + GrpcClientProperties properties = bindProperties(map); + var defaultChannel = properties.getDefaultChannel(); + assertThat(defaultChannel.getMaxInboundMessageSize()).isEqualTo(DataSize.ofBytes(1000)); + assertThat(defaultChannel.getMaxInboundMetadataSize()).isEqualTo(DataSize.ofBytes(256)); + } + + @Test + void withServiceConfig() { + Map map = new HashMap<>(); + // we have to at least bind one property or bind() fails + map.put("spring.grpc.client.%s.service-config.something.key".formatted("default-channel"), "value"); + GrpcClientProperties properties = bindProperties(map); + var channel = properties.getDefaultChannel(); + assertThat(channel.getServiceConfig()).hasSize(1); + assertThat(channel.getServiceConfig().get("something")).isInstanceOf(Map.class); + } + + } + + @Nested + class GetChannelAPI { + + @Test + void withDefaultNameReturnsDefaultChannel() { + var properties = new GrpcClientProperties(); + var defaultChannel = properties.getChannel("default"); + assertThat(properties).extracting("defaultChannel").isSameAs(defaultChannel); + assertThat(properties).extracting("channels", InstanceOfAssertFactories.MAP).isEmpty(); + } + + @Test + void withKnownNameReturnsKnownChannel() { + Map map = new HashMap<>(); + // we have to at least bind one property or bind() fails + map.put("spring.grpc.client.channels.c1.enable-keep-alive", "false"); + GrpcClientProperties properties = bindProperties(map); + var channel = properties.getChannel("c1"); + assertThat(properties).extracting("channels", InstanceOfAssertFactories.MAP) + .containsExactly(entry("c1", channel)); + } + + @Test + void withUnknownNameReturnsNewChannelWithCopiedDefaults() { + var defaultChannel = new ChannelConfig(); + defaultChannel.setAddress("static://my-server:9999"); + defaultChannel.setDefaultLoadBalancingPolicy("custom"); + defaultChannel.getHealth().setEnabled(true); + defaultChannel.getHealth().setServiceName("custom-service"); + defaultChannel.setEnableKeepAlive(true); + defaultChannel.setIdleTimeout(Duration.ofMinutes(1)); + defaultChannel.setKeepAliveTime(Duration.ofMinutes(4)); + defaultChannel.setKeepAliveTimeout(Duration.ofMinutes(6)); + defaultChannel.setKeepAliveWithoutCalls(true); + defaultChannel.setMaxInboundMessageSize(DataSize.ofMegabytes(100)); + defaultChannel.setMaxInboundMetadataSize(DataSize.ofMegabytes(200)); + defaultChannel.setUserAgent("me"); + defaultChannel.setDefaultDeadline(Duration.ofMinutes(1)); + defaultChannel.getSsl().setEnabled(true); + defaultChannel.getSsl().setBundle("custom-bundle"); + var properties = newProperties(defaultChannel, Collections.emptyMap()); + var newChannel = properties.getChannel("new-channel"); + assertThat(newChannel).usingRecursiveComparison().isEqualTo(defaultChannel); + assertThat(properties).extracting("channels", InstanceOfAssertFactories.MAP).isEmpty(); + } + + @Test + void withUnknownNameReturnsNewChannelWithOwnAddress() { + var defaultChannel = new ChannelConfig(); + defaultChannel.setAddress("static://my-server:9999"); + var properties = newProperties(defaultChannel, Collections.emptyMap()); + var newChannel = properties.getChannel("other-server:8888"); + assertThat(newChannel).usingRecursiveComparison().ignoringFields("address").isEqualTo(defaultChannel); + assertThat(newChannel).hasFieldOrPropertyWithValue("address", "static://other-server:8888"); + assertThat(properties).extracting("channels", InstanceOfAssertFactories.MAP).isEmpty(); + } + + } + + @Nested + class GetTargetAPI { + + @Test + void channelWithStaticAddressReturnsStrippedAddress() { + var defaultChannel = new ChannelConfig(); + var channel1 = new ChannelConfig(); + channel1.setAddress("static://my-server:8888"); + var properties = newProperties(defaultChannel, Map.of("c1", channel1)); + assertThat(properties.getTarget("c1")).isEqualTo("my-server:8888"); + assertThat(properties).extracting("channels", InstanceOfAssertFactories.MAP) + .containsExactly(entry("c1", channel1)); + } + + @Test + void channelWithTcpAddressReturnsStrippedAddress() { + var defaultChannel = new ChannelConfig(); + var channel1 = new ChannelConfig(); + channel1.setAddress("tcp://my-server:8888"); + var properties = newProperties(defaultChannel, Map.of("c1", channel1)); + assertThat(properties.getTarget("c1")).isEqualTo("my-server:8888"); + assertThat(properties).extracting("channels", InstanceOfAssertFactories.MAP) + .containsExactly(entry("c1", channel1)); + } + + @Test + void channelWithAddressPropertyPlaceholdersPopulatesFromEnvironment() { + var defaultChannel = new ChannelConfig(); + var channel1 = new ChannelConfig(); + channel1.setAddress("my-server-${channelName}:8888"); + var properties = newProperties(defaultChannel, Map.of("c1", channel1)); + var env = new MockEnvironment(); + env.setProperty("channelName", "foo"); + properties.setEnvironment(env); + assertThat(properties.getTarget("c1")).isEqualTo("my-server-foo:8888"); + } + + } + + @Nested + class CopyDefaultsAPI { + + @Test + void copyFromDefaultChannel() { + var properties = new GrpcClientProperties(); + var defaultChannel = properties.getDefaultChannel(); + var newChannel = defaultChannel.copy(); + assertThat(newChannel).usingRecursiveComparison().isEqualTo(defaultChannel); + assertThat(newChannel.getServiceConfig()).isEqualTo(defaultChannel.getServiceConfig()); + } + + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfigurationTests.java new file mode 100644 index 000000000000..cefda3c929b5 --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfigurationTests.java @@ -0,0 +1,48 @@ +/* + * 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.grpc.client.autoconfigure.codec; + +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GrpcCodecConfiguration}. + * + * @author Andrei Lisa + */ +class GrpcCodecConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcCodecConfiguration.class)); + + @Test + void testCompressorRegistryBean() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(CompressorRegistry.class)); + } + + @Test + void testDecompressorRegistryBean() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(DecompressorRegistry.class)); + } + +} diff --git a/module/spring-boot-grpc-client/src/test/resources/logback-test.xml b/module/spring-boot-grpc-client/src/test/resources/logback-test.xml new file mode 100644 index 000000000000..b8a41480d7d6 --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/resources/logback-test.xml @@ -0,0 +1,4 @@ + + + + diff --git a/module/spring-boot-grpc-server/build.gradle b/module/spring-boot-grpc-server/build.gradle new file mode 100644 index 000000000000..88dce1015131 --- /dev/null +++ b/module/spring-boot-grpc-server/build.gradle @@ -0,0 +1,67 @@ +/* + * 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. + */ + +plugins { + id "java-library" + id "org.springframework.boot.auto-configuration" + id "org.springframework.boot.configuration-properties" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot gRPC Server" + + +dependencies { + api(project(":core:spring-boot")) + api("org.springframework.grpc:spring-grpc-core") + + compileOnly("com.fasterxml.jackson.core:jackson-annotations") + + optional(project(":core:spring-boot-autoconfigure")) + optional(project(":module:spring-boot-actuator")) + optional(project(":module:spring-boot-actuator-autoconfigure")) + optional(project(":module:spring-boot-health")) + optional(project(":module:spring-boot-micrometer-observation")) + optional(project(":module:spring-boot-security")) + optional(project(":module:spring-boot-security-oauth2-client")) + optional(project(":module:spring-boot-security-oauth2-resource-server")) + optional("io.grpc:grpc-servlet-jakarta") + optional("io.grpc:grpc-services") + optional("io.grpc:grpc-netty") + optional("io.grpc:grpc-netty-shaded") + optional("io.grpc:grpc-inprocess") + optional("io.grpc:grpc-kotlin-stub") { + exclude group: "javax.annotation", module: "javax.annotation-api" + } + optional("io.micrometer:micrometer-core") + optional("io.netty:netty-transport-native-epoll") + optional("io.projectreactor:reactor-core") + optional("jakarta.servlet:jakarta.servlet-api") + optional("org.springframework:spring-web") + optional("org.springframework.security:spring-security-config") + optional("org.springframework.security:spring-security-oauth2-client") + optional("org.springframework.security:spring-security-oauth2-resource-server") + optional("org.springframework.security:spring-security-oauth2-jose") + optional("org.springframework.security:spring-security-web") + + testCompileOnly("com.fasterxml.jackson.core:jackson-annotations") + + testImplementation(project(":core:spring-boot-test")) + testImplementation(project(":test-support:spring-boot-test-support")) + + testRuntimeOnly("ch.qos.logback:logback-classic") +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcNativeServer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcNativeServer.java new file mode 100644 index 000000000000..51be07d1fc4c --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcNativeServer.java @@ -0,0 +1,41 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that determines if the gRPC server implementation + * should be one of the native varieties (e.g. Netty, Shaded Netty) - i.e. not the servlet + * container. + * + * @author Chris Bono + * @author Dave Syer + * @since 4.0.0 + * @see OnGrpcNativeServerCondition + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Conditional(OnGrpcNativeServerCondition.class) +public @interface ConditionalOnGrpcNativeServer { + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java new file mode 100644 index 000000000000..de80378dcc5a --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java @@ -0,0 +1,45 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.grpc.BindableService; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that only matches when the + * {@code io.grpc.BindableService} class is on the classpath and the + * {@code spring.grpc.server.enabled} property is not explicitly set to {@code false}. + * + * @author Freeman Freeman + * @author Chris Bono + * @since 4.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@ConditionalOnClass(BindableService.class) +@ConditionalOnProperty(prefix = "spring.grpc.server", name = "enabled", matchIfMissing = true) +public @interface ConditionalOnGrpcServerEnabled { + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServletServer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServletServer.java new file mode 100644 index 000000000000..02ac7ec16ebe --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServletServer.java @@ -0,0 +1,50 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.grpc.servlet.jakarta.GrpcServlet; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that determines if the Servlet container should be + * used to run the gRPC server. The condition matches only when the app is a servlet web + * application and the {@code io.grpc.servlet.jakarta.GrpcServlet} class is on the + * classpath and the {@code spring.grpc.server.servlet.enabled} property is not explicitly + * set to {@code false}. + * + * @author Chris Bono + * @author Dave Syer + * @since 4.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnClass(GrpcServlet.class) +@ConditionalOnProperty(prefix = "spring.grpc.server", name = "servlet.enabled", havingValue = "true", + matchIfMissing = true) +public @interface ConditionalOnGrpcServletServer { + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java new file mode 100644 index 000000000000..9f984ed34850 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java @@ -0,0 +1,89 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import io.grpc.ServerBuilder; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.grpc.server.DefaultGrpcServerFactory; +import org.springframework.util.unit.DataSize; + +/** + * Helper class used to map {@link GrpcServerProperties} to + * {@link DefaultGrpcServerFactory}. + * + * @param the type of server builder + * @author Chris Bono + */ +class DefaultServerFactoryPropertyMapper> { + + final GrpcServerProperties properties; + + DefaultServerFactoryPropertyMapper(GrpcServerProperties properties) { + this.properties = properties; + } + + /** + * Map the properties to the server factory's server builder. + * @param serverBuilder the builder + */ + void customizeServerBuilder(T serverBuilder) { + PropertyMapper mapper = PropertyMapper.get(); + customizeKeepAlive(serverBuilder, mapper); + customizeInboundLimits(serverBuilder, mapper); + } + + /** + * Map the keep-alive properties to the server factory's server builder. + * @param serverBuilder the builder + * @param mapper the property mapper + */ + void customizeKeepAlive(T serverBuilder, PropertyMapper mapper) { + GrpcServerProperties.KeepAlive keepAliveProps = this.properties.getKeepAlive(); + mapper.from(keepAliveProps.getTime()).to(durationProperty(serverBuilder::keepAliveTime)); + mapper.from(keepAliveProps.getTimeout()).to(durationProperty(serverBuilder::keepAliveTimeout)); + mapper.from(keepAliveProps.getMaxIdle()).to(durationProperty(serverBuilder::maxConnectionIdle)); + mapper.from(keepAliveProps.getMaxAge()).to(durationProperty(serverBuilder::maxConnectionAge)); + mapper.from(keepAliveProps.getMaxAgeGrace()).to(durationProperty(serverBuilder::maxConnectionAgeGrace)); + mapper.from(keepAliveProps.getPermitTime()).to(durationProperty(serverBuilder::permitKeepAliveTime)); + mapper.from(keepAliveProps.isPermitWithoutCalls()).to(serverBuilder::permitKeepAliveWithoutCalls); + } + + /** + * Map the inbound limits properties to the server factory's server builder. + * @param serverBuilder the builder + * @param mapper the property mapper + */ + void customizeInboundLimits(T serverBuilder, PropertyMapper mapper) { + mapper.from(this.properties.getMaxInboundMessageSize()) + .asInt(DataSize::toBytes) + .to(serverBuilder::maxInboundMessageSize); + mapper.from(this.properties.getMaxInboundMetadataSize()) + .asInt(DataSize::toBytes) + .to(serverBuilder::maxInboundMetadataSize); + } + + Consumer durationProperty(BiConsumer setter) { + return (duration) -> setter.accept(duration.toNanos(), TimeUnit.NANOSECONDS); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java new file mode 100644 index 000000000000..178cb7746d89 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java @@ -0,0 +1,130 @@ +/* + * 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.grpc.server.autoconfigure; + +import io.grpc.BindableService; +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.ServerBuilder; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.grpc.server.autoconfigure.codec.GrpcCodecConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.grpc.server.exception.ReactiveStubBeanDefinitionRegistrar; +import org.springframework.grpc.server.service.DefaultGrpcServiceConfigurer; +import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer; +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring gRPC server-side + * components. + *

+ * Spring gRPC must be on the classpath and at least one {@link BindableService} bean + * registered in the context in order for the auto-configuration to execute. + * + * @author David Syer + * @author Chris Bono + * @since 4.0.0 + */ +@AutoConfiguration(after = GrpcServerFactoryAutoConfiguration.class) +@ConditionalOnGrpcServerEnabled +@ConditionalOnClass({ GrpcServerFactory.class }) +@ConditionalOnBean(BindableService.class) +@EnableConfigurationProperties(GrpcServerProperties.class) +@Import({ GrpcCodecConfiguration.class }) +public final class GrpcServerAutoConfiguration { + + @ConditionalOnMissingBean + @Bean + ServerBuilderCustomizers serverBuilderCustomizers(ObjectProvider> customizers) { + return new ServerBuilderCustomizers(customizers.orderedStream().toList()); + } + + @ConditionalOnMissingBean(GrpcServiceConfigurer.class) + @Bean + DefaultGrpcServiceConfigurer grpcServiceConfigurer(ApplicationContext applicationContext) { + return new DefaultGrpcServiceConfigurer(applicationContext); + } + + @ConditionalOnMissingBean(GrpcServiceDiscoverer.class) + @Bean + DefaultGrpcServiceDiscoverer grpcServiceDiscoverer(ApplicationContext applicationContext) { + return new DefaultGrpcServiceDiscoverer(applicationContext); + } + + @ConditionalOnBean(CompressorRegistry.class) + @Bean + > ServerBuilderCustomizer compressionServerConfigurer(CompressorRegistry registry) { + return (builder) -> builder.compressorRegistry(registry); + } + + @ConditionalOnBean(DecompressorRegistry.class) + @Bean + > ServerBuilderCustomizer decompressionServerConfigurer( + DecompressorRegistry registry) { + return (builder) -> builder.decompressorRegistry(registry); + } + + @ConditionalOnBean(GrpcServerExecutorProvider.class) + @Bean + > ServerBuilderCustomizer executorServerConfigurer( + GrpcServerExecutorProvider provider) { + return new ServerBuilderCustomizerImplementation<>(provider); + } + + private final class ServerBuilderCustomizerImplementation> + implements ServerBuilderCustomizer, Ordered { + + private final GrpcServerExecutorProvider provider; + + private ServerBuilderCustomizerImplementation(GrpcServerExecutorProvider provider) { + this.provider = provider; + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public void customize(T builder) { + builder.executor(this.provider.getExecutor()); + } + + } + + @ConditionalOnClass(name = "com.salesforce.reactivegrpc.common.Function") + @Configuration + @Import(ReactiveStubBeanDefinitionRegistrar.class) + static class ReactiveStubConfiguration { + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java new file mode 100644 index 000000000000..9a1551cf5da0 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java @@ -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.grpc.server.autoconfigure; + +import java.util.concurrent.Executor; + +public interface GrpcServerExecutorProvider { + + /** + * Returns a {@link Executor} for the gRPC server, if it needs tio be customized. + * @return the executor to use for the gRPC server + */ + Executor getExecutor(); + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java new file mode 100644 index 000000000000..6572c52343ee --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java @@ -0,0 +1,114 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.util.List; + +import io.grpc.BindableService; +import io.grpc.servlet.jakarta.GrpcServlet; +import io.grpc.servlet.jakarta.ServletServerBuilder; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.util.unit.DataSize; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC server factories. + *

+ * gRPC must be on the classpath and at least one {@link BindableService} bean registered + * in the context in order for the auto-configuration to execute. + * + * @author David Syer + * @author Chris Bono + * @author Toshiaki Maki + * @since 4.0.0 + */ +@AutoConfiguration +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +@ConditionalOnGrpcServerEnabled +@ConditionalOnClass(GrpcServerFactory.class) +@ConditionalOnBean(BindableService.class) +public final class GrpcServerFactoryAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnGrpcNativeServer + static class GrpcServerFactoryConfiguration { + + @Configuration(proxyBeanMethods = false) + @Import({ GrpcServerFactoryConfigurations.ShadedNettyServerFactoryConfiguration.class, + GrpcServerFactoryConfigurations.NettyServerFactoryConfiguration.class, + GrpcServerFactoryConfigurations.InProcessServerFactoryConfiguration.class }) + static class NettyServerFactoryConfiguration { + + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnGrpcServletServer + public static class GrpcServletConfiguration { + + private static Log logger = LogFactory.getLog(GrpcServletConfiguration.class); + + @Bean + ServletRegistrationBean grpcServlet(GrpcServerProperties properties, + GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer, + ServerBuilderCustomizers serverBuilderCustomizers) { + List serviceNames = serviceDiscoverer.listServiceNames(); + if (logger.isInfoEnabled()) { + serviceNames.forEach((service) -> logger.info("Registering gRPC service: " + service)); + } + List paths = serviceNames.stream().map((service) -> "/" + service + "/*").toList(); + ServletServerBuilder servletServerBuilder = new ServletServerBuilder(); + serviceDiscoverer.findServices() + .stream() + .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, null)) + .forEach(servletServerBuilder::addService); + PropertyMapper mapper = PropertyMapper.get(); + mapper.from(properties.getMaxInboundMessageSize()) + .asInt(DataSize::toBytes) + .to(servletServerBuilder::maxInboundMessageSize); + serverBuilderCustomizers.customize(servletServerBuilder); + ServletRegistrationBean servlet = new ServletRegistrationBean<>( + servletServerBuilder.buildServlet()); + servlet.setUrlMappings(paths); + return servlet; + } + + @Configuration(proxyBeanMethods = false) + @Import(GrpcServerFactoryConfigurations.InProcessServerFactoryConfiguration.class) + static class InProcessConfiguration { + + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java new file mode 100644 index 000000000000..ca31400c26ee --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java @@ -0,0 +1,194 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.util.List; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; + +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.netty.NettyServerBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.InProcessGrpcServerFactory; +import org.springframework.grpc.server.NettyGrpcServerFactory; +import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.grpc.server.ServerServiceDefinitionFilter; +import org.springframework.grpc.server.ShadedNettyGrpcServerFactory; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.grpc.server.service.ServerInterceptorFilter; +import org.springframework.util.Assert; + +/** + * Configurations for {@link GrpcServerFactory gRPC server factories}. + * + * @author Chris Bono + */ +class GrpcServerFactoryConfigurations { + + private static void applyServerFactoryCustomizers(ObjectProvider customizers, + GrpcServerFactory factory) { + customizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class) + @ConditionalOnMissingBean(value = GrpcServerFactory.class, ignored = InProcessGrpcServerFactory.class) + @ConditionalOnProperty(prefix = "spring.grpc.server.inprocess.", name = "exclusive", havingValue = "false", + matchIfMissing = true) + @EnableConfigurationProperties(GrpcServerProperties.class) + static class ShadedNettyServerFactoryConfiguration { + + @Bean + ShadedNettyGrpcServerFactory shadedNettyGrpcServerFactory(GrpcServerProperties properties, + GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer, + ServerBuilderCustomizers serverBuilderCustomizers, SslBundles bundles, + ObjectProvider customizers) { + ShadedNettyServerFactoryPropertyMapper mapper = new ShadedNettyServerFactoryPropertyMapper(properties); + List> builderCustomizers = List + .of(mapper::customizeServerBuilder, serverBuilderCustomizers::customize); + KeyManagerFactory keyManager = null; + TrustManagerFactory trustManager = null; + if (properties.getSsl().isEnabled()) { + String bundleName = properties.getSsl().getBundle(); + Assert.notNull(bundleName, () -> "SSL bundleName must not be null"); + SslBundle bundle = bundles.getBundle(bundleName); + keyManager = bundle.getManagers().getKeyManagerFactory(); + trustManager = properties.getSsl().isSecure() ? bundle.getManagers().getTrustManagerFactory() + : io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory.INSTANCE; + } + ShadedNettyGrpcServerFactory factory = new ShadedNettyGrpcServerFactory(properties.getAddress(), + builderCustomizers, keyManager, trustManager, properties.getSsl().getClientAuth()); + applyServerFactoryCustomizers(customizers, factory); + serviceDiscoverer.findServices() + .stream() + .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, factory)) + .forEach(factory::addService); + return factory; + } + + @ConditionalOnBean(ShadedNettyGrpcServerFactory.class) + @ConditionalOnMissingBean(name = "shadedNettyGrpcServerLifecycle") + @Bean + GrpcServerLifecycle shadedNettyGrpcServerLifecycle(ShadedNettyGrpcServerFactory factory, + GrpcServerProperties properties, ApplicationEventPublisher eventPublisher) { + return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod(), eventPublisher); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(NettyServerBuilder.class) + @ConditionalOnMissingBean(value = GrpcServerFactory.class, ignored = InProcessGrpcServerFactory.class) + @ConditionalOnProperty(prefix = "spring.grpc.server.inprocess.", name = "exclusive", havingValue = "false", + matchIfMissing = true) + @EnableConfigurationProperties(GrpcServerProperties.class) + static class NettyServerFactoryConfiguration { + + @Bean + NettyGrpcServerFactory nettyGrpcServerFactory(GrpcServerProperties properties, + GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer, + ServerBuilderCustomizers serverBuilderCustomizers, SslBundles bundles, + ObjectProvider customizers) { + NettyServerFactoryPropertyMapper mapper = new NettyServerFactoryPropertyMapper(properties); + List> builderCustomizers = List + .of(mapper::customizeServerBuilder, serverBuilderCustomizers::customize); + KeyManagerFactory keyManager = null; + TrustManagerFactory trustManager = null; + if (properties.getSsl().isEnabled()) { + String bundleName = properties.getSsl().getBundle(); + Assert.notNull(bundleName, () -> "SSL bundleName must not be null"); + SslBundle bundle = bundles.getBundle(bundleName); + keyManager = bundle.getManagers().getKeyManagerFactory(); + trustManager = properties.getSsl().isSecure() ? bundle.getManagers().getTrustManagerFactory() + : InsecureTrustManagerFactory.INSTANCE; + } + NettyGrpcServerFactory factory = new NettyGrpcServerFactory(properties.getAddress(), builderCustomizers, + keyManager, trustManager, properties.getSsl().getClientAuth()); + applyServerFactoryCustomizers(customizers, factory); + serviceDiscoverer.findServices() + .stream() + .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, factory)) + .forEach(factory::addService); + return factory; + } + + @ConditionalOnBean(NettyGrpcServerFactory.class) + @ConditionalOnMissingBean(name = "nettyGrpcServerLifecycle") + @Bean + GrpcServerLifecycle nettyGrpcServerLifecycle(NettyGrpcServerFactory factory, GrpcServerProperties properties, + ApplicationEventPublisher eventPublisher) { + return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod(), eventPublisher); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(InProcessGrpcServerFactory.class) + @ConditionalOnMissingBean(InProcessGrpcServerFactory.class) + @ConditionalOnProperty(prefix = "spring.grpc.server.inprocess", name = "name") + @EnableConfigurationProperties(GrpcServerProperties.class) + static class InProcessServerFactoryConfiguration { + + @Bean + InProcessGrpcServerFactory inProcessGrpcServerFactory(GrpcServerProperties properties, + GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer, + ServerBuilderCustomizers serverBuilderCustomizers, + ObjectProvider interceptorFilter, + ObjectProvider serviceFilter, + ObjectProvider customizers) { + var mapper = new InProcessServerFactoryPropertyMapper(properties); + List> builderCustomizers = List + .of(mapper::customizeServerBuilder, serverBuilderCustomizers::customize); + InProcessGrpcServerFactory factory = new InProcessGrpcServerFactory(properties.getInprocess().getName(), + builderCustomizers); + factory.setInterceptorFilter(interceptorFilter.getIfAvailable()); + factory.setServiceFilter(serviceFilter.getIfAvailable()); + applyServerFactoryCustomizers(customizers, factory); + serviceDiscoverer.findServices() + .stream() + .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, factory)) + .forEach(factory::addService); + return factory; + } + + @ConditionalOnBean(InProcessGrpcServerFactory.class) + @ConditionalOnMissingBean(name = "inProcessGrpcServerLifecycle") + @Bean + GrpcServerLifecycle inProcessGrpcServerLifecycle(InProcessGrpcServerFactory factory, + GrpcServerProperties properties, ApplicationEventPublisher eventPublisher) { + return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod(), eventPublisher); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java new file mode 100644 index 000000000000..95a3f385f0da --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java @@ -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.grpc.server.autoconfigure; + +import org.springframework.grpc.server.GrpcServerFactory; + +public interface GrpcServerFactoryCustomizer { + + /** + * Customize the given {@link GrpcServerFactory}. + * @param serverFactory the server factory to customize + */ + void customize(GrpcServerFactory serverFactory); + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java new file mode 100644 index 000000000000..a8dedd52637d --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java @@ -0,0 +1,71 @@ +/* + * 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.grpc.server.autoconfigure; + +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcServerInterceptor; +import io.micrometer.core.instrument.kotlin.ObservationCoroutineContextServerInterceptor; +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.GrpcServerFactory; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side observations. + * + * @author Sunny Tang + * @author Chris Bono + * @author Dave Syer + * @since 4.0.0 + */ +@AutoConfiguration( + afterName = "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration") +@ConditionalOnGrpcServerEnabled +@ConditionalOnClass({ GrpcServerFactory.class, ObservationRegistry.class, ObservationGrpcServerInterceptor.class }) +@ConditionalOnBean(ObservationRegistry.class) +@ConditionalOnProperty(name = "spring.grpc.server.observation.enabled", havingValue = "true", matchIfMissing = true) +public final class GrpcServerObservationAutoConfiguration { + + @Bean + @Order(0) + @GlobalServerInterceptor + ObservationGrpcServerInterceptor observationGrpcServerInterceptor(ObservationRegistry observationRegistry) { + return new ObservationGrpcServerInterceptor(observationRegistry); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = "io.grpc.kotlin.AbstractCoroutineStub") + static class GrpcServerCoroutineStubConfiguration { + + @Bean + @Order(10) + @GlobalServerInterceptor + ObservationCoroutineContextServerInterceptor observationCoroutineGrpcServerInterceptor( + ObservationRegistry observationRegistry) { + return new ObservationCoroutineContextServerInterceptor(observationRegistry); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java new file mode 100644 index 000000000000..c4140cbacfc6 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java @@ -0,0 +1,458 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; + +import io.grpc.TlsServerCredentials.ClientAuth; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.convert.DataSizeUnit; +import org.springframework.boot.convert.DurationUnit; +import org.springframework.grpc.internal.GrpcUtils; +import org.springframework.util.unit.DataSize; +import org.springframework.util.unit.DataUnit; + +@ConfigurationProperties(prefix = "spring.grpc.server") +public class GrpcServerProperties { + + /** + * Server should listen to any IPv4 and IPv6 address. + */ + public static final String ANY_IP_ADDRESS = "*"; + + /** + * Server address to bind to. The default is any IP address ('*'). + */ + private String host = ANY_IP_ADDRESS; + + /** + * Server port to listen on. When the value is 0, a random available port is selected. + * The default is 9090. + */ + private int port = GrpcUtils.DEFAULT_PORT; + + /** + * Maximum time to wait for the server to gracefully shutdown. When the value is + * negative, the server waits forever. When the value is 0, the server will force + * shutdown immediately. The default is 30 seconds. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration shutdownGracePeriod = Duration.ofSeconds(30); + + /** + * Maximum message size allowed to be received by the server (default 4MiB). + */ + @DataSizeUnit(DataUnit.BYTES) + private DataSize maxInboundMessageSize = DataSize.ofBytes(4194304); + + /** + * Maximum metadata size allowed to be received by the server (default 8KiB). + */ + @DataSizeUnit(DataUnit.BYTES) + private DataSize maxInboundMetadataSize = DataSize.ofBytes(8192); + + private final Health health = new Health(); + + private final KeepAlive keepAlive = new KeepAlive(); + + private final Inprocess inprocess = new Inprocess(); + + /** + * The address to bind to. could be a host:port combination or a pseudo URL like + * static://host:port. Can not be set if host or port are set independently. + */ + @Nullable private String address; + + public String getAddress() { + return (this.address != null) ? this.address : this.host + ":" + this.port; + } + + public void setAddress(String address) { + this.address = address; + } + + private final Ssl ssl = new Ssl(); + + public Ssl getSsl() { + return this.ssl; + } + + public String getHost() { + return this.host; + } + + public void setHost(String host) { + if (this.address != null) { + throw new IllegalStateException("Cannot set host when address is already set"); + } + this.host = host; + } + + public int getPort() { + if (this.address != null) { + return GrpcUtils.getPort(this.address); + } + return this.port; + } + + public void setPort(int port) { + if (this.address != null) { + throw new IllegalStateException("Cannot set port when address is already set"); + } + this.port = port; + } + + public @Nullable Duration getShutdownGracePeriod() { + return this.shutdownGracePeriod; + } + + public void setShutdownGracePeriod(Duration shutdownGracePeriod) { + this.shutdownGracePeriod = shutdownGracePeriod; + } + + public DataSize getMaxInboundMessageSize() { + return this.maxInboundMessageSize; + } + + public void setMaxInboundMessageSize(DataSize maxInboundMessageSize) { + this.maxInboundMessageSize = maxInboundMessageSize; + } + + public DataSize getMaxInboundMetadataSize() { + return this.maxInboundMetadataSize; + } + + public void setMaxInboundMetadataSize(DataSize maxInboundMetadataSize) { + this.maxInboundMetadataSize = maxInboundMetadataSize; + } + + public Health getHealth() { + return this.health; + } + + public KeepAlive getKeepAlive() { + return this.keepAlive; + } + + public Inprocess getInprocess() { + return this.inprocess; + } + + public static class Health { + + /** + * Whether to auto-configure Health feature on the gRPC server. + */ + private Boolean enabled = true; + + private final ActuatorAdapt actuator = new ActuatorAdapt(); + + public Boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public ActuatorAdapt getActuator() { + return this.actuator; + } + + } + + public static class ActuatorAdapt { + + /** + * Whether to adapt Actuator health indicators into gRPC health checks. + */ + private Boolean enabled = true; + + /** + * Whether to update the overall gRPC server health (the '' service) with the + * aggregate status of the configured health indicators. + */ + private Boolean updateOverallHealth = true; + + /** + * How often to update the health status. + */ + private Duration updateRate = Duration.ofSeconds(5); + + /** + * The initial delay before updating the health status the very first time. + */ + private Duration updateInitialDelay = Duration.ofSeconds(5); + + /** + * List of Actuator health indicator paths to adapt into gRPC health checks. + */ + private List healthIndicatorPaths = new ArrayList<>(); + + public Boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public Boolean getUpdateOverallHealth() { + return this.updateOverallHealth; + } + + public void setUpdateOverallHealth(Boolean updateOverallHealth) { + this.updateOverallHealth = updateOverallHealth; + } + + public Duration getUpdateRate() { + return this.updateRate; + } + + public void setUpdateRate(Duration updateRate) { + this.updateRate = updateRate; + } + + public Duration getUpdateInitialDelay() { + return this.updateInitialDelay; + } + + public void setUpdateInitialDelay(Duration updateInitialDelay) { + this.updateInitialDelay = updateInitialDelay; + } + + public List getHealthIndicatorPaths() { + return this.healthIndicatorPaths; + } + + public void setHealthIndicatorPaths(List healthIndicatorPaths) { + this.healthIndicatorPaths = healthIndicatorPaths; + } + + } + + public static class KeepAlive { + + /** + * Duration without read activity before sending a keep alive ping (default 2h). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration time = Duration.ofHours(2); + + /** + * Maximum time to wait for read activity after sending a keep alive ping. If + * sender does not receive an acknowledgment within this time, it will close the + * connection (default 20s). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration timeout = Duration.ofSeconds(20); + + /** + * Maximum time a connection can remain idle before being gracefully terminated + * (default infinite). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration maxIdle = null; + + /** + * Maximum time a connection may exist before being gracefully terminated (default + * infinite). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration maxAge = null; + + /** + * Maximum time for graceful connection termination (default infinite). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration maxAgeGrace = null; + + /** + * Maximum keep-alive time clients are permitted to configure (default 5m). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration permitTime = Duration.ofMinutes(5); + + /** + * Whether clients are permitted to send keep alive pings when there are no + * outstanding RPCs on the connection (default false). + */ + private boolean permitWithoutCalls = false; + + public @Nullable Duration getTime() { + return this.time; + } + + public void setTime(Duration time) { + this.time = time; + } + + public @Nullable Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + public @Nullable Duration getMaxIdle() { + return this.maxIdle; + } + + public void setMaxIdle(Duration maxIdle) { + this.maxIdle = maxIdle; + } + + public @Nullable Duration getMaxAge() { + return this.maxAge; + } + + public void setMaxAge(Duration maxAge) { + this.maxAge = maxAge; + } + + public @Nullable Duration getMaxAgeGrace() { + return this.maxAgeGrace; + } + + public void setMaxAgeGrace(Duration maxAgeGrace) { + this.maxAgeGrace = maxAgeGrace; + } + + public @Nullable Duration getPermitTime() { + return this.permitTime; + } + + public void setPermitTime(Duration permitTime) { + this.permitTime = permitTime; + } + + public boolean isPermitWithoutCalls() { + return this.permitWithoutCalls; + } + + public void setPermitWithoutCalls(boolean permitWithoutCalls) { + this.permitWithoutCalls = permitWithoutCalls; + } + + } + + public static class Ssl { + + /** + * Whether to enable SSL support. Enabled automatically if "bundle" is provided + * unless specified otherwise. + */ + private @Nullable Boolean enabled; + + /** + * Client authentication mode. + */ + private ClientAuth clientAuth = ClientAuth.NONE; + + /** + * SSL bundle name. Should match a bundle configured in spring.ssl.bundle. + */ + private @Nullable String bundle; + + /** + * Flag to indicate that client authentication is secure (i.e. certificates are + * checked). Do not set this to false in production. + */ + private boolean secure = true; + + public boolean isEnabled() { + return (this.enabled != null) ? this.enabled : this.bundle != null; + } + + public void copyDefaultsFrom(Ssl config) { + if (this.enabled == null) { + this.enabled = config.enabled; + } + if (this.bundle == null) { + this.bundle = config.bundle; + } + + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public @Nullable String getBundle() { + return this.bundle; + } + + public void setBundle(String bundle) { + this.bundle = bundle; + } + + public void setClientAuth(ClientAuth clientAuth) { + this.clientAuth = clientAuth; + } + + public ClientAuth getClientAuth() { + return this.clientAuth; + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + public boolean isSecure() { + return this.secure; + } + + } + + public static class Inprocess { + + /** + * The name of the in-process server or null to not start the in-process server. + */ + private @Nullable String name; + + /** + * Whether the inprocess server factory should be the only server factory + * available. When the value is true no other server factory will be configured. + */ + private @Nullable Boolean exclusive; + + public @Nullable String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public @Nullable Boolean getExclusive() { + return this.exclusive; + } + + public void setExclusive(Boolean exclusive) { + this.exclusive = exclusive; + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.java new file mode 100644 index 000000000000..c3c1ed0f960c --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.java @@ -0,0 +1,54 @@ +/* + * 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.grpc.server.autoconfigure; + +import io.grpc.BindableService; +import io.grpc.protobuf.services.ProtoReflectionServiceV1; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.grpc.server.GrpcServerFactory; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC Reflection service + *

+ * This auto-configuration is enabled by default. To disable it, set the configuration + * flag {spring.grpc.server.reflection.enabled=false} in your application properties. + * + * @author Haris Zujo + * @author Dave Syer + * @author Chris Bono + * @author Andrey Litvitski + * @since 4.0.0 + */ +@AutoConfiguration(before = GrpcServerFactoryAutoConfiguration.class) +@ConditionalOnGrpcServerEnabled +@ConditionalOnClass({ GrpcServerFactory.class, ProtoReflectionServiceV1.class }) +@ConditionalOnBean(BindableService.class) +@ConditionalOnProperty(name = "spring.grpc.server.reflection.enabled", havingValue = "true", matchIfMissing = true) +public final class GrpcServerReflectionAutoConfiguration { + + @Bean + BindableService serverReflection() { + return ProtoReflectionServiceV1.newInstance(); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessServerFactoryPropertyMapper.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessServerFactoryPropertyMapper.java new file mode 100644 index 000000000000..00736791cf24 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessServerFactoryPropertyMapper.java @@ -0,0 +1,41 @@ +/* + * 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.grpc.server.autoconfigure; + +import io.grpc.inprocess.InProcessServerBuilder; + +import org.springframework.boot.context.properties.PropertyMapper; + +/** + * Helper class used to map {@link GrpcServerProperties} to + * {@link InProcessServerBuilder}. + * + * @author Chris Bono + */ +class InProcessServerFactoryPropertyMapper extends DefaultServerFactoryPropertyMapper { + + InProcessServerFactoryPropertyMapper(GrpcServerProperties properties) { + super(properties); + } + + @Override + void customizeServerBuilder(InProcessServerBuilder serverBuilder) { + PropertyMapper mapper = PropertyMapper.get(); + customizeInboundLimits(serverBuilder, mapper); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyServerFactoryPropertyMapper.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyServerFactoryPropertyMapper.java new file mode 100644 index 000000000000..53124a910ade --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyServerFactoryPropertyMapper.java @@ -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.grpc.server.autoconfigure; + +import io.grpc.netty.NettyServerBuilder; + +import org.springframework.grpc.server.NettyGrpcServerFactory; + +/** + * Helper class used to map {@link GrpcServerProperties} to + * {@link NettyGrpcServerFactory}. + * + * @author Chris Bono + */ +class NettyServerFactoryPropertyMapper extends DefaultServerFactoryPropertyMapper { + + NettyServerFactoryPropertyMapper(GrpcServerProperties properties) { + super(properties); + } + + @Override + void customizeServerBuilder(NettyServerBuilder nettyServerBuilder) { + super.customizeServerBuilder(nettyServerBuilder); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnGrpcNativeServerCondition.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnGrpcNativeServerCondition.java new file mode 100644 index 000000000000..7de51852c75e --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnGrpcNativeServerCondition.java @@ -0,0 +1,69 @@ +/* + * 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.grpc.server.autoconfigure; + +import io.grpc.servlet.jakarta.GrpcServlet; + +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that determines if the gRPC server implementation + * should be one of the native varieties (e.g. Netty, Shaded Netty) - i.e. not the servlet + * container. The condition matches when the app is not a Reactive web application OR the + * {@code io.grpc.servlet.jakarta.GrpcServlet} class is not on the classpath OR the app is + * a servlet web application and the {@code io.grpc.servlet.jakarta.GrpcServlet} is on the + * classpath BUT the {@code spring.grpc.server.servlet.enabled} property is explicitly set + * to {@code false}. + * + * @author Dave Syer + * @author Chris Bono + */ +class OnGrpcNativeServerCondition extends AnyNestedCondition { + + OnGrpcNativeServerCondition() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnNotWebApplication + static class OnNonWebApplication { + + } + + @ConditionalOnMissingClass("io.grpc.servlet.jakarta.GrpcServlet") + static class OnGrpcServletClass { + + } + + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) + @ConditionalOnClass(GrpcServlet.class) + @ConditionalOnProperty(prefix = "spring.grpc.server", name = "servlet.enabled", havingValue = "false") + static class OnExplicitlyDisabledServlet { + + } + + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + static class OnExplicitlyDisabledWebflux { + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java new file mode 100644 index 000000000000..973414a9f109 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java @@ -0,0 +1,59 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import io.grpc.ServerBuilder; + +import org.springframework.boot.util.LambdaSafe; +import org.springframework.grpc.server.ServerBuilderCustomizer; + +/** + * Invokes the available {@link ServerBuilderCustomizer} instances in the context for a + * given {@link ServerBuilder}. + * + * @author Chris Bono + * @since 4.0.0 + */ +public class ServerBuilderCustomizers { + + private final List> customizers; + + ServerBuilderCustomizers(List> customizers) { + this.customizers = (customizers != null) ? new ArrayList<>(customizers) : Collections.emptyList(); + } + + /** + * Customize the specified {@link ServerBuilder}. Locates all + * {@link ServerBuilderCustomizer} beans able to handle the specified instance and + * invoke {@link ServerBuilderCustomizer#customize} on them. + * @param the type of server builder + * @param serverBuilder the builder to customize + * @return the customized builder + */ + @SuppressWarnings("unchecked") + > T customize(T serverBuilder) { + LambdaSafe.callbacks(ServerBuilderCustomizer.class, this.customizers, serverBuilder) + .withLogger(ServerBuilderCustomizers.class) + .invoke((customizer) -> customizer.customize(serverBuilder)); + return serverBuilder; + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.java new file mode 100644 index 000000000000..923c67d3c66b --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.java @@ -0,0 +1,39 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.util.Map; + +import org.springframework.boot.EnvironmentPostProcessor; +import org.springframework.boot.SpringApplication; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.util.ClassUtils; + +public class ServletEnvironmentPostProcessor implements EnvironmentPostProcessor { + + private static final boolean SERVLET_AVAILABLE = ClassUtils.isPresent("io.grpc.servlet.jakarta.GrpcServlet", null); + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + if (SERVLET_AVAILABLE) { + environment.getPropertySources() + .addFirst(new MapPropertySource("grpc-servlet", Map.of("server.http2.enabled", "true"))); + } + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyServerFactoryPropertyMapper.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyServerFactoryPropertyMapper.java new file mode 100644 index 000000000000..3b9c0fbab3f5 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyServerFactoryPropertyMapper.java @@ -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.grpc.server.autoconfigure; + +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; + +import org.springframework.grpc.server.ShadedNettyGrpcServerFactory; + +/** + * Helper class used to map {@link GrpcServerProperties} to + * {@link ShadedNettyGrpcServerFactory}. + * + * @author Chris Bono + */ +class ShadedNettyServerFactoryPropertyMapper extends DefaultServerFactoryPropertyMapper { + + ShadedNettyServerFactoryPropertyMapper(GrpcServerProperties properties) { + super(properties); + } + + @Override + void customizeServerBuilder(NettyServerBuilder nettyServerBuilder) { + super.customizeServerBuilder(nettyServerBuilder); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfiguration.java new file mode 100644 index 000000000000..9bec055f950b --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfiguration.java @@ -0,0 +1,57 @@ +/* + * 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.grpc.server.autoconfigure.codec; + +import io.grpc.Codec; +import io.grpc.Compressor; +import io.grpc.CompressorRegistry; +import io.grpc.Decompressor; +import io.grpc.DecompressorRegistry; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * The configuration that contains all codec related beans for clients/servers. + * + * @author Andrei Lisa + * @since 4.0.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(Codec.class) +public class GrpcCodecConfiguration { + + @Bean + @ConditionalOnMissingBean + CompressorRegistry compressorRegistry(ObjectProvider compressors) { + CompressorRegistry registry = CompressorRegistry.getDefaultInstance(); + compressors.orderedStream().forEachOrdered(registry::register); + return registry; + } + + @Bean + @ConditionalOnMissingBean + DecompressorRegistry decompressorRegistry(ObjectProvider decompressors) { + DecompressorRegistry registry = DecompressorRegistry.getDefaultInstance(); + decompressors.orderedStream().forEachOrdered((decompressor) -> registry.with(decompressor, false)); + return registry; + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/package-info.java new file mode 100644 index 000000000000..918b696933f2 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * Auto-configuration for gRPC server codecs. + */ +@NullMarked +package org.springframework.boot.grpc.server.autoconfigure.codec; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java new file mode 100644 index 000000000000..c388c6d73e73 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java @@ -0,0 +1,61 @@ +/* + * 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.grpc.server.autoconfigure.exception; + +import io.grpc.Grpc; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnGrpcServerEnabled; +import org.springframework.context.annotation.Bean; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.exception.CompositeGrpcExceptionHandler; +import org.springframework.grpc.server.exception.GrpcExceptionHandler; +import org.springframework.grpc.server.exception.GrpcExceptionHandlerInterceptor; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side exception + * handling. + * + * @author Dave Syer + * @author Chris Bono + * @since 4.0.0 + */ +@AutoConfiguration +@ConditionalOnGrpcServerEnabled +@ConditionalOnClass({ Grpc.class, GrpcServerFactory.class }) +@ConditionalOnBean(GrpcExceptionHandler.class) +@ConditionalOnMissingBean(GrpcExceptionHandlerInterceptor.class) +@ConditionalOnProperty(prefix = "spring.grpc.server.exception-handler", name = "enabled", havingValue = "true", + matchIfMissing = true) +public final class GrpcExceptionHandlerAutoConfiguration { + + @GlobalServerInterceptor + @Bean + GrpcExceptionHandlerInterceptor globalExceptionHandlerInterceptor( + ObjectProvider exceptionHandler) { + return new GrpcExceptionHandlerInterceptor(new CompositeGrpcExceptionHandler( + exceptionHandler.orderedStream().toArray(GrpcExceptionHandler[]::new))); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/package-info.java new file mode 100644 index 000000000000..9e2fb4a769ac --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * Auto-configuration for gRPC server exception handling. + */ +@NullMarked +package org.springframework.boot.grpc.server.autoconfigure.exception; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapter.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapter.java new file mode 100644 index 000000000000..ae0df28c28e1 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapter.java @@ -0,0 +1,113 @@ +/* + * 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.grpc.server.autoconfigure.health; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import io.grpc.protobuf.services.HealthStatusManager; + +import org.springframework.boot.actuate.health.HealthEndpoint; +import org.springframework.boot.actuate.health.StatusAggregator; +import org.springframework.boot.health.contributor.HealthIndicator; +import org.springframework.boot.health.contributor.Status; +import org.springframework.core.log.LogAccessor; +import org.springframework.util.Assert; + +/** + * Adapts {@link HealthIndicator Actuator health indicators} into gRPC health checks by + * periodically invoking {@link HealthEndpoint health endpoints} and updating the health + * status in gRPC {@link HealthStatusManager}. + * + * @author Chris Bono + * @since 4.0.0 + */ +public class ActuatorHealthAdapter { + + private static final String INVALID_INDICATOR_MSG = "Unable to determine health for '%s' - check that your configured health-indicator-paths point to available indicators"; + + private final LogAccessor logger = new LogAccessor(getClass()); + + private final HealthStatusManager healthStatusManager; + + private final HealthEndpoint healthEndpoint; + + private final StatusAggregator statusAggregator; + + private final boolean updateOverallHealth; + + private final List healthIndicatorPaths; + + protected ActuatorHealthAdapter(HealthStatusManager healthStatusManager, HealthEndpoint healthEndpoint, + StatusAggregator statusAggregator, boolean updateOverallHealth, List healthIndicatorPaths) { + this.healthStatusManager = healthStatusManager; + this.healthEndpoint = healthEndpoint; + this.statusAggregator = statusAggregator; + this.updateOverallHealth = updateOverallHealth; + Assert.notEmpty(healthIndicatorPaths, () -> "at least one health indicator path is required"); + this.healthIndicatorPaths = healthIndicatorPaths; + } + + protected void updateHealthStatus() { + var individualStatuses = this.updateIndicatorsHealthStatus(); + if (this.updateOverallHealth) { + this.updateOverallHealthStatus(individualStatuses); + } + } + + protected Set updateIndicatorsHealthStatus() { + Set statuses = new HashSet<>(); + this.healthIndicatorPaths.forEach((healthIndicatorPath) -> { + var healthComponent = this.healthEndpoint.healthForPath(healthIndicatorPath.split("/")); + if (healthComponent == null) { + this.logger.warn(() -> INVALID_INDICATOR_MSG.formatted(healthIndicatorPath)); + } + else { + this.logger.trace(() -> "Actuator returned '%s' for indicator '%s'".formatted(healthComponent, + healthIndicatorPath)); + var actuatorStatus = healthComponent.getStatus(); + var grpcStatus = toServingStatus(actuatorStatus.getCode()); + this.healthStatusManager.setStatus(healthIndicatorPath, grpcStatus); + this.logger.trace(() -> "Updated gRPC health status to '%s' for service '%s'".formatted(grpcStatus, + healthIndicatorPath)); + statuses.add(actuatorStatus); + } + }); + return statuses; + } + + protected void updateOverallHealthStatus(Set individualStatuses) { + var overallActuatorStatus = this.statusAggregator.getAggregateStatus(individualStatuses); + var overallGrpcStatus = toServingStatus(overallActuatorStatus.getCode()); + this.logger.trace(() -> "Actuator aggregate status '%s' for overall health".formatted(overallActuatorStatus)); + this.healthStatusManager.setStatus("", overallGrpcStatus); + this.logger.trace(() -> "Updated overall gRPC health status to '%s'".formatted(overallGrpcStatus)); + } + + protected ServingStatus toServingStatus(String actuatorHealthStatusCode) { + return switch (actuatorHealthStatusCode) { + case "UP" -> ServingStatus.SERVING; + case "DOWN" -> ServingStatus.NOT_SERVING; + case "OUT_OF_SERVICE" -> ServingStatus.NOT_SERVING; + case "UNKNOWN" -> ServingStatus.UNKNOWN; + default -> ServingStatus.UNKNOWN; + }; + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvoker.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvoker.java new file mode 100644 index 000000000000..b1780a911c1e --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvoker.java @@ -0,0 +1,65 @@ +/* + * 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.grpc.server.autoconfigure.health; + +import java.time.Duration; +import java.time.Instant; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder; +import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; + +/** + * Periodically invokes the {@link ActuatorHealthAdapter} in the background. + * + * @author Chris Bono + */ +class ActuatorHealthAdapterInvoker implements InitializingBean, DisposableBean { + + private final ActuatorHealthAdapter healthAdapter; + + private final SimpleAsyncTaskScheduler taskScheduler; + + private final Duration updateInitialDelay; + + private final Duration updateFixedRate; + + ActuatorHealthAdapterInvoker(ActuatorHealthAdapter healthAdapter, SimpleAsyncTaskSchedulerBuilder schedulerBuilder, + Duration updateInitialDelay, Duration updateFixedRate) { + this.healthAdapter = healthAdapter; + this.taskScheduler = schedulerBuilder.threadNamePrefix("healthAdapter-").build(); + this.updateInitialDelay = updateInitialDelay; + this.updateFixedRate = updateFixedRate; + } + + @Override + public void afterPropertiesSet() { + this.taskScheduler.scheduleAtFixedRate(this::updateHealthStatus, Instant.now().plus(this.updateInitialDelay), + this.updateFixedRate); + } + + @Override + public void destroy() { + this.taskScheduler.close(); + } + + void updateHealthStatus() { + this.healthAdapter.updateHealthStatus(); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java new file mode 100644 index 000000000000..abcdd1b62eba --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java @@ -0,0 +1,134 @@ +/* + * 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.grpc.server.autoconfigure.health; + +import java.util.List; + +import io.grpc.BindableService; +import io.grpc.protobuf.services.HealthStatusManager; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.health.HealthEndpoint; +import org.springframework.boot.actuate.health.StatusAggregator; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.BindResult; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnGrpcServerEnabled; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties; +import org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side health service. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + * @author Chris Bono + * @since 4.0.0 + */ +@AutoConfiguration(before = GrpcServerFactoryAutoConfiguration.class) +@ConditionalOnGrpcServerEnabled +@ConditionalOnClass({ GrpcServerFactory.class, HealthStatusManager.class }) +@ConditionalOnBean(BindableService.class) +@ConditionalOnProperty(name = "spring.grpc.server.health.enabled", havingValue = "true", matchIfMissing = true) +public final class GrpcServerHealthAutoConfiguration { + + @Bean(destroyMethod = "enterTerminalState") + @ConditionalOnMissingBean + HealthStatusManager healthStatusManager() { + return new HealthStatusManager(); + } + + @Bean + BindableService grpcHealthService(HealthStatusManager healthStatusManager) { + return healthStatusManager.getHealthService(); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(HealthEndpoint.class) + @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class) + @AutoConfigureAfter(value = TaskSchedulingAutoConfiguration.class, + name = "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration") + @ConditionalOnProperty(name = "spring.grpc.server.health.actuator.enabled", havingValue = "true", + matchIfMissing = true) + @Conditional(OnHealthIndicatorPathsCondition.class) + @EnableConfigurationProperties(GrpcServerProperties.class) + @EnableScheduling + static class ActuatorHealthAdapterConfiguration { + + @Bean + @ConditionalOnMissingBean + ActuatorHealthAdapter healthAdapter(HealthStatusManager healthStatusManager, HealthEndpoint healthEndpoint, + StatusAggregator statusAggregator, GrpcServerProperties serverProperties) { + return new ActuatorHealthAdapter(healthStatusManager, healthEndpoint, statusAggregator, + serverProperties.getHealth().getActuator().getUpdateOverallHealth(), + serverProperties.getHealth().getActuator().getHealthIndicatorPaths()); + } + + @Bean + ActuatorHealthAdapterInvoker healthAdapterInvoker(ActuatorHealthAdapter healthAdapter, + SimpleAsyncTaskSchedulerBuilder schedulerBuilder, GrpcServerProperties serverProperties) { + return new ActuatorHealthAdapterInvoker(healthAdapter, schedulerBuilder, + serverProperties.getHealth().getActuator().getUpdateInitialDelay(), + serverProperties.getHealth().getActuator().getUpdateRate()); + } + + } + + /** + * Condition to determine if + * {@code spring.grpc.server.health.actuator.health-indicator-paths} is specified with + * at least one entry. + */ + static class OnHealthIndicatorPathsCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + String propertyName = "spring.grpc.server.health.actuator.health-indicator-paths"; + BindResult> property = Binder.get(context.getEnvironment()) + .bind(propertyName, Bindable.listOf(String.class)); + ConditionMessage.Builder messageBuilder = ConditionMessage + .forCondition("Health indicator paths (at least one)"); + if (property.isBound() && !property.get().isEmpty()) { + return ConditionOutcome + .match(messageBuilder.because("property %s found with at least one entry".formatted(propertyName))); + } + return ConditionOutcome.noMatch( + messageBuilder.because("property %s not found with at least one entry".formatted(propertyName))); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java new file mode 100644 index 000000000000..d7ef5d4d129f --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * Auto-configuration for gRPC server health adapter. + */ +@NullMarked +package org.springframework.boot.grpc.server.autoconfigure.health; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java new file mode 100644 index 000000000000..5cd06d1019bb --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** + * Auto-configuration for gRPC server. + */ + +@NullMarked +package org.springframework.boot.grpc.server.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java new file mode 100644 index 000000000000..13358ea45623 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java @@ -0,0 +1,70 @@ +/* + * 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.grpc.server.autoconfigure.security; + +import org.springframework.context.ApplicationContext; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; +import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.util.matcher.AndRequestMatcher; +import org.springframework.security.web.util.matcher.NegatedRequestMatcher; + +/** + * A custom {@link AbstractHttpConfigurer} that disables CSRF protection for gRPC + * requests. + *

+ * This configurer checks the application context to determine if CSRF protection should + * be disabled for gRPC requests based on the property + * {@code spring.grpc.server.security.csrf.enabled}. By default, CSRF protection is + * disabled unless explicitly enabled in the application properties. + *

+ * + * @author Dave Syer + * @since 4.0.0 + * @see AbstractHttpConfigurer + * @see HttpSecurity + */ +public class GrpcDisableCsrfHttpConfigurer extends AbstractHttpConfigurer { + + @Override + public void init(HttpSecurity http) throws Exception { + ApplicationContext context = http.getSharedObject(ApplicationContext.class); + if (context != null && context.getBeanNamesForType(GrpcServiceDiscoverer.class).length == 1 + && isServletEnabledAndCsrfDisabled(context) && isCsrfConfigurerPresent(http)) { + http.csrf(this::disable); + } + } + + @SuppressWarnings("unchecked") + private boolean isCsrfConfigurerPresent(HttpSecurity http) { + return http.getConfigurer(CsrfConfigurer.class) != null; + } + + private void disable(CsrfConfigurer csrf) { + csrf.requireCsrfProtectionMatcher(new AndRequestMatcher(CsrfFilter.DEFAULT_CSRF_MATCHER, + new NegatedRequestMatcher(GrpcServletRequest.all()))); + } + + private boolean isServletEnabledAndCsrfDisabled(ApplicationContext context) { + return context.getEnvironment().getProperty("spring.grpc.server.servlet.enabled", Boolean.class, true) + && !context.getEnvironment() + .getProperty("spring.grpc.server.security.csrf.enabled", Boolean.class, false); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequest.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequest.java new file mode 100644 index 000000000000..83172c6e3904 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequest.java @@ -0,0 +1,131 @@ +/* + * 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.grpc.server.autoconfigure.security; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import reactor.core.publisher.Mono; + +import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServletRequest.GrpcServletRequestMatcher; +import org.springframework.boot.security.reactive.ApplicationContextServerWebExchangeMatcher; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * Factory for a request matcher used to match against resource locations for gRPC + * services. + * + * @author Dave Syer + * @since 4.0.0 + */ +public final class GrpcReactiveRequest { + + private GrpcReactiveRequest() { + } + + /** + * Returns a matcher that includes all gRPC services from the application context. The + * {@link GrpcReactiveRequestMatcher#excluding(String...) excluding} method can be + * used to remove specific services by name if required. For example: + * + *
+	 * GrpcReactiveRequest.all().excluding("my-service")
+	 * 
+ * @return the configured {@link ServerWebExchangeMatcher} + */ + public static GrpcReactiveRequestMatcher all() { + return new GrpcReactiveRequestMatcher(); + } + + /** + * The request matcher used to match against resource locations. + */ + public static final class GrpcReactiveRequestMatcher + extends ApplicationContextServerWebExchangeMatcher { + + private final Set exclusions; + + private volatile ServerWebExchangeMatcher delegate; + + private GrpcReactiveRequestMatcher() { + this(new HashSet<>()); + } + + private GrpcReactiveRequestMatcher(Set exclusions) { + super(GrpcServiceDiscoverer.class); + this.exclusions = exclusions; + this.delegate = (request) -> MatchResult.notMatch(); + } + + /** + * Return a new {@link GrpcServletRequestMatcher} based on this one but excluding + * the specified services. + * @param rest additional services to exclude + * @return a new {@link GrpcServletRequestMatcher} + */ + public GrpcReactiveRequestMatcher excluding(String... rest) { + return excluding(Set.of(rest)); + } + + /** + * Return a new {@link GrpcServletRequestMatcher} based on this one but excluding + * the specified services. + * @param exclusions additional service names to exclude + * @return a new {@link GrpcServletRequestMatcher} + */ + public GrpcReactiveRequestMatcher excluding(Set exclusions) { + Assert.notNull(exclusions, "Exclusions must not be null"); + Set subset = new LinkedHashSet<>(this.exclusions); + subset.addAll(exclusions); + return new GrpcReactiveRequestMatcher(subset); + } + + @Override + protected void initialized(Supplier context) { + List matchers = getDelegateMatchers(context.get()).toList(); + this.delegate = matchers.isEmpty() ? (request) -> MatchResult.notMatch() + : new OrServerWebExchangeMatcher(matchers); + } + + private Stream getDelegateMatchers(GrpcServiceDiscoverer context) { + return getPatterns(context).map(PathPatternParserServerWebExchangeMatcher::new); + } + + private Stream getPatterns(GrpcServiceDiscoverer context) { + return context.listServiceNames() + .stream() + .filter((service) -> !this.exclusions.stream().anyMatch((type) -> type.equals(service))) + .map((service) -> "/" + service + "/**"); + } + + @Override + protected Mono matches(ServerWebExchange exchange, Supplier context) { + return this.delegate.matches(exchange); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java new file mode 100644 index 000000000000..6524f6559673 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java @@ -0,0 +1,113 @@ +/* + * 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.grpc.server.autoconfigure.security; + +import io.grpc.internal.GrpcUtil; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnGrpcNativeServer; +import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnGrpcServerEnabled; +import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnGrpcServletServer; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerExecutorProvider; +import org.springframework.boot.grpc.server.autoconfigure.exception.GrpcExceptionHandlerAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.security.GrpcSecurityAutoConfiguration.ExceptionHandlerConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.security.GrpcSecurityAutoConfiguration.GrpcNativeSecurityConfigurerConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.security.GrpcSecurityAutoConfiguration.GrpcServletSecurityConfigurerConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.exception.GrpcExceptionHandler; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.grpc.server.security.SecurityContextServerInterceptor; +import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler; +import org.springframework.security.concurrent.DelegatingSecurityContextExecutor; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.web.SecurityFilterChain; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side security. + * + * @author Dave Syer + * @author Chris Bono + * @author Andrey Litvitski + * @since 4.0.0 + */ +@AutoConfiguration(before = GrpcExceptionHandlerAutoConfiguration.class, + afterName = "org.springframework.boot.security.autoconfigure.servlet.SecurityAutoConfiguration") +@ConditionalOnGrpcServerEnabled +@ConditionalOnClass({ GrpcServerFactory.class, ObjectPostProcessor.class }) +@Import({ ExceptionHandlerConfiguration.class, GrpcNativeSecurityConfigurerConfiguration.class, + GrpcServletSecurityConfigurerConfiguration.class }) +public final class GrpcSecurityAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @Import(AuthenticationConfiguration.class) + static class ExceptionHandlerConfiguration { + + @Bean + GrpcExceptionHandler accessExceptionHandler() { + return new SecurityGrpcExceptionHandler(); + } + + } + + @ConditionalOnBean(ObjectPostProcessor.class) + @Configuration(proxyBeanMethods = false) + @ConditionalOnGrpcNativeServer + static class GrpcNativeSecurityConfigurerConfiguration { + + @Bean + GrpcSecurity grpcSecurity(ObjectPostProcessor objectPostProcessor, + AuthenticationConfiguration authenticationConfiguration, ApplicationContext context) throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = authenticationConfiguration + .authenticationManagerBuilder(objectPostProcessor, context); + authenticationManagerBuilder + .parentAuthenticationManager(authenticationConfiguration.getAuthenticationManager()); + return new GrpcSecurity(objectPostProcessor, authenticationManagerBuilder, context); + } + + } + + @ConditionalOnBean(SecurityFilterChain.class) + @ConditionalOnGrpcServletServer + @Configuration(proxyBeanMethods = false) + static class GrpcServletSecurityConfigurerConfiguration { + + @Bean + @GlobalServerInterceptor + SecurityContextServerInterceptor securityContextInterceptor() { + return new SecurityContextServerInterceptor(); + } + + @Bean + @ConditionalOnMissingBean(GrpcServerExecutorProvider.class) + GrpcServerExecutorProvider grpcServerExecutorProvider() { + return () -> new DelegatingSecurityContextExecutor(GrpcUtil.SHARED_CHANNEL_EXECUTOR.create()); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequest.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequest.java new file mode 100644 index 000000000000..1436ddac3779 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequest.java @@ -0,0 +1,137 @@ +/* + * 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.grpc.server.autoconfigure.security; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.Assert; +import org.springframework.web.context.WebApplicationContext; + +/** + * Factory for a request matcher used to match against resource locations for gRPC + * services. + * + * @author Dave Syer + * @since 4.0.0 + */ +public final class GrpcServletRequest { + + private GrpcServletRequest() { + } + + /** + * Returns a matcher that includes all gRPC services from the application context. The + * {@link GrpcServletRequestMatcher#excluding(String...) excluding} method can be used + * to remove specific services by name if required. For example: + * + *
+	 * GrpcServletRequest.all().excluding("my-service")
+	 * 
+ * @return the configured {@link RequestMatcher} + */ + public static GrpcServletRequestMatcher all() { + return new GrpcServletRequestMatcher(); + } + + /** + * The request matcher used to match against resource locations. + */ + public static final class GrpcServletRequestMatcher + extends ApplicationContextRequestMatcher { + + private final Set exclusions; + + private volatile RequestMatcher delegate; + + private GrpcServletRequestMatcher() { + this(new HashSet<>()); + } + + private GrpcServletRequestMatcher(Set exclusions) { + super(GrpcServiceDiscoverer.class); + this.exclusions = exclusions; + this.delegate = (request) -> false; + } + + /** + * Return a new {@link GrpcServletRequestMatcher} based on this one but excluding + * the specified services. + * @param rest additional services to exclude + * @return a new {@link GrpcServletRequestMatcher} + */ + public GrpcServletRequestMatcher excluding(String... rest) { + return excluding(Set.of(rest)); + } + + /** + * Return a new {@link GrpcServletRequestMatcher} based on this one but excluding + * the specified services. + * @param exclusions additional service names to exclude + * @return a new {@link GrpcServletRequestMatcher} + */ + public GrpcServletRequestMatcher excluding(Set exclusions) { + Assert.notNull(exclusions, "Exclusions must not be null"); + Set subset = new LinkedHashSet<>(this.exclusions); + subset.addAll(exclusions); + return new GrpcServletRequestMatcher(subset); + } + + @Override + protected void initialized(Supplier context) { + List matchers = getDelegateMatchers(context.get()).toList(); + this.delegate = matchers.isEmpty() ? (request) -> false : new OrRequestMatcher(matchers); + } + + @Override + protected boolean ignoreApplicationContext(WebApplicationContext context) { + return context.getBeanNamesForType(GrpcServiceDiscoverer.class).length != 1; + } + + private Stream getDelegateMatchers(GrpcServiceDiscoverer context) { + return getPatterns(context).map((path) -> { + Assert.hasText(path, "Path must not be empty"); + return PathPatternRequestMatcher.withDefaults().matcher(path); + }); + } + + private Stream getPatterns(GrpcServiceDiscoverer context) { + return context.listServiceNames() + .stream() + .filter((service) -> !this.exclusions.stream().anyMatch((type) -> type.equals(service))) + .map((service) -> "/" + service + "/**"); + } + + @Override + protected boolean matches(HttpServletRequest request, Supplier context) { + return this.delegate.matches(request); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ClientAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ClientAutoConfiguration.java new file mode 100644 index 000000000000..f4b272902a26 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ClientAutoConfiguration.java @@ -0,0 +1,61 @@ +/* + * 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.grpc.server.autoconfigure.security; + +import java.util.ArrayList; +import java.util.List; + +import io.grpc.BindableService; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.security.oauth2.client.autoconfigure.ConditionalOnOAuth2ClientRegistrationProperties; +import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties; +import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientPropertiesMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC OAuth2 security. + * + * @author Dave Syer + * @since 4.0.0 + */ +// Copied from Spring Boot (https://github.com/spring-projects/spring-boot/issues/40997, ] +// https://github.com/spring-projects/spring-boot/issues/15877) +@AutoConfiguration( + afterName = "org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientAutoConfiguration") +@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, InMemoryClientRegistrationRepository.class }) +@ConditionalOnOAuth2ClientRegistrationProperties +@EnableConfigurationProperties(OAuth2ClientProperties.class) +public final class OAuth2ClientAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(ClientRegistrationRepository.class) + InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) { + List registrations = new ArrayList<>( + new OAuth2ClientPropertiesMapper(properties).asClientRegistrations().values()); + return new InMemoryClientRegistrationRepository(registrations); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java new file mode 100644 index 000000000000..9480aea41611 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java @@ -0,0 +1,331 @@ +/* + * 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.grpc.server.autoconfigure.security; + +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import io.grpc.BindableService; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration.GrpcServletConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.security.OAuth2ResourceServerAutoConfiguration.Oauth2ResourceServerConfiguration; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnIssuerLocationJwtDecoder; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.ConditionalOnPublicKeyJwtDecoder; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.OAuth2ResourceServerProperties; +import org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.JwkSetUriJwtDecoderBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.jwt.JwtClaimValidator; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtValidators; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder; +import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; +import org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector; +import org.springframework.util.CollectionUtils; + +import static org.springframework.security.config.Customizer.withDefaults; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for gRPC OAuth2 resource server. + * + * @author Dave Syer + * @since 4.0.0 + */ +// All copied from Spring Boot +// (https://github.com/spring-projects/spring-boot/issues/43978), except the +// 2 @Beans of type AuthenticationProcessInterceptor +@AutoConfiguration( + beforeName = "org.springframework.boot.security.autoconfigure.servlet.UserDetailsServiceAutoConfiguration", + afterName = { "org.springframework.boot.security.autoconfigure.servlet.SecurityAutoConfiguration", + "org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration" }, + after = { GrpcSecurityAutoConfiguration.class, GrpcServerFactoryAutoConfiguration.class }) +@EnableConfigurationProperties(OAuth2ResourceServerProperties.class) +@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, InMemoryClientRegistrationRepository.class, + BearerTokenAuthenticationToken.class, ObjectPostProcessor.class }) +@ConditionalOnMissingBean(GrpcServletConfiguration.class) +@ConditionalOnBean({ BindableService.class, GrpcSecurityAutoConfiguration.class }) +@Import({ Oauth2ResourceServerConfiguration.JwtConfiguration.class, + Oauth2ResourceServerConfiguration.OpaqueTokenConfiguration.class }) +public final class OAuth2ResourceServerAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + static class Oauth2ResourceServerConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(JwtDecoder.class) + @Import({ OAuth2ResourceServerJwtConfiguration.JwtConverterConfiguration.class, + OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration.class, + OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration.class }) + static class JwtConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @Import({ OAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class, + OAuth2ResourceServerOpaqueTokenConfiguration.OAuth2SecurityFilterChainConfiguration.class }) + static class OpaqueTokenConfiguration { + + } + + } + + @Configuration(proxyBeanMethods = false) + static class OAuth2ResourceServerOpaqueTokenConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(OpaqueTokenIntrospector.class) + static class OpaqueTokenIntrospectionClientConfiguration { + + @Bean + @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri") + SpringOpaqueTokenIntrospector blockingOpaqueTokenIntrospector(OAuth2ResourceServerProperties properties) { + OAuth2ResourceServerProperties.Opaquetoken opaqueToken = properties.getOpaquetoken(); + return SpringOpaqueTokenIntrospector.withIntrospectionUri(opaqueToken.getIntrospectionUri()) + .clientId(opaqueToken.getClientId()) + .clientSecret(opaqueToken.getClientSecret()) + .build(); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(AuthenticationProcessInterceptor.class) + static class OAuth2SecurityFilterChainConfiguration { + + @Bean + @ConditionalOnBean(OpaqueTokenIntrospector.class) + @GlobalServerInterceptor + AuthenticationProcessInterceptor opaqueTokenAuthenticationProcessInterceptor(GrpcSecurity http) + throws Exception { + http.authorizeRequests((requests) -> requests.allRequests().authenticated()); + http.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults())); + return http.build(); + } + + } + + } + + @Configuration(proxyBeanMethods = false) + static class OAuth2ResourceServerJwtConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(JwtDecoder.class) + static class JwtDecoderConfiguration { + + private final OAuth2ResourceServerProperties.Jwt properties; + + private final List> additionalValidators; + + JwtDecoderConfiguration(OAuth2ResourceServerProperties properties, + ObjectProvider> additionalValidators) { + this.properties = properties.getJwt(); + this.additionalValidators = additionalValidators.orderedStream().toList(); + } + + @Bean + @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") + JwtDecoder blockingJwtDecoderByJwkKeySetUri( + ObjectProvider customizers) { + JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()) + .jwsAlgorithms(this::jwsAlgorithms); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + NimbusJwtDecoder nimbusJwtDecoder = builder.build(); + String issuerUri = this.properties.getIssuerUri(); + OAuth2TokenValidator defaultValidator = (issuerUri != null) + ? JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators.createDefault(); + nimbusJwtDecoder.setJwtValidator(getValidators(defaultValidator)); + return nimbusJwtDecoder; + } + + private void jwsAlgorithms(Set signatureAlgorithms) { + for (String algorithm : this.properties.getJwsAlgorithms()) { + signatureAlgorithms.add(SignatureAlgorithm.from(algorithm)); + } + } + + private OAuth2TokenValidator getValidators(OAuth2TokenValidator defaultValidator) { + List audiences = this.properties.getAudiences(); + if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) { + return defaultValidator; + } + List> validators = new ArrayList<>(); + validators.add(defaultValidator); + if (!CollectionUtils.isEmpty(audiences)) { + validators.add(audValidator(audiences)); + } + validators.addAll(this.additionalValidators); + return new DelegatingOAuth2TokenValidator<>(validators); + } + + private JwtClaimValidator> audValidator(List audiences) { + return new JwtClaimValidator<>(JwtClaimNames.AUD, (aud) -> nullSafeDisjoint(aud, audiences)); + } + + private boolean nullSafeDisjoint(List c1, List c2) { + return c1 != null && !Collections.disjoint(c1, c2); + } + + @Bean + @ConditionalOnPublicKeyJwtDecoder + JwtDecoder blockingJwtDecoderByPublicKeyValue() throws Exception { + RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA") + .generatePublic(new X509EncodedKeySpec(getKeySpec(this.properties.readPublicKey()))); + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(publicKey) + .signatureAlgorithm(SignatureAlgorithm.from(exactlyOneAlgorithm())) + .build(); + jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefault())); + return jwtDecoder; + } + + private byte[] getKeySpec(String keyValue) { + keyValue = keyValue.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", ""); + return Base64.getMimeDecoder().decode(keyValue); + } + + private String exactlyOneAlgorithm() { + List algorithms = this.properties.getJwsAlgorithms(); + int count = (algorithms != null) ? algorithms.size() : 0; + if (count != 1) { + throw new IllegalStateException( + "Creating a JWT decoder using a public key requires exactly one JWS algorithm but " + count + + " were configured"); + } + return algorithms.get(0); + } + + @Bean + @ConditionalOnIssuerLocationJwtDecoder + SupplierJwtDecoder blockingJwtDecoderByIssuerUri( + ObjectProvider customizers) { + return new SupplierJwtDecoder(() -> { + String issuerUri = this.properties.getIssuerUri(); + JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withIssuerLocation(issuerUri); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + NimbusJwtDecoder jwtDecoder = builder.build(); + jwtDecoder.setJwtValidator(getValidators(JwtValidators.createDefaultWithIssuer(issuerUri))); + return jwtDecoder; + }); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(AuthenticationProcessInterceptor.class) + static class OAuth2SecurityFilterChainConfiguration { + + @Bean + @ConditionalOnBean(JwtDecoder.class) + @GlobalServerInterceptor + AuthenticationProcessInterceptor jwtAuthenticationProcessInterceptor(GrpcSecurity http) throws Exception { + http.authorizeRequests((requests) -> requests.allRequests().authenticated()); + http.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(withDefaults())); + return http.build(); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(JwtAuthenticationConverter.class) + @Conditional(JwtConverterPropertiesCondition.class) + static class JwtConverterConfiguration { + + private final OAuth2ResourceServerProperties.Jwt properties; + + JwtConverterConfiguration(OAuth2ResourceServerProperties properties) { + this.properties = properties.getJwt(); + } + + @Bean + JwtAuthenticationConverter getJwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + PropertyMapper map = PropertyMapper.get(); + map.from(this.properties.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix); + map.from(this.properties.getAuthoritiesClaimDelimiter()) + .to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter); + map.from(this.properties.getAuthoritiesClaimName()) + .to(grantedAuthoritiesConverter::setAuthoritiesClaimName); + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + map.from(this.properties.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); + return jwtAuthenticationConverter; + } + + } + + private static class JwtConverterPropertiesCondition extends AnyNestedCondition { + + JwtConverterPropertiesCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authority-prefix") + static class OnAuthorityPrefix { + + } + + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "principal-claim-name") + static class OnPrincipalClaimName { + + } + + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", + name = "authorities-claim-name") + static class OnAuthoritiesClaimName { + + } + + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java new file mode 100644 index 000000000000..264c92d6e9a6 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * Auto-configuration for gRPC server security. + */ +@NullMarked +package org.springframework.boot.grpc.server.autoconfigure.security; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 000000000000..68dbe4f646cc --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,51 @@ +{ + "groups": [], + "properties": [ + { + "name": "spring.grpc.server.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable server autoconfiguration.", + "defaultValue": true + }, + { + "name": "spring.grpc.server.exception-handling.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable user-defined global exception handling on the gRPC server.", + "defaultValue": true + }, + { + "name": "spring.grpc.server.inprocess.exclusive", + "type": "java.lang.Boolean", + "description": "Whether the inprocess server factory should be the only server factory available. When the value is true, no other server factory will be configured.", + "defaultValue": true + }, + { + "name": "spring.grpc.server.observations.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Observations on the server.", + "defaultValue": true + }, + { + "name": "spring.grpc.server.port", + "defaultValue": "9090" + }, + { + "name": "spring.grpc.server.reflection.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Reflection on the gRPC server.", + "defaultValue": true + }, + { + "name": "spring.grpc.server.security.csrf.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable CSRF protection on gRPC requests.", + "defaultValue": false + }, + { + "name": "spring.grpc.server.servlet.enabled", + "type": "java.lang.Boolean", + "description": "Whether to use a servlet server in a servlet-based web application. When the value is false, a native gRPC server will be created as long as one is available, and it will listen on its own port. Should only be needed if the GrpcServlet is on the classpath", + "defaultValue": true + } + ] +} diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring.factories b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..49203d9172bf --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring.factories @@ -0,0 +1,5 @@ +org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer=\ +org.springframework.boot.grpc.server.autoconfigure.security.GrpcDisableCsrfHttpConfigurer + +org.springframework.boot.env.EnvironmentPostProcessor=\ +org.springframework.boot.grpc.server.autoconfigure.ServletEnvironmentPostProcessor diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000000..bfb64d0cd23f --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,9 @@ +org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.GrpcServerObservationAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.GrpcServerReflectionAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.exception.GrpcExceptionHandlerAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.security.GrpcSecurityAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.security.OAuth2ClientAutoConfiguration +org.springframework.boot.grpc.server.autoconfigure.security.OAuth2ResourceServerAutoConfiguration diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java new file mode 100644 index 000000000000..a27067e48054 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java @@ -0,0 +1,529 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.grpc.BindableService; +import io.grpc.Grpc; +import io.grpc.ServerBuilder; +import io.grpc.ServerServiceDefinition; +import io.grpc.ServiceDescriptor; +import io.grpc.netty.NettyServerBuilder; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.InProcessGrpcServerFactory; +import org.springframework.grpc.server.NettyGrpcServerFactory; +import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.grpc.server.ServerServiceDefinitionFilter; +import org.springframework.grpc.server.ShadedNettyGrpcServerFactory; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.service.DefaultGrpcServiceConfigurer; +import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer; +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.grpc.server.service.ServerInterceptorFilter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link GrpcServerAutoConfiguration}. + * + * @author Chris Bono + * @author Andrey Litvitski + */ +@SuppressWarnings("rawtypes") +class GrpcServerAutoConfigurationTests { + + private final BindableService service = mock(); + + private final ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); + + @BeforeEach + void prepareForTest() { + given(this.service.bindService()).willReturn(this.serviceDefinition); + } + + private ApplicationContextRunner contextRunner() { + // NOTE: we use noop server lifecycle to avoid startup + ApplicationContextRunner runner = new ApplicationContextRunner(); + return contextRunner(runner); + } + + private ApplicationContextRunner contextRunner(ApplicationContextRunner runner) { + return runner + .withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class, + GrpcServerFactoryAutoConfiguration.class, SslAutoConfiguration.class)) + .withBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean(BindableService.class, () -> this.service); + } + + private WebApplicationContextRunner webContextRunner(WebApplicationContextRunner runner) { + return runner + .withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class, + GrpcServerFactoryAutoConfiguration.class, SslAutoConfiguration.class)) + .withBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean(BindableService.class, () -> this.service); + } + + private ApplicationContextRunner contextRunnerWithLifecyle() { + // NOTE: we use noop server lifecycle to avoid startup + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class, + GrpcServerFactoryAutoConfiguration.class, SslAutoConfiguration.class)) + .withBean(BindableService.class, () -> this.service); + } + + @Test + void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(BindableService.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class)); + } + + @Test + void whenNoBindableServicesRegisteredAutoConfigurationIsSkipped() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetFalseThenAutoConfigurationIsSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() { + this.contextRunner().run((context) -> assertThat(context).hasSingleBean(GrpcServerAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcServerAutoConfiguration.class)); + } + + @Test + void whenHasUserDefinedGrpcServiceDiscovererDoesNotAutoConfigureBean() { + GrpcServiceDiscoverer customGrpcServiceDiscoverer = mock(GrpcServiceDiscoverer.class); + this.contextRunnerWithLifecyle() + .withBean("customGrpcServiceDiscoverer", GrpcServiceDiscoverer.class, () -> customGrpcServiceDiscoverer) + .withPropertyValues("spring.grpc.server.port=0") + .run((context) -> assertThat(context).getBean(GrpcServiceDiscoverer.class) + .isSameAs(customGrpcServiceDiscoverer)); + } + + @Test + void grpcServiceDiscovererAutoConfiguredAsExpected() { + this.contextRunnerWithLifecyle() + .withPropertyValues("spring.grpc.server.port=0") + .run((context) -> assertThat(context).getBean(GrpcServiceDiscoverer.class) + .isInstanceOf(DefaultGrpcServiceDiscoverer.class)); + } + + @Test + void whenHasUserDefinedServerBuilderCustomizersDoesNotAutoConfigureBean() { + ServerBuilderCustomizers customCustomizers = mock(ServerBuilderCustomizers.class); + this.contextRunner() + .withBean("customCustomizers", ServerBuilderCustomizers.class, () -> customCustomizers) + .run((context) -> assertThat(context).getBean(ServerBuilderCustomizers.class).isSameAs(customCustomizers)); + } + + @Test + void serverBuilderCustomizersAutoConfiguredAsExpected() { + this.contextRunner() + .withUserConfiguration(ServerBuilderCustomizersConfig.class) + .run((context) -> assertThat(context).getBean(ServerBuilderCustomizers.class) + .extracting("customizers", InstanceOfAssertFactories.list(ServerBuilderCustomizer.class)) + .contains(ServerBuilderCustomizersConfig.CUSTOMIZER_BAR, + ServerBuilderCustomizersConfig.CUSTOMIZER_FOO)); + } + + @Test + void whenHasUserDefinedServerFactoryDoesNotAutoConfigureBean() { + GrpcServerFactory customServerFactory = mock(GrpcServerFactory.class); + this.contextRunner() + .withBean("customServerFactory", GrpcServerFactory.class, () -> customServerFactory) + .run((context) -> assertThat(context).getBean(GrpcServerFactory.class).isSameAs(customServerFactory)); + } + + @Test + void userDefinedServerFactoryWithInProcessServerFactory() { + GrpcServerFactory customServerFactory = mock(GrpcServerFactory.class); + this.contextRunner() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withBean("customServerFactory", GrpcServerFactory.class, () -> customServerFactory) + .run((context) -> assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("customServerFactory", "inProcessGrpcServerFactory")); + } + + @Test + void whenShadedAndNonShadedNettyOnClasspathShadedNettyFactoryIsAutoConfigured() { + this.contextRunner() + .run((context) -> assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(ShadedNettyGrpcServerFactory.class)); + } + + @Test + void shadedNettyFactoryWithInProcessServerFactory() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .run((context) -> assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("shadedNettyGrpcServerFactory", "inProcessGrpcServerFactory")); + } + + @Test + void whenOnlyNonShadedNettyOnClasspathNonShadedNettyFactoryIsAutoConfigured() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .run((context) -> assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(NettyGrpcServerFactory.class)); + } + + @Test + void nonShadedNettyFactoryWithInProcessServerFactory() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .run((context) -> assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("nettyGrpcServerFactory", "inProcessGrpcServerFactory")); + } + + @Test + void whenShadedNettyAndNettyNotOnClasspathNoServerFactoryIsAutoConfigured() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerFactory.class)); + } + + @Test + void noServerFactoryWithInProcessServerFactory() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .run((context) -> assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(InProcessGrpcServerFactory.class)); + } + + @Test + void shadedNettyServerFactoryAutoConfiguredWithCustomLifecycle() { + GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class); + this.contextRunnerWithLifecyle() + .withBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle) + .run((context) -> { + assertThat(context).getBean(GrpcServerFactory.class).isInstanceOf(ShadedNettyGrpcServerFactory.class); + assertThat(context).getBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class) + .isSameAs(customServerLifecycle); + }); + } + + @Test + void nettyServerFactoryAutoConfiguredWithCustomLifecycle() { + GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class); + this.contextRunnerWithLifecyle() + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle) + .run((context) -> { + assertThat(context).getBean(GrpcServerFactory.class).isInstanceOf(NettyGrpcServerFactory.class); + assertThat(context).getBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class) + .isSameAs(customServerLifecycle); + }); + } + + @Test + void inProcessServerFactoryAutoConfiguredWithCustomLifecycle() { + GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class); + this.contextRunnerWithLifecyle() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle) + .run((context) -> { + assertThat(context).getBean(GrpcServerFactory.class).isInstanceOf(InProcessGrpcServerFactory.class); + assertThat(context).getBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class) + .isSameAs(customServerLifecycle); + }); + } + + @Test + void shadedNettyServerFactoryAutoConfiguredAsExpected() { + serverFactoryAutoConfiguredAsExpected( + this.contextRunner() + .withPropertyValues("spring.grpc.server.host=myhost", "spring.grpc.server.port=6160"), + ShadedNettyGrpcServerFactory.class, "myhost:6160", "shadedNettyGrpcServerLifecycle"); + } + + @Test + void serverFactoryAutoConfiguredInWebAppWhenServletDisabled() { + serverFactoryAutoConfiguredAsExpected( + this.webContextRunner(new WebApplicationContextRunner()) + .withPropertyValues("spring.grpc.server.host=myhost", "spring.grpc.server.port=6160") + .withPropertyValues("spring.grpc.server.servlet.enabled=false"), + GrpcServerFactory.class, "myhost:6160", "shadedNettyGrpcServerLifecycle"); + } + + @Test + void nettyServerFactoryAutoConfiguredAsExpected() { + serverFactoryAutoConfiguredAsExpected(this.contextRunner() + .withPropertyValues("spring.grpc.server.host=myhost", "spring.grpc.server.port=6160") + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)), + NettyGrpcServerFactory.class, "myhost:6160", "nettyGrpcServerLifecycle"); + } + + @Test + void inProcessServerFactoryAutoConfiguredAsExpected() { + serverFactoryAutoConfiguredAsExpected( + this.contextRunner() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)), + InProcessGrpcServerFactory.class, "foo", "inProcessGrpcServerLifecycle"); + } + + private void serverFactoryAutoConfiguredAsExpected(AbstractApplicationContextRunner contextRunner, + Class expectedServerFactoryType, String expectedAddress, String expectedLifecycleBeanName) { + contextRunner.run((context) -> { + assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(expectedServerFactoryType) + .hasFieldOrPropertyWithValue("address", expectedAddress) + .extracting("serviceList", InstanceOfAssertFactories.list(ServerServiceDefinition.class)) + .singleElement() + .extracting(ServerServiceDefinition::getServiceDescriptor) + .extracting(ServiceDescriptor::getName) + .isEqualTo("my-service"); + assertThat(context).getBean(expectedLifecycleBeanName, GrpcServerLifecycle.class).isNotNull(); + }); + } + + @Test + void shadedNettyServerFactoryAutoConfiguredWithCustomizers() { + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder builder = mock(); + serverFactoryAutoConfiguredWithCustomizers(this.contextRunnerWithLifecyle(), builder, + ShadedNettyGrpcServerFactory.class); + } + + @Test + void nettyServerFactoryAutoConfiguredWithCustomizers() { + // FilteredClassLoader hides the class from the auto-configuration but not from + // the Java SPI used by ServerBuilder.forPort(int) which by default returns + // shaded Netty. This results in class cast exception when + // NettyGrpcServerFactory is expecting a non-shaded server builder. We static + // mock the builder to return non-shaded Netty - which would happen in + // real world. + try (MockedStatic serverBuilderForPort = Mockito.mockStatic(Grpc.class)) { + serverBuilderForPort.when(() -> Grpc.newServerBuilderForPort(anyInt(), any())) + .thenAnswer((Answer) (invocation) -> NettyServerBuilder + .forPort(invocation.getArgument(0))); + NettyServerBuilder builder = mock(); + serverFactoryAutoConfiguredWithCustomizers(this.contextRunnerWithLifecyle() + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)), + builder, NettyGrpcServerFactory.class); + } + } + + @SuppressWarnings("unchecked") + private > void serverFactoryAutoConfiguredWithCustomizers( + ApplicationContextRunner contextRunner, ServerBuilder mockServerBuilder, + Class expectedServerFactoryType) { + ServerBuilderCustomizer customizer1 = (serverBuilder) -> serverBuilder.keepAliveTime(40L, TimeUnit.SECONDS); + ServerBuilderCustomizer customizer2 = (serverBuilder) -> serverBuilder.keepAliveTime(50L, TimeUnit.SECONDS); + ServerBuilderCustomizers customizers = new ServerBuilderCustomizers(List.of(customizer1, customizer2)); + contextRunner.withPropertyValues("spring.grpc.server.port=0", "spring.grpc.server.keep-alive.time=30s") + .withBean("serverBuilderCustomizers", ServerBuilderCustomizers.class, () -> customizers) + .run((context) -> assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(expectedServerFactoryType) + .extracting("serverBuilderCustomizers", InstanceOfAssertFactories.list(ServerBuilderCustomizer.class)) + .satisfies((allCustomizers) -> { + allCustomizers.forEach((c) -> c.customize(mockServerBuilder)); + InOrder ordered = inOrder(mockServerBuilder); + ordered.verify(mockServerBuilder) + .keepAliveTime(Duration.ofSeconds(30L).toNanos(), TimeUnit.NANOSECONDS); + ordered.verify(mockServerBuilder).keepAliveTime(40L, TimeUnit.SECONDS); + ordered.verify(mockServerBuilder).keepAliveTime(50L, TimeUnit.SECONDS); + })); + } + + @Test + void nettyServerFactoryAutoConfiguredWithSsl() { + serverFactoryAutoConfiguredAsExpected(this.contextRunner() + .withPropertyValues("spring.grpc.server.ssl.bundle=ssltest", + "spring.ssl.bundle.jks.ssltest.keystore.location=classpath:org/springframework/boot/grpc/server/autoconfigure/test.jks", + "spring.ssl.bundle.jks.ssltest.keystore.password=secret", + "spring.ssl.bundle.jks.ssltest.key.password=password", "spring.grpc.server.host=myhost", + "spring.grpc.server.port=6160") + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)), + NettyGrpcServerFactory.class, "myhost:6160", "nettyGrpcServerLifecycle"); + } + + @Nested + class WithAllFactoriesServiceFilterAutoConfig { + + @Test + void whenNoServiceFilterThenFactoryUsesNoFilter() { + GrpcServerAutoConfigurationTests.this.contextRunner() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .run((context) -> assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(InProcessGrpcServerFactory.class) + .extracting("serviceFilter") + .isNull()); + } + + @Test + void whenUniqueServiceFilterThenFactoryUsesFilter() { + ServerServiceDefinitionFilter serviceFilter = mock(); + GrpcServerAutoConfigurationTests.this.contextRunner() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withBean(ServerServiceDefinitionFilter.class, () -> serviceFilter) + .run((context) -> assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(InProcessGrpcServerFactory.class) + .extracting("serviceFilter") + .isSameAs(serviceFilter)); + } + + @Test + void whenMultipleServiceFiltersThenThrowsException() { + GrpcServerAutoConfigurationTests.this.contextRunnerWithLifecyle() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withBean("filter1", ServerServiceDefinitionFilter.class, Mockito::mock) + .withBean("filter2", ServerServiceDefinitionFilter.class, Mockito::mock) + .run((context) -> assertThat(context).hasFailed() + .getFailure() + .hasMessageContaining("expected single matching bean but found 2: filter1,filter2")); + } + + } + + @Nested + class WithGrpcServiceConfigurerAutoConfig { + + @Test + void whenHasUserDefinedBeanDoesNotAutoConfigureBean() { + GrpcServiceConfigurer customGrpcServiceConfigurer = mock(GrpcServiceConfigurer.class); + GrpcServerAutoConfigurationTests.this.contextRunner() + .withBean("customGrpcServiceConfigurer", GrpcServiceConfigurer.class, () -> customGrpcServiceConfigurer) + .run((context) -> assertThat(context).getBean(GrpcServiceConfigurer.class) + .isSameAs(customGrpcServiceConfigurer)); + } + + @Test + void configurerAutoConfiguredAsExpected() { + GrpcServerAutoConfigurationTests.this.contextRunnerWithLifecyle() + .withPropertyValues("spring.grpc.server.port=0") + .run((context) -> assertThat(context).getBean(GrpcServiceConfigurer.class) + .isInstanceOf(DefaultGrpcServiceConfigurer.class)); + } + + @Test + void whenNoServerInterceptorFilterThenConfigurerUsesNoFilter() { + GrpcServerAutoConfigurationTests.this.contextRunnerWithLifecyle() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .run((context) -> assertThat(context).getBean(InProcessGrpcServerFactory.class) + .extracting("interceptorFilter") + .isNull()); + } + + @Test + void whenUniqueServerInterceptorFilterThenConfigurerUsesFilter() { + ServerInterceptorFilter interceptorFilter = mock(); + GrpcServerAutoConfigurationTests.this.contextRunnerWithLifecyle() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withBean(ServerInterceptorFilter.class, () -> interceptorFilter) + .run((context) -> assertThat(context).getBean(InProcessGrpcServerFactory.class) + .extracting("interceptorFilter") + .isSameAs(interceptorFilter)); + } + + @Test + void whenMultipleServerInterceptorFiltersThenThrowsException() { + GrpcServerAutoConfigurationTests.this.contextRunnerWithLifecyle() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withBean("filter1", ServerInterceptorFilter.class, Mockito::mock) + .withBean("filter2", ServerInterceptorFilter.class, Mockito::mock) + .run((context) -> assertThat(context).hasFailed() + .getFailure() + .hasMessageContaining("expected single matching bean but found 2: filter1,filter2")); + } + + } + + @Configuration(proxyBeanMethods = false) + static class ServerBuilderCustomizersConfig { + + static ServerBuilderCustomizer CUSTOMIZER_FOO = mock(); + + static ServerBuilderCustomizer CUSTOMIZER_BAR = mock(); + + @Bean + @Order(200) + ServerBuilderCustomizer customizerFoo() { + return CUSTOMIZER_FOO; + } + + @Bean + @Order(100) + ServerBuilderCustomizer customizerBar() { + return CUSTOMIZER_BAR; + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java new file mode 100644 index 000000000000..015efde7fb39 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java @@ -0,0 +1,117 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.util.List; +import java.util.Map; + +import io.grpc.ServerInterceptor; +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcServerInterceptor; +import io.micrometer.observation.ObservationRegistry; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.grpc.server.GlobalServerInterceptor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for the {@link GrpcServerObservationAutoConfiguration}. + */ +class GrpcServerObservationAutoConfigurationTests { + + private final ApplicationContextRunner baseContextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerObservationAutoConfiguration.class)); + + private ApplicationContextRunner validContextRunner() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerObservationAutoConfiguration.class)) + .withBean("observationRegistry", ObservationRegistry.class, Mockito::mock); + } + + @Test + void whenObservationRegistryNotOnClasspathAutoConfigSkipped() { + this.validContextRunner() + .withClassLoader(new FilteredClassLoader(ObservationRegistry.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenObservationGrpcServerInterceptorNotOnClasspathAutoConfigSkipped() { + this.validContextRunner() + .withClassLoader(new FilteredClassLoader(ObservationGrpcServerInterceptor.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenObservationRegistryNotProvidedThenAutoConfigSkipped() { + this.baseContextRunner + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenObservationPropertyEnabledThenAutoConfigNotSkipped() { + this.validContextRunner() + .withPropertyValues("spring.grpc.server.observation.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenObservationPropertyDisabledThenAutoConfigIsSkipped() { + this.validContextRunner() + .withPropertyValues("spring.grpc.server.observation.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetFalseThenAutoConfigurationIsSkipped() { + this.validContextRunner() + .withPropertyValues("spring.grpc.server.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() { + this.validContextRunner() + .run((context) -> assertThat(context).hasSingleBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { + this.validContextRunner() + .withPropertyValues("spring.grpc.server.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenAllConditionsAreMetThenInterceptorConfiguredAsExpected() { + this.validContextRunner() + .run((context) -> assertThat(context).hasSingleBean(ObservationGrpcServerInterceptor.class) + .has(new Condition<>((beans) -> { + Map annotated = beans.getBeansWithAnnotation(GlobalServerInterceptor.class); + List interceptors = beans.getBeanProvider(ServerInterceptor.class) + .orderedStream() + .toList(); + return annotated.size() == 2 && interceptors.get(0) instanceof ObservationGrpcServerInterceptor; + }, "Two global interceptors expected"))); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java new file mode 100644 index 000000000000..262d94efc645 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java @@ -0,0 +1,190 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.util.unit.DataSize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link GrpcServerProperties}. + * + * @author Chris Bono + */ +class GrpcServerPropertiesTests { + + private GrpcServerProperties bindProperties(Map map) { + return new Binder(new MapConfigurationPropertySource(map)) + .bind("spring.grpc.server", GrpcServerProperties.class) + .get(); + } + + @Nested + class BaseProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.host", "my-server-ip"); + map.put("spring.grpc.server.port", "3130"); + map.put("spring.grpc.server.shutdown-grace-period", "15"); + GrpcServerProperties properties = bindProperties(map); + assertThat(properties.getAddress()).isEqualTo("my-server-ip:3130"); + assertThat(properties.getPort()).isEqualTo(3130); + assertThat(properties.getShutdownGracePeriod()).isEqualTo(Duration.ofSeconds(15)); + } + + } + + @Nested + class HealthProperties { + + @Test + void bindWithNoSettings() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.host", "my-server-ip"); + GrpcServerProperties.Health properties = bindProperties(map).getHealth(); + assertThat(properties.getEnabled()).isTrue(); + assertThat(properties.getActuator().getEnabled()).isTrue(); + assertThat(properties.getActuator().getHealthIndicatorPaths()).isEmpty(); + assertThat(properties.getActuator().getUpdateOverallHealth()).isTrue(); + assertThat(properties.getActuator().getUpdateRate()).isEqualTo(Duration.ofSeconds(5)); + assertThat(properties.getActuator().getUpdateInitialDelay()).isEqualTo(Duration.ofSeconds(5)); + } + + @Test + void bindWithoutUnitsSpecified() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.health.enabled", "false"); + map.put("spring.grpc.server.health.actuator.enabled", "false"); + map.put("spring.grpc.server.health.actuator.health-indicator-paths", "a,b,c"); + map.put("spring.grpc.server.health.actuator.update-overall-health", "false"); + map.put("spring.grpc.server.health.actuator.update-rate", "2s"); + map.put("spring.grpc.server.health.actuator.update-initial-delay", "1m"); + GrpcServerProperties.Health properties = bindProperties(map).getHealth(); + assertThat(properties.getEnabled()).isFalse(); + assertThat(properties.getActuator().getEnabled()).isFalse(); + assertThat(properties.getActuator().getHealthIndicatorPaths()).containsExactly("a", "b", "c"); + assertThat(properties.getActuator().getUpdateOverallHealth()).isFalse(); + assertThat(properties.getActuator().getUpdateRate()).isEqualTo(Duration.ofSeconds(2)); + assertThat(properties.getActuator().getUpdateInitialDelay()).isEqualTo(Duration.ofMinutes(1)); + } + + } + + @Nested + class KeepAliveProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.keep-alive.time", "45m"); + map.put("spring.grpc.server.keep-alive.timeout", "40s"); + map.put("spring.grpc.server.keep-alive.max-idle", "1h"); + map.put("spring.grpc.server.keep-alive.max-age", "3h"); + map.put("spring.grpc.server.keep-alive.max-age-grace", "21s"); + map.put("spring.grpc.server.keep-alive.permit-time", "33s"); + map.put("spring.grpc.server.keep-alive.permit-without-calls", "true"); + GrpcServerProperties.KeepAlive properties = bindProperties(map).getKeepAlive(); + assertThatPropertiesSetAsExpected(properties); + } + + @Test + void bindWithoutUnitsSpecified() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.keep-alive.time", "2700"); + map.put("spring.grpc.server.keep-alive.timeout", "40"); + map.put("spring.grpc.server.keep-alive.max-idle", "3600"); + map.put("spring.grpc.server.keep-alive.max-age", "10800"); + map.put("spring.grpc.server.keep-alive.max-age-grace", "21"); + map.put("spring.grpc.server.keep-alive.permit-time", "33"); + map.put("spring.grpc.server.keep-alive.permit-without-calls", "true"); + GrpcServerProperties.KeepAlive properties = bindProperties(map).getKeepAlive(); + assertThatPropertiesSetAsExpected(properties); + } + + private void assertThatPropertiesSetAsExpected(GrpcServerProperties.KeepAlive properties) { + assertThat(properties.getTime()).isEqualTo(Duration.ofMinutes(45)); + assertThat(properties.getTimeout()).isEqualTo(Duration.ofSeconds(40)); + assertThat(properties.getMaxIdle()).isEqualTo(Duration.ofHours(1)); + assertThat(properties.getMaxAge()).isEqualTo(Duration.ofHours(3)); + assertThat(properties.getMaxAgeGrace()).isEqualTo(Duration.ofSeconds(21)); + assertThat(properties.getPermitTime()).isEqualTo(Duration.ofSeconds(33)); + assertThat(properties.isPermitWithoutCalls()).isTrue(); + } + + } + + @Nested + class InboundLimitsProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.max-inbound-message-size", "20MB"); + map.put("spring.grpc.server.max-inbound-metadata-size", "1MB"); + GrpcServerProperties properties = bindProperties(map); + assertThat(properties.getMaxInboundMessageSize()).isEqualTo(DataSize.ofMegabytes(20)); + assertThat(properties.getMaxInboundMetadataSize()).isEqualTo(DataSize.ofMegabytes(1)); + } + + @Test + void bindWithoutUnits() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.max-inbound-message-size", "1048576"); + map.put("spring.grpc.server.max-inbound-metadata-size", "1024"); + GrpcServerProperties properties = bindProperties(map); + assertThat(properties.getMaxInboundMessageSize()).isEqualTo(DataSize.ofMegabytes(1)); + assertThat(properties.getMaxInboundMetadataSize()).isEqualTo(DataSize.ofKilobytes(1)); + } + + } + + @Nested + class AddressProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.address", "my-server-ip:3130"); + GrpcServerProperties properties = bindProperties(map); + assertThat(properties.getAddress()).isEqualTo("my-server-ip:3130"); + assertThat(properties.getPort()).isEqualTo(3130); + } + + @Test + void illegalBecauseAddressAndPortSpecified() { + Map map = new HashMap<>(); + map.put("spring.grpc.server.address", "my-server-ip:3130"); + map.put("spring.grpc.server.port", "10000"); + assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindProperties(map)); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfigurationTests.java new file mode 100644 index 000000000000..b8328a81e25e --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfigurationTests.java @@ -0,0 +1,97 @@ +/* + * 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.grpc.server.autoconfigure; + +import io.grpc.BindableService; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GrpcServerReflectionAutoConfiguration}. + * + * @author Haris Zujo + * @author Chris Bono + * @author Andrey Litvitski + */ +class GrpcServerReflectionAutoConfigurationTests { + + private ApplicationContextRunner contextRunner() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerReflectionAutoConfiguration.class)) + .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean(BindableService.class, Mockito::mock); + } + + @Test + void whenAutoConfigurationIsNotSkippedThenCreatesReflectionServiceBean() { + this.contextRunner().run((context) -> assertThat(context).hasBean("serverReflection")); + } + + @Test + void whenNoBindableServiceDefinedThenAutoConfigurationIsSkipped() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerReflectionAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerReflectionAutoConfiguration.class)); + } + + @Test + void whenReflectionEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() { + this.contextRunner() + .run((context) -> assertThat(context).hasSingleBean(GrpcServerReflectionAutoConfiguration.class)); + } + + @Test + void whenReflectionEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.reflection.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcServerReflectionAutoConfiguration.class)); + } + + @Test + void whenReflectionEnabledPropertySetFalseThenAutoConfigurationIsSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.reflection.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerReflectionAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() { + this.contextRunner() + .run((context) -> assertThat(context).hasSingleBean(GrpcServerReflectionAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcServerReflectionAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetFalseThenAutoConfigurationIsSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerReflectionAutoConfiguration.class)); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServletAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServletAutoConfigurationTests.java new file mode 100644 index 000000000000..d66bb67f640c --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServletAutoConfigurationTests.java @@ -0,0 +1,114 @@ +/* + * 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.grpc.server.autoconfigure; + +import io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; +import io.grpc.internal.GrpcUtil; +import io.grpc.servlet.jakarta.GrpcServlet; +import io.grpc.servlet.jakarta.ServletServerBuilder; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration.GrpcServletConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.util.unit.DataSize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link GrpcServerAutoConfiguration}. + * + * @author Chris Bono + * @author Toshiaki Maki + */ +class GrpcServletAutoConfigurationTests { + + private WebApplicationContextRunner contextRunner() { + BindableService service = mock(); + ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); + given(service.bindService()).willReturn(serviceDefinition); + // NOTE: we use noop server lifecycle to avoid startup + return new WebApplicationContextRunner().withConfiguration(AutoConfigurations.of(SslAutoConfiguration.class, + GrpcServerAutoConfiguration.class, GrpcServerFactoryAutoConfiguration.class)) + .withBean(BindableService.class, () -> service); + } + + @Test + void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(BindableService.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServletConfiguration.class) + .doesNotHaveBean(ServletRegistrationBean.class)); + } + + @Test + void whenNoBindableServicesRegisteredAutoConfigurationIsSkipped() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServletConfiguration.class) + .doesNotHaveBean(ServletRegistrationBean.class)); + } + + @Test + void whenGrpcServletNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(GrpcServlet.class)) + .withPropertyValues("spring.grpc.server.port=0") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServletConfiguration.class) + .doesNotHaveBean(ServletRegistrationBean.class)); + } + + @Test + void whenWebApplicationServletIsAutoConfigured() { + this.contextRunner().run((context) -> assertThat(context).getBean(ServletRegistrationBean.class).isNotNull()); + } + + @Test + void whenCustomizerIsRegistered() { + ServerBuilderCustomizer customizer = mock(); + this.contextRunner() + .withBean(ServerBuilderCustomizer.class, () -> customizer) + .run((context) -> then(customizer).should().customize(any(ServletServerBuilder.class))); + } + + @Test + void whenMaxInboundMessageSizeIsSetThenItIsUsed() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.max-inbound-message-size=10KB") + .run((context) -> assertThat(context).getBean(ServletRegistrationBean.class) + .hasFieldOrPropertyWithValue("servlet.servletAdapter.maxInboundMessageSize", + Math.toIntExact(DataSize.ofKilobytes(10).toBytes()))); + } + + @Test + void whenMaxInboundMessageSizeIsNotSetThenDefaultIsUsed() { + this.contextRunner() + .run((context) -> assertThat(context).getBean(ServletRegistrationBean.class) + .hasFieldOrPropertyWithValue("servlet.servletAdapter.maxInboundMessageSize", + GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE)); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizersTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizersTests.java new file mode 100644 index 000000000000..43e762376180 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizersTests.java @@ -0,0 +1,123 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.grpc.ServerBuilder; +import io.grpc.netty.NettyServerBuilder; +import org.junit.jupiter.api.Test; + +import org.springframework.grpc.server.ServerBuilderCustomizer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ServerBuilderCustomizers}. + * + * @author Chris Bono + */ +class ServerBuilderCustomizersTests { + + @Test + void customizeWithNullCustomizersShouldDoNothing() { + ServerBuilder serverBuilder = mock(ServerBuilder.class); + new ServerBuilderCustomizers(null).customize(serverBuilder); + then(serverBuilder).shouldHaveNoInteractions(); + } + + @Test + void customizeSimpleServerBuilder() { + ServerBuilderCustomizers customizers = new ServerBuilderCustomizers( + List.of(new SimpleServerBuilderCustomizer())); + NettyServerBuilder serverBuilder = mock(NettyServerBuilder.class); + customizers.customize(serverBuilder); + then(serverBuilder).should().maxConnectionAge(100L, TimeUnit.SECONDS); + } + + @Test + void customizeShouldCheckGeneric() { + List> list = new ArrayList<>(); + list.add(new TestCustomizer<>()); + list.add(new TestNettyServerBuilderCustomizer()); + list.add(new TestShadedNettyServerBuilderCustomizer()); + ServerBuilderCustomizers customizers = new ServerBuilderCustomizers(list); + + customizers.customize(mock(ServerBuilder.class)); + assertThat(list.get(0).getCount()).isOne(); + assertThat(list.get(1).getCount()).isZero(); + assertThat(list.get(2).getCount()).isZero(); + + customizers.customize(mock(NettyServerBuilder.class)); + assertThat(list.get(0).getCount()).isEqualTo(2); + assertThat(list.get(1).getCount()).isOne(); + assertThat(list.get(2).getCount()).isZero(); + + customizers.customize(mock(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)); + assertThat(list.get(0).getCount()).isEqualTo(3); + assertThat(list.get(1).getCount()).isOne(); + assertThat(list.get(2).getCount()).isOne(); + } + + static class SimpleServerBuilderCustomizer implements ServerBuilderCustomizer { + + @Override + public void customize(NettyServerBuilder serverBuilder) { + serverBuilder.maxConnectionAge(100, TimeUnit.SECONDS); + } + + } + + /** + * Test customizer that will match all {@link ServerBuilderCustomizer}. + */ + static class TestCustomizer> implements ServerBuilderCustomizer { + + private int count; + + @Override + public void customize(T serverBuilder) { + this.count++; + } + + int getCount() { + return this.count; + } + + } + + /** + * Test customizer that will match only {@link NettyServerBuilder}. + */ + static class TestNettyServerBuilderCustomizer extends TestCustomizer { + + } + + /** + * Test customizer that will match only + * {@link io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder}. + */ + static class TestShadedNettyServerBuilderCustomizer + extends TestCustomizer { + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerFactoryPropertyMappersTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerFactoryPropertyMappersTests.java new file mode 100644 index 000000000000..0cf7940cc6f4 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerFactoryPropertyMappersTests.java @@ -0,0 +1,85 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import io.grpc.ServerBuilder; +import org.junit.jupiter.api.Test; + +import org.springframework.util.unit.DataSize; + +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DefaultServerFactoryPropertyMapper}, + * {@link NettyServerFactoryPropertyMapper}, and + * {@link ShadedNettyServerFactoryPropertyMapper}. + * + * @author Chris Bono + */ +class ServerFactoryPropertyMappersTests { + + @Test + void customizeShadedNettyServerBuilder() { + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder builder = mock(); + customizeServerBuilder(ShadedNettyServerFactoryPropertyMapper::new, () -> builder); + } + + @Test + void customizeNettyServerBuilder() { + io.grpc.netty.NettyServerBuilder builder = mock(); + customizeServerBuilder(NettyServerFactoryPropertyMapper::new, () -> builder); + } + + @Test + > void customizeBaseServerBuilder() { + T builder = mock(); + customizeServerBuilder(DefaultServerFactoryPropertyMapper::new, () -> builder); + } + + private , X extends DefaultServerFactoryPropertyMapper> void customizeServerBuilder( + Function mapperFactory, Supplier mockBuilderToCustomize) { + GrpcServerProperties properties = new GrpcServerProperties(); + properties.getKeepAlive().setTime(Duration.ofHours(1)); + properties.getKeepAlive().setTimeout(Duration.ofSeconds(10)); + properties.getKeepAlive().setMaxIdle(Duration.ofHours(2)); + properties.getKeepAlive().setMaxAge(Duration.ofHours(3)); + properties.getKeepAlive().setMaxAgeGrace(Duration.ofSeconds(45)); + properties.getKeepAlive().setPermitTime(Duration.ofMinutes(7)); + properties.getKeepAlive().setPermitWithoutCalls(true); + properties.setMaxInboundMessageSize(DataSize.ofMegabytes(333)); + properties.setMaxInboundMetadataSize(DataSize.ofKilobytes(111)); + X mapper = mapperFactory.apply(properties); + T builder = mockBuilderToCustomize.get(); + mapper.customizeServerBuilder(builder); + then(builder).should().keepAliveTime(Duration.ofHours(1).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should().keepAliveTimeout(Duration.ofSeconds(10).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should().maxConnectionIdle(Duration.ofHours(2).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should().maxConnectionAge(Duration.ofHours(3).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should().maxConnectionAgeGrace(Duration.ofSeconds(45).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should().permitKeepAliveTime(Duration.ofMinutes(7).toNanos(), TimeUnit.NANOSECONDS); + then(builder).should().permitKeepAliveWithoutCalls(true); + then(builder).should().maxInboundMessageSize(Math.toIntExact(DataSize.ofMegabytes(333).toBytes())); + then(builder).should().maxInboundMetadataSize(Math.toIntExact(DataSize.ofKilobytes(111).toBytes())); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfigurationTests.java new file mode 100644 index 000000000000..c2338d5974c7 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfigurationTests.java @@ -0,0 +1,48 @@ +/* + * 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.grpc.server.autoconfigure.codec; + +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GrpcCodecConfiguration}. + * + * @author Andrei Lisa + */ +class GrpcCodecConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcCodecConfiguration.class)); + + @Test + void testCompressorRegistryBean() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(CompressorRegistry.class)); + } + + @Test + void testDecompressorRegistryBean() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(DecompressorRegistry.class)); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfigurationTests.java new file mode 100644 index 000000000000..efcfdf985bd6 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfigurationTests.java @@ -0,0 +1,128 @@ +/* + * 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.grpc.server.autoconfigure.exception; + +import io.grpc.Grpc; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.exception.GrpcExceptionHandler; +import org.springframework.grpc.server.exception.GrpcExceptionHandlerInterceptor; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GrpcExceptionHandlerAutoConfiguration}. + * + * @author Chris Bono + */ +class GrpcExceptionHandlerAutoConfigurationTests { + + private ApplicationContextRunner contextRunner() { + // NOTE: we use noop server lifecycle to avoid startup + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcExceptionHandlerAutoConfiguration.class)) + .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean("mockGrpcExceptionHandler", GrpcExceptionHandler.class, Mockito::mock); + } + + @Test + void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(Grpc.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcExceptionHandlerAutoConfiguration.class)); + } + + @Test + void whenSprimgGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcExceptionHandlerAutoConfiguration.class)); + } + + @Test + void whenNoGrpcExceptionHandlerRegisteredAutoConfigurationIsSkipped() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcExceptionHandlerAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcExceptionHandlerAutoConfiguration.class)); + } + + @Test + void whenExceptionHandlerPropertyNotSetExceptionHandlerIsAutoConfigured() { + this.contextRunner() + .run((context) -> assertThat(context).hasSingleBean(GrpcExceptionHandlerAutoConfiguration.class)); + } + + @Test + void whenExceptionHandlerPropertyIsTrueExceptionHandlerIsAutoConfigured() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.exception-handler.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcExceptionHandlerAutoConfiguration.class)); + } + + @Test + void whenExceptionHandlerPropertyIsFalseAutoConfigurationIsSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.exception-handler.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcExceptionHandlerAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetFalseThenAutoConfigurationIsSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcExceptionHandlerAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() { + this.contextRunner() + .run((context) -> assertThat(context).hasSingleBean(GrpcExceptionHandlerAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcExceptionHandlerAutoConfiguration.class)); + } + + @Test + void whenHasUserDefinedGrpcExceptionHandlerInterceptorDoesNotAutoConfigureBean() { + GrpcExceptionHandlerInterceptor customInterceptor = Mockito.mock(); + this.contextRunner() + .withBean("customInterceptor", GrpcExceptionHandlerInterceptor.class, () -> customInterceptor) + .run((context) -> assertThat(context).getBean(GrpcExceptionHandlerInterceptor.class) + .isSameAs(customInterceptor)); + } + + @Test + void exceptionHandlerInterceptorAutoConfiguredAsExpected() { + this.contextRunner() + .run((context) -> assertThat(context).getBean(GrpcExceptionHandlerInterceptor.class) + .extracting("exceptionHandler.exceptionHandlers", + InstanceOfAssertFactories.array(GrpcExceptionHandler[].class)) + .containsExactly(context.getBean(GrpcExceptionHandler.class))); + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvokerTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvokerTests.java new file mode 100644 index 000000000000..c905f9613f76 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvokerTests.java @@ -0,0 +1,52 @@ +/* + * 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.grpc.server.autoconfigure.health; + +import java.time.Duration; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder; + +import static org.mockito.BDDMockito.atLeast; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ActuatorHealthAdapterInvoker}. + */ +class ActuatorHealthAdapterInvokerTests { + + @Test + void healthAdapterInvokedOnSchedule() { + ActuatorHealthAdapter healthAdapter = mock(); + ActuatorHealthAdapterInvoker invoker = new ActuatorHealthAdapterInvoker(healthAdapter, + new SimpleAsyncTaskSchedulerBuilder(), Duration.ofSeconds(5), Duration.ofSeconds(3)); + try { + invoker.afterPropertiesSet(); + Awaitility.await() + .between(Duration.ofSeconds(6), Duration.ofSeconds(12)) + .untilAsserted(() -> then(healthAdapter).should(atLeast(2)).updateHealthStatus()); + } + finally { + invoker.destroy(); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterTests.java new file mode 100644 index 000000000000..498688838a73 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterTests.java @@ -0,0 +1,155 @@ +/* + * 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.grpc.server.autoconfigure.health; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import io.grpc.protobuf.services.HealthStatusManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.actuate.health.HealthDescriptor; +import org.springframework.boot.actuate.health.HealthEndpoint; +import org.springframework.boot.actuate.health.StatusAggregator; +import org.springframework.boot.health.contributor.Status; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.never; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ActuatorHealthAdapter}. + */ +class ActuatorHealthAdapterTests { + + private HealthStatusManager mockHealthStatusManager; + + private HealthEndpoint mockHealthEndpoint; + + private StatusAggregator mockStatusAggregator; + + @BeforeEach + void prepareMocks() { + this.mockHealthStatusManager = mock(); + this.mockHealthEndpoint = mock(); + this.mockStatusAggregator = mock(); + } + + @Disabled("TODO figure out how to mock HealthDescriptor") + @Test + void whenIndicatorPathsFoundStatusIsUpdated() { + var service1 = "check1"; + var service2 = "component2/check2"; + var service3 = "component3a/component3b/check3"; + given(this.mockHealthEndpoint.healthForPath("check1")).willReturn(healthOf(Status.UP)); + given(this.mockHealthEndpoint.healthForPath("component2", "check2")).willReturn(healthOf(Status.DOWN)); + given(this.mockHealthEndpoint.healthForPath("component3a", "component3b", "check3")) + .willReturn(healthOf(Status.UNKNOWN)); + given(this.mockStatusAggregator.getAggregateStatus(anySet())).willReturn(Status.UNKNOWN); + var healthAdapter = new ActuatorHealthAdapter(this.mockHealthStatusManager, this.mockHealthEndpoint, + this.mockStatusAggregator, true, List.of(service1, service2, service3)); + healthAdapter.updateHealthStatus(); + then(this.mockHealthStatusManager).should().setStatus(service1, ServingStatus.SERVING); + then(this.mockHealthStatusManager).should().setStatus(service2, ServingStatus.NOT_SERVING); + then(this.mockHealthStatusManager).should().setStatus(service3, ServingStatus.UNKNOWN); + ArgumentCaptor> statusesArgCaptor = ArgumentCaptor.captor(); + then(this.mockStatusAggregator).should().getAggregateStatus(statusesArgCaptor.capture()); + assertThat(statusesArgCaptor.getValue()) + .containsExactlyInAnyOrderElementsOf(Set.of(Status.UP, Status.DOWN, Status.UNKNOWN)); + then(this.mockHealthStatusManager).should().setStatus("", ServingStatus.UNKNOWN); + } + + @Disabled("TODO figure out how to mock HealthDescriptor") + @Test + void whenOverallHealthIsFalseOverallStatusIsNotUpdated() { + var service1 = "check1"; + given(this.mockHealthEndpoint.healthForPath("check1")).willReturn(healthOf(Status.UP)); + var healthAdapter = new ActuatorHealthAdapter(this.mockHealthStatusManager, this.mockHealthEndpoint, + this.mockStatusAggregator, false, List.of(service1)); + healthAdapter.updateHealthStatus(); + then(this.mockStatusAggregator).shouldHaveNoInteractions(); + then(this.mockHealthStatusManager).should(never()).setStatus(eq(""), any(ServingStatus.class)); + } + + @Disabled("TODO figure out how to mock HealthDescriptor") + @Test + void whenIndicatorPathNotFoundStatusIsNotUpdated() { + var healthAdapter = new ActuatorHealthAdapter(this.mockHealthStatusManager, this.mockHealthEndpoint, + this.mockStatusAggregator, false, List.of("check1")); + healthAdapter.updateHealthStatus(); + then(this.mockHealthStatusManager).shouldHaveNoInteractions(); + } + + @Disabled("TODO figure out how to mock HealthDescriptor") + @Test + void whenNoIndicatorPathsSpecifiedThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new ActuatorHealthAdapter(this.mockHealthStatusManager, this.mockHealthEndpoint, + this.mockStatusAggregator, false, Collections.emptyList())) + .withMessage("at least one health indicator path is required"); + } + + private HealthDescriptor healthOf(Status status) { + HealthDescriptor healthDescriptor = mock(); + given(healthDescriptor.getStatus()).willReturn(status); + return healthDescriptor; + } + + @Nested + class ToServingStatusApi { + + private final ActuatorHealthAdapter healthAdapter = new ActuatorHealthAdapter( + ActuatorHealthAdapterTests.this.mockHealthStatusManager, + ActuatorHealthAdapterTests.this.mockHealthEndpoint, + ActuatorHealthAdapterTests.this.mockStatusAggregator, false, List.of("check1")); + + @Test + void whenActuatorStatusIsUpThenServingStatusIsUp() { + assertThat(this.healthAdapter.toServingStatus(Status.UP.getCode())).isEqualTo(ServingStatus.SERVING); + } + + @Test + void whenActuatorStatusIsUnknownThenServingStatusIsUnknown() { + assertThat(this.healthAdapter.toServingStatus(Status.UNKNOWN.getCode())).isEqualTo(ServingStatus.UNKNOWN); + } + + @Test + void whenActuatorStatusIsDownThenServingStatusIsNotServing() { + assertThat(this.healthAdapter.toServingStatus(Status.DOWN.getCode())).isEqualTo(ServingStatus.NOT_SERVING); + } + + @Test + void whenActuatorStatusIsOutOfServiceThenServingStatusIsNotServing() { + assertThat(this.healthAdapter.toServingStatus(Status.OUT_OF_SERVICE.getCode())) + .isEqualTo(ServingStatus.NOT_SERVING); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java new file mode 100644 index 000000000000..31446ca114d0 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java @@ -0,0 +1,282 @@ +/* + * 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.grpc.server.autoconfigure.health; + +import java.util.Arrays; + +import io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; +import io.grpc.protobuf.services.HealthStatusManager; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; +import org.springframework.boot.actuate.health.HealthEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.ServerBuilderCustomizers; +import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration.ActuatorHealthAdapterConfiguration; +import org.springframework.boot.health.autoconfigure.contributor.HealthContributorAutoConfiguration; +import org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link GrpcServerHealthAutoConfiguration}. + * + * @author Chris Bono + * @author Andrey Litvitski + */ +class GrpcServerHealthAutoConfigurationTests { + + private ApplicationContextRunner contextRunner() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerHealthAutoConfiguration.class)) + .withBean(BindableService.class, Mockito::mock); + } + + @Test + void whenAutoConfigurationIsNotSkippedThenCreatesDefaultBeans() { + this.contextRunner() + .run((context) -> assertThat(context).hasSingleBean(HealthStatusManager.class) + .hasBean("grpcHealthService")); + } + + @Test + void whenNoBindableServiceDefinedDoesNotAutoConfigureBean() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(GrpcServerHealthAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerHealthAutoConfiguration.class)); + } + + @Test + void whenHealthStatusManagerNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(HealthStatusManager.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerHealthAutoConfiguration.class)); + } + + @Test + void whenHealthPropertyNotSetHealthIsAutoConfigured() { + this.contextRunner() + .run((context) -> assertThat(context).hasSingleBean(GrpcServerHealthAutoConfiguration.class)); + } + + @Test + void whenHealthPropertyIsTrueHealthIsAutoConfigured() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.health.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcServerHealthAutoConfiguration.class)); + } + + @Test + void whenHealthPropertyIsFalseAutoConfigurationIsSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.health.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerHealthAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetFalseThenAutoConfigurationIsSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerHealthAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() { + this.contextRunner() + .run((context) -> assertThat(context).hasSingleBean(GrpcServerHealthAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcServerHealthAutoConfiguration.class)); + } + + @Test + void healthIsAutoConfiguredBeforeGrpcServerFactory() { + BindableService service = mock(); + ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); + given(service.bindService()).willReturn(serviceDefinition); + this.contextRunner() + .withConfiguration(AutoConfigurations.of(GrpcServerFactoryAutoConfiguration.class)) + .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean("serverBuilderCustomizers", ServerBuilderCustomizers.class, Mockito::mock) + .withBean("grpcServicesDiscoverer", GrpcServiceDiscoverer.class, Mockito::mock) + .withBean("grpcServiceConfigurer", GrpcServiceConfigurer.class, Mockito::mock) + .withBean("sslBundles", SslBundles.class, Mockito::mock) + .withPropertyValues("spring.grpc.server.port=0") + .run((context) -> assertThatBeanDefinitionsContainInOrder(context, GrpcServerHealthAutoConfiguration.class, + GrpcServerFactoryAutoConfiguration.class)); + } + + @Disabled("Will be tested in an integration test once the Actuator adapter is implemented") + @Test + void enterTerminalStateIsCalledWhenStatusManagerIsStopped() { + } + + @Test + void whenHasUserDefinedHealthStatusManagerDoesNotAutoConfigureBean() { + HealthStatusManager customHealthStatusManager = mock(); + this.contextRunner() + .withBean("customHealthStatusManager", HealthStatusManager.class, () -> customHealthStatusManager) + .withPropertyValues("spring.grpc.server.health.enabled=false") + .run((context) -> assertThat(context).getBean(HealthStatusManager.class) + .isSameAs(customHealthStatusManager)); + } + + @Test + void healthStatusManagerAutoConfiguredAsExpected() { + this.contextRunner().run((context) -> { + assertThat(context).hasSingleBean(GrpcServerHealthAutoConfiguration.class); + assertThat(context).hasSingleBean(HealthStatusManager.class); + assertThat(context).getBean("grpcHealthService", BindableService.class).isNotNull(); + }); + } + + private void assertThatBeanDefinitionsContainInOrder(ConfigurableApplicationContext context, + Class... configClasses) { + var configBeanDefNames = Arrays.stream(configClasses).map(this::beanDefinitionNameForConfigClass).toList(); + var filteredBeanDefNames = Arrays.stream(context.getBeanDefinitionNames()) + .filter(configBeanDefNames::contains) + .toList(); + assertThat(filteredBeanDefNames).containsExactlyElementsOf(configBeanDefNames); + } + + private String beanDefinitionNameForConfigClass(Class configClass) { + var fullName = configClass.getName(); + return StringUtils.uncapitalize(fullName); + } + + @Nested + class ActuatorHealthAdapterConfigurationTests { + + private ApplicationContextRunner validContextRunner() { + return GrpcServerHealthAutoConfigurationTests.this.contextRunner() + .withPropertyValues("spring.grpc.server.health.actuator.health-indicator-paths=my-indicator") + .withConfiguration(AutoConfigurations.of(HealthEndpointAutoConfiguration.class, + HealthContributorRegistryAutoConfiguration.class, HealthContributorAutoConfiguration.class, + TaskSchedulingAutoConfiguration.class)); + } + + @Test + void adapterIsAutoConfiguredAfterHealthAutoConfiguration() { + this.validContextRunner() + .run((context) -> assertThatBeanDefinitionsContainInOrder(context, + HealthEndpointAutoConfiguration.class, ActuatorHealthAdapterConfiguration.class)); + } + + @Test + void adapterIsAutoConfiguredAfterTaskSchedulingAutoConfiguration() { + this.validContextRunner() + .run((context) -> assertThatBeanDefinitionsContainInOrder(context, + TaskSchedulingAutoConfiguration.class, ActuatorHealthAdapterConfiguration.class)); + } + + @Test + void whenHealthEndpointNotOnClasspathAutoConfigurationIsSkipped() { + GrpcServerHealthAutoConfigurationTests.this.contextRunner() + .withConfiguration(AutoConfigurations.of(TaskSchedulingAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(HealthEndpoint.class)) + .run((context) -> assertThat(context) + .doesNotHaveBean(GrpcServerHealthAutoConfiguration.ActuatorHealthAdapterConfiguration.class)); + } + + @Test + void whenHealthEndpointNotAvailableAutoConfigurationIsSkipped() { + this.validContextRunner() + .withPropertyValues("management.endpoint.health.enabled=false") + .withConfiguration(AutoConfigurations.of(TaskSchedulingAutoConfiguration.class)) + .run((context) -> assertThat(context) + .doesNotHaveBean(GrpcServerHealthAutoConfiguration.ActuatorHealthAdapterConfiguration.class)); + } + + @Test + void whenActuatorPropertyNotSetAdapterIsAutoConfigured() { + this.validContextRunner() + .run((context) -> assertThat(context) + .hasSingleBean(GrpcServerHealthAutoConfiguration.ActuatorHealthAdapterConfiguration.class)); + } + + @Test + void whenActuatorPropertyIsTrueAdapterIsAutoConfigured() { + this.validContextRunner() + .withPropertyValues("spring.grpc.server.health.actuator.enabled=true") + .run((context) -> assertThat(context) + .hasSingleBean(GrpcServerHealthAutoConfiguration.ActuatorHealthAdapterConfiguration.class)); + } + + @Test + void whenActuatorPropertyIsFalseAdapterIsNotAutoConfigured() { + this.validContextRunner() + .withPropertyValues("spring.grpc.server.health.actuator.enabled=false") + .run((context) -> assertThat(context) + .doesNotHaveBean(GrpcServerHealthAutoConfiguration.ActuatorHealthAdapterConfiguration.class)); + } + + @Test + void whenHealthIndicatorPathsIsNotSpecifiedAdapterIsNotAutoConfigured() { + GrpcServerHealthAutoConfigurationTests.this.contextRunner() + .withConfiguration(AutoConfigurations.of(HealthEndpointAutoConfiguration.class, + TaskSchedulingAutoConfiguration.class)) + .run((context) -> assertThat(context) + .doesNotHaveBean(GrpcServerHealthAutoConfiguration.ActuatorHealthAdapterConfiguration.class)); + } + + @Test + void whenHealthIndicatorPathsIsSpecifiedEmptyAdapterIsNotAutoConfigured() { + GrpcServerHealthAutoConfigurationTests.this.contextRunner() + .withPropertyValues("spring.grpc.server.health.actuator.health-indicator-paths=") + .withConfiguration(AutoConfigurations.of(HealthEndpointAutoConfiguration.class, + TaskSchedulingAutoConfiguration.class)) + .run((context) -> assertThat(context) + .doesNotHaveBean(GrpcServerHealthAutoConfiguration.ActuatorHealthAdapterConfiguration.class)); + } + + @Test + void whenHasUserDefinedAdapterDoesNotAutoConfigureBean() { + ActuatorHealthAdapter customAdapter = mock(); + this.validContextRunner() + .withBean("customAdapter", ActuatorHealthAdapter.class, () -> customAdapter) + .run((context) -> assertThat(context).getBean(ActuatorHealthAdapter.class).isSameAs(customAdapter)); + } + + @Test + void adapterAutoConfiguredAsExpected() { + this.validContextRunner() + .run((context) -> assertThat(context).hasSingleBean(ActuatorHealthAdapter.class) + .hasSingleBean(ActuatorHealthAdapterInvoker.class)); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequestTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequestTests.java new file mode 100644 index 000000000000..cb21b2154e0b --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequestTests.java @@ -0,0 +1,87 @@ +/* + * 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.grpc.server.autoconfigure.security; + +import io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.grpc.server.autoconfigure.security.GrpcReactiveRequest.GrpcReactiveRequestMatcher; +import org.springframework.context.ApplicationContext; +import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.MockServerHttpResponse; +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver; +import org.springframework.web.server.session.DefaultWebSessionManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +class GrpcReactiveRequestTests { + + private StaticWebApplicationContext context = new StaticWebApplicationContext(); + + @BeforeEach + void setup() { + MockService service = mock(); + ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); + given(service.bindService()).willReturn(serviceDefinition); + this.context.registerBean(BindableService.class, () -> service); + this.context.registerBean(GrpcServiceDiscoverer.class, () -> new DefaultGrpcServiceDiscoverer(this.context)); + } + + @Test + void requestMatches() { + GrpcReactiveRequestMatcher matcher = GrpcReactiveRequest.all(); + MockExchange request = mockRequest("/my-service/Method"); + assertThat(matcher.matches(request).block().isMatch()).isTrue(); + } + + private MockExchange mockRequest(String path) { + MockServerHttpRequest servletContext = MockServerHttpRequest.get(path).build(); + MockExchange request = new MockExchange(servletContext, this.context); + return request; + } + + interface MockService extends BindableService { + + } + + static class MockExchange extends DefaultServerWebExchange { + + private ApplicationContext context; + + MockExchange(MockServerHttpRequest request, ApplicationContext context) { + super(request, new MockServerHttpResponse(), new DefaultWebSessionManager(), ServerCodecConfigurer.create(), + new AcceptHeaderLocaleContextResolver()); + this.context = context; + } + + @Override + public ApplicationContext getApplicationContext() { + return this.context; + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfigurationTests.java new file mode 100644 index 000000000000..ea091ff10a51 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfigurationTests.java @@ -0,0 +1,118 @@ +/* + * 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.grpc.server.autoconfigure.security; + +import io.grpc.BindableService; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.exception.GrpcExceptionHandler; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler; +import org.springframework.security.config.ObjectPostProcessor; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link GrpcServerAutoConfiguration}. + * + * @author Chris Bono + */ +class GrpcSecurityAutoConfigurationTests { + + private ApplicationContextRunner contextRunner() { + // NOTE: we use noop server lifecycle to avoid startup + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcSecurityAutoConfiguration.class)) + .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock); + } + + @Test + void whenSpringSecurityNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(ObjectPostProcessor.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcSecurityAutoConfiguration.class)); + } + + @Test + void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(BindableService.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcSecurityAutoConfiguration.class)); + } + + @Test + void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcSecurityAutoConfiguration.class)); + } + + @Test + void whenSpringGrpcAndSpringSecurityPresentGrpcSecurityIsCreated() { + new ApplicationContextRunner() + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .withConfiguration(AutoConfigurations.of(GrpcSecurityAutoConfiguration.class)) + .withUserConfiguration(ExtraConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(GrpcSecurity.class)); + } + + @Test + void whenServerEnabledPropertySetFalseThenAutoConfigurationIsSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(GrpcSecurityAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() { + this.contextRunner().run((context) -> assertThat(context).hasSingleBean(GrpcSecurityAutoConfiguration.class)); + } + + @Test + void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(GrpcSecurityAutoConfiguration.class)); + } + + @Test + void grpcSecurityAutoConfiguredAsExpected() { + this.contextRunner().run((context) -> { + assertThat(context).getBean(GrpcExceptionHandler.class).isInstanceOf(SecurityGrpcExceptionHandler.class); + assertThat(context).getBean(AuthenticationProcessInterceptor.class).isNull(); + }); + } + + @EnableMethodSecurity + @Configuration(proxyBeanMethods = false) + static class ExtraConfiguration { + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequestTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequestTests.java new file mode 100644 index 000000000000..28131ce87baf --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequestTests.java @@ -0,0 +1,98 @@ +/* + * 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.grpc.server.autoconfigure.security; + +import io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServletRequest.GrpcServletRequestMatcher; +import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.StaticWebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +class GrpcServletRequestTests { + + private StaticWebApplicationContext context = new StaticWebApplicationContext(); + + @BeforeEach + void setup() { + MockService service = mock(); + ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); + given(service.bindService()).willReturn(serviceDefinition); + this.context.registerBean(BindableService.class, () -> service); + this.context.registerBean(GrpcServiceDiscoverer.class, () -> new DefaultGrpcServiceDiscoverer(this.context)); + } + + @Test + void requestMatches() { + GrpcServletRequestMatcher matcher = GrpcServletRequest.all(); + MockHttpServletRequest request = mockRequest("/my-service/Method"); + assertThat(matcher.matches(request)).isTrue(); + } + + @Test + void noMatch() { + GrpcServletRequestMatcher matcher = GrpcServletRequest.all(); + MockHttpServletRequest request = mockRequest("/other-service/Method"); + assertThat(matcher.matches(request)).isFalse(); + } + + @Test + void requestMatcherExcludes() { + GrpcServletRequestMatcher matcher = GrpcServletRequest.all().excluding("my-service"); + MockHttpServletRequest request = mockRequest("/my-service/Method"); + assertThat(matcher.matches(request)).isFalse(); + } + + @Test + void noServices() { + GrpcServletRequestMatcher matcher = GrpcServletRequest.all(); + MockHttpServletRequest request = mockRequestNoServices("/my-service/Method"); + assertThat(matcher.matches(request)).isFalse(); + } + + private MockHttpServletRequest mockRequestNoServices(String path) { + MockServletContext servletContext = new MockServletContext(); + servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, + new StaticWebApplicationContext()); + MockHttpServletRequest request = new MockHttpServletRequest(servletContext); + request.setPathInfo(path); + return request; + } + + private MockHttpServletRequest mockRequest(String path) { + MockServletContext servletContext = new MockServletContext(); + servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); + MockHttpServletRequest request = new MockHttpServletRequest(servletContext); + request.setRequestURI(path); + return request; + } + + interface MockService extends BindableService { + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfigurationTests.java new file mode 100644 index 000000000000..dfa0ffd5f1f3 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfigurationTests.java @@ -0,0 +1,175 @@ +/* + * 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.grpc.server.autoconfigure.security; + +import io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.context.annotation.UserConfigurations; +import org.springframework.boot.context.event.ApplicationFailedEvent; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.security.autoconfigure.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.context.servlet.AnnotationConfigServletWebApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.security.config.Customizer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link GrpcServerAutoConfiguration}. + * + * @author Chris Bono + */ +class OAuth2ResourceServerAutoConfigurationTests { + + private BindableService service = mock(); + + { + ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); + given(this.service.bindService()).willReturn(serviceDefinition); + + } + + private ApplicationContextRunner contextRunner() { + // NOTE: we use noop server lifecycle to avoid startup + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OAuth2ResourceServerAutoConfiguration.class, + GrpcSecurityAutoConfiguration.class)) + .withBean(BindableService.class, () -> this.service) + .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock); + } + + @Disabled("TODO fix GrpcSecurity mismatch w/ Spring Security 6/7") + @Test + void jwtConfiguredWhenIssuerIsProvided() { + this.contextRunner() + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000") + .run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class)); + } + + @Disabled("TODO fix GrpcSecurity mismatch w/ Spring Security 6/7") + @Test + void jwtConfiguredWhenJwkSetIsProvided() { + this.contextRunner() + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000") + .run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class)); + } + + @Disabled("TODO fix GrpcSecurity mismatch w/ Spring Security 6/7") + @Test + void customInterceptorWhenJwkSetIsProvided() { + this.contextRunner() + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .withConfiguration(UserConfigurations.of(CustomInterceptorConfiguration.class)) + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000") + .run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class)); + } + + @Test + void notConfiguredWhenIssuerNotProvided() { + this.contextRunner() + .run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class)); + } + + @Test + void notConfiguredInWebApplication() { + new WebApplicationContextRunner().withConfiguration(AutoConfigurations.of( + GrpcServerFactoryAutoConfiguration.class, GrpcServerAutoConfiguration.class, + SecurityAutoConfiguration.class, + org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration.class, + OAuth2ResourceServerAutoConfiguration.class, GrpcSecurityAutoConfiguration.class)) + .withBean(BindableService.class, () -> this.service) + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000") + .run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class)); + } + + @Test + void notConfiguredInWebApplicationWithNoBindableService() { + new WebApplicationContextRunner(WebApplicationContextRunner.withMockServletContext(MyContext::new)) + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .withConfiguration(AutoConfigurations.of(GrpcServerFactoryAutoConfiguration.class, + GrpcServerAutoConfiguration.class, SecurityAutoConfiguration.class, + org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration.class, + OAuth2ResourceServerAutoConfiguration.class, GrpcSecurityAutoConfiguration.class)) + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000") + .run((context) -> assertThat(context).doesNotHaveBean(AuthenticationProcessInterceptor.class)); + } + + // @Disabled("TODO fix GrpcSecurity mismatch w/ Spring Security 6/7") + @Test + void configuredInWebApplicationWithGrpcNative() { + new WebApplicationContextRunner(WebApplicationContextRunner.withMockServletContext(MyContext::new)) + .withConfiguration(AutoConfigurations.of(GrpcServerFactoryAutoConfiguration.class, + GrpcServerAutoConfiguration.class, SslAutoConfiguration.class, SecurityAutoConfiguration.class, + org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration.class, + OAuth2ResourceServerAutoConfiguration.class, GrpcSecurityAutoConfiguration.class)) + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .withBean(BindableService.class, () -> this.service) + .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000", + "spring.grpc.server.servlet.enabled=false", "spring.grpc.server.port=0") + .run((context) -> assertThat(context).hasSingleBean(AuthenticationProcessInterceptor.class)); + } + + // Utility class to ensure ApplicationFailedEvent is published + static class MyContext extends AnnotationConfigServletWebApplicationContext { + + @Override + public void refresh() { + try { + super.refresh(); + } + catch (Throwable ex) { + publishEvent(new ApplicationFailedEvent(new SpringApplication(this), new String[0], this, ex)); + throw ex; + } + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomInterceptorConfiguration { + + @Bean + @GlobalServerInterceptor + AuthenticationProcessInterceptor jwtSecurityFilterChain(GrpcSecurity grpc) throws Exception { + return grpc.authorizeRequests((requests) -> requests.allRequests().authenticated()) + .oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults())) + .build(); + } + + } + +} diff --git a/module/spring-boot-grpc-server/src/test/resources/logback-test.xml b/module/spring-boot-grpc-server/src/test/resources/logback-test.xml new file mode 100644 index 000000000000..b8a41480d7d6 --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/resources/logback-test.xml @@ -0,0 +1,4 @@ + + + + diff --git a/module/spring-boot-grpc-server/src/test/resources/org/springframework/boot/grpc/server/autoconfigure/test.jks b/module/spring-boot-grpc-server/src/test/resources/org/springframework/boot/grpc/server/autoconfigure/test.jks new file mode 100644 index 0000000000000000000000000000000000000000..0fc3e802f75461dd074facb9611d350db4d5960f GIT binary patch literal 1276 zcmezO_TO6u1_mZ5W@O+hNi8nXP0YzmEM{O}OjTL=XFE`?-k{cikBv*4jgf^>i%F1? zk(GfZ`?F{4vBFug6<%MmmXJ+)mEgQoslXV6!5_H^yana6V1?kw1w zbE0Bi&GHlLHzfosgjrwL)qKcc5I;l0LF?W2lpQg%-cHp$l(#o)?Jkath1@gQN{hG8 zig@wKv#0R7vd_QC=JG%%Ffy=4=$RT=0v*d`(8R=M(8RcU0W%XL6BCP-)w&Y~JZv0V zZ64=rS(uqv84M~6g$xAPm_u3EggJBalM{0?@{3DgVjNh+*s+LlVG-lTBF2m)W*{fd zYiMC$VQ64zW@K(?5e4L0B5?=MWswHLZ0z7LVq$~_7BeF|vl9agPmO-znfkD()@R+bGrT$(AXmN24#zv*XRX z#$UNu(Lmln78u;Jd@N!tBKmU@J0!OJc3G%!N>OO@P1n+F-CorAVRmOQaA8six!iWP z)M3lXpnJ*TI=kIlH(Yxia-ls?xvctEx&P5B6()tKm`>%bo~@fX9{j%TtMU1G!|pw& zZ6BRjIqQ^`bIxR@OmMno&8^H%tpq36Esh&T(+MJ_la+#pV>+3scS%> customizers, + @Nullable ServerServiceDefinitionFilter serviceFilter) { + var factory = new TestInProcessGrpcServerFactory(this.address, customizers, serviceFilter); + serviceDiscoverer.findServices() + .stream() + .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, factory)) + .forEach(factory::addService); + return factory; + } + + @Bean + @ConditionalOnClass({ AbstractStub.class, GrpcChannelFactory.class }) + @Order(Ordered.HIGHEST_PRECEDENCE) + TestInProcessGrpcChannelFactory testInProcessGrpcChannelFactory( + ClientInterceptorsConfigurer interceptorsConfigurer) { + return new TestInProcessGrpcChannelFactory(this.address, interceptorsConfigurer); + } + + @Bean(name = "inProcessGrpcServerLifecycle") + @ConditionalOnBean(InProcessGrpcServerFactory.class) + @Order(Ordered.HIGHEST_PRECEDENCE) + GrpcServerLifecycle inProcessGrpcServerLifecycle(InProcessGrpcServerFactory factory, + ApplicationEventPublisher eventPublisher) { + return new GrpcServerLifecycle(factory, Duration.ofSeconds(30), eventPublisher); + } + + /** + * Specialization of {@link InProcessGrpcServerFactory}. + */ + public static class TestInProcessGrpcServerFactory extends InProcessGrpcServerFactory { + + public TestInProcessGrpcServerFactory(String address, + List> serverBuilderCustomizers, + @Nullable ServerServiceDefinitionFilter serviceFilter) { + super(address, serverBuilderCustomizers); + setServiceFilter(serviceFilter); + } + + } + + /** + * Specialization of {@link InProcessGrpcChannelFactory} that allows the channel + * factory to support all targets, not just those that start with 'in-process:'. + */ + public static class TestInProcessGrpcChannelFactory extends InProcessGrpcChannelFactory { + + TestInProcessGrpcChannelFactory(String address, ClientInterceptorsConfigurer interceptorsConfigurer) { + super(Collections.emptyList(), interceptorsConfigurer); + setVirtualTargets((path) -> address); + } + + /** + * {@inheritDoc} + * @param target the target string as described in method javadocs + * @return {@code true} so that the test factory can handle all targets not just + * those prefixed with 'in-process:' + */ + @Override + public boolean supports(String target) { + return true; + } + + /** + * {@inheritDoc} + *

+ * Overrides the parent behavior so that the channel factory can handle all + * targets, not just those that prefixed with 'in-process:'. + * @param target the target of the channel + * @param creds the credentials for the channel which are ignored in this case + * @return a new inprocess channel builder instance + */ + @Override + protected InProcessChannelBuilder newChannelBuilder(String target, ChannelCredentials creds) { + if (target.startsWith("in-process:")) { + return super.newChannelBuilder(target, creds); + } + return InProcessChannelBuilder.forName(target); + } + + } + +} diff --git a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTransportContextCustomizerFactory.java b/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTransportContextCustomizerFactory.java new file mode 100644 index 000000000000..2aaf39c00593 --- /dev/null +++ b/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTransportContextCustomizerFactory.java @@ -0,0 +1,91 @@ +/* + * 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.test.autoconfigure.grpc; + +import java.util.List; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.TestContextAnnotationUtils; + +/** + * {@link ContextCustomizerFactory} that starts an in-process gRPC server and replaces the + * regular server and channel factories (e.g. Netty). The customizer can be disabled via + * the {@link AutoConfigureInProcessTransport} annotation or the + * {@value #ENABLED_PROPERTY} property. + * + * @author Chris Bono + */ +class InProcessTransportContextCustomizerFactory implements ContextCustomizerFactory { + + static final String ENABLED_PROPERTY = "spring.test.grpc.inprocess.enabled"; + + @Override + public ContextCustomizer createContextCustomizer(Class testClass, + List configAttributes) { + AutoConfigureInProcessTransport annotation = TestContextAnnotationUtils.findMergedAnnotation(testClass, + AutoConfigureInProcessTransport.class); + return new InProcessTransportContextCustomizer(annotation); + } + + private static class InProcessTransportContextCustomizer implements ContextCustomizer { + + private final @Nullable AutoConfigureInProcessTransport annotation; + + InProcessTransportContextCustomizer(@Nullable AutoConfigureInProcessTransport annotation) { + this.annotation = annotation; + } + + @Override + public void customizeContext(ConfigurableApplicationContext context, + MergedContextConfiguration mergedContextConfiguration) { + if (this.annotation == null + || !context.getEnvironment().getProperty(ENABLED_PROPERTY, Boolean.class, false)) { + return; + } + TestPropertyValues + .of("spring.grpc.client.inprocess.exclusive=true", "spring.grpc.server.inprocess.exclusive=true") + .applyTo(context); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + InProcessTransportContextCustomizer that = (InProcessTransportContextCustomizer) o; + return Objects.equals(this.annotation, that.annotation); + } + + @Override + public int hashCode() { + return Objects.hash(this.annotation); + } + + } + +} diff --git a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/LocalGrpcPort.java b/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/LocalGrpcPort.java new file mode 100644 index 000000000000..778d449d62ed --- /dev/null +++ b/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/LocalGrpcPort.java @@ -0,0 +1,42 @@ +/* + * 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.test.autoconfigure.grpc; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.annotation.Value; + +/** + * Annotation at the field or method/constructor parameter level that injects the gRPC + * server port that was allocated at runtime. Provides a convenient alternative for + * @Value("${local.grpc.port}"). + * + * @author Dave Syer + * @author Chris Bono + * @since 4.0.0 + */ +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Value("${local.grpc.port}") +public @interface LocalGrpcPort { + +} diff --git a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/ServerPortInfoApplicationContextInitializer.java b/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/ServerPortInfoApplicationContextInitializer.java new file mode 100644 index 000000000000..a9736be6ce0e --- /dev/null +++ b/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/ServerPortInfoApplicationContextInitializer.java @@ -0,0 +1,87 @@ +/* + * 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.test.autoconfigure.grpc; + +import java.util.HashMap; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; +import org.springframework.grpc.server.InProcessGrpcServerFactory; +import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent; +import org.springframework.util.Assert; + +/** + * {@link ApplicationContextInitializer} implementation to start the management context on + * a random port if the main server's port is 0 and the management context is expected on + * a different port. + * + * @author Dave Syer + * @author Chris Bono + */ +class ServerPortInfoApplicationContextInitializer implements + ApplicationContextInitializer, ApplicationListener { + + private static final String PROPERTY_SOURCE_NAME = "grpc.server.ports"; + + private @Nullable ConfigurableApplicationContext applicationContext; + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + this.applicationContext = applicationContext; + applicationContext.addApplicationListener(this); + } + + @Override + public void onApplicationEvent(GrpcServerStartedEvent event) { + if (event.getSource().getFactory() instanceof InProcessGrpcServerFactory) { + return; + } + String propertyName = "local.grpc.port"; + Assert.notNull(this.applicationContext, "ApplicationContext must not be null"); + setPortProperty(this.applicationContext, propertyName, event.getPort()); + } + + private void setPortProperty(ApplicationContext context, String propertyName, int port) { + if (context instanceof ConfigurableApplicationContext configurableContext) { + setPortProperty(configurableContext.getEnvironment(), propertyName, port); + } + if (context.getParent() != null) { + setPortProperty(context.getParent(), propertyName, port); + } + } + + @SuppressWarnings("unchecked") + private void setPortProperty(ConfigurableEnvironment environment, String propertyName, int port) { + MutablePropertySources sources = environment.getPropertySources(); + PropertySource source = sources.get(PROPERTY_SOURCE_NAME); + if (source == null) { + source = new MapPropertySource(PROPERTY_SOURCE_NAME, new HashMap<>()); + sources.addFirst(source); + } + ((Map) source.getSource()).put(propertyName, port); + } + +} diff --git a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/package-info.java b/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/package-info.java new file mode 100644 index 000000000000..3acb78aa1eba --- /dev/null +++ b/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * Auto-configuration for Spring gRPC tests. + */ +@NullMarked +package org.springframework.boot.test.autoconfigure.grpc; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfigurationTests.java b/module/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfigurationTests.java new file mode 100644 index 000000000000..90a165b61ae1 --- /dev/null +++ b/module/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfigurationTests.java @@ -0,0 +1,99 @@ +/* + * 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.test.autoconfigure.grpc; + +import io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.grpc.client.GrpcChannelFactory; +import org.springframework.grpc.server.GrpcServerFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link InProcessTestAutoConfiguration}. + * + * @author Chris Bono + */ +class InProcessTestAutoConfigurationTests { + + private final BindableService service = mock(); + + private final ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); + + @BeforeEach + void prepareForTest() { + given(this.service.bindService()).willReturn(this.serviceDefinition); + } + + private ApplicationContextRunner contextRunner() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(InProcessTestAutoConfiguration.class, + GrpcServerAutoConfiguration.class, GrpcServerFactoryAutoConfiguration.class, + SslAutoConfiguration.class, GrpcClientAutoConfiguration.class)) + .withBean(BindableService.class, () -> this.service); + } + + @Test + void whenTestInProcessEnabledPropIsSetToTrueDoesAutoConfigureBeans() { + this.contextRunner() + .withPropertyValues("spring.test.grpc.inprocess.enabled=true", "spring.grpc.server.inprocess.name=foo", + "spring.grpc.server.port=0") + .run((context) -> { + assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("testInProcessGrpcServerFactory", "nettyGrpcServerFactory"); + assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("testInProcessGrpcChannelFactory", "nettyGrpcChannelFactory"); + }); + } + + @Test + void whenTestInProcessEnabledPropIsNotSetDoesNotAutoConfigureBeans() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.inprocess.name=foo", "spring.grpc.server.port=0") + .run((context) -> { + assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("inProcessGrpcServerFactory", "nettyGrpcServerFactory"); + assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("inProcessGrpcChannelFactory", "nettyGrpcChannelFactory"); + }); + } + + @Test + void whenTestInProcessEnabledPropIsSetToFalseDoesNotAutoConfigureBeans() { + this.contextRunner() + .withPropertyValues("spring.test.grpc.inprocess.enabled=false", "spring.grpc.server.inprocess.name=foo", + "spring.grpc.server.port=0") + .run((context) -> { + assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("inProcessGrpcServerFactory", "nettyGrpcServerFactory"); + assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("inProcessGrpcChannelFactory", "nettyGrpcChannelFactory"); + }); + } + +} diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index ebbe475fd31d..a08779857caa 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -492,6 +492,30 @@ bom { site("https://groovy-lang.org") } } + library("Grpc Java", "1.74.0") { + group("io.grpc") { + bom("grpc-bom") + } + links { + github("https://github.com/grpc/grpc-java") + docs("https://grpc.io/docs/languages/java/") + releaseNotes("https://github.com/grpc/grpc-java/releases/tag/v{version}") + } + } + library("Grpc Kotlin", "1.4.3") { + group("io.grpc") { + modules = [ + "grpc-kotlin-stub" { + exclude group: "javax.annotation", module: "javax.annotation-api" + } + ] + } + links { + github("https://github.com/grpc/grpc-kotlin") + docs("https://grpc.io/docs/languages/kotlin/") + releaseNotes("https://github.com/grpc/grpc-kotlin/releases/tag/v{version}") + } + } library("Gson", "2.13.2") { group("com.google.code.gson") { modules = [ @@ -1762,6 +1786,27 @@ bom { releaseNotes("https://github.com/prometheus/client_java/releases/tag/parent-{version}") } } + library("Protobuf Common Protos", "2.61.2") { + group("com.google.api.grpc") { + modules = [ + "proto-google-common-protos" + ] + } + links { + github("https://github.com/googleapis/sdk-platform-java") + releaseNotes("https://github.com/googleapis/sdk-platform-java/releases/tag/v-{version}") + } + } + library("Protobuf Java", "4.31.1") { + group("com.google.protobuf") { + bom("protobuf-bom") + } + links { + site("https://protobuf.dev") + github("https://github.com/protocolbuffers/protobuf") + releaseNotes("https://github.com/protocolbuffers/protobuf/releases/tag/v{version}") + } + } library("Pulsar", "4.1.0") { group("org.apache.pulsar") { bom("pulsar-bom") { @@ -2006,6 +2051,7 @@ bom { "spring-boot-elasticsearch", "spring-boot-flyway", "spring-boot-freemarker", + "spring-boot-grpc-server", "spring-boot-graphql", "spring-boot-graphql-test", "spring-boot-groovy-templates", @@ -2332,6 +2378,23 @@ bom { releaseNotes("https://github.com/spring-projects/spring-graphql/releases/tag/v{version}") } } + library("Spring gRPC", "1.0.0-SNAPSHOT") { + considerSnapshots() + group("org.springframework.grpc") { + modules = [ + "spring-grpc-core" + ] + } + links { + site("https://spring.io/projects/spring-grpc") + 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" + .formatted(version.forMajorMinorGeneration())) + releaseNotes("https://github.com/spring-projects/spring-grpc/releases/tag/v{version}") + } + } library("Spring HATEOAS", "3.0.0-M5") { considerSnapshots() group("org.springframework.hateoas") { diff --git a/platform/spring-boot-internal-dependencies/build.gradle b/platform/spring-boot-internal-dependencies/build.gradle index 54b72acde413..dcceb5379950 100644 --- a/platform/spring-boot-internal-dependencies/build.gradle +++ b/platform/spring-boot-internal-dependencies/build.gradle @@ -84,11 +84,6 @@ bom { ] } } - library("gRPC", "1.73.0") { - group("io.grpc") { - bom("grpc-bom") - } - } library("Janino", "3.1.12") { group("org.codehaus.janino") { bom("janino") { diff --git a/settings.gradle b/settings.gradle index e34f9c1805ca..c9c3b760a02b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -113,6 +113,8 @@ include "module:spring-boot-data-redis" include "module:spring-boot-data-redis-test" include "module:spring-boot-data-rest" include "module:spring-boot-devtools" +include "module:spring-boot-grpc-client" +include "module:spring-boot-grpc-server" include "module:spring-boot-elasticsearch" include "module:spring-boot-flyway" include "module:spring-boot-freemarker" @@ -229,6 +231,10 @@ include "starter:spring-boot-starter-flyway" include "starter:spring-boot-starter-freemarker" include "starter:spring-boot-starter-graphql" include "starter:spring-boot-starter-groovy-templates" +include "starter:spring-boot-starter-grpc" +include "starter:spring-boot-starter-grpc-client" +include "starter:spring-boot-starter-grpc-server" +include "starter:spring-boot-starter-grpc-server-web" include "starter:spring-boot-starter-gson" include "starter:spring-boot-starter-hateoas" include "starter:spring-boot-starter-hazelcast" @@ -335,6 +341,7 @@ include ":smoke-test:spring-boot-smoke-test-data-rest" include ":smoke-test:spring-boot-smoke-test-devtools" include ":smoke-test:spring-boot-smoke-test-flyway" include ":smoke-test:spring-boot-smoke-test-graphql" +include ":smoke-test:spring-boot-smoke-test-grpc" include ":smoke-test:spring-boot-smoke-test-hateoas" include ":smoke-test:spring-boot-smoke-test-hibernate" include ":smoke-test:spring-boot-smoke-test-integration" diff --git a/smoke-test/spring-boot-smoke-test-grpc/build.gradle b/smoke-test/spring-boot-smoke-test-grpc/build.gradle new file mode 100644 index 000000000000..233e3470e076 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc/build.gradle @@ -0,0 +1,67 @@ +/* + * 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. + */ + +plugins { + id "java" + id 'com.google.protobuf' version '0.9.5' +} + +description = "Spring Boot gRPC smoke test" + +dependencies { + implementation(platform(project(":platform:spring-boot-dependencies"))) + implementation(project(":starter:spring-boot-starter-actuator")) + implementation(project(":starter:spring-boot-starter-grpc")) + + testImplementation(project(":starter:spring-boot-starter-test")) + testImplementation("io.grpc:grpc-inprocess") + testImplementation("org.awaitility:awaitility") + testImplementation("org.testcontainers:junit-jupiter") + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + + +architectureCheck { + nullMarked = false +} + +tasks.withType(io.spring.javaformat.gradle.tasks.CheckFormat) { + exclude "smoketest/grpc/proto" +} + +// FIXME get from 'protobuf-java-version' from dep mgmt +def protobufJavaVersion = "4.30.2" +// FIXME get from 'grpc-java-version' from dep mgmt +def grpcVersion = "1.74.0" + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protobufJavaVersion}" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" + } + } + generateProtoTasks { + all()*.plugins { + grpc { + option '@generated=omit' + } + } + } +} diff --git a/smoke-test/spring-boot-smoke-test-grpc/src/main/java/smoketest/grpc/GrpcServerApplication.java b/smoke-test/spring-boot-smoke-test-grpc/src/main/java/smoketest/grpc/GrpcServerApplication.java new file mode 100644 index 000000000000..18873e62fd4e --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc/src/main/java/smoketest/grpc/GrpcServerApplication.java @@ -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 smoketest.grpc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GrpcServerApplication { + + public static void main(String[] args) { + SpringApplication.run(GrpcServerApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc/src/main/java/smoketest/grpc/GrpcServerService.java b/smoke-test/spring-boot-smoke-test-grpc/src/main/java/smoketest/grpc/GrpcServerService.java new file mode 100644 index 000000000000..7bab517adc37 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc/src/main/java/smoketest/grpc/GrpcServerService.java @@ -0,0 +1,67 @@ +/* + * 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 smoketest.grpc; + +import io.grpc.stub.StreamObserver; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import smoketest.grpc.proto.HelloReply; +import smoketest.grpc.proto.HelloRequest; +import smoketest.grpc.proto.SimpleGrpc; + +import org.springframework.stereotype.Service; + +@Service +public class GrpcServerService extends SimpleGrpc.SimpleImplBase { + + private static Log log = LogFactory.getLog(GrpcServerService.class); + + @Override + public void sayHello(HelloRequest req, StreamObserver responseObserver) { + log.info("Hello " + req.getName()); + if (req.getName().startsWith("error")) { + throw new IllegalArgumentException("Bad name: " + req.getName()); + } + if (req.getName().startsWith("internal")) { + throw new RuntimeException(); + } + HelloReply reply = HelloReply.newBuilder().setMessage("Hello ==> " + req.getName()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + + @Override + public void streamHello(HelloRequest req, StreamObserver responseObserver) { + log.info("Hello " + req.getName()); + int count = 0; + while (count < 10) { + HelloReply reply = HelloReply.newBuilder().setMessage("Hello(" + count + ") ==> " + req.getName()).build(); + responseObserver.onNext(reply); + count++; + try { + Thread.sleep(1000L); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + responseObserver.onError(ex); + return; + } + } + responseObserver.onCompleted(); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc/src/main/java/smoketest/grpc/package-info.java b/smoke-test/spring-boot-smoke-test-grpc/src/main/java/smoketest/grpc/package-info.java new file mode 100644 index 000000000000..dd0921aa14ca --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc/src/main/java/smoketest/grpc/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +@NullMarked +package smoketest.grpc; + +import org.jspecify.annotations.NullMarked; diff --git a/smoke-test/spring-boot-smoke-test-grpc/src/main/proto/hello.proto b/smoke-test/spring-boot-smoke-test-grpc/src/main/proto/hello.proto new file mode 100644 index 000000000000..825d235abf02 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc/src/main/proto/hello.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "smoketest.grpc.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; +} diff --git a/smoke-test/spring-boot-smoke-test-grpc/src/main/resources/application.properties b/smoke-test/spring-boot-smoke-test-grpc/src/main/resources/application.properties new file mode 100644 index 000000000000..566605a793b1 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=grpc-server diff --git a/smoke-test/spring-boot-smoke-test-grpc/src/test/java/smoketest/grpc/GrpcServerApplicationHealthTests.java b/smoke-test/spring-boot-smoke-test-grpc/src/test/java/smoketest/grpc/GrpcServerApplicationHealthTests.java new file mode 100644 index 000000000000..2b46a871dadd --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc/src/test/java/smoketest/grpc/GrpcServerApplicationHealthTests.java @@ -0,0 +1,174 @@ +/* + * 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 smoketest.grpc; + +import java.time.Duration; + +import io.grpc.ManagedChannel; +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.awaitility.Awaitility; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import smoketest.grpc.proto.HelloReply; +import smoketest.grpc.proto.HelloRequest; +import smoketest.grpc.proto.SimpleGrpc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.health.autoconfigure.contributor.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.health.contributor.Health; +import org.springframework.boot.health.contributor.HealthIndicator; +import org.springframework.boot.test.autoconfigure.grpc.AutoConfigureInProcessTransport; +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.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Integration tests for gRPC server health feature. + */ +class GrpcServerApplicationHealthTests { + + @Nested + @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 + class WithClientHealthEnabled { + + @Test + void loadBalancerRespectsServerHealth(@Autowired GrpcChannelFactory channels, + @Autowired HealthStatusManager healthStatusManager) { + ManagedChannel channel = channels.createChannel("health-test"); + SimpleGrpc.SimpleBlockingStub 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 void updateHealthStatusAndWait(String serviceName, ServingStatus healthStatus, + HealthStatusManager healthStatusManager) { + healthStatusManager.setStatus(serviceName, healthStatus); + try { + Thread.sleep(2000L); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + private void assertThatResponseIsServedToChannel(SimpleGrpc.SimpleBlockingStub client) { + HelloReply response = client.sayHello(HelloRequest.newBuilder().setName("Alien").build()); + assertThat(response.getMessage()).isEqualTo("Hello ==> Alien"); + } + + private void assertThatResponseIsNotServedToChannel(SimpleGrpc.SimpleBlockingStub client) { + assertThatExceptionOfType(StatusRuntimeException.class) + .isThrownBy(() -> client.sayHello(HelloRequest.newBuilder().setName("Alien").build())) + .withMessageContaining("UNAVAILABLE: Health-check service responded NOT_SERVING for 'my-service'"); + } + + } + + @Nested + @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 + class WithActuatorHealthAdapter { + + @Test + void healthIndicatorsAdaptedToGrpcHealthStatus(@Autowired GrpcChannelFactory channels) { + var channel = channels.createChannel("0.0.0.0:0"); + var healthStub = HealthGrpc.newBlockingStub(channel); + var 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 void assertThatGrpcHealthStatusIs(HealthBlockingStub healthBlockingStub, String service, + ServingStatus expectedStatus, Duration maxWaitTime) { + Awaitility.await().atMost(maxWaitTime).ignoreException(StatusRuntimeException.class).untilAsserted(() -> { + var healthRequest = HealthCheckRequest.newBuilder().setService(service).build(); + var healthResponse = healthBlockingStub.check(healthRequest); + assertThat(healthResponse.getStatus()).isEqualTo(expectedStatus); + // verify the overall status as well + var overallHealthRequest = HealthCheckRequest.newBuilder().setService("").build(); + var overallHealthResponse = healthBlockingStub.check(overallHealthRequest); + assertThat(overallHealthResponse.getStatus()).isEqualTo(expectedStatus); + }); + } + + @TestConfiguration + static class MyHealthIndicatorsConfig { + + @ConditionalOnEnabledHealthIndicator("custom") + @Bean + CustomHealthIndicator customHealthIndicator() { + return new CustomHealthIndicator(); + } + + } + + static class CustomHealthIndicator implements HealthIndicator { + + static boolean SERVICE_IS_UP = true; + + @Override + public Health health() { + return SERVICE_IS_UP ? Health.up().build() : Health.down().build(); + } + + } + + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc/src/test/java/smoketest/grpc/GrpcServerApplicationTests.java b/smoke-test/spring-boot-smoke-test-grpc/src/test/java/smoketest/grpc/GrpcServerApplicationTests.java new file mode 100644 index 000000000000..022f7205545f --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc/src/test/java/smoketest/grpc/GrpcServerApplicationTests.java @@ -0,0 +1,99 @@ +/* + * 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 smoketest.grpc; + +import io.grpc.ManagedChannel; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import smoketest.grpc.proto.HelloReply; +import smoketest.grpc.proto.HelloRequest; +import smoketest.grpc.proto.SimpleGrpc; + +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.client.GrpcChannelFactory; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(useMainMethod = UseMainMethod.ALWAYS) +@DirtiesContext +class GrpcServerApplicationTests { + + static void main(String[] args) { + new SpringApplicationBuilder(GrpcServerApplication.class).run(); + } + + private void assertThatResponseIsServedToChannel(ManagedChannel clientChannel) { + SimpleGrpc.SimpleBlockingStub client = SimpleGrpc.newBlockingStub(clientChannel); + HelloReply response = client.sayHello(HelloRequest.newBuilder().setName("Alien").build()); + assertThat(response.getMessage()).isEqualTo("Hello ==> Alien"); + } + + @Nested + @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 ServerUnsecured { + + @Test + void clientChannelWithoutSsl(@Autowired GrpcChannelFactory channels) { + assertThatResponseIsServedToChannel(channels.createChannel("test-channel")); + } + + } + + @Nested + @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 + class ServerWithSsl { + + @Test + void clientChannelWithSsl(@Autowired GrpcChannelFactory channels) { + assertThatResponseIsServedToChannel(channels.createChannel("test-channel")); + } + + } + + @Nested + @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 + class ServerWithClientAuth { + + @Test + void clientChannelWithSsl(@Autowired GrpcChannelFactory channels) { + assertThatResponseIsServedToChannel(channels.createChannel("test-channel")); + } + + } + +} diff --git a/smoke-test/spring-boot-smoke-test-grpc/src/test/resources/application-ssl.properties b/smoke-test/spring-boot-smoke-test-grpc/src/test/resources/application-ssl.properties new file mode 100644 index 000000000000..50ad5c4cbe6f --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-grpc/src/test/resources/application-ssl.properties @@ -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 \ No newline at end of file diff --git a/smoke-test/spring-boot-smoke-test-grpc/src/test/resources/test.jks b/smoke-test/spring-boot-smoke-test-grpc/src/test/resources/test.jks new file mode 100644 index 0000000000000000000000000000000000000000..6aa9a28053a591e41453e665e5024e8a8cb78b3d GIT binary patch literal 2264 zcmchYX*3iJ7sqE|hQS!q5Mv)4GM2$i#uAFqC`%7x7baWA*i&dRX>3`uq(XS?3XSYp z%38`&ib7E$8j~$cF^}gt?|I+noW8#w?uYxk=iGD8|K9Vzd#pVc0002(2k@T|2@MMI zqxqr2AhQO*TVi`j@((S;e;g;l$#dAA{>vf0kX$R(Qn4oKgGEYjZ5zti2dw?Z6A zh%LuFCNI?9o+Z1duJL-++e#cjO`zlK?u9s030=k_*wD1#-$FbIDRDnA^vo@fm( zzjt(3VJrGOr0iHXSTM|rYN#>RZ@Dp`PwB2zrDQffLvuoR2~V3ReYa0&vU^dXd8isV zsAf*@!8s%xBvHLseXn6f?1kefe(8uAmAbaF$x{Ykzb6c6jdUwY1$y4tFzsj7 zIghr!T#ODfu@Po!a29@kXQ8kY#(LE<0o7?7PQ|eMeY@Equ?R-6*f@Na3o&stDQ=6( zQzDSQhCnS(9Bu9W_~giknP0vECqUsr4_9y_}nEU`cy z4}dApnAip92wMwgzciAFpc3i}+-#Zlq+iF7d1y}d4Qsp8=%l1N8NIs161I`HmkcpQ zY4*CUCFJJf(2!M{`&qQ}3($KeTQ=)mMrBs`DOb;%Of0tC)9he_p~w&CO#DfCgx(%s z{@|D(brX_Gb}ZDLmGej*JgEl0Et>q~kgTXuJg-PwvRjNx8sBbIShxD=xOySzw{;^X zAvrh5HTg>Xq@<{#^!Kg}B?qz@b<{ebD)yaSf&RChBIJQo-?Ahzw@qopSe^e&>^IuU zydM4Y1_C&>k7u|}=; z63R7$H6zat=hNExxEwXu1fQ*ytuEkP!{w{|#6TIEq1#*ck=6_NM*ILF65tmD-O5&R zMI!-MT<3U~t@}(CN4@RlZ~1I>C=!ywF)dNI{VvH;5Y3(Z4jY^%_c&fsm4Q`<1g|qX z&!h29jXjVE3nJnet*L)XL?-8<>qDbVGP%i^NwOZfwWO7?Mr!X7 zl}sG@9S_5}}td}$xrWIYY=e(VVBiv%A+M-{M z!3_^Tc=pV?niT!{D`!{e@W;MvrZ(OER{x7itVAtwE~spPtPtma|J=5dv&_oE!5H#` zdgXJ;+gJ4hI}*9QX9jpL`Gb)yCe%1}t!&O-^sihyZys%%5uF~WhsR_w(q7;vV5d4P zr%ZUA2}kO+L^2ePTgGT9Ua71w<+)poSyjTdLq&xbUn`<6&SpwFp(HRHUyU6J3WZ_! zfztko79+94Tq%mTYj53(RYcL&1~5`I#+w3`(Q|r+P(aT z%?r(^?IWw~19CB&uvXf(f7&BnEE{zwK4piVU`I4j1j?v5d4N<7VUJ8nM`$7S*mfKR z#9-JzPRZ?{M!@L+0N^V)IyeeP2T|^UK|m0QD+Ibs!wEoml^N!YO#vW~j~jraX(0A3 z6Kux?IRLez`O^X;{!4g%BhcRn>^H*qKZ3*|{_YGuz)KCJcu;)DSES5D2tDE`C02YR0R%Vy1T7k|RQ;3g<0icA$AuP0pOvc~jGl zz+NeKv_FT_;GWK&8XlDUv&hv9kxg?@c!bu?83i=YQ$S!K09Y)Glg3Hz?@|)ZCBlVz zP8i}#XZkMoje3I=h&I!!s_m?Qi@1MR`yv7X*yEs47qOs^t^?&=;*IQ!q&)gq_Sx5* z?fhU8Q*PSe*w7y)FH#P!9R^Xw!lTT+zI39L<&8cViaj$A(Z2Cg7!{V?uuyi#vlNCg z40i}2ivw&y&1-&Nh&WMG`&aIt>)(#tKTJ}^@696Kw1-{IzSOTnFF+0@k$o3%ZHS;Q#;t literal 0 HcmV?d00001 diff --git a/starter/spring-boot-starter-grpc-client/build.gradle b/starter/spring-boot-starter-grpc-client/build.gradle new file mode 100644 index 000000000000..0f7b675db33b --- /dev/null +++ b/starter/spring-boot-starter-grpc-client/build.gradle @@ -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. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter using Spring gRPC client" + + +dependencies { + api(project(":starter:spring-boot-starter")) + api(project(":module:spring-boot-grpc-client")) + api("io.grpc:grpc-netty") + api("io.grpc:grpc-stub") +} diff --git a/starter/spring-boot-starter-grpc-server-web/build.gradle b/starter/spring-boot-starter-grpc-server-web/build.gradle new file mode 100644 index 000000000000..97f6d5d42a03 --- /dev/null +++ b/starter/spring-boot-starter-grpc-server-web/build.gradle @@ -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. + */ + +plugins { + id "org.springframework.boot.starter" +} +description = "Starter using Spring gRPC Servlet server" + + +dependencies { + api(project(":starter:spring-boot-starter-web")) + api(project(":module:spring-boot-grpc-server")) + api("io.grpc:grpc-servlet-jakarta") +} diff --git a/starter/spring-boot-starter-grpc-server/build.gradle b/starter/spring-boot-starter-grpc-server/build.gradle new file mode 100644 index 000000000000..45e52fc2a92b --- /dev/null +++ b/starter/spring-boot-starter-grpc-server/build.gradle @@ -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. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter using Spring gRPC server" + + +dependencies { + api(project(":starter:spring-boot-starter")) + api(project(":module:spring-boot-grpc-server")) + api("io.grpc:grpc-netty") + api("io.grpc:grpc-services") +} diff --git a/starter/spring-boot-starter-grpc/build.gradle b/starter/spring-boot-starter-grpc/build.gradle new file mode 100644 index 000000000000..0dbb6e144f35 --- /dev/null +++ b/starter/spring-boot-starter-grpc/build.gradle @@ -0,0 +1,28 @@ +/* + * 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. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter using Spring gRPC server and client" + + +dependencies { + api(project(":starter:spring-boot-starter")) + api(project(":starter:spring-boot-starter-grpc-client")) + api(project(":starter:spring-boot-starter-grpc-server")) +} From 854cd346a50af2ff3fdcbda28c3277c1267110ea Mon Sep 17 00:00:00 2001 From: onobc Date: Mon, 22 Sep 2025 14:00:12 -0500 Subject: [PATCH 02/21] Update gRPC and protobuf versions Updates the following gRPC related library versions: - `grpc-bom` from `1.74.0` to `1.75.0` - `protobuf-bum` from `4.31.1` to `4.32.1` Signed-off-by: onobc --- platform/spring-boot-dependencies/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index a08779857caa..5c9a6a34e715 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -492,7 +492,7 @@ bom { site("https://groovy-lang.org") } } - library("Grpc Java", "1.74.0") { + library("Grpc Java", "1.75.0") { group("io.grpc") { bom("grpc-bom") } @@ -1797,7 +1797,7 @@ bom { releaseNotes("https://github.com/googleapis/sdk-platform-java/releases/tag/v-{version}") } } - library("Protobuf Java", "4.31.1") { + library("Protobuf Java", "4.32.1") { group("com.google.protobuf") { bom("protobuf-bom") } From fb1183170bf1948a7848608d945f0927bd3f7bde Mon Sep 17 00:00:00 2001 From: onobc Date: Mon, 22 Sep 2025 14:29:45 -0500 Subject: [PATCH 03/21] Update gRPC Kotlin version to 1.5.0 This updates the gRPC Kotlin version from `1.4.3` to `1.5.0` Signed-off-by: onobc --- platform/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 5c9a6a34e715..d8b239dbac56 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -502,7 +502,7 @@ bom { releaseNotes("https://github.com/grpc/grpc-java/releases/tag/v{version}") } } - library("Grpc Kotlin", "1.4.3") { + library("Grpc Kotlin", "1.5.0") { group("io.grpc") { modules = [ "grpc-kotlin-stub" { From a9b6527ec58a027157682da22725ba0b06958258 Mon Sep 17 00:00:00 2001 From: onobc Date: Mon, 22 Sep 2025 14:41:35 -0500 Subject: [PATCH 04/21] Update gRPC and protobuf versions (take 2) Updates the gRPC and protobuf versions in the gRPC smoke test: - `grpc-bom` from `1.74.0` to `1.75.0` - `protobuf-bum` from `4.31.1` to `4.32.1` Signed-off-by: onobc --- smoke-test/spring-boot-smoke-test-grpc/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smoke-test/spring-boot-smoke-test-grpc/build.gradle b/smoke-test/spring-boot-smoke-test-grpc/build.gradle index 233e3470e076..18ff7ed52bee 100644 --- a/smoke-test/spring-boot-smoke-test-grpc/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc/build.gradle @@ -44,9 +44,9 @@ tasks.withType(io.spring.javaformat.gradle.tasks.CheckFormat) { } // FIXME get from 'protobuf-java-version' from dep mgmt -def protobufJavaVersion = "4.30.2" +def protobufJavaVersion = "4.32.1" // FIXME get from 'grpc-java-version' from dep mgmt -def grpcVersion = "1.74.0" +def grpcVersion = "1.75.0" protobuf { protoc { From 549d7ad4208d8813a19c92f9dd9f9e7cab963189 Mon Sep 17 00:00:00 2001 From: onobc Date: Tue, 23 Sep 2025 11:50:37 -0500 Subject: [PATCH 05/21] Use the non-deprecated env post processor key Uses `org.springframework.boot.EnvironmentPostProcessor` instead of the deprecated `org.springframework.boot.env.EnvironmentPostProcessor` key in `spring.factories` for the `spring-boot-grpc-server` module. Signed-off-by: onobc --- .../src/main/resources/META-INF/spring.factories | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring.factories b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring.factories index 49203d9172bf..fc5b89e9e454 100644 --- a/module/spring-boot-grpc-server/src/main/resources/META-INF/spring.factories +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/spring.factories @@ -1,5 +1,5 @@ org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer=\ org.springframework.boot.grpc.server.autoconfigure.security.GrpcDisableCsrfHttpConfigurer -org.springframework.boot.env.EnvironmentPostProcessor=\ +org.springframework.boot.EnvironmentPostProcessor=\ org.springframework.boot.grpc.server.autoconfigure.ServletEnvironmentPostProcessor From ed0e6a16ef244730a5367be3ebb9bdadce684e77 Mon Sep 17 00:00:00 2001 From: onobc Date: Tue, 23 Sep 2025 12:08:57 -0500 Subject: [PATCH 06/21] Fix config prop name in gRPC client and server The config prop name was mismatched in the both the gRPC client and server modules additional-spring-configuration-metadata.json files. This makes them consistent using the singular form of the property `spring.grpc.(client|server).observation.enabled`. Signed-off-by: onobc --- .../META-INF/additional-spring-configuration-metadata.json | 2 +- .../META-INF/additional-spring-configuration-metadata.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 52beeba738d9..b2914ae3fdc3 100644 --- a/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -20,7 +20,7 @@ "defaultValue": true }, { - "name": "spring.grpc.client.observations.enabled", + "name": "spring.grpc.client.observation.enabled", "type": "java.lang.Boolean", "description": "Whether to enable Observations on the client.", "defaultValue": true diff --git a/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 68dbe4f646cc..cfafdb62f4e3 100644 --- a/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/module/spring-boot-grpc-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -20,7 +20,7 @@ "defaultValue": true }, { - "name": "spring.grpc.server.observations.enabled", + "name": "spring.grpc.server.observation.enabled", "type": "java.lang.Boolean", "description": "Whether to enable Observations on the server.", "defaultValue": true From 42af221351bf28ffb90f12bc76d78fa293eea653 Mon Sep 17 00:00:00 2001 From: onobc Date: Tue, 23 Sep 2025 22:30:51 -0500 Subject: [PATCH 07/21] Fix checkstyle Signed-off-by: onobc --- .../test/autoconfigure/grpc/InProcessTestAutoConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfiguration.java b/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfiguration.java index 3e1ab07ea360..ad6da08bf21a 100644 --- a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfiguration.java +++ b/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfiguration.java @@ -20,11 +20,11 @@ import java.util.Collections; import java.util.List; -import io.grpc.stub.AbstractStub; import io.grpc.BindableService; import io.grpc.ChannelCredentials; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.AbstractStub; import org.jspecify.annotations.Nullable; import org.springframework.boot.autoconfigure.AutoConfiguration; From a1c2b04468f047396e55f0a04afd2e616328f142 Mon Sep 17 00:00:00 2001 From: onobc Date: Thu, 25 Sep 2025 17:48:34 -0500 Subject: [PATCH 08/21] Move grpc test out of spring-boot-test-autoconfigure Moves the grpc test code into its own module (`spring-boot-grc-test`) due to the modularization of the test module. Signed-off-by: onobc --- module/spring-boot-grpc-test/build.gradle | 41 +++++++++++++++++++ .../AutoConfigureInProcessTransport.java | 3 +- .../InProcessTestAutoConfiguration.java | 2 +- ...cessTransportContextCustomizerFactory.java | 2 +- .../test/autoconfigure}/LocalGrpcPort.java | 2 +- ...PortInfoApplicationContextInitializer.java | 2 +- .../test/autoconfigure}/package-info.java | 2 +- .../main/resources/META-INF/spring.factories | 7 ++++ ...re.AutoConfigureInProcessTransport.imports | 1 + .../InProcessTestAutoConfigurationTests.java | 2 +- settings.gradle | 1 + .../spring-boot-smoke-test-grpc/build.gradle | 9 ++-- .../GrpcServerApplicationHealthTests.java | 2 +- 13 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 module/spring-boot-grpc-test/build.gradle rename module/{spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc => spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure}/AutoConfigureInProcessTransport.java (95%) rename module/{spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc => spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure}/InProcessTestAutoConfiguration.java (99%) rename module/{spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc => spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure}/InProcessTransportContextCustomizerFactory.java (98%) rename module/{spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc => spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure}/LocalGrpcPort.java (96%) rename module/{spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc => spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure}/ServerPortInfoApplicationContextInitializer.java (98%) rename module/{spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc => spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure}/package-info.java (92%) create mode 100644 module/spring-boot-grpc-test/src/main/resources/META-INF/spring.factories create mode 100644 module/spring-boot-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.grpc.test.autoconfigure.AutoConfigureInProcessTransport.imports rename module/{spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/grpc => spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure}/InProcessTestAutoConfigurationTests.java (98%) diff --git a/module/spring-boot-grpc-test/build.gradle b/module/spring-boot-grpc-test/build.gradle new file mode 100644 index 000000000000..58b67c806c86 --- /dev/null +++ b/module/spring-boot-grpc-test/build.gradle @@ -0,0 +1,41 @@ +/* + * 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. + */ + +plugins { + id "java-library" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot gRPC Test" + +dependencies { + api(project(":module:spring-boot-grpc-client")) + api(project(":module:spring-boot-grpc-server")) + + implementation(project(":core:spring-boot-test")) + + optional(project(":module:spring-boot-test-autoconfigure")) + optional("io.grpc:grpc-inprocess") + optional("io.grpc:grpc-stub") + optional("org.junit.jupiter:junit-jupiter-api") + + testImplementation(project(":test-support:spring-boot-test-support")) + testImplementation("io.grpc:grpc-netty") + + testRuntimeOnly("ch.qos.logback:logback-classic") + //testRuntimeOnly("tools.jackson.core:jackson-databind") +} diff --git a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/AutoConfigureInProcessTransport.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureInProcessTransport.java similarity index 95% rename from module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/AutoConfigureInProcessTransport.java rename to module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureInProcessTransport.java index 9e91618d49fc..56ba22506aa0 100644 --- a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/AutoConfigureInProcessTransport.java +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureInProcessTransport.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.test.autoconfigure.grpc; +package org.springframework.boot.grpc.test.autoconfigure; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -48,6 +48,7 @@ * Whether to start an in-process test gRPC server. Defaults to {@code true}. * @return whether to start an in-process gRPC server */ + @SuppressWarnings("unused") boolean enabled() default true; } diff --git a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfiguration.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfiguration.java similarity index 99% rename from module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfiguration.java rename to module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfiguration.java index ad6da08bf21a..3faedc7900e4 100644 --- a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfiguration.java +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.test.autoconfigure.grpc; +package org.springframework.boot.grpc.test.autoconfigure; import java.time.Duration; import java.util.Collections; diff --git a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTransportContextCustomizerFactory.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTransportContextCustomizerFactory.java similarity index 98% rename from module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTransportContextCustomizerFactory.java rename to module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTransportContextCustomizerFactory.java index 2aaf39c00593..bd1ccdf70d30 100644 --- a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTransportContextCustomizerFactory.java +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTransportContextCustomizerFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.test.autoconfigure.grpc; +package org.springframework.boot.grpc.test.autoconfigure; import java.util.List; import java.util.Objects; diff --git a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/LocalGrpcPort.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/LocalGrpcPort.java similarity index 96% rename from module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/LocalGrpcPort.java rename to module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/LocalGrpcPort.java index 778d449d62ed..e8c837b338a6 100644 --- a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/LocalGrpcPort.java +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/LocalGrpcPort.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.test.autoconfigure.grpc; +package org.springframework.boot.grpc.test.autoconfigure; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/ServerPortInfoApplicationContextInitializer.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/ServerPortInfoApplicationContextInitializer.java similarity index 98% rename from module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/ServerPortInfoApplicationContextInitializer.java rename to module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/ServerPortInfoApplicationContextInitializer.java index a9736be6ce0e..b9f00f609aaa 100644 --- a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/ServerPortInfoApplicationContextInitializer.java +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/ServerPortInfoApplicationContextInitializer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.test.autoconfigure.grpc; +package org.springframework.boot.grpc.test.autoconfigure; import java.util.HashMap; import java.util.Map; diff --git a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/package-info.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/package-info.java similarity index 92% rename from module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/package-info.java rename to module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/package-info.java index 3acb78aa1eba..c6785eb5dd75 100644 --- a/module/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/grpc/package-info.java +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/package-info.java @@ -18,6 +18,6 @@ * Auto-configuration for Spring gRPC tests. */ @NullMarked -package org.springframework.boot.test.autoconfigure.grpc; +package org.springframework.boot.grpc.test.autoconfigure; import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-test/src/main/resources/META-INF/spring.factories b/module/spring-boot-grpc-test/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..329a7197e24e --- /dev/null +++ b/module/spring-boot-grpc-test/src/main/resources/META-INF/spring.factories @@ -0,0 +1,7 @@ +# Spring Test Context Customizer Factories +org.springframework.test.context.ContextCustomizerFactory=\ +org.springframework.boot.grpc.test.autoconfigure.InProcessTransportContextCustomizerFactory + +# Application Context Initializers +org.springframework.context.ApplicationContextInitializer=\ +org.springframework.boot.grpc.test.autoconfigure.ServerPortInfoApplicationContextInitializer diff --git a/module/spring-boot-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.grpc.test.autoconfigure.AutoConfigureInProcessTransport.imports b/module/spring-boot-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.grpc.test.autoconfigure.AutoConfigureInProcessTransport.imports new file mode 100644 index 000000000000..7abb17f2a061 --- /dev/null +++ b/module/spring-boot-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.grpc.test.autoconfigure.AutoConfigureInProcessTransport.imports @@ -0,0 +1 @@ +org.springframework.boot.grpc.test.autoconfigure.InProcessTestAutoConfiguration diff --git a/module/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfigurationTests.java b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfigurationTests.java similarity index 98% rename from module/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfigurationTests.java rename to module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfigurationTests.java index 90a165b61ae1..bc44b72659c1 100644 --- a/module/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/grpc/InProcessTestAutoConfigurationTests.java +++ b/module/spring-boot-grpc-test/src/test/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.test.autoconfigure.grpc; +package org.springframework.boot.grpc.test.autoconfigure; import io.grpc.BindableService; import io.grpc.ServerServiceDefinition; diff --git a/settings.gradle b/settings.gradle index c9c3b760a02b..cd644e6964ca 100644 --- a/settings.gradle +++ b/settings.gradle @@ -115,6 +115,7 @@ include "module:spring-boot-data-rest" include "module:spring-boot-devtools" include "module:spring-boot-grpc-client" include "module:spring-boot-grpc-server" +include "module:spring-boot-grpc-test" include "module:spring-boot-elasticsearch" include "module:spring-boot-flyway" include "module:spring-boot-freemarker" diff --git a/smoke-test/spring-boot-smoke-test-grpc/build.gradle b/smoke-test/spring-boot-smoke-test-grpc/build.gradle index 18ff7ed52bee..dabe58e4386a 100644 --- a/smoke-test/spring-boot-smoke-test-grpc/build.gradle +++ b/smoke-test/spring-boot-smoke-test-grpc/build.gradle @@ -26,12 +26,13 @@ dependencies { implementation(project(":starter:spring-boot-starter-actuator")) implementation(project(":starter:spring-boot-starter-grpc")) + compileOnly("com.fasterxml.jackson.core:jackson-annotations") + + testCompileOnly("com.fasterxml.jackson.core:jackson-annotations") + + testImplementation(project(":module:spring-boot-grpc-test")) testImplementation(project(":starter:spring-boot-starter-test")) testImplementation("io.grpc:grpc-inprocess") - testImplementation("org.awaitility:awaitility") - testImplementation("org.testcontainers:junit-jupiter") - - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/smoke-test/spring-boot-smoke-test-grpc/src/test/java/smoketest/grpc/GrpcServerApplicationHealthTests.java b/smoke-test/spring-boot-smoke-test-grpc/src/test/java/smoketest/grpc/GrpcServerApplicationHealthTests.java index 2b46a871dadd..651098bf9510 100644 --- a/smoke-test/spring-boot-smoke-test-grpc/src/test/java/smoketest/grpc/GrpcServerApplicationHealthTests.java +++ b/smoke-test/spring-boot-smoke-test-grpc/src/test/java/smoketest/grpc/GrpcServerApplicationHealthTests.java @@ -33,10 +33,10 @@ import smoketest.grpc.proto.SimpleGrpc; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.grpc.test.autoconfigure.AutoConfigureInProcessTransport; import org.springframework.boot.health.autoconfigure.contributor.ConditionalOnEnabledHealthIndicator; import org.springframework.boot.health.contributor.Health; import org.springframework.boot.health.contributor.HealthIndicator; -import org.springframework.boot.test.autoconfigure.grpc.AutoConfigureInProcessTransport; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; From 5c81286e366ff8458cd5d8082dc1fca448b57678 Mon Sep 17 00:00:00 2001 From: onobc Date: Fri, 26 Sep 2025 13:09:44 -0500 Subject: [PATCH 09/21] Incorporate feedback from server code review Addresses the following: - nits - visibility (move to package protected etc..) - javadocs - make server properties anemic Does not cover the following points: - rename / restructure the ConditionalOn... for server - observation API verify/check on direction - security concerns (need more review but should be good the way it is) Signed-off-by: onobc --- .../DefaultServerFactoryPropertyMapper.java | 34 +-- .../{codec => }/GrpcCodecConfiguration.java | 7 +- .../GrpcServerAutoConfiguration.java | 1 - .../GrpcServerExecutorProvider.java | 9 +- .../GrpcServerFactoryConfigurations.java | 10 +- .../GrpcServerFactoryCustomizer.java | 8 + .../autoconfigure/GrpcServerProperties.java | 207 ++++++++---------- .../ServerBuilderCustomizers.java | 3 +- .../ServletEnvironmentPostProcessor.java | 8 +- .../autoconfigure/codec/package-info.java | 23 -- .../GrpcCodecConfigurationTests.java | 2 +- .../GrpcServerPropertiesTests.java | 14 +- ...rpcServerHealthAutoConfigurationTests.java | 25 --- 13 files changed, 154 insertions(+), 197 deletions(-) rename module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/{codec => }/GrpcCodecConfiguration.java (90%) delete mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/package-info.java rename module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/{codec => }/GrpcCodecConfigurationTests.java (95%) diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java index 9f984ed34850..c395653cc805 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java @@ -36,7 +36,7 @@ */ class DefaultServerFactoryPropertyMapper> { - final GrpcServerProperties properties; + private final GrpcServerProperties properties; DefaultServerFactoryPropertyMapper(GrpcServerProperties properties) { this.properties = properties; @@ -47,37 +47,37 @@ class DefaultServerFactoryPropertyMapper> { * @param serverBuilder the builder */ void customizeServerBuilder(T serverBuilder) { - PropertyMapper mapper = PropertyMapper.get(); - customizeKeepAlive(serverBuilder, mapper); - customizeInboundLimits(serverBuilder, mapper); + PropertyMapper map = PropertyMapper.get(); + customizeKeepAlive(serverBuilder, map); + customizeInboundLimits(serverBuilder, map); } /** * Map the keep-alive properties to the server factory's server builder. * @param serverBuilder the builder - * @param mapper the property mapper + * @param map the property mapper */ - void customizeKeepAlive(T serverBuilder, PropertyMapper mapper) { + void customizeKeepAlive(T serverBuilder, PropertyMapper map) { GrpcServerProperties.KeepAlive keepAliveProps = this.properties.getKeepAlive(); - mapper.from(keepAliveProps.getTime()).to(durationProperty(serverBuilder::keepAliveTime)); - mapper.from(keepAliveProps.getTimeout()).to(durationProperty(serverBuilder::keepAliveTimeout)); - mapper.from(keepAliveProps.getMaxIdle()).to(durationProperty(serverBuilder::maxConnectionIdle)); - mapper.from(keepAliveProps.getMaxAge()).to(durationProperty(serverBuilder::maxConnectionAge)); - mapper.from(keepAliveProps.getMaxAgeGrace()).to(durationProperty(serverBuilder::maxConnectionAgeGrace)); - mapper.from(keepAliveProps.getPermitTime()).to(durationProperty(serverBuilder::permitKeepAliveTime)); - mapper.from(keepAliveProps.isPermitWithoutCalls()).to(serverBuilder::permitKeepAliveWithoutCalls); + map.from(keepAliveProps.getTime()).to(durationProperty(serverBuilder::keepAliveTime)); + map.from(keepAliveProps.getTimeout()).to(durationProperty(serverBuilder::keepAliveTimeout)); + map.from(keepAliveProps.getMaxIdle()).to(durationProperty(serverBuilder::maxConnectionIdle)); + map.from(keepAliveProps.getMaxAge()).to(durationProperty(serverBuilder::maxConnectionAge)); + map.from(keepAliveProps.getMaxAgeGrace()).to(durationProperty(serverBuilder::maxConnectionAgeGrace)); + map.from(keepAliveProps.getPermitTime()).to(durationProperty(serverBuilder::permitKeepAliveTime)); + map.from(keepAliveProps.isPermitWithoutCalls()).to(serverBuilder::permitKeepAliveWithoutCalls); } /** * Map the inbound limits properties to the server factory's server builder. * @param serverBuilder the builder - * @param mapper the property mapper + * @param map the property mapper */ - void customizeInboundLimits(T serverBuilder, PropertyMapper mapper) { - mapper.from(this.properties.getMaxInboundMessageSize()) + void customizeInboundLimits(T serverBuilder, PropertyMapper map) { + map.from(this.properties.getMaxInboundMessageSize()) .asInt(DataSize::toBytes) .to(serverBuilder::maxInboundMessageSize); - mapper.from(this.properties.getMaxInboundMetadataSize()) + map.from(this.properties.getMaxInboundMetadataSize()) .asInt(DataSize::toBytes) .to(serverBuilder::maxInboundMetadataSize); } diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfiguration.java similarity index 90% rename from module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfiguration.java rename to module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfiguration.java index 9bec055f950b..9ebbb68985df 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.grpc.server.autoconfigure.codec; +package org.springframework.boot.grpc.server.autoconfigure; import io.grpc.Codec; import io.grpc.Compressor; @@ -29,14 +29,13 @@ import org.springframework.context.annotation.Configuration; /** - * The configuration that contains all codec related beans for clients/servers. + * The configuration that contains all codec related beans for gRPC servers. * * @author Andrei Lisa - * @since 4.0.0 */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Codec.class) -public class GrpcCodecConfiguration { +class GrpcCodecConfiguration { @Bean @ConditionalOnMissingBean diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java index 178cb7746d89..0ed662c98aef 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java @@ -28,7 +28,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.grpc.server.autoconfigure.codec.GrpcCodecConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java index 9a1551cf5da0..ac5f1d8c849e 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java @@ -18,10 +18,17 @@ import java.util.concurrent.Executor; +/** + * Strategy interface to determine the {@link Executor} to use for the gRPC server. + * + * @author Chris Bono + * @since 4.0.0 + */ +@FunctionalInterface public interface GrpcServerExecutorProvider { /** - * Returns a {@link Executor} for the gRPC server, if it needs tio be customized. + * Returns a {@link Executor} for the gRPC server, if it needs to be customized. * @return the executor to use for the gRPC server */ Executor getExecutor(); diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java index ca31400c26ee..3065640510ca 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java @@ -78,7 +78,7 @@ ShadedNettyGrpcServerFactory shadedNettyGrpcServerFactory(GrpcServerProperties p .of(mapper::customizeServerBuilder, serverBuilderCustomizers::customize); KeyManagerFactory keyManager = null; TrustManagerFactory trustManager = null; - if (properties.getSsl().isEnabled()) { + if (properties.getSsl().determineEnabled()) { String bundleName = properties.getSsl().getBundle(); Assert.notNull(bundleName, () -> "SSL bundleName must not be null"); SslBundle bundle = bundles.getBundle(bundleName); @@ -86,7 +86,7 @@ ShadedNettyGrpcServerFactory shadedNettyGrpcServerFactory(GrpcServerProperties p trustManager = properties.getSsl().isSecure() ? bundle.getManagers().getTrustManagerFactory() : io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory.INSTANCE; } - ShadedNettyGrpcServerFactory factory = new ShadedNettyGrpcServerFactory(properties.getAddress(), + ShadedNettyGrpcServerFactory factory = new ShadedNettyGrpcServerFactory(properties.determineAddress(), builderCustomizers, keyManager, trustManager, properties.getSsl().getClientAuth()); applyServerFactoryCustomizers(customizers, factory); serviceDiscoverer.findServices() @@ -124,7 +124,7 @@ NettyGrpcServerFactory nettyGrpcServerFactory(GrpcServerProperties properties, .of(mapper::customizeServerBuilder, serverBuilderCustomizers::customize); KeyManagerFactory keyManager = null; TrustManagerFactory trustManager = null; - if (properties.getSsl().isEnabled()) { + if (properties.getSsl().determineEnabled()) { String bundleName = properties.getSsl().getBundle(); Assert.notNull(bundleName, () -> "SSL bundleName must not be null"); SslBundle bundle = bundles.getBundle(bundleName); @@ -132,8 +132,8 @@ NettyGrpcServerFactory nettyGrpcServerFactory(GrpcServerProperties properties, trustManager = properties.getSsl().isSecure() ? bundle.getManagers().getTrustManagerFactory() : InsecureTrustManagerFactory.INSTANCE; } - NettyGrpcServerFactory factory = new NettyGrpcServerFactory(properties.getAddress(), builderCustomizers, - keyManager, trustManager, properties.getSsl().getClientAuth()); + NettyGrpcServerFactory factory = new NettyGrpcServerFactory(properties.determineAddress(), + builderCustomizers, keyManager, trustManager, properties.getSsl().getClientAuth()); applyServerFactoryCustomizers(customizers, factory); serviceDiscoverer.findServices() .stream() diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java index 95a3f385f0da..1965c1cc10e9 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java @@ -18,6 +18,14 @@ import org.springframework.grpc.server.GrpcServerFactory; +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link GrpcServerFactory server factory} before it is fully initialized. + * + * @author Chris Bono + * @since 4.0.0 + */ +@FunctionalInterface public interface GrpcServerFactoryCustomizer { /** diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java index c4140cbacfc6..bfa5573fc485 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java @@ -40,23 +40,21 @@ public class GrpcServerProperties { public static final String ANY_IP_ADDRESS = "*"; /** - * Server address to bind to. The default is any IP address ('*'). + * The address to bind to in the form 'host:port' or a pseudo URL like + * 'static://host:port'. When the address is set it takes precedence over any + * configured host/port values. */ - private String host = ANY_IP_ADDRESS; + private @Nullable String address; /** - * Server port to listen on. When the value is 0, a random available port is selected. - * The default is 9090. + * Server host to bind to. The default is any IP address ('*'). */ - private int port = GrpcUtils.DEFAULT_PORT; + private String host = ANY_IP_ADDRESS; /** - * Maximum time to wait for the server to gracefully shutdown. When the value is - * negative, the server waits forever. When the value is 0, the server will force - * shutdown immediately. The default is 30 seconds. + * Server port to listen on. When the value is 0, a random available port is selected. */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration shutdownGracePeriod = Duration.ofSeconds(30); + private int port = GrpcUtils.DEFAULT_PORT; /** * Maximum message size allowed to be received by the server (default 4MiB). @@ -70,30 +68,37 @@ public class GrpcServerProperties { @DataSizeUnit(DataUnit.BYTES) private DataSize maxInboundMetadataSize = DataSize.ofBytes(8192); - private final Health health = new Health(); + /** + * Maximum time to wait for the server to gracefully shutdown. When the value is + * negative, the server waits forever. When the value is 0, the server will force + * shutdown immediately. The default is 30 seconds. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration shutdownGracePeriod = Duration.ofSeconds(30); - private final KeepAlive keepAlive = new KeepAlive(); + private final Health health = new Health(); private final Inprocess inprocess = new Inprocess(); - /** - * The address to bind to. could be a host:port combination or a pseudo URL like - * static://host:port. Can not be set if host or port are set independently. - */ - @Nullable private String address; + private final KeepAlive keepAlive = new KeepAlive(); - public String getAddress() { - return (this.address != null) ? this.address : this.host + ":" + this.port; + private final Ssl ssl = new Ssl(); + + public @Nullable String getAddress() { + return this.address; } - public void setAddress(String address) { + public void setAddress(@Nullable String address) { this.address = address; } - private final Ssl ssl = new Ssl(); - - public Ssl getSsl() { - return this.ssl; + /** + * Returns the configured address or an address created from the configured host and + * port if no address has been set. + * @return the address to bind to + */ + public String determineAddress() { + return (this.address != null) ? this.address : this.host + ":" + this.port; } public String getHost() { @@ -101,34 +106,17 @@ public String getHost() { } public void setHost(String host) { - if (this.address != null) { - throw new IllegalStateException("Cannot set host when address is already set"); - } this.host = host; } public int getPort() { - if (this.address != null) { - return GrpcUtils.getPort(this.address); - } return this.port; } public void setPort(int port) { - if (this.address != null) { - throw new IllegalStateException("Cannot set port when address is already set"); - } this.port = port; } - public @Nullable Duration getShutdownGracePeriod() { - return this.shutdownGracePeriod; - } - - public void setShutdownGracePeriod(Duration shutdownGracePeriod) { - this.shutdownGracePeriod = shutdownGracePeriod; - } - public DataSize getMaxInboundMessageSize() { return this.maxInboundMessageSize; } @@ -145,16 +133,28 @@ public void setMaxInboundMetadataSize(DataSize maxInboundMetadataSize) { this.maxInboundMetadataSize = maxInboundMetadataSize; } + public Duration getShutdownGracePeriod() { + return this.shutdownGracePeriod; + } + + public void setShutdownGracePeriod(Duration shutdownGracePeriod) { + this.shutdownGracePeriod = shutdownGracePeriod; + } + public Health getHealth() { return this.health; } + public Inprocess getInprocess() { + return this.inprocess; + } + public KeepAlive getKeepAlive() { return this.keepAlive; } - public Inprocess getInprocess() { - return this.inprocess; + public Ssl getSsl() { + return this.ssl; } public static class Health { @@ -162,36 +162,36 @@ public static class Health { /** * Whether to auto-configure Health feature on the gRPC server. */ - private Boolean enabled = true; + private boolean enabled = true; - private final ActuatorAdapt actuator = new ActuatorAdapt(); + private final Actuator actuator = new Actuator(); - public Boolean getEnabled() { + public boolean getEnabled() { return this.enabled; } - public void setEnabled(Boolean enabled) { + public void setEnabled(boolean enabled) { this.enabled = enabled; } - public ActuatorAdapt getActuator() { + public Actuator getActuator() { return this.actuator; } } - public static class ActuatorAdapt { + public static class Actuator { /** * Whether to adapt Actuator health indicators into gRPC health checks. */ - private Boolean enabled = true; + private boolean enabled = true; /** * Whether to update the overall gRPC server health (the '' service) with the * aggregate status of the configured health indicators. */ - private Boolean updateOverallHealth = true; + private boolean updateOverallHealth = true; /** * How often to update the health status. @@ -208,19 +208,19 @@ public static class ActuatorAdapt { */ private List healthIndicatorPaths = new ArrayList<>(); - public Boolean getEnabled() { + public boolean getEnabled() { return this.enabled; } - public void setEnabled(Boolean enabled) { + public void setEnabled(boolean enabled) { this.enabled = enabled; } - public Boolean getUpdateOverallHealth() { + public boolean getUpdateOverallHealth() { return this.updateOverallHealth; } - public void setUpdateOverallHealth(Boolean updateOverallHealth) { + public void setUpdateOverallHealth(boolean updateOverallHealth) { this.updateOverallHealth = updateOverallHealth; } @@ -250,6 +250,23 @@ public void setHealthIndicatorPaths(List healthIndicatorPaths) { } + public static class Inprocess { + + /** + * The name of the in-process server or null to not start the in-process server. + */ + private @Nullable String name; + + public @Nullable String getName() { + return this.name; + } + + public void setName(@Nullable String name) { + this.name = name; + } + + } + public static class KeepAlive { /** @@ -271,20 +288,20 @@ public static class KeepAlive { * (default infinite). */ @DurationUnit(ChronoUnit.SECONDS) - private @Nullable Duration maxIdle = null; + private @Nullable Duration maxIdle; /** * Maximum time a connection may exist before being gracefully terminated (default * infinite). */ @DurationUnit(ChronoUnit.SECONDS) - private @Nullable Duration maxAge = null; + private @Nullable Duration maxAge; /** * Maximum time for graceful connection termination (default infinite). */ @DurationUnit(ChronoUnit.SECONDS) - private @Nullable Duration maxAgeGrace = null; + private @Nullable Duration maxAgeGrace; /** * Maximum keep-alive time clients are permitted to configure (default 5m). @@ -296,13 +313,13 @@ public static class KeepAlive { * Whether clients are permitted to send keep alive pings when there are no * outstanding RPCs on the connection (default false). */ - private boolean permitWithoutCalls = false; + private boolean permitWithoutCalls; public @Nullable Duration getTime() { return this.time; } - public void setTime(Duration time) { + public void setTime(@Nullable Duration time) { this.time = time; } @@ -310,7 +327,7 @@ public void setTime(Duration time) { return this.timeout; } - public void setTimeout(Duration timeout) { + public void setTimeout(@Nullable Duration timeout) { this.timeout = timeout; } @@ -318,7 +335,7 @@ public void setTimeout(Duration timeout) { return this.maxIdle; } - public void setMaxIdle(Duration maxIdle) { + public void setMaxIdle(@Nullable Duration maxIdle) { this.maxIdle = maxIdle; } @@ -326,7 +343,7 @@ public void setMaxIdle(Duration maxIdle) { return this.maxAge; } - public void setMaxAge(Duration maxAge) { + public void setMaxAge(@Nullable Duration maxAge) { this.maxAge = maxAge; } @@ -334,7 +351,7 @@ public void setMaxAge(Duration maxAge) { return this.maxAgeGrace; } - public void setMaxAgeGrace(Duration maxAgeGrace) { + public void setMaxAgeGrace(@Nullable Duration maxAgeGrace) { this.maxAgeGrace = maxAgeGrace; } @@ -342,7 +359,7 @@ public void setMaxAgeGrace(Duration maxAgeGrace) { return this.permitTime; } - public void setPermitTime(Duration permitTime) { + public void setPermitTime(@Nullable Duration permitTime) { this.permitTime = permitTime; } @@ -359,8 +376,7 @@ public void setPermitWithoutCalls(boolean permitWithoutCalls) { public static class Ssl { /** - * Whether to enable SSL support. Enabled automatically if "bundle" is provided - * unless specified otherwise. + * Whether to enable SSL support. */ private @Nullable Boolean enabled; @@ -380,29 +396,29 @@ public static class Ssl { */ private boolean secure = true; - public boolean isEnabled() { - return (this.enabled != null) ? this.enabled : this.bundle != null; + public @Nullable Boolean getEnabled() { + return this.enabled; } - public void copyDefaultsFrom(Ssl config) { - if (this.enabled == null) { - this.enabled = config.enabled; - } - if (this.bundle == null) { - this.bundle = config.bundle; - } - + public void setEnabled(@Nullable Boolean enabled) { + this.enabled = enabled; } - public void setEnabled(boolean enabled) { - this.enabled = enabled; + /** + * Determine whether to enable SSL support. When the {@code enabled} property is + * specified it determines enablement. Otherwise, the support is enabled if the + * {@code bundle} is provided. + * @return whether to enable SSL support + */ + public boolean determineEnabled() { + return (this.enabled != null) ? this.enabled : this.bundle != null; } public @Nullable String getBundle() { return this.bundle; } - public void setBundle(String bundle) { + public void setBundle(@Nullable String bundle) { this.bundle = bundle; } @@ -424,35 +440,4 @@ public boolean isSecure() { } - public static class Inprocess { - - /** - * The name of the in-process server or null to not start the in-process server. - */ - private @Nullable String name; - - /** - * Whether the inprocess server factory should be the only server factory - * available. When the value is true no other server factory will be configured. - */ - private @Nullable Boolean exclusive; - - public @Nullable String getName() { - return this.name; - } - - public void setName(String name) { - this.name = name; - } - - public @Nullable Boolean getExclusive() { - return this.exclusive; - } - - public void setExclusive(Boolean exclusive) { - this.exclusive = exclusive; - } - - } - } diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java index 973414a9f109..7af249e3fae7 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java @@ -30,9 +30,8 @@ * given {@link ServerBuilder}. * * @author Chris Bono - * @since 4.0.0 */ -public class ServerBuilderCustomizers { +class ServerBuilderCustomizers { private final List> customizers; diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.java index 923c67d3c66b..c3a345780c92 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.java @@ -24,7 +24,13 @@ import org.springframework.core.env.MapPropertySource; import org.springframework.util.ClassUtils; -public class ServletEnvironmentPostProcessor implements EnvironmentPostProcessor { +/** + * An {@link EnvironmentPostProcessor} that sets the {@code server.http2.enabled} property + * to {@code true} when {@code io.grpc.servlet.jakarta.GrpcServlet} is on the classpath. + * + * @author Dave Syer + */ +class ServletEnvironmentPostProcessor implements EnvironmentPostProcessor { private static final boolean SERVLET_AVAILABLE = ClassUtils.isPresent("io.grpc.servlet.jakarta.GrpcServlet", null); diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/package-info.java deleted file mode 100644 index 918b696933f2..000000000000 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/codec/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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. - */ - -/** - * Auto-configuration for gRPC server codecs. - */ -@NullMarked -package org.springframework.boot.grpc.server.autoconfigure.codec; - -import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfigurationTests.java similarity index 95% rename from module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfigurationTests.java rename to module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfigurationTests.java index c2338d5974c7..20dc047258ac 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/codec/GrpcCodecConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.grpc.server.autoconfigure.codec; +package org.springframework.boot.grpc.server.autoconfigure; import io.grpc.CompressorRegistry; import io.grpc.DecompressorRegistry; diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java index 262d94efc645..22423827e8e8 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java @@ -23,13 +23,11 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.boot.context.properties.bind.BindException; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.util.unit.DataSize; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link GrpcServerProperties}. @@ -54,8 +52,10 @@ void bind() { map.put("spring.grpc.server.port", "3130"); map.put("spring.grpc.server.shutdown-grace-period", "15"); GrpcServerProperties properties = bindProperties(map); - assertThat(properties.getAddress()).isEqualTo("my-server-ip:3130"); + assertThat(properties.getHost()).isEqualTo("my-server-ip"); assertThat(properties.getPort()).isEqualTo(3130); + assertThat(properties.getAddress()).isNull(); + assertThat(properties.determineAddress()).isEqualTo("my-server-ip:3130"); assertThat(properties.getShutdownGracePeriod()).isEqualTo(Duration.ofSeconds(15)); } @@ -174,15 +174,17 @@ void bind() { map.put("spring.grpc.server.address", "my-server-ip:3130"); GrpcServerProperties properties = bindProperties(map); assertThat(properties.getAddress()).isEqualTo("my-server-ip:3130"); - assertThat(properties.getPort()).isEqualTo(3130); + assertThat(properties.determineAddress()).isEqualTo("my-server-ip:3130"); } @Test - void illegalBecauseAddressAndPortSpecified() { + void addressTakesPrecedenceOverHostAndPort() { Map map = new HashMap<>(); map.put("spring.grpc.server.address", "my-server-ip:3130"); + map.put("spring.grpc.server.host", "foo"); map.put("spring.grpc.server.port", "10000"); - assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindProperties(map)); + GrpcServerProperties properties = bindProperties(map); + assertThat(properties.getAddress()).isEqualTo("my-server-ip:3130"); } } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java index 31446ca114d0..4b92003e1140 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java @@ -19,7 +19,6 @@ import java.util.Arrays; import io.grpc.BindableService; -import io.grpc.ServerServiceDefinition; import io.grpc.protobuf.services.HealthStatusManager; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; @@ -30,22 +29,15 @@ import org.springframework.boot.actuate.health.HealthEndpoint; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration; -import org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration; -import org.springframework.boot.grpc.server.autoconfigure.ServerBuilderCustomizers; import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration.ActuatorHealthAdapterConfiguration; import org.springframework.boot.health.autoconfigure.contributor.HealthContributorAutoConfiguration; import org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration; -import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; -import org.springframework.grpc.server.service.GrpcServiceConfigurer; -import org.springframework.grpc.server.service.GrpcServiceDiscoverer; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** @@ -122,23 +114,6 @@ void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() { .run((context) -> assertThat(context).hasSingleBean(GrpcServerHealthAutoConfiguration.class)); } - @Test - void healthIsAutoConfiguredBeforeGrpcServerFactory() { - BindableService service = mock(); - ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); - given(service.bindService()).willReturn(serviceDefinition); - this.contextRunner() - .withConfiguration(AutoConfigurations.of(GrpcServerFactoryAutoConfiguration.class)) - .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) - .withBean("serverBuilderCustomizers", ServerBuilderCustomizers.class, Mockito::mock) - .withBean("grpcServicesDiscoverer", GrpcServiceDiscoverer.class, Mockito::mock) - .withBean("grpcServiceConfigurer", GrpcServiceConfigurer.class, Mockito::mock) - .withBean("sslBundles", SslBundles.class, Mockito::mock) - .withPropertyValues("spring.grpc.server.port=0") - .run((context) -> assertThatBeanDefinitionsContainInOrder(context, GrpcServerHealthAutoConfiguration.class, - GrpcServerFactoryAutoConfiguration.class)); - } - @Disabled("Will be tested in an integration test once the Actuator adapter is implemented") @Test void enterTerminalStateIsCalledWhenStatusManagerIsStopped() { From c2322138d2ddf1153596819b5f68f33ee7055a63 Mon Sep 17 00:00:00 2001 From: onobc Date: Fri, 26 Sep 2025 16:37:52 -0500 Subject: [PATCH 10/21] Incorporate feedback from server code review Consolidate the conditional annotations into an aggregate that checks for server and service enabled as well as one that checks that Spring gRPC is on the classpath. Signed-off-by: onobc --- .../ConditionalOnGrpcServerEnabled.java | 22 +-- .../ConditionalOnSpringGrpc.java | 43 ++++++ .../GrpcServerAutoConfiguration.java | 3 +- .../GrpcServerFactoryAutoConfiguration.java | 4 +- ...rpcServerObservationAutoConfiguration.java | 8 +- ...GrpcServerReflectionAutoConfiguration.java | 8 +- .../OnEnabledGrpcServerCondition.java | 73 +++++++++ ...GrpcExceptionHandlerAutoConfiguration.java | 12 +- .../GrpcServerHealthAutoConfiguration.java | 12 +- .../GrpcSecurityAutoConfiguration.java | 5 +- .../OAuth2ClientAutoConfiguration.java | 7 +- ...OAuth2ResourceServerAutoConfiguration.java | 7 +- .../GrpcServerAutoConfigurationTests.java | 7 + ...rverObservationAutoConfigurationTests.java | 16 ++ ...erverReflectionAutoConfigurationTests.java | 16 ++ .../GrpcServletAutoConfigurationTests.java | 8 + .../OnEnabledGrpcServerConditionTests.java | 146 ++++++++++++++++++ ...xceptionHandlerAutoConfigurationTests.java | 6 +- ...rpcServerHealthAutoConfigurationTests.java | 15 ++ 19 files changed, 366 insertions(+), 52 deletions(-) create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnSpringGrpc.java create mode 100644 module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerCondition.java create mode 100644 module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerConditionTests.java diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java index de80378dcc5a..9d47c9f0bdb7 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java @@ -21,16 +21,15 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import io.grpc.BindableService; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Conditional; /** - * {@link Conditional @Conditional} that only matches when the - * {@code io.grpc.BindableService} class is on the classpath and the - * {@code spring.grpc.server.enabled} property is not explicitly set to {@code false}. + * {@link Conditional @Conditional} that checks whether the gRPC server and optionally a + * specific gRPC service is enabled. It matches if the value of the + * {@code spring.grpc.server.enabled} property is not explicitly set to {@code false} and + * if the {@link #value() gRPC service name} is set, that the + * {@code spring.grpc.server..enabled} property is not explicitly set to + * {@code false}. * * @author Freeman Freeman * @author Chris Bono @@ -38,8 +37,13 @@ */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) -@ConditionalOnClass(BindableService.class) -@ConditionalOnProperty(prefix = "spring.grpc.server", name = "enabled", matchIfMissing = true) +@Conditional(OnEnabledGrpcServerCondition.class) public @interface ConditionalOnGrpcServerEnabled { + /** + * Name of the gRPC service. + * @return the name of the gRPC service + */ + String value() default ""; + } diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnSpringGrpc.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnSpringGrpc.java new file mode 100644 index 000000000000..0e48757af410 --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnSpringGrpc.java @@ -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.grpc.server.autoconfigure; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.grpc.BindableService; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Conditional; +import org.springframework.grpc.server.GrpcServerFactory; + +/** + * {@link Conditional @Conditional} that only matches when Spring gRPC is on the classpath + * (i.e. {@link BindableService} and {@link GrpcServerFactory} are on the classpath). + * + * @author Freeman Freeman + * @author Chris Bono + * @since 4.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class }) +public @interface ConditionalOnSpringGrpc { + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java index 0ed662c98aef..697f54af5c98 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java @@ -33,7 +33,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; -import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.ServerBuilderCustomizer; import org.springframework.grpc.server.exception.ReactiveStubBeanDefinitionRegistrar; import org.springframework.grpc.server.service.DefaultGrpcServiceConfigurer; @@ -53,8 +52,8 @@ * @since 4.0.0 */ @AutoConfiguration(after = GrpcServerFactoryAutoConfiguration.class) +@ConditionalOnSpringGrpc @ConditionalOnGrpcServerEnabled -@ConditionalOnClass({ GrpcServerFactory.class }) @ConditionalOnBean(BindableService.class) @EnableConfigurationProperties(GrpcServerProperties.class) @Import({ GrpcCodecConfiguration.class }) diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java index 6572c52343ee..76cafd55d012 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java @@ -28,14 +28,12 @@ import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; -import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.service.GrpcServiceConfigurer; import org.springframework.grpc.server.service.GrpcServiceDiscoverer; import org.springframework.util.unit.DataSize; @@ -53,8 +51,8 @@ */ @AutoConfiguration @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +@ConditionalOnSpringGrpc @ConditionalOnGrpcServerEnabled -@ConditionalOnClass(GrpcServerFactory.class) @ConditionalOnBean(BindableService.class) public final class GrpcServerFactoryAutoConfiguration { diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java index a8dedd52637d..b0739440f393 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java @@ -24,12 +24,10 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.grpc.server.GlobalServerInterceptor; -import org.springframework.grpc.server.GrpcServerFactory; /** * {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side observations. @@ -41,10 +39,10 @@ */ @AutoConfiguration( afterName = "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration") -@ConditionalOnGrpcServerEnabled -@ConditionalOnClass({ GrpcServerFactory.class, ObservationRegistry.class, ObservationGrpcServerInterceptor.class }) +@ConditionalOnSpringGrpc +@ConditionalOnClass({ ObservationRegistry.class, ObservationGrpcServerInterceptor.class }) +@ConditionalOnGrpcServerEnabled("observation") @ConditionalOnBean(ObservationRegistry.class) -@ConditionalOnProperty(name = "spring.grpc.server.observation.enabled", havingValue = "true", matchIfMissing = true) public final class GrpcServerObservationAutoConfiguration { @Bean diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.java index c3c1ed0f960c..fffd3e690638 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.java @@ -23,9 +23,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; -import org.springframework.grpc.server.GrpcServerFactory; /** * {@link EnableAutoConfiguration Auto-configuration} for gRPC Reflection service @@ -40,10 +38,10 @@ * @since 4.0.0 */ @AutoConfiguration(before = GrpcServerFactoryAutoConfiguration.class) -@ConditionalOnGrpcServerEnabled -@ConditionalOnClass({ GrpcServerFactory.class, ProtoReflectionServiceV1.class }) +@ConditionalOnSpringGrpc +@ConditionalOnClass({ ProtoReflectionServiceV1.class }) +@ConditionalOnGrpcServerEnabled("reflection") @ConditionalOnBean(BindableService.class) -@ConditionalOnProperty(name = "spring.grpc.server.reflection.enabled", havingValue = "true", matchIfMissing = true) public final class GrpcServerReflectionAutoConfiguration { @Bean diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerCondition.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerCondition.java new file mode 100644 index 000000000000..913ed012ba5d --- /dev/null +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerCondition.java @@ -0,0 +1,73 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.StringUtils; + +/** + * {@link SpringBootCondition} to check whether gRPC server/service is enabled. + * + * @author Chris Bono + * @see ConditionalOnGrpcServerEnabled + */ +class OnEnabledGrpcServerCondition extends SpringBootCondition { + + private static final String SERVER_PROPERTY = "spring.grpc.server.enabled"; + + private static final String SERVICE_PROPERTY = "spring.grpc.server.%s.enabled"; + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + Boolean serverEnabled = context.getEnvironment().getProperty(SERVER_PROPERTY, Boolean.class); + if (serverEnabled != null && !serverEnabled) { + return new ConditionOutcome(serverEnabled, + ConditionMessage.forCondition(ConditionalOnGrpcServerEnabled.class) + .because(SERVER_PROPERTY + " is " + serverEnabled)); + } + String serviceName = getServiceName(metadata); + if (StringUtils.hasLength(serviceName)) { + Boolean serviceEnabled = context.getEnvironment() + .getProperty(SERVICE_PROPERTY.formatted(serviceName), Boolean.class); + if (serviceEnabled != null) { + return new ConditionOutcome(serviceEnabled, + ConditionMessage.forCondition(ConditionalOnGrpcServerEnabled.class) + .because(SERVICE_PROPERTY.formatted(serviceName) + " is " + serviceEnabled)); + } + } + return ConditionOutcome.match(ConditionMessage.forCondition(ConditionalOnGrpcServerEnabled.class) + .because("server and service are enabled by default")); + } + + private static @Nullable String getServiceName(AnnotatedTypeMetadata metadata) { + Map attributes = metadata + .getAnnotationAttributes(ConditionalOnGrpcServerEnabled.class.getName()); + if (attributes == null) { + return null; + } + return (String) attributes.get("value"); + } + +} diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java index c388c6d73e73..8cb0ed97c774 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java @@ -16,19 +16,15 @@ package org.springframework.boot.grpc.server.autoconfigure.exception; -import io.grpc.Grpc; - import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnGrpcServerEnabled; +import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnSpringGrpc; import org.springframework.context.annotation.Bean; import org.springframework.grpc.server.GlobalServerInterceptor; -import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.exception.CompositeGrpcExceptionHandler; import org.springframework.grpc.server.exception.GrpcExceptionHandler; import org.springframework.grpc.server.exception.GrpcExceptionHandlerInterceptor; @@ -42,12 +38,10 @@ * @since 4.0.0 */ @AutoConfiguration -@ConditionalOnGrpcServerEnabled -@ConditionalOnClass({ Grpc.class, GrpcServerFactory.class }) +@ConditionalOnSpringGrpc +@ConditionalOnGrpcServerEnabled("exception-handler") @ConditionalOnBean(GrpcExceptionHandler.class) @ConditionalOnMissingBean(GrpcExceptionHandlerInterceptor.class) -@ConditionalOnProperty(prefix = "spring.grpc.server.exception-handler", name = "enabled", havingValue = "true", - matchIfMissing = true) public final class GrpcExceptionHandlerAutoConfiguration { @GlobalServerInterceptor diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java index abcdd1b62eba..c94454e6aed0 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java @@ -32,7 +32,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -40,6 +39,7 @@ import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnGrpcServerEnabled; +import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnSpringGrpc; import org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration; import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties; import org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder; @@ -48,7 +48,6 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.core.type.AnnotatedTypeMetadata; -import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.scheduling.annotation.EnableScheduling; /** @@ -59,10 +58,10 @@ * @since 4.0.0 */ @AutoConfiguration(before = GrpcServerFactoryAutoConfiguration.class) -@ConditionalOnGrpcServerEnabled -@ConditionalOnClass({ GrpcServerFactory.class, HealthStatusManager.class }) +@ConditionalOnSpringGrpc +@ConditionalOnClass(HealthStatusManager.class) +@ConditionalOnGrpcServerEnabled("health") @ConditionalOnBean(BindableService.class) -@ConditionalOnProperty(name = "spring.grpc.server.health.enabled", havingValue = "true", matchIfMissing = true) public final class GrpcServerHealthAutoConfiguration { @Bean(destroyMethod = "enterTerminalState") @@ -81,8 +80,7 @@ BindableService grpcHealthService(HealthStatusManager healthStatusManager) { @ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class) @AutoConfigureAfter(value = TaskSchedulingAutoConfiguration.class, name = "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration") - @ConditionalOnProperty(name = "spring.grpc.server.health.actuator.enabled", havingValue = "true", - matchIfMissing = true) + @ConditionalOnGrpcServerEnabled("health.actuator") @Conditional(OnHealthIndicatorPathsCondition.class) @EnableConfigurationProperties(GrpcServerProperties.class) @EnableScheduling diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java index 6524f6559673..d8ffa7a8a423 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java @@ -26,6 +26,7 @@ import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnGrpcNativeServer; import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnGrpcServerEnabled; import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnGrpcServletServer; +import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnSpringGrpc; import org.springframework.boot.grpc.server.autoconfigure.GrpcServerExecutorProvider; import org.springframework.boot.grpc.server.autoconfigure.exception.GrpcExceptionHandlerAutoConfiguration; import org.springframework.boot.grpc.server.autoconfigure.security.GrpcSecurityAutoConfiguration.ExceptionHandlerConfiguration; @@ -36,7 +37,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.grpc.server.GlobalServerInterceptor; -import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.exception.GrpcExceptionHandler; import org.springframework.grpc.server.security.GrpcSecurity; import org.springframework.grpc.server.security.SecurityContextServerInterceptor; @@ -57,8 +57,9 @@ */ @AutoConfiguration(before = GrpcExceptionHandlerAutoConfiguration.class, afterName = "org.springframework.boot.security.autoconfigure.servlet.SecurityAutoConfiguration") +@ConditionalOnSpringGrpc +@ConditionalOnClass(ObjectPostProcessor.class) @ConditionalOnGrpcServerEnabled -@ConditionalOnClass({ GrpcServerFactory.class, ObjectPostProcessor.class }) @Import({ ExceptionHandlerConfiguration.class, GrpcNativeSecurityConfigurerConfiguration.class, GrpcServletSecurityConfigurerConfiguration.class }) public final class GrpcSecurityAutoConfiguration { diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ClientAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ClientAutoConfiguration.java index f4b272902a26..d4c64c088cb0 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ClientAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ClientAutoConfiguration.java @@ -19,18 +19,16 @@ import java.util.ArrayList; import java.util.List; -import io.grpc.BindableService; - import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnSpringGrpc; import org.springframework.boot.security.oauth2.client.autoconfigure.ConditionalOnOAuth2ClientRegistrationProperties; import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties; import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientPropertiesMapper; import org.springframework.context.annotation.Bean; -import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; @@ -45,7 +43,8 @@ // https://github.com/spring-projects/spring-boot/issues/15877) @AutoConfiguration( afterName = "org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientAutoConfiguration") -@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, InMemoryClientRegistrationRepository.class }) +@ConditionalOnSpringGrpc +@ConditionalOnClass(InMemoryClientRegistrationRepository.class) @ConditionalOnOAuth2ClientRegistrationProperties @EnableConfigurationProperties(OAuth2ClientProperties.class) public final class OAuth2ClientAutoConfiguration { diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java index 9480aea41611..ceee64aab1f6 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.grpc.server.autoconfigure.ConditionalOnSpringGrpc; import org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration; import org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration.GrpcServletConfiguration; import org.springframework.boot.grpc.server.autoconfigure.security.OAuth2ResourceServerAutoConfiguration.Oauth2ResourceServerConfiguration; @@ -49,7 +50,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.grpc.server.GlobalServerInterceptor; -import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.security.AuthenticationProcessInterceptor; import org.springframework.grpc.server.security.GrpcSecurity; import org.springframework.security.config.ObjectPostProcessor; @@ -89,8 +89,9 @@ "org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration" }, after = { GrpcSecurityAutoConfiguration.class, GrpcServerFactoryAutoConfiguration.class }) @EnableConfigurationProperties(OAuth2ResourceServerProperties.class) -@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class, InMemoryClientRegistrationRepository.class, - BearerTokenAuthenticationToken.class, ObjectPostProcessor.class }) +@ConditionalOnSpringGrpc +@ConditionalOnClass({ InMemoryClientRegistrationRepository.class, BearerTokenAuthenticationToken.class, + ObjectPostProcessor.class }) @ConditionalOnMissingBean(GrpcServletConfiguration.class) @ConditionalOnBean({ BindableService.class, GrpcSecurityAutoConfiguration.class }) @Import({ Oauth2ResourceServerConfiguration.JwtConfiguration.class, diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java index a27067e48054..41b6e2bfb9f6 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java @@ -123,6 +123,13 @@ void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class)); } + @Test + void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class)); + } + @Test void whenNoBindableServicesRegisteredAutoConfigurationIsSkipped() { new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class)) diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java index 015efde7fb39..336724257976 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; +import io.grpc.BindableService; import io.grpc.ServerInterceptor; import io.micrometer.core.instrument.binder.grpc.ObservationGrpcServerInterceptor; import io.micrometer.observation.ObservationRegistry; @@ -30,6 +31,7 @@ import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.GrpcServerFactory; import static org.assertj.core.api.Assertions.assertThat; @@ -47,6 +49,20 @@ private ApplicationContextRunner validContextRunner() { .withBean("observationRegistry", ObservationRegistry.class, Mockito::mock); } + @Test + void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.validContextRunner() + .withClassLoader(new FilteredClassLoader(BindableService.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + + @Test + void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.validContextRunner() + .withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerObservationAutoConfiguration.class)); + } + @Test void whenObservationRegistryNotOnClasspathAutoConfigSkipped() { this.validContextRunner() diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfigurationTests.java index b8328a81e25e..c416e70586a9 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfigurationTests.java @@ -21,7 +21,9 @@ import org.mockito.Mockito; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; import static org.assertj.core.api.Assertions.assertThat; @@ -47,6 +49,20 @@ void whenAutoConfigurationIsNotSkippedThenCreatesReflectionServiceBean() { this.contextRunner().run((context) -> assertThat(context).hasBean("serverReflection")); } + @Test + void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(BindableService.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerReflectionAutoConfiguration.class)); + } + + @Test + void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerReflectionAutoConfiguration.class)); + } + @Test void whenNoBindableServiceDefinedThenAutoConfigurationIsSkipped() { new ApplicationContextRunner() diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServletAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServletAutoConfigurationTests.java index d66bb67f640c..ffbd424a4eb1 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServletAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServletAutoConfigurationTests.java @@ -30,6 +30,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.ServerBuilderCustomizer; import org.springframework.util.unit.DataSize; @@ -65,6 +66,13 @@ void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { .doesNotHaveBean(ServletRegistrationBean.class)); } + @Test + void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServletConfiguration.class)); + } + @Test void whenNoBindableServicesRegisteredAutoConfigurationIsSkipped() { new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class)) diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerConditionTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerConditionTests.java new file mode 100644 index 000000000000..531be45103ff --- /dev/null +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerConditionTests.java @@ -0,0 +1,146 @@ +/* + * 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.grpc.server.autoconfigure; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link OnEnabledGrpcServerCondition}. + * + * @author Chris Bono + */ +class OnEnabledGrpcServerConditionTests { + + @Test + void shouldMatchIfNoPropertyIsSet() { + OnEnabledGrpcServerCondition condition = new OnEnabledGrpcServerCondition(); + ConditionOutcome outcome = condition.getMatchOutcome(mockConditionContext(), mockMetadata("")); + assertThat(outcome.isMatch()).isTrue(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnGrpcServerEnabled server and service are enabled by default"); + } + + @Test + void shouldMatchIfOnlyGlobalPropertyIsSetAndIsTrue() { + OnEnabledGrpcServerCondition condition = new OnEnabledGrpcServerCondition(); + ConditionOutcome outcome = condition + .getMatchOutcome(mockConditionContext(Map.of("spring.grpc.server.enabled", "true")), mockMetadata("")); + assertThat(outcome.isMatch()).isTrue(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnGrpcServerEnabled server and service are enabled by default"); + } + + @Test + void shouldNotMatchIfOnlyGlobalPropertyIsSetAndIsFalse() { + OnEnabledGrpcServerCondition condition = new OnEnabledGrpcServerCondition(); + ConditionOutcome outcome = condition + .getMatchOutcome(mockConditionContext(Map.of("spring.grpc.server.enabled", "false")), mockMetadata("")); + assertThat(outcome.isMatch()).isFalse(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnGrpcServerEnabled spring.grpc.server.enabled is false"); + } + + @Test + void shouldMatchIfOnlyServicePropertyIsSetAndIsTrue() { + OnEnabledGrpcServerCondition condition = new OnEnabledGrpcServerCondition(); + ConditionOutcome outcome = condition.getMatchOutcome( + mockConditionContext(Map.of("spring.grpc.server.myservice.enabled", "true")), + mockMetadata("myservice")); + assertThat(outcome.isMatch()).isTrue(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnGrpcServerEnabled spring.grpc.server.myservice.enabled is true"); + } + + @Test + void shouldNotMatchIfOnlyServicePropertyIsSetAndIsFalse() { + OnEnabledGrpcServerCondition condition = new OnEnabledGrpcServerCondition(); + ConditionOutcome outcome = condition.getMatchOutcome( + mockConditionContext(Map.of("spring.grpc.server.myservice.enabled", "false")), + mockMetadata("myservice")); + assertThat(outcome.isMatch()).isFalse(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnGrpcServerEnabled spring.grpc.server.myservice.enabled is false"); + } + + @Test + void shouldMatchIfGlobalPropertyIsTrueAndServicePropertyIsTrue() { + OnEnabledGrpcServerCondition condition = new OnEnabledGrpcServerCondition(); + ConditionOutcome outcome = condition.getMatchOutcome( + mockConditionContext( + Map.of("spring.grpc.server.enabled", "true", "spring.grpc.server.myservice.enabled", "true")), + mockMetadata("myservice")); + assertThat(outcome.isMatch()).isTrue(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnGrpcServerEnabled spring.grpc.server.myservice.enabled is true"); + } + + @Test + void shouldNotMatchIfGlobalPropertyIsTrueAndServicePropertyIsFalse() { + OnEnabledGrpcServerCondition condition = new OnEnabledGrpcServerCondition(); + ConditionOutcome outcome = condition.getMatchOutcome( + mockConditionContext( + Map.of("spring.grpc.server.enabled", "true", "spring.grpc.server.myservice.enabled", "false")), + mockMetadata("myservice")); + assertThat(outcome.isMatch()).isFalse(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnGrpcServerEnabled spring.grpc.server.myservice.enabled is false"); + } + + @Test + void shouldNotMatchIfGlobalPropertyIsFalseAndServicePropertyIsTrue() { + OnEnabledGrpcServerCondition condition = new OnEnabledGrpcServerCondition(); + ConditionOutcome outcome = condition.getMatchOutcome( + mockConditionContext( + Map.of("spring.grpc.server.enabled", "false", "spring.grpc.server.myservice.enabled", "true")), + mockMetadata("myservice")); + assertThat(outcome.isMatch()).isFalse(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnGrpcServerEnabled spring.grpc.server.enabled is false"); + } + + private ConditionContext mockConditionContext() { + return mockConditionContext(Collections.emptyMap()); + } + + private ConditionContext mockConditionContext(Map properties) { + ConditionContext context = mock(ConditionContext.class); + MockEnvironment environment = new MockEnvironment(); + properties.forEach(environment::setProperty); + given(context.getEnvironment()).willReturn(environment); + return context; + } + + private AnnotatedTypeMetadata mockMetadata(String serviceName) { + AnnotatedTypeMetadata metadata = mock(AnnotatedTypeMetadata.class); + given(metadata.getAnnotationAttributes(ConditionalOnGrpcServerEnabled.class.getName())) + .willReturn(Map.of("value", serviceName)); + return metadata; + } + +} diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfigurationTests.java index efcfdf985bd6..f28eed584181 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfigurationTests.java @@ -16,7 +16,7 @@ package org.springframework.boot.grpc.server.autoconfigure.exception; -import io.grpc.Grpc; +import io.grpc.BindableService; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -49,12 +49,12 @@ private ApplicationContextRunner contextRunner() { @Test void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { this.contextRunner() - .withClassLoader(new FilteredClassLoader(Grpc.class)) + .withClassLoader(new FilteredClassLoader(BindableService.class)) .run((context) -> assertThat(context).doesNotHaveBean(GrpcExceptionHandlerAutoConfiguration.class)); } @Test - void whenSprimgGrpcNotOnClasspathAutoConfigurationIsSkipped() { + void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() { this.contextRunner() .withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) .run((context) -> assertThat(context).doesNotHaveBean(GrpcExceptionHandlerAutoConfiguration.class)); diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java index 4b92003e1140..001fb30ad264 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java @@ -35,6 +35,7 @@ import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -67,6 +68,20 @@ void whenNoBindableServiceDefinedDoesNotAutoConfigureBean() { .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerHealthAutoConfiguration.class)); } + @Test + void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(BindableService.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerHealthAutoConfiguration.class)); + } + + @Test + void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(GrpcServerFactory.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerHealthAutoConfiguration.class)); + } + @Test void whenHealthStatusManagerNotOnClasspathAutoConfigurationIsSkipped() { this.contextRunner() From bbc77a35c9ba2675102185326b524b537c500dcc Mon Sep 17 00:00:00 2001 From: onobc Date: Tue, 30 Sep 2025 10:33:59 -0500 Subject: [PATCH 11/21] Consistent order for annotations in server auto config Signed-off-by: onobc --- .../autoconfigure/security/GrpcSecurityAutoConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java index d8ffa7a8a423..c74ee8a4b92c 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java @@ -76,8 +76,8 @@ GrpcExceptionHandler accessExceptionHandler() { } @ConditionalOnBean(ObjectPostProcessor.class) - @Configuration(proxyBeanMethods = false) @ConditionalOnGrpcNativeServer + @Configuration(proxyBeanMethods = false) static class GrpcNativeSecurityConfigurerConfiguration { @Bean From e46aa6d4a02815494ab86854e6155dbae164f62c Mon Sep 17 00:00:00 2001 From: onobc Date: Tue, 30 Sep 2025 10:34:18 -0500 Subject: [PATCH 12/21] Make ChannelBuilderCustomizers package-protected Signed-off-by: onobc --- .../grpc/client/autoconfigure/ChannelBuilderCustomizers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java index 663b564cf50c..33d94d2479b2 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java @@ -32,7 +32,7 @@ * @author Chris Bono * @since 4.0.0 */ -public class ChannelBuilderCustomizers { +class ChannelBuilderCustomizers { private final List> customizers; From 4d6d524d82e8f9aa5b7d8a33a147b594b524e9be Mon Sep 17 00:00:00 2001 From: onobc Date: Tue, 30 Sep 2025 11:55:31 -0500 Subject: [PATCH 13/21] Make GrpcCodecConfiguration package protected Collapses the GrpcCodecConfiguration into the parent package to avoid exposing it publicly. Signed-off-by: onobc --- .../ChannelBuilderCustomizers.java | 1 - .../GrpcClientAutoConfiguration.java | 1 - .../{codec => }/GrpcCodecConfiguration.java | 5 ++-- .../autoconfigure/codec/package-info.java | 23 ------------------- .../GrpcCodecConfigurationTests.java | 2 +- 5 files changed, 3 insertions(+), 29 deletions(-) rename module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/{codec => }/GrpcCodecConfiguration.java (94%) delete mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/package-info.java rename module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/{codec => }/GrpcCodecConfigurationTests.java (95%) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java index 33d94d2479b2..fe0eadd59146 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java @@ -30,7 +30,6 @@ * {@link ManagedChannelBuilder}. * * @author Chris Bono - * @since 4.0.0 */ class ChannelBuilderCustomizers { diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java index a14f0aa716d2..2036d285f294 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java @@ -26,7 +26,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.grpc.client.autoconfigure.codec.GrpcCodecConfiguration; import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfiguration.java similarity index 94% rename from module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfiguration.java rename to module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfiguration.java index ea01da35c325..edb9e1980db0 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfiguration.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.grpc.client.autoconfigure.codec; +package org.springframework.boot.grpc.client.autoconfigure; import io.grpc.Codec; import io.grpc.Compressor; @@ -32,11 +32,10 @@ * The configuration that contains all codec related beans for clients. * * @author Andrei Lisa - * @since 4.0.0 */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Codec.class) -public class GrpcCodecConfiguration { +class GrpcCodecConfiguration { @Bean @ConditionalOnMissingBean diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/package-info.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/package-info.java deleted file mode 100644 index 82c09391fd1f..000000000000 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/codec/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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. - */ - -/** - * Auto-configuration for gRPC client codecs. - */ -@NullMarked -package org.springframework.boot.grpc.client.autoconfigure.codec; - -import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java similarity index 95% rename from module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfigurationTests.java rename to module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java index cefda3c929b5..d3be49fa4eaf 100644 --- a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/codec/GrpcCodecConfigurationTests.java +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.grpc.client.autoconfigure.codec; +package org.springframework.boot.grpc.client.autoconfigure; import io.grpc.CompressorRegistry; import io.grpc.DecompressorRegistry; From dffd4edade30d6b3bc0c07e56cc66dd53e04b7fc Mon Sep 17 00:00:00 2001 From: onobc Date: Tue, 30 Sep 2025 12:05:04 -0500 Subject: [PATCH 14/21] Remove ClientInterceptorsConfiguration Collapses the single bean registered by ClientInterceptorsConfiguration into the 2 usages to avoid a public configuration class. Signed-off-by: onobc --- .../ClientInterceptorsConfiguration.java | 40 ------------------- .../GrpcClientAutoConfiguration.java | 11 ++++- .../InProcessTestAutoConfiguration.java | 11 +++-- 3 files changed, 17 insertions(+), 45 deletions(-) delete mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientInterceptorsConfiguration.java diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientInterceptorsConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientInterceptorsConfiguration.java deleted file mode 100644 index 2716b6e30f36..000000000000 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientInterceptorsConfiguration.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.grpc.client.autoconfigure; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.grpc.client.ClientInterceptorsConfigurer; - -/** - * Configuration for {@link ClientInterceptorsConfigurer}. - * - * @author Chris Bono - * @since 4.0.0 - */ -@Configuration(proxyBeanMethods = false) -public class ClientInterceptorsConfiguration { - - @Bean - @ConditionalOnMissingBean - ClientInterceptorsConfigurer clientInterceptorsConfigurer(ApplicationContext applicationContext) { - return new ClientInterceptorsConfigurer(applicationContext); - } - -} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java index 2036d285f294..76618f7dc6f2 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java @@ -27,22 +27,29 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.ssl.SslBundles; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.grpc.client.ChannelCredentialsProvider; +import org.springframework.grpc.client.ClientInterceptorsConfigurer; import org.springframework.grpc.client.CoroutineStubFactory; import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; @AutoConfiguration(before = CompositeChannelFactoryAutoConfiguration.class) @ConditionalOnGrpcClientEnabled @EnableConfigurationProperties(GrpcClientProperties.class) -@Import({ GrpcCodecConfiguration.class, ClientInterceptorsConfiguration.class, - GrpcChannelFactoryConfigurations.ShadedNettyChannelFactoryConfiguration.class, +@Import({ GrpcCodecConfiguration.class, GrpcChannelFactoryConfigurations.ShadedNettyChannelFactoryConfiguration.class, GrpcChannelFactoryConfigurations.NettyChannelFactoryConfiguration.class, GrpcChannelFactoryConfigurations.InProcessChannelFactoryConfiguration.class, ClientScanConfiguration.class }) public final class GrpcClientAutoConfiguration { + @Bean + @ConditionalOnMissingBean + ClientInterceptorsConfigurer clientInterceptorsConfigurer(ApplicationContext applicationContext) { + return new ClientInterceptorsConfigurer(applicationContext); + } + @Bean @ConditionalOnMissingBean(ChannelCredentialsProvider.class) NamedChannelCredentialsProvider channelCredentialsProvider(SslBundles bundles, GrpcClientProperties properties) { diff --git a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfiguration.java b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfiguration.java index 3faedc7900e4..83581ef03fad 100644 --- a/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfiguration.java +++ b/module/spring-boot-grpc-test/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfiguration.java @@ -31,12 +31,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.grpc.client.autoconfigure.ClientInterceptorsConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration; import org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration; +import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.grpc.client.ClientInterceptorsConfigurer; @@ -63,11 +63,16 @@ @ConditionalOnClass({ InProcessServerBuilder.class, InProcessChannelBuilder.class, InProcessGrpcServerFactory.class, InProcessGrpcChannelFactory.class }) @ConditionalOnBooleanProperty("spring.test.grpc.inprocess.enabled") -@Import(ClientInterceptorsConfiguration.class) public final class InProcessTestAutoConfiguration { private final String address = InProcessServerBuilder.generateName(); + @Bean + @ConditionalOnMissingBean + ClientInterceptorsConfigurer clientInterceptorsConfigurer(ApplicationContext applicationContext) { + return new ClientInterceptorsConfigurer(applicationContext); + } + @Bean @ConditionalOnClass({ BindableService.class, GrpcServerFactory.class }) @ConditionalOnBean(BindableService.class) From 148c6d5c0710d8687efad52c4db999ee04fb83da Mon Sep 17 00:00:00 2001 From: onobc Date: Tue, 30 Sep 2025 22:02:20 -0500 Subject: [PATCH 15/21] Cleanup ClientScanConfiguration.java Signed-off-by: onobc --- .../ClientScanConfiguration.java | 88 --------- .../DefaultGrpcClientRegistrations.java | 68 +++++++ .../GrpcClientAutoConfiguration.java | 9 + .../ClientScanConfigurationTests.java | 179 ------------------ .../DefaultGrpcClientRegistrationsTests.java | 84 ++++++++ .../GrpcClientAutoConfigurationTests.java | 17 +- 6 files changed, 177 insertions(+), 268 deletions(-) delete mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientScanConfiguration.java create mode 100644 module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrations.java delete mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ClientScanConfigurationTests.java create mode 100644 module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrationsTests.java diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientScanConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientScanConfiguration.java deleted file mode 100644 index e2aa4a566759..000000000000 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientScanConfiguration.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.grpc.client.autoconfigure; - -import java.util.ArrayList; -import java.util.List; - -import org.jspecify.annotations.Nullable; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.boot.autoconfigure.AutoConfigurationPackages; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.boot.grpc.client.autoconfigure.ClientScanConfiguration.DefaultGrpcClientRegistrations; -import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.ChannelConfig; -import org.springframework.context.EnvironmentAware; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.core.env.Environment; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.grpc.client.AbstractGrpcClientRegistrar; -import org.springframework.grpc.client.GrpcClientFactory; -import org.springframework.grpc.client.GrpcClientFactory.GrpcClientRegistrationSpec; -import org.springframework.util.Assert; - -@Configuration(proxyBeanMethods = false) -@ConditionalOnMissingBean(GrpcClientFactory.class) -@Import(DefaultGrpcClientRegistrations.class) -public class ClientScanConfiguration { - - static class DefaultGrpcClientRegistrations extends AbstractGrpcClientRegistrar - implements EnvironmentAware, BeanFactoryAware { - - private @Nullable Environment environment; - - private @Nullable BeanFactory beanFactory; - - @Override - public void setEnvironment(Environment environment) { - this.environment = environment; - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } - - @Override - protected GrpcClientRegistrationSpec[] collect(AnnotationMetadata meta) { - Assert.notNull(this.environment, "Environment must not be null"); - Assert.notNull(this.beanFactory, "BeanFactory must not be null"); - Binder binder = Binder.get(this.environment); - boolean hasDefaultChannel = binder.bind("spring.grpc.client.default-channel", ChannelConfig.class) - .isBound(); - if (hasDefaultChannel) { - List packages = new ArrayList<>(); - if (AutoConfigurationPackages.has(this.beanFactory)) { - packages.addAll(AutoConfigurationPackages.get(this.beanFactory)); - } - GrpcClientProperties props = binder.bind("spring.grpc.client", GrpcClientProperties.class) - .orElseGet(GrpcClientProperties::new); - - return new GrpcClientRegistrationSpec[] { GrpcClientRegistrationSpec.of("default") - .factory(props.getDefaultStubFactory()) - .packages(packages.toArray(new String[0])) }; - } - return new GrpcClientRegistrationSpec[0]; - } - - } - -} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrations.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrations.java new file mode 100644 index 000000000000..26e4ef51b8e2 --- /dev/null +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrations.java @@ -0,0 +1,68 @@ +/* + * 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.grpc.client.autoconfigure; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigurationPackages; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.ChannelConfig; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.grpc.client.AbstractGrpcClientRegistrar; +import org.springframework.grpc.client.GrpcClientFactory.GrpcClientRegistrationSpec; + +/** + * Default implementation of {@link AbstractGrpcClientRegistrar} that registers client + * bean definitions for the default configured gRPC channel if the + * {@code spring.grpc.client.default-channel} property is set. + * + * @author Dave Syer + * @author Chris Bono + */ +class DefaultGrpcClientRegistrations extends AbstractGrpcClientRegistrar { + + private final Environment environment; + + private final BeanFactory beanFactory; + + DefaultGrpcClientRegistrations(Environment environment, BeanFactory beanFactory) { + this.environment = environment; + this.beanFactory = beanFactory; + } + + @Override + protected GrpcClientRegistrationSpec[] collect(AnnotationMetadata meta) { + Binder binder = Binder.get(this.environment); + boolean hasDefaultChannel = binder.bind("spring.grpc.client.default-channel", ChannelConfig.class).isBound(); + if (hasDefaultChannel) { + List packages = new ArrayList<>(); + if (AutoConfigurationPackages.has(this.beanFactory)) { + packages.addAll(AutoConfigurationPackages.get(this.beanFactory)); + } + GrpcClientProperties clientProperties = binder.bind("spring.grpc.client", GrpcClientProperties.class) + .orElseGet(GrpcClientProperties::new); + return new GrpcClientRegistrationSpec[] { GrpcClientRegistrationSpec.of("default") + .factory(clientProperties.getDefaultStubFactory()) + .packages(packages.toArray(new String[0])) }; + } + return new GrpcClientRegistrationSpec[0]; + } + +} diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java index 76618f7dc6f2..95895fef06aa 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java @@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration.ClientScanConfiguration; import org.springframework.boot.ssl.SslBundles; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -35,6 +36,7 @@ import org.springframework.grpc.client.ClientInterceptorsConfigurer; import org.springframework.grpc.client.CoroutineStubFactory; import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; +import org.springframework.grpc.client.GrpcClientFactory; @AutoConfiguration(before = CompositeChannelFactoryAutoConfiguration.class) @ConditionalOnGrpcClientEnabled @@ -82,6 +84,13 @@ ChannelBuilderCustomizers channelBuilderCustomizers(ObjectProvider properties = new HashMap<>(); - properties.put("spring.grpc.client.default-stub-factory", ReactorStubFactory.class.getName()); - properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090"); - - MockEnvironment environment = new MockEnvironment(); - environment.getPropertySources().addFirst(new MapPropertySource("test", properties)); - - ClientScanConfiguration.DefaultGrpcClientRegistrations registrations = new ClientScanConfiguration.DefaultGrpcClientRegistrations(); - registrations.setEnvironment(environment); - - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - context.setEnvironment(environment); - AutoConfigurationPackages.register(context, "org.springframework.grpc.autoconfigure.client"); - registrations.setBeanFactory(context.getBeanFactory()); - - GrpcClientRegistrationSpec[] specs = registrations.collect(null); - - assertThat(specs).hasSize(1); - assertThat(specs[0].factory()).isEqualTo(ReactorStubFactory.class); - } - - @Test - void testDefaultStubFactory() { - Map properties = new HashMap<>(); - properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090"); - - MockEnvironment environment = new MockEnvironment(); - environment.getPropertySources().addFirst(new MapPropertySource("test", properties)); - - ClientScanConfiguration.DefaultGrpcClientRegistrations registrations = new ClientScanConfiguration.DefaultGrpcClientRegistrations(); - registrations.setEnvironment(environment); - - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - context.setEnvironment(environment); - AutoConfigurationPackages.register(context, "org.springframework.grpc.autoconfigure.client"); - registrations.setBeanFactory(context.getBeanFactory()); - - GrpcClientRegistrationSpec[] specs = registrations.collect(null); - - assertThat(specs).hasSize(1); - assertThat(specs[0].factory()).isEqualTo(BlockingStubFactory.class); - } - - @Test - void testCoroutineStubFactory() { - Map properties = new HashMap<>(); - properties.put("spring.grpc.client.default-stub-factory", CoroutineStubFactory.class.getName()); - properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090"); - - MockEnvironment env = new MockEnvironment(); - env.getPropertySources().addFirst(new MapPropertySource("test", properties)); - - var regs = new ClientScanConfiguration.DefaultGrpcClientRegistrations(); - regs.setEnvironment(env); - - try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { - context.setEnvironment(env); - AutoConfigurationPackages.register(context, "org.springframework.grpc.autoconfigure.client"); - regs.setBeanFactory(context.getBeanFactory()); - - GrpcClientRegistrationSpec[] specs = regs.collect(null); - assertThat(specs).hasSize(1); - assertThat(specs[0].factory()).isEqualTo(CoroutineStubFactory.class); - } - } - - @Test - void testInvalidStubFactoryValueThrowsBindException() { - Map properties = new HashMap<>(); - properties.put("spring.grpc.client.default-stub-factory", "com.example.InvalidStubFactory"); - properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090"); - - MockEnvironment env = new MockEnvironment(); - env.getPropertySources().addFirst(new MapPropertySource("test", properties)); - - var regs = new ClientScanConfiguration.DefaultGrpcClientRegistrations(); - regs.setEnvironment(env); - - try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { - context.setEnvironment(env); - AutoConfigurationPackages.register(context, "org.springframework.grpc.autoconfigure.client"); - regs.setBeanFactory(context.getBeanFactory()); - - assertThatExceptionOfType(BindException.class).isThrownBy(() -> regs.collect(null)) - .isInstanceOf(BindException.class) - .withMessageContaining("spring.grpc.client.default-stub-factory"); - } - } - - @Nested - @TestConfiguration(proxyBeanMethods = false) - @SpringBootTest(classes = { ClientScanConfiguration.class, ClientScanConfigurationSpringBootTest.TestConfig.class }, - properties = { "spring.grpc.client.default-channel.address=static://localhost:9090", - "spring.grpc.client.default-stub-factory=org.springframework.grpc.client.ReactorStubFactory" }) - class ClientScanConfigurationSpringBootTest { - - @Autowired - private ApplicationContext context; - - @Autowired - private GrpcClientProperties props; - - @Autowired - private Environment env; - - @Test - void propertyIsBoundAsBeanAndUsable() { - assertThat(this.props.getDefaultStubFactory()).isEqualTo(ReactorStubFactory.class); - - GrpcClientProperties rebound = Binder.get(this.env) - .bind("spring.grpc.client", GrpcClientProperties.class) - .get(); - - assertThat(rebound.getDefaultStubFactory()).isEqualTo(ReactorStubFactory.class); - } - - @Configuration - @EnableConfigurationProperties(GrpcClientProperties.class) - static class TestConfig { - - } - - } - -} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrationsTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrationsTests.java new file mode 100644 index 000000000000..365ba6189811 --- /dev/null +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrationsTests.java @@ -0,0 +1,84 @@ +/* + * 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.grpc.client.autoconfigure; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurationPackages; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.env.MapPropertySource; +import org.springframework.grpc.client.BlockingStubFactory; +import org.springframework.grpc.client.CoroutineStubFactory; +import org.springframework.grpc.client.GrpcClientFactory.GrpcClientRegistrationSpec; +import org.springframework.grpc.client.ReactorStubFactory; +import org.springframework.grpc.client.StubFactory; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for the {@link DefaultGrpcClientRegistrations}. + * + * @author CheolHwan Ihn + * @author Chris Bono + */ +class DefaultGrpcClientRegistrationsTests { + + private static void assertThatPropertiesResultInExpectedStubFactory(Map properties, + Class> expectedStubFactoryClass) { + MockEnvironment environment = new MockEnvironment(); + environment.getPropertySources() + .addFirst(new MapPropertySource("defaultGrpcClientRegistrationsTests", properties)); + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + AutoConfigurationPackages.register(context, "org.springframework.boot.grpc.client.autoconfigure"); + DefaultGrpcClientRegistrations registrations = new DefaultGrpcClientRegistrations(environment, + context.getBeanFactory()); + + GrpcClientRegistrationSpec[] specs = registrations.collect(null); + + assertThat(specs).hasSize(1); + assertThat(specs[0].factory()).isEqualTo(expectedStubFactoryClass); + } + } + + @Test + void withReactorStubFactory() { + Map properties = new HashMap<>(); + properties.put("spring.grpc.client.default-stub-factory", ReactorStubFactory.class.getName()); + properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090"); + assertThatPropertiesResultInExpectedStubFactory(properties, ReactorStubFactory.class); + } + + @Test + void withDefaultStubFactory() { + Map properties = new HashMap<>(); + properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090"); + assertThatPropertiesResultInExpectedStubFactory(properties, BlockingStubFactory.class); + } + + @Test + void withCoroutineStubFactory() { + Map properties = new HashMap<>(); + properties.put("spring.grpc.client.default-stub-factory", CoroutineStubFactory.class.getName()); + properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090"); + assertThatPropertiesResultInExpectedStubFactory(properties, CoroutineStubFactory.class); + } + +} diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java index 175be92bf2cb..3e0addd9b81c 100644 --- a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java @@ -35,6 +35,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration.ClientScanConfiguration; import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -44,6 +45,7 @@ import org.springframework.grpc.client.ChannelCredentialsProvider; import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; import org.springframework.grpc.client.GrpcChannelFactory; +import org.springframework.grpc.client.GrpcClientFactory; import org.springframework.grpc.client.InProcessGrpcChannelFactory; import org.springframework.grpc.client.NettyGrpcChannelFactory; import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory; @@ -211,7 +213,7 @@ void decompressionCustomizerAutoConfiguredAsExpected() { @Test void whenHasUserDefinedChannelBuilderCustomizersDoesNotAutoConfigureBean() { - ChannelBuilderCustomizers customCustomizers = mock(ChannelBuilderCustomizers.class); + ChannelBuilderCustomizers customCustomizers = mock(); this.contextRunner() .withBean("customCustomizers", ChannelBuilderCustomizers.class, () -> customCustomizers) .run((context) -> assertThat(context).getBean(ChannelBuilderCustomizers.class).isSameAs(customCustomizers)); @@ -227,6 +229,19 @@ void channelBuilderCustomizersAutoConfiguredAsExpected() { ChannelBuilderCustomizersConfig.CUSTOMIZER_FOO)); } + @Test + void clientScanConfigurationAutoConfiguredAsExpected() { + this.contextRunner().run((context) -> assertThat(context).hasSingleBean(ClientScanConfiguration.class)); + } + + @Test + void whenHasUserDefinedClientFactoryDoesNotAutoConfigureClientScanConfiguration() { + GrpcClientFactory clientFactory = mock(); + this.contextRunner() + .withBean("customClientFactory", GrpcClientFactory.class, () -> clientFactory) + .run((context) -> assertThat(context).doesNotHaveBean(ClientScanConfiguration.class)); + } + @Test void whenInProcessEnabledPropNotSetDoesAutoconfigureInProcess() { this.contextRunner() From 0206340f8f821fc6870894d7ed906ce96eea1851 Mon Sep 17 00:00:00 2001 From: onobc Date: Wed, 1 Oct 2025 13:32:47 -0500 Subject: [PATCH 16/21] Add javadoc and remove var usages Signed-off-by: onobc --- .../autoconfigure/GrpcChannelFactoryConfigurations.java | 5 +++-- .../autoconfigure/GrpcChannelFactoryCustomizer.java | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java index 57a8af5ae711..2aeadd57a830 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java @@ -62,7 +62,8 @@ ShadedNettyGrpcChannelFactory shadedNettyGrpcChannelFactory(GrpcClientProperties ChannelCredentialsProvider credentials) { List> builderCustomizers = List .of(channelBuilderCustomizers::customize); - var factory = new ShadedNettyGrpcChannelFactory(builderCustomizers, interceptorsConfigurer); + ShadedNettyGrpcChannelFactory factory = new ShadedNettyGrpcChannelFactory(builderCustomizers, + interceptorsConfigurer); factory.setCredentialsProvider(credentials); factory.setVirtualTargets(properties); channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); @@ -87,7 +88,7 @@ NettyGrpcChannelFactory nettyGrpcChannelFactory(GrpcClientProperties properties, ChannelCredentialsProvider credentials) { List> builderCustomizers = List .of(channelBuilderCustomizers::customize); - var factory = new NettyGrpcChannelFactory(builderCustomizers, interceptorsConfigurer); + NettyGrpcChannelFactory factory = new NettyGrpcChannelFactory(builderCustomizers, interceptorsConfigurer); factory.setCredentialsProvider(credentials); factory.setVirtualTargets(properties); channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java index c3a665ca8b32..7e640a3e7b83 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java @@ -18,6 +18,14 @@ import org.springframework.grpc.client.GrpcChannelFactory; +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link GrpcChannelFactory} before it is fully initialized, in particular to tune its + * configuration. + * + * @author Chris Bono + * @since 4.0.0 + */ public interface GrpcChannelFactoryCustomizer { /** From a60daf6219a9721d655876601d539ea637515118 Mon Sep 17 00:00:00 2001 From: onobc Date: Wed, 1 Oct 2025 13:57:01 -0500 Subject: [PATCH 17/21] Fix ordering of members / getters / setters in client properties Signed-off-by: onobc --- .../autoconfigure/GrpcClientProperties.java | 297 ++++++++---------- 1 file changed, 135 insertions(+), 162 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java index 0da9e0dcf4da..237d8cd940c3 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java @@ -40,14 +40,14 @@ public class GrpcClientProperties implements EnvironmentAware, VirtualTargets { /** - * The default channel configuration to use for new channels. + * Map of channels configured by name. */ - private final ChannelConfig defaultChannel = new ChannelConfig(); + private final Map channels = new HashMap<>(); /** - * Map of channels configured by name. + * The default channel configuration to use for new channels. */ - private final Map channels = new HashMap<>(); + private final ChannelConfig defaultChannel = new ChannelConfig(); /** * Default stub factory to use for all channels. @@ -61,14 +61,14 @@ public class GrpcClientProperties implements EnvironmentAware, VirtualTargets { this.environment = new StandardEnvironment(); } - public ChannelConfig getDefaultChannel() { - return this.defaultChannel; - } - public Map getChannels() { return this.channels; } + public ChannelConfig getDefaultChannel() { + return this.defaultChannel; + } + public Class> getDefaultStubFactory() { return this.defaultStubFactory; } @@ -134,69 +134,107 @@ public static class ChannelConfig { */ private String address = "static://localhost:9090"; - public String getAddress() { - return this.address; - } + /** + * The default deadline for RPCs performed on this channel. + */ + private @Nullable Duration defaultDeadline; - public void setAddress(final String address) { - this.address = address; - } + /** + * The load balancing policy the channel should use. + */ + private String defaultLoadBalancingPolicy = "round_robin"; + + /** + * Whether keep alive is enabled on the channel. + */ + private boolean enableKeepAlive; - // -------------------------------------------------- - // defaultLoadBalancingPolicy - // -------------------------------------------------- + private final Health health = new Health(); /** - * The default load balancing policy the channel should use. + * The duration without ongoing RPCs before going to idle mode. */ - private String defaultLoadBalancingPolicy = "round_robin"; + @DurationUnit(ChronoUnit.SECONDS) + private Duration idleTimeout = Duration.ofSeconds(20); - public String getDefaultLoadBalancingPolicy() { - return this.defaultLoadBalancingPolicy; - } + /** + * The delay before sending a keepAlive. Note that shorter intervals increase the + * network burden for the server and this value can not be lower than + * 'permitKeepAliveTime' on the server. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration keepAliveTime = Duration.ofMinutes(5); - public void setDefaultLoadBalancingPolicy(final String defaultLoadBalancingPolicy) { - this.defaultLoadBalancingPolicy = defaultLoadBalancingPolicy; - } + /** + * The default timeout for a keepAlives ping request. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration keepAliveTimeout = Duration.ofSeconds(20); - // -------------------------------------------------- + /** + * Whether a keepAlive will be performed when there are no outstanding RPC on a + * connection. + */ + private boolean keepAliveWithoutCalls; - private final Health health = new Health(); + /** + * Maximum message size allowed to be received by the channel (default 4MiB). Set + * to '-1' to use the highest possible limit (not recommended). + */ + private DataSize maxInboundMessageSize = DataSize.ofBytes(4194304); - public Health getHealth() { - return this.health; - } + /** + * Maximum metadata size allowed to be received by the channel (default 8KiB). Set + * to '-1' to use the highest possible limit (not recommended). + */ + private DataSize maxInboundMetadataSize = DataSize.ofBytes(8192); + + /** + * The negotiation type for the channel. + */ + private NegotiationType negotiationType = NegotiationType.PLAINTEXT; + + /** + * Flag to say that strict SSL checks are not enabled (so the remote certificate + * could be anonymous). + */ + private boolean secure = true; /** * Map representation of the service config to use for the channel. */ private final Map serviceConfig = new HashMap<>(); - public Map getServiceConfig() { - return this.serviceConfig; - } + private final Ssl ssl = new Ssl(); /** - * The negotiation type for the channel. + * The custom User-Agent for the channel. */ - private NegotiationType negotiationType = NegotiationType.PLAINTEXT; + private @Nullable String userAgent; - public NegotiationType getNegotiationType() { - return this.negotiationType; + public String getAddress() { + return this.address; } - public void setNegotiationType(NegotiationType negotiationType) { - this.negotiationType = negotiationType; + public void setAddress(final String address) { + this.address = address; } - // -------------------------------------------------- - // KeepAlive - // -------------------------------------------------- + public @Nullable Duration getDefaultDeadline() { + return this.defaultDeadline; + } - /** - * Whether keep alive is enabled on the channel. - */ - private boolean enableKeepAlive = false; + public void setDefaultDeadline(@Nullable Duration defaultDeadline) { + this.defaultDeadline = defaultDeadline; + } + + public String getDefaultLoadBalancingPolicy() { + return this.defaultLoadBalancingPolicy; + } + + public void setDefaultLoadBalancingPolicy(final String defaultLoadBalancingPolicy) { + this.defaultLoadBalancingPolicy = defaultLoadBalancingPolicy; + } public boolean isEnableKeepAlive() { return this.enableKeepAlive; @@ -206,13 +244,9 @@ public void setEnableKeepAlive(boolean enableKeepAlive) { this.enableKeepAlive = enableKeepAlive; } - // -------------------------------------------------- - - /** - * The duration without ongoing RPCs before going to idle mode. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration idleTimeout = Duration.ofSeconds(20); + public Health getHealth() { + return this.health; + } public Duration getIdleTimeout() { return this.idleTimeout; @@ -222,16 +256,6 @@ public void setIdleTimeout(Duration idleTimeout) { this.idleTimeout = idleTimeout; } - // -------------------------------------------------- - - /** - * The delay before sending a keepAlive. Note that shorter intervals increase the - * network burden for the server and this value can not be lower than - * 'permitKeepAliveTime' on the server. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration keepAliveTime = Duration.ofMinutes(5); - public Duration getKeepAliveTime() { return this.keepAliveTime; } @@ -240,14 +264,6 @@ public void setKeepAliveTime(Duration keepAliveTime) { this.keepAliveTime = keepAliveTime; } - // -------------------------------------------------- - - /** - * The default timeout for a keepAlives ping request. - */ - @DurationUnit(ChronoUnit.SECONDS) - private Duration keepAliveTimeout = Duration.ofSeconds(20); - public Duration getKeepAliveTimeout() { return this.keepAliveTimeout; } @@ -256,14 +272,6 @@ public void setKeepAliveTimeout(Duration keepAliveTimeout) { this.keepAliveTimeout = keepAliveTimeout; } - // -------------------------------------------------- - - /** - * Whether a keepAlive will be performed when there are no outstanding RPC on a - * connection. - */ - private boolean keepAliveWithoutCalls = false; - public boolean isKeepAliveWithoutCalls() { return this.keepAliveWithoutCalls; } @@ -272,22 +280,6 @@ public void setKeepAliveWithoutCalls(boolean keepAliveWithoutCalls) { this.keepAliveWithoutCalls = keepAliveWithoutCalls; } - // -------------------------------------------------- - // Message Transfer - // -------------------------------------------------- - - /** - * Maximum message size allowed to be received by the channel (default 4MiB). Set - * to '-1' to use the highest possible limit (not recommended). - */ - private DataSize maxInboundMessageSize = DataSize.ofBytes(4194304); - - /** - * Maximum metadata size allowed to be received by the channel (default 8KiB). Set - * to '-1' to use the highest possible limit (not recommended). - */ - private DataSize maxInboundMetadataSize = DataSize.ofBytes(8192); - public DataSize getMaxInboundMessageSize() { return this.maxInboundMessageSize; } @@ -318,39 +310,43 @@ else if (maxSize != null && maxSize.toBytes() == -1) { } } - // -------------------------------------------------- + public NegotiationType getNegotiationType() { + return this.negotiationType; + } - /** - * The custom User-Agent for the channel. - */ - private @Nullable String userAgent = null; + public void setNegotiationType(NegotiationType negotiationType) { + this.negotiationType = negotiationType; + } - public @Nullable String getUserAgent() { - return this.userAgent; + public boolean isSecure() { + return this.secure; } - public void setUserAgent(@Nullable String userAgent) { - this.userAgent = userAgent; + public void setSecure(boolean secure) { + this.secure = secure; } - /** - * The default deadline for RPCs performed on this channel. - */ - private @Nullable Duration defaultDeadline = null; + public Map getServiceConfig() { + return this.serviceConfig; + } - public @Nullable Duration getDefaultDeadline() { - return this.defaultDeadline; + public Ssl getSsl() { + return this.ssl; } - public void setDefaultDeadline(@Nullable Duration defaultDeadline) { - this.defaultDeadline = defaultDeadline; + public @Nullable String getUserAgent() { + return this.userAgent; + } + + public void setUserAgent(@Nullable String userAgent) { + this.userAgent = userAgent; } /** * Provide a copy of the channel instance. * @return a copy of the channel instance. */ - public ChannelConfig copy() { + ChannelConfig copy() { ChannelConfig copy = new ChannelConfig(); copy.address = this.address; copy.defaultLoadBalancingPolicy = this.defaultLoadBalancingPolicy; @@ -370,105 +366,82 @@ public ChannelConfig copy() { return copy; } - // -------------------------------------------------- - - /** - * Flag to say that strict SSL checks are not enabled (so the remote certificate - * could be anonymous). - */ - private boolean secure = true; - - public boolean isSecure() { - return this.secure; - } - - public void setSecure(boolean secure) { - this.secure = secure; - } - - // -------------------------------------------------- - - private final Ssl ssl = new Ssl(); - - public Ssl getSsl() { - return this.ssl; - } - - public static class Ssl { + public static class Health { /** - * Whether to enable SSL support. Enabled automatically if "bundle" is - * provided unless specified otherwise. + * Whether to enable client-side health check for the channel. */ - private @Nullable Boolean enabled; + private boolean enabled; /** - * SSL bundle name. + * Name of the service to check health on. */ - private @Nullable String bundle; + private @Nullable String serviceName; public boolean isEnabled() { - return (this.enabled != null) ? this.enabled : this.bundle != null; + return this.enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } - public @Nullable String getBundle() { - return this.bundle; + public @Nullable String getServiceName() { + return this.serviceName; } - public void setBundle(@Nullable String bundle) { - this.bundle = bundle; + public void setServiceName(String serviceName) { + this.serviceName = serviceName; } /** * Copies the values from another instance. * @param other instance to copy values from */ - public void copyValuesFrom(Ssl other) { + void copyValuesFrom(Health other) { this.enabled = other.enabled; - this.bundle = other.bundle; + this.serviceName = other.serviceName; } } - public static class Health { + public static class Ssl { /** - * Whether to enable client-side health check for the channel. + * Whether to enable SSL support. Enabled automatically if "bundle" is + * provided unless specified otherwise. */ - private boolean enabled = false; + private @Nullable Boolean enabled; /** - * Name of the service to check health on. + * SSL bundle name. */ - private @Nullable String serviceName; + private @Nullable String bundle; + // TODO bono public boolean isEnabled() { - return this.enabled; + return (this.enabled != null) ? this.enabled : this.bundle != null; } public void setEnabled(boolean enabled) { this.enabled = enabled; } - public @Nullable String getServiceName() { - return this.serviceName; + public @Nullable String getBundle() { + return this.bundle; } - public void setServiceName(String serviceName) { - this.serviceName = serviceName; + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; } /** * Copies the values from another instance. * @param other instance to copy values from */ - public void copyValuesFrom(Health other) { + void copyValuesFrom(Ssl other) { this.enabled = other.enabled; - this.serviceName = other.serviceName; + this.bundle = other.bundle; } } From c5346c6af68ccda8d0ee3e6a665c4ec53f9c101a Mon Sep 17 00:00:00 2001 From: onobc Date: Wed, 1 Oct 2025 14:13:29 -0500 Subject: [PATCH 18/21] Make `isEnabled` anemic (move logic to determineEnabled) Signed-off-by: onobc --- .../client/autoconfigure/GrpcClientProperties.java | 11 +++++++---- .../NamedChannelCredentialsProvider.java | 5 +++-- .../autoconfigure/GrpcClientPropertiesTests.java | 14 +++++++++++++- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java index 237d8cd940c3..48bfc399266e 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java @@ -418,15 +418,18 @@ public static class Ssl { */ private @Nullable String bundle; - // TODO bono - public boolean isEnabled() { - return (this.enabled != null) ? this.enabled : this.bundle != null; + public @Nullable Boolean isEnabled() { + return this.enabled; } - public void setEnabled(boolean enabled) { + public void setEnabled(@Nullable Boolean enabled) { this.enabled = enabled; } + public boolean determineEnabled() { + return (this.enabled != null) ? this.enabled : this.bundle != null; + } + public @Nullable String getBundle() { return this.bundle; } diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java index 45778bcefe15..ed492fed0f9f 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java @@ -50,10 +50,11 @@ public NamedChannelCredentialsProvider(SslBundles bundles, GrpcClientProperties @Override public ChannelCredentials getChannelCredentials(String path) { ChannelConfig channel = this.properties.getChannel(path); - if (!channel.getSsl().isEnabled() && channel.getNegotiationType() == NegotiationType.PLAINTEXT) { + boolean sslEnabled = channel.getSsl().determineEnabled(); + if (!sslEnabled && channel.getNegotiationType() == NegotiationType.PLAINTEXT) { return InsecureChannelCredentials.create(); } - if (channel.getSsl().isEnabled()) { + if (sslEnabled) { String bundleName = channel.getSsl().getBundle(); Assert.notNull(bundleName, "Bundle name must not be null when SSL is enabled"); SslBundle bundle = this.bundles.getBundle(bundleName); diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java index 8f6d9452c20d..71eddc8e8ddb 100644 --- a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java @@ -92,7 +92,8 @@ private void withDefaultValues(String channelName, assertThat(channel.getMaxInboundMetadataSize()).isEqualTo(DataSize.ofBytes(8192)); assertThat(channel.getUserAgent()).isNull(); assertThat(channel.isSecure()).isTrue(); - assertThat(channel.getSsl().isEnabled()).isFalse(); + assertThat(channel.getSsl().isEnabled()).isNull(); + assertThat(channel.getSsl().determineEnabled()).isFalse(); assertThat(channel.getSsl().getBundle()).isNull(); } @@ -144,6 +145,7 @@ private void withSpecifiedValues(String channelName, assertThat(channel.getUserAgent()).isEqualTo("me"); assertThat(channel.isSecure()).isFalse(); assertThat(channel.getSsl().isEnabled()).isTrue(); + assertThat(channel.getSsl().determineEnabled()).isTrue(); assertThat(channel.getSsl().getBundle()).isEqualTo("my-bundle"); } @@ -182,6 +184,16 @@ void withServiceConfig() { assertThat(channel.getServiceConfig().get("something")).isInstanceOf(Map.class); } + @Test + void whenBundleNameSetThenDetermineEnabledReturnsTrue() { + Map map = new HashMap<>(); + map.put("spring.grpc.client.default-channel.ssl.bundle", "my-bundle"); + GrpcClientProperties properties = bindProperties(map); + var channel = properties.getDefaultChannel(); + assertThat(channel.getSsl().isEnabled()).isNull(); + assertThat(channel.getSsl().determineEnabled()).isTrue(); + } + } @Nested From 850990b26474f21be22d496e7bf17cdc2370fcfc Mon Sep 17 00:00:00 2001 From: onobc Date: Wed, 1 Oct 2025 14:20:56 -0500 Subject: [PATCH 19/21] Make NamedChannelCredentialsProvider package-protected Signed-off-by: onobc --- .../autoconfigure/NamedChannelCredentialsProvider.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java index ed492fed0f9f..0e8cc2b3ca73 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java @@ -34,15 +34,14 @@ * Provides channel credentials using channel configuration and {@link SslBundles}. * * @author David Syer - * @since 4.0.0 */ -public class NamedChannelCredentialsProvider implements ChannelCredentialsProvider { +class NamedChannelCredentialsProvider implements ChannelCredentialsProvider { private final SslBundles bundles; private final GrpcClientProperties properties; - public NamedChannelCredentialsProvider(SslBundles bundles, GrpcClientProperties properties) { + NamedChannelCredentialsProvider(SslBundles bundles, GrpcClientProperties properties) { this.bundles = bundles; this.properties = properties; } From b3ee513f824e71eed974205e8dfff42091a1fc37 Mon Sep 17 00:00:00 2001 From: onobc Date: Wed, 1 Oct 2025 15:49:52 -0500 Subject: [PATCH 20/21] Fix code configuration (client-side) - only use default de/compressor registry when user provides no custom de/compressor - update tests accordingly Signed-off-by: onobc --- .../autoconfigure/GrpcCodecConfiguration.java | 34 ++++++++++- .../GrpcClientAutoConfigurationTests.java | 26 ++++---- .../GrpcCodecConfigurationTests.java | 60 +++++++++++++++++-- 3 files changed, 100 insertions(+), 20 deletions(-) diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfiguration.java index edb9e1980db0..77ded0c30100 100644 --- a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfiguration.java +++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfiguration.java @@ -21,6 +21,7 @@ import io.grpc.CompressorRegistry; import io.grpc.Decompressor; import io.grpc.DecompressorRegistry; +import io.grpc.ManagedChannelBuilder; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -37,19 +38,46 @@ @ConditionalOnClass(Codec.class) class GrpcCodecConfiguration { + /** + * The compressor registry that is set on the + * {@link ManagedChannelBuilder#compressorRegistry(CompressorRegistry) channel + * builder}. + * @param compressors the compressors to use on the registry + * @return a new {@link CompressorRegistry#newEmptyInstance() registry} with the + * specified compressors or the {@link CompressorRegistry#getDefaultInstance() default + * registry} if no custom compressors are available in the application context. + */ @Bean @ConditionalOnMissingBean CompressorRegistry compressorRegistry(ObjectProvider compressors) { - CompressorRegistry registry = CompressorRegistry.getDefaultInstance(); + if (compressors.stream().count() == 0) { + return CompressorRegistry.getDefaultInstance(); + } + CompressorRegistry registry = CompressorRegistry.newEmptyInstance(); compressors.orderedStream().forEachOrdered(registry::register); return registry; } + /** + * The decompressor registry that is set on the + * {@link ManagedChannelBuilder#decompressorRegistry(DecompressorRegistry) channel + * builder}. + * @param decompressors the decompressors to use on the registry + * @return a new {@link DecompressorRegistry#emptyInstance() registry} with the + * specified decompressors or the {@link DecompressorRegistry#getDefaultInstance() + * default registry} if no custom decompressors are available in the application + * context. + */ @Bean @ConditionalOnMissingBean DecompressorRegistry decompressorRegistry(ObjectProvider decompressors) { - DecompressorRegistry registry = DecompressorRegistry.getDefaultInstance(); - decompressors.orderedStream().forEachOrdered((decompressor) -> registry.with(decompressor, false)); + if (decompressors.stream().count() == 0) { + return DecompressorRegistry.getDefaultInstance(); + } + DecompressorRegistry registry = DecompressorRegistry.emptyInstance(); + for (Decompressor decompressor : decompressors.orderedStream().toList()) { + registry = registry.with(decompressor, false); + } return registry; } diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java index 3e0addd9b81c..bf76c99d20ab 100644 --- a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java @@ -164,17 +164,6 @@ void clientPropertiesChannelCustomizerAutoConfiguredWithoutHealthAsExpected() { }); } - @Test - void whenNoCompressorRegistryAutoConfigurationIsSkipped() { - // Codec class guards the imported GrpcCodecConfiguration which provides the - // registry - this.contextRunner() - .withClassLoader(new FilteredClassLoader(Codec.class)) - .run((context) -> assertThat(context) - .getBean("compressionClientCustomizer", GrpcChannelBuilderCustomizer.class) - .isNull()); - } - @Test void compressionCustomizerAutoConfiguredAsExpected() { this.contextRunner().run((context) -> { @@ -188,13 +177,13 @@ void compressionCustomizerAutoConfiguredAsExpected() { } @Test - void whenNoDecompressorRegistryAutoConfigurationIsSkipped() { + void whenNoCompressorRegistryThenCompressionCustomizerIsNotConfigured() { // Codec class guards the imported GrpcCodecConfiguration which provides the // registry this.contextRunner() .withClassLoader(new FilteredClassLoader(Codec.class)) .run((context) -> assertThat(context) - .getBean("decompressionClientCustomizer", GrpcChannelBuilderCustomizer.class) + .getBean("compressionClientCustomizer", GrpcChannelBuilderCustomizer.class) .isNull()); } @@ -211,6 +200,17 @@ void decompressionCustomizerAutoConfiguredAsExpected() { }); } + @Test + void whenNoDecompressorRegistryAutoConfigurationIsSkipped() { + // Codec class guards the imported GrpcCodecConfiguration which provides the + // registry + this.contextRunner() + .withClassLoader(new FilteredClassLoader(Codec.class)) + .run((context) -> assertThat(context) + .getBean("decompressionClientCustomizer", GrpcChannelBuilderCustomizer.class) + .isNull()); + } + @Test void whenHasUserDefinedChannelBuilderCustomizersDoesNotAutoConfigureBean() { ChannelBuilderCustomizers customCustomizers = mock(); diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java index d3be49fa4eaf..a36f0b79cb86 100644 --- a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java @@ -16,14 +16,20 @@ package org.springframework.boot.grpc.client.autoconfigure; +import io.grpc.Codec; +import io.grpc.Compressor; import io.grpc.CompressorRegistry; +import io.grpc.Decompressor; import io.grpc.DecompressorRegistry; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for {@link GrpcCodecConfiguration}. @@ -36,13 +42,59 @@ class GrpcCodecConfigurationTests { .withConfiguration(AutoConfigurations.of(GrpcCodecConfiguration.class)); @Test - void testCompressorRegistryBean() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(CompressorRegistry.class)); + void whenCodecNotOnClasspathThenAutoconfigurationSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcCodecConfiguration.class)); } @Test - void testDecompressorRegistryBean() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(DecompressorRegistry.class)); + void whenHasCustomCompressorRegistryDoesNotAutoConfigureBean() { + CompressorRegistry customRegistry = mock(); + this.contextRunner.withBean("customCompressorRegistry", CompressorRegistry.class, () -> customRegistry) + .run((context) -> assertThat(context).getBean(CompressorRegistry.class).isSameAs(customRegistry)); + } + + @Test + void compressorRegistryAutoConfiguredAsExpected() { + this.contextRunner.run((context) -> assertThat(context).getBean(CompressorRegistry.class) + .isSameAs(CompressorRegistry.getDefaultInstance())); + } + + @Test + void whenCustomCompressorsThenCompressorRegistryIsNewInstance() { + Compressor compressor = mock(); + given(compressor.getMessageEncoding()).willReturn("foo"); + this.contextRunner.withBean(Compressor.class, () -> compressor).run((context) -> { + assertThat(context).hasSingleBean(CompressorRegistry.class); + CompressorRegistry registry = context.getBean(CompressorRegistry.class); + assertThat(registry).isNotSameAs(CompressorRegistry.getDefaultInstance()); + assertThat(registry.lookupCompressor("foo")).isSameAs(compressor); + }); + } + + @Test + void whenHasCustomDecompressorRegistryDoesNotAutoConfigureBean() { + DecompressorRegistry customRegistry = mock(); + this.contextRunner.withBean("customDecompressorRegistry", DecompressorRegistry.class, () -> customRegistry) + .run((context) -> assertThat(context).getBean(DecompressorRegistry.class).isSameAs(customRegistry)); + } + + @Test + void decompressorRegistryAutoConfiguredAsExpected() { + this.contextRunner.run((context) -> assertThat(context).getBean(DecompressorRegistry.class) + .isSameAs(DecompressorRegistry.getDefaultInstance())); + } + + @Test + void whenCustomDecompressorsThenDecompressorRegistryIsNewInstance() { + Decompressor decompressor = mock(); + given(decompressor.getMessageEncoding()).willReturn("foo"); + this.contextRunner.withBean(Decompressor.class, () -> decompressor).run((context) -> { + assertThat(context).hasSingleBean(DecompressorRegistry.class); + DecompressorRegistry registry = context.getBean(DecompressorRegistry.class); + assertThat(registry).isNotSameAs(DecompressorRegistry.getDefaultInstance()); + assertThat(registry.lookupDecompressor("foo")).isSameAs(decompressor); + }); } } From a653f223b32e0b3c06516817dbfdc04b4ffed511 Mon Sep 17 00:00:00 2001 From: onobc Date: Wed, 1 Oct 2025 16:59:36 -0500 Subject: [PATCH 21/21] Fix codec configuration (server side) - only use default de/compressor registry when user provides no custom de/compressor - update tests accordingly Signed-off-by: onobc --- .../GrpcClientAutoConfigurationTests.java | 2 +- .../autoconfigure/GrpcCodecConfiguration.java | 32 +++++++++- .../GrpcCodecConfigurationTests.java | 60 +++++++++++++++++-- .../GrpcServerAutoConfigurationTests.java | 57 ++++++++++++++++++ 4 files changed, 143 insertions(+), 8 deletions(-) diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java index bf76c99d20ab..f109af556774 100644 --- a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java +++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java @@ -201,7 +201,7 @@ void decompressionCustomizerAutoConfiguredAsExpected() { } @Test - void whenNoDecompressorRegistryAutoConfigurationIsSkipped() { + void whenNoDecompressorRegistryThenDecompressionCustomizerIsNotConfigured() { // Codec class guards the imported GrpcCodecConfiguration which provides the // registry this.contextRunner() diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfiguration.java index 9ebbb68985df..77ef45b044b3 100644 --- a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfiguration.java +++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfiguration.java @@ -21,6 +21,7 @@ import io.grpc.CompressorRegistry; import io.grpc.Decompressor; import io.grpc.DecompressorRegistry; +import io.grpc.ServerBuilder; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -37,19 +38,44 @@ @ConditionalOnClass(Codec.class) class GrpcCodecConfiguration { + /** + * The compressor registry that is set on the + * {@link ServerBuilder#compressorRegistry(CompressorRegistry) server builder} . + * @param compressors the compressors to use on the registry + * @return a new {@link CompressorRegistry#newEmptyInstance() registry} with the + * specified compressors or the {@link CompressorRegistry#getDefaultInstance() default + * registry} if no custom compressors are available in the application context. + */ @Bean @ConditionalOnMissingBean CompressorRegistry compressorRegistry(ObjectProvider compressors) { - CompressorRegistry registry = CompressorRegistry.getDefaultInstance(); + if (compressors.stream().count() == 0) { + return CompressorRegistry.getDefaultInstance(); + } + CompressorRegistry registry = CompressorRegistry.newEmptyInstance(); compressors.orderedStream().forEachOrdered(registry::register); return registry; } + /** + * The decompressor registry that is set on the + * {@link ServerBuilder#decompressorRegistry(DecompressorRegistry) server builder}. + * @param decompressors the decompressors to use on the registry + * @return a new {@link DecompressorRegistry#emptyInstance() registry} with the + * specified decompressors or the {@link DecompressorRegistry#getDefaultInstance() + * default registry} if no custom decompressors are available in the application + * context. + */ @Bean @ConditionalOnMissingBean DecompressorRegistry decompressorRegistry(ObjectProvider decompressors) { - DecompressorRegistry registry = DecompressorRegistry.getDefaultInstance(); - decompressors.orderedStream().forEachOrdered((decompressor) -> registry.with(decompressor, false)); + if (decompressors.stream().count() == 0) { + return DecompressorRegistry.getDefaultInstance(); + } + DecompressorRegistry registry = DecompressorRegistry.emptyInstance(); + for (Decompressor decompressor : decompressors.orderedStream().toList()) { + registry = registry.with(decompressor, false); + } return registry; } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfigurationTests.java index 20dc047258ac..479319e29d6c 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfigurationTests.java @@ -16,14 +16,20 @@ package org.springframework.boot.grpc.server.autoconfigure; +import io.grpc.Codec; +import io.grpc.Compressor; import io.grpc.CompressorRegistry; +import io.grpc.Decompressor; import io.grpc.DecompressorRegistry; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for {@link GrpcCodecConfiguration}. @@ -36,13 +42,59 @@ class GrpcCodecConfigurationTests { .withConfiguration(AutoConfigurations.of(GrpcCodecConfiguration.class)); @Test - void testCompressorRegistryBean() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(CompressorRegistry.class)); + void whenCodecNotOnClasspathThenAutoconfigurationSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcCodecConfiguration.class)); } @Test - void testDecompressorRegistryBean() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(DecompressorRegistry.class)); + void whenHasCustomCompressorRegistryDoesNotAutoConfigureBean() { + CompressorRegistry customRegistry = mock(); + this.contextRunner.withBean("customCompressorRegistry", CompressorRegistry.class, () -> customRegistry) + .run((context) -> assertThat(context).getBean(CompressorRegistry.class).isSameAs(customRegistry)); + } + + @Test + void compressorRegistryAutoConfiguredAsExpected() { + this.contextRunner.run((context) -> assertThat(context).getBean(CompressorRegistry.class) + .isSameAs(CompressorRegistry.getDefaultInstance())); + } + + @Test + void whenCustomCompressorsThenCompressorRegistryIsNewInstance() { + Compressor compressor = mock(); + given(compressor.getMessageEncoding()).willReturn("foo"); + this.contextRunner.withBean(Compressor.class, () -> compressor).run((context) -> { + assertThat(context).hasSingleBean(CompressorRegistry.class); + CompressorRegistry registry = context.getBean(CompressorRegistry.class); + assertThat(registry).isNotSameAs(CompressorRegistry.getDefaultInstance()); + assertThat(registry.lookupCompressor("foo")).isSameAs(compressor); + }); + } + + @Test + void whenHasCustomDecompressorRegistryDoesNotAutoConfigureBean() { + DecompressorRegistry customRegistry = mock(); + this.contextRunner.withBean("customDecompressorRegistry", DecompressorRegistry.class, () -> customRegistry) + .run((context) -> assertThat(context).getBean(DecompressorRegistry.class).isSameAs(customRegistry)); + } + + @Test + void decompressorRegistryAutoConfiguredAsExpected() { + this.contextRunner.run((context) -> assertThat(context).getBean(DecompressorRegistry.class) + .isSameAs(DecompressorRegistry.getDefaultInstance())); + } + + @Test + void whenCustomDecompressorsThenDecompressorRegistryIsNewInstance() { + Decompressor decompressor = mock(); + given(decompressor.getMessageEncoding()).willReturn("foo"); + this.contextRunner.withBean(Decompressor.class, () -> decompressor).run((context) -> { + assertThat(context).hasSingleBean(DecompressorRegistry.class); + DecompressorRegistry registry = context.getBean(DecompressorRegistry.class); + assertThat(registry).isNotSameAs(DecompressorRegistry.getDefaultInstance()); + assertThat(registry.lookupDecompressor("foo")).isSameAs(decompressor); + }); } } diff --git a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java index 41b6e2bfb9f6..552c3777b3d3 100644 --- a/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java +++ b/module/spring-boot-grpc-server/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java @@ -21,6 +21,9 @@ import java.util.concurrent.TimeUnit; import io.grpc.BindableService; +import io.grpc.Codec; +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; import io.grpc.Grpc; import io.grpc.ServerBuilder; import io.grpc.ServerServiceDefinition; @@ -61,6 +64,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -409,6 +413,59 @@ void nettyServerFactoryAutoConfiguredWithSsl() { NettyGrpcServerFactory.class, "myhost:6160", "nettyGrpcServerLifecycle"); } + @Nested + class WithCodecConfiguration { + + @SuppressWarnings("unchecked") + @Test + void compressionCustomizerAutoConfiguredAsExpected() { + GrpcServerAutoConfigurationTests.this.contextRunner().run((context) -> { + assertThat(context).getBean("compressionServerConfigurer", ServerBuilderCustomizer.class).isNotNull(); + var customizer = context.getBean("compressionServerConfigurer", ServerBuilderCustomizer.class); + var compressorRegistry = context.getBean(CompressorRegistry.class); + ServerBuilder builder = mock(); + customizer.customize(builder); + then(builder).should().compressorRegistry(compressorRegistry); + }); + } + + @Test + void whenNoCompressorRegistryThenCompressionCustomizerIsNotConfigured() { + // Codec class guards the imported GrpcCodecConfiguration which provides the + // registry + GrpcServerAutoConfigurationTests.this.contextRunner() + .withClassLoader(new FilteredClassLoader(Codec.class)) + .run((context) -> assertThat(context) + .getBean("compressionServerConfigurer", ServerBuilderCustomizer.class) + .isNull()); + } + + @SuppressWarnings("unchecked") + @Test + void decompressionCustomizerAutoConfiguredAsExpected() { + GrpcServerAutoConfigurationTests.this.contextRunner().run((context) -> { + assertThat(context).getBean("decompressionServerConfigurer", ServerBuilderCustomizer.class).isNotNull(); + var customizer = context.getBean("decompressionServerConfigurer", ServerBuilderCustomizer.class); + var decompressorRegistry = context.getBean(DecompressorRegistry.class); + ServerBuilder builder = mock(); + customizer.customize(builder); + then(builder).should().decompressorRegistry(decompressorRegistry); + }); + } + + @Test + void whenNoDecompressorRegistryThenDecompressionCustomizerIsNotConfigured() { + // Codec class guards the imported GrpcCodecConfiguration which provides the + // registry + GrpcServerAutoConfigurationTests.this.contextRunner() + .withClassLoader(new FilteredClassLoader(Codec.class)) + .run((context) -> assertThat(context) + .getBean("decompressionClientCustomizer", ServerBuilderCustomizer.class) + .isNull()); + } + + } + @Nested class WithAllFactoriesServiceFilterAutoConfig {