From 691f506aa41ac11eb0479df0f253921587b5be23 Mon Sep 17 00:00:00 2001 From: onobc Date: Thu, 6 Nov 2025 11:15:55 -0600 Subject: [PATCH 01/21] Add starters back into project. This commit brings the starters back into the project. Signed-off-by: onobc --- pom.xml | 16 +- .../pom.xml | 200 ++++++ .../ChannelBuilderCustomizers.java | 59 ++ ...entPropertiesChannelBuilderCustomizer.java | 82 +++ ...positeChannelFactoryAutoConfiguration.java | 67 ++ .../ConditionalOnGrpcClientEnabled.java | 45 ++ .../ConfigurationPropertiesMapUtils.java | 98 +++ .../DefaultGrpcClientRegistrations.java | 68 ++ .../GrpcChannelFactoryConfigurations.java | 125 ++++ .../GrpcChannelFactoryCustomizer.java | 37 ++ .../GrpcClientAutoConfiguration.java | 106 ++++ ...rpcClientObservationAutoConfiguration.java | 46 ++ .../autoconfigure/GrpcClientProperties.java | 465 ++++++++++++++ .../autoconfigure/GrpcCodecConfiguration.java | 84 +++ .../NamedChannelCredentialsProvider.java | 79 +++ .../client/autoconfigure/package-info.java | 23 + ...itional-spring-configuration-metadata.json | 29 + ...ot.autoconfigure.AutoConfiguration.imports | 3 + .../ChannelBuilderCustomizersTests.java | 125 ++++ ...eChannelFactoryAutoConfigurationTests.java | 142 +++++ .../ConfigurationPropertiesMapUtilsTests.java | 83 +++ .../DefaultGrpcClientRegistrationsTests.java | 84 +++ .../GrpcClientAutoConfigurationTests.java | 451 +++++++++++++ ...ientObservationAutoConfigurationTests.java | 119 ++++ .../GrpcClientPropertiesTests.java | 311 +++++++++ .../GrpcCodecConfigurationTests.java | 101 +++ .../src/test/resources/logback-test.xml | 4 + .../pom.xml | 36 ++ spring-grpc-dependencies/pom.xml | 38 +- .../pom.xml | 201 ++++++ .../ConditionalOnGrpcNativeServer.java | 41 ++ .../ConditionalOnGrpcServerEnabled.java | 49 ++ .../ConditionalOnGrpcServletServer.java | 50 ++ .../ConditionalOnSpringGrpc.java | 43 ++ .../DefaultServerFactoryPropertyMapper.java | 89 +++ .../autoconfigure/GrpcCodecConfiguration.java | 82 +++ .../GrpcServerAutoConfiguration.java | 128 ++++ .../GrpcServerExecutorProvider.java | 36 ++ .../GrpcServerFactoryAutoConfiguration.java | 113 ++++ .../GrpcServerFactoryConfigurations.java | 194 ++++++ .../GrpcServerFactoryCustomizer.java | 37 ++ ...rpcServerObservationAutoConfiguration.java | 69 ++ .../autoconfigure/GrpcServerProperties.java | 444 +++++++++++++ ...GrpcServerReflectionAutoConfiguration.java | 52 ++ .../InProcessServerFactoryPropertyMapper.java | 41 ++ .../NettyServerFactoryPropertyMapper.java | 40 ++ .../OnEnabledGrpcServerCondition.java | 73 +++ .../OnGrpcNativeServerCondition.java | 69 ++ .../ServerBuilderCustomizers.java | 58 ++ .../ServletEnvironmentPostProcessor.java | 45 ++ ...hadedNettyServerFactoryPropertyMapper.java | 40 ++ ...GrpcExceptionHandlerAutoConfiguration.java | 55 ++ .../autoconfigure/exception/package-info.java | 23 + .../health/ActuatorHealthAdapter.java | 113 ++++ .../health/ActuatorHealthAdapterInvoker.java | 65 ++ .../GrpcServerHealthAutoConfiguration.java | 132 ++++ .../autoconfigure/health/package-info.java | 23 + .../server/autoconfigure/package-info.java | 24 + .../GrpcDisableCsrfHttpConfigurer.java | 70 +++ .../security/GrpcReactiveRequest.java | 131 ++++ .../GrpcSecurityAutoConfiguration.java | 114 ++++ .../security/GrpcServletRequest.java | 137 ++++ .../OAuth2ClientAutoConfiguration.java | 60 ++ ...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 + .../GrpcCodecConfigurationTests.java | 101 +++ .../GrpcServerAutoConfigurationTests.java | 594 ++++++++++++++++++ ...rverObservationAutoConfigurationTests.java | 134 ++++ .../GrpcServerPropertiesTests.java | 192 ++++++ ...erverReflectionAutoConfigurationTests.java | 114 ++++ .../GrpcServletAutoConfigurationTests.java | 123 ++++ .../OnEnabledGrpcServerConditionTests.java | 146 +++++ .../ServerBuilderCustomizersTests.java | 124 ++++ .../ServerFactoryPropertyMappersTests.java | 86 +++ ...xceptionHandlerAutoConfigurationTests.java | 129 ++++ .../ActuatorHealthAdapterInvokerTests.java | 52 ++ .../health/ActuatorHealthAdapterTests.java | 156 +++++ ...rpcServerHealthAutoConfigurationTests.java | 273 ++++++++ .../security/GrpcReactiveRequestTests.java | 88 +++ .../GrpcSecurityAutoConfigurationTests.java | 119 ++++ .../security/GrpcServletRequestTests.java | 99 +++ ...2ResourceServerAutoConfigurationTests.java | 177 ++++++ .../src/test/resources/logback-test.xml | 4 + .../boot/grpc/server/autoconfigure/test.jks | Bin 0 -> 1276 bytes .../pom.xml | 36 ++ .../pom.xml | 32 + spring-grpc-spring-boot-starter/pom.xml | 39 ++ .../pom.xml | 92 +++ .../AutoConfigureInProcessTransport.java | 54 ++ .../InProcessTestAutoConfiguration.java | 163 +++++ ...cessTransportContextCustomizerFactory.java | 91 +++ .../test/autoconfigure/LocalGrpcPort.java | 42 ++ ...PortInfoApplicationContextInitializer.java | 87 +++ .../grpc/test/autoconfigure/package-info.java | 23 + .../main/resources/META-INF/spring.factories | 6 + ...re.AutoConfigureInProcessTransport.imports | 1 + .../InProcessTestAutoConfigurationTests.java | 100 +++ 100 files changed, 9959 insertions(+), 4 deletions(-) create mode 100644 spring-grpc-client-spring-boot-autoconfigure/pom.xml create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientPropertiesChannelBuilderCustomizer.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientEnabled.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConfigurationPropertiesMapUtils.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrations.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfiguration.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizersTests.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ConfigurationPropertiesMapUtilsTests.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrationsTests.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java create mode 100644 spring-grpc-client-spring-boot-autoconfigure/src/test/resources/logback-test.xml create mode 100644 spring-grpc-client-spring-boot-starter/pom.xml create mode 100644 spring-grpc-server-spring-boot-autoconfigure/pom.xml create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcNativeServer.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServletServer.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnSpringGrpc.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfiguration.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessServerFactoryPropertyMapper.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyServerFactoryPropertyMapper.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerCondition.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnGrpcNativeServerCondition.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyServerFactoryPropertyMapper.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/package-info.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapter.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvoker.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequest.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequest.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ClientAutoConfiguration.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfigurationTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfigurationTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServletAutoConfigurationTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerConditionTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizersTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerFactoryPropertyMappersTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfigurationTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvokerTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequestTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfigurationTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequestTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfigurationTests.java create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/resources/logback-test.xml create mode 100644 spring-grpc-server-spring-boot-autoconfigure/src/test/resources/org/springframework/boot/grpc/server/autoconfigure/test.jks create mode 100644 spring-grpc-server-spring-boot-starter/pom.xml create mode 100644 spring-grpc-server-web-spring-boot-starter/pom.xml create mode 100644 spring-grpc-spring-boot-starter/pom.xml create mode 100644 spring-grpc-test-spring-boot-autoconfigure/pom.xml create mode 100644 spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureInProcessTransport.java create mode 100644 spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfiguration.java create mode 100644 spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTransportContextCustomizerFactory.java create mode 100644 spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/LocalGrpcPort.java create mode 100644 spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/ServerPortInfoApplicationContextInitializer.java create mode 100644 spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/package-info.java create mode 100644 spring-grpc-test-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories create mode 100644 spring-grpc-test-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.grpc.test.autoconfigure.AutoConfigureInProcessTransport.imports create mode 100644 spring-grpc-test-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfigurationTests.java diff --git a/pom.xml b/pom.xml index 40f6e4b5..c3ede736 100644 --- a/pom.xml +++ b/pom.xml @@ -14,11 +14,21 @@ Building gRPC applications with Spring Boot + spring-grpc-build-dependencies - spring-grpc-docs - spring-grpc-dependencies spring-grpc-core - + spring-grpc-dependencies + spring-grpc-docs + + + + spring-grpc-client-spring-boot-autoconfigure + spring-grpc-client-spring-boot-starter + spring-grpc-server-spring-boot-autoconfigure + spring-grpc-server-spring-boot-starter + spring-grpc-server-web-spring-boot-starter + spring-grpc-spring-boot-starter + spring-grpc-test-spring-boot-autoconfigure diff --git a/spring-grpc-client-spring-boot-autoconfigure/pom.xml b/spring-grpc-client-spring-boot-autoconfigure/pom.xml new file mode 100644 index 00000000..2f8eb12e --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/pom.xml @@ -0,0 +1,200 @@ + + + 4.0.0 + + org.springframework.grpc + spring-grpc + 1.0.0-SNAPSHOT + + spring-grpc-client-spring-boot-autoconfigure + jar + Spring gRPC Client Auto Configuration + Spring gRPC Client Auto Configuration + https://github.com/spring-projects/spring-grpc + + + https://github.com/spring-projects/spring-grpc + git://github.com/spring-projects/spring-grpc.git + git@github.com:spring-projects/spring-grpc.git + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + + org.springframework.boot + spring-boot + + + org.springframework.grpc + spring-grpc-core + + + + com.fasterxml.jackson.core + jackson-annotations + true + + + + org.springframework.boot + spring-boot-autoconfigure + true + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springframework.boot + spring-boot-actuator + true + + + org.springframework.boot + spring-boot-actuator-autoconfigure + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-health + true + + + org.springframework.boot + spring-boot-micrometer-observation + true + + + org.springframework.boot + spring-boot-security + true + + + org.springframework.boot + spring-boot-security-oauth2-client + true + + + org.springframework.boot + spring-boot-security-oauth2-resource-server + true + + + + io.grpc + grpc-servlet-jakarta + true + + + io.grpc + grpc-stub + true + + + io.grpc + grpc-netty + true + + + io.grpc + grpc-netty-shaded + true + + + io.grpc + grpc-inprocess + true + + + io.grpc + grpc-kotlin-stub + true + + + javax.annotation + javax.annotation-api + + + + + io.micrometer + micrometer-core + true + + + io.netty + netty-transport-native-epoll + true + + + io.projectreactor + reactor-core + true + + + jakarta.servlet + jakarta.servlet-api + true + + + + org.springframework + spring-web + true + + + org.springframework.security + spring-security-config + true + + + org.springframework.security + spring-security-oauth2-client + true + + + org.springframework.security + spring-security-oauth2-resource-server + true + + + org.springframework.security + spring-security-oauth2-jose + true + + + org.springframework.security + spring-security-web + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java new file mode 100644 index 00000000..a597e463 --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.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.client.autoconfigure; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.util.LambdaSafe; +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; + +import io.grpc.ManagedChannelBuilder; + +/** + * Invokes the available {@link GrpcChannelBuilderCustomizer} instances for a given + * {@link ManagedChannelBuilder}. + * + * @author Chris Bono + */ +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/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientPropertiesChannelBuilderCustomizer.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientPropertiesChannelBuilderCustomizer.java new file mode 100644 index 00000000..0c193a7b --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientPropertiesChannelBuilderCustomizer.java @@ -0,0 +1,82 @@ +/* + * 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.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +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; + +import io.grpc.ManagedChannelBuilder; + +/** + * 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 = channel.extractServiceConfig(); + 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/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java new file mode 100644 index 00000000..563c388e --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/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/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientEnabled.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientEnabled.java new file mode 100644 index 00000000..6589b7a6 --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/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 org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Conditional; + +import io.grpc.stub.AbstractStub; + +/** + * {@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/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConfigurationPropertiesMapUtils.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConfigurationPropertiesMapUtils.java new file mode 100644 index 00000000..a68a1957 --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConfigurationPropertiesMapUtils.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.client.autoconfigure; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +/** + * Utility to help w/ conversion of configuration properties map values. + *

+ * By default, Spring Boot turns a Yaml property whose value is a list of enum strings + * (e.g. {@code statusCodes: [ "ONE, TWO" ]} into a map with integer keys. This utility + * allows these integer keyed maps to instead be represented as list. + * + * @author Chris Bono + */ +final class ConfigurationPropertiesMapUtils { + + private static final String NUMBER = "[0-9]+"; + + private ConfigurationPropertiesMapUtils() { + } + + /** + * Converts any maps with integer keys into lists for the specified configuration map. + * @param input the configuration map to convert + * @return the map with integer keyed map values converted into lists + */ + static Map convertIntegerKeyedMapsToLists(Map input) { + Map map = new HashMap<>(); + for (Map.Entry entry : input.entrySet()) { + map.put(entry.getKey(), extract(entry.getValue())); + } + return map; + } + + @SuppressWarnings("unchecked") + private static @Nullable Object extract(@Nullable Object input) { + if (input == null) { + return null; + } + if (input instanceof Map) { + return map((Map) input); + } + return input; + } + + private static Object map(Map input) { + Map map = new HashMap<>(); + List list = new ArrayList<>(); + boolean maybeList = true; + for (Map.Entry entry : input.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (maybeList && key.matches(NUMBER)) { + int index = Integer.parseInt(key); + while (index >= list.size()) { + list.add(null); + } + list.set(index, extract(value)); + } + else { + maybeList = false; + if (!list.isEmpty()) { + // Not really a list after all + for (int i = 0; i < list.size(); i++) { + map.put(String.valueOf(i), list.get(i)); + } + list.clear(); + } + map.put(key, extract(value)); + } + } + if (list.size() > 0) { + return list; + } + return map; + } + +} diff --git a/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrations.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrations.java new file mode 100644 index 00000000..26e4ef51 --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/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/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java new file mode 100644 index 00000000..e283c399 --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java @@ -0,0 +1,125 @@ +/* + * 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 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; + +import io.grpc.Channel; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.netty.NettyChannelBuilder; + +/** + * 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); + ShadedNettyGrpcChannelFactory 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); + NettyGrpcChannelFactory 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/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java new file mode 100644 index 00000000..7e640a3e --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * 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 { + + /** + * Customize the given {@link GrpcChannelFactory}. + * @param factory the factory to customize + */ + void customize(GrpcChannelFactory factory); + +} diff --git a/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java new file mode 100644 index 00000000..6203824e --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java @@ -0,0 +1,106 @@ +/* + * 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.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.GrpcClientAutoConfiguration.ClientScanConfiguration; +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; +import org.springframework.grpc.client.GrpcClientFactory; + +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.ManagedChannelBuilder; + +@AutoConfiguration(before = CompositeChannelFactoryAutoConfiguration.class) +@ConditionalOnGrpcClientEnabled +@EnableConfigurationProperties(GrpcClientProperties.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) { + 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) + @ConditionalOnMissingBean(GrpcClientFactory.class) + @Import(DefaultGrpcClientRegistrations.class) + static class ClientScanConfiguration { + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = "io.grpc.kotlin.AbstractCoroutineStub") + static class GrpcClientCoroutineStubConfiguration { + + @Bean + @ConditionalOnMissingBean + CoroutineStubFactory coroutineStubFactory() { + return new CoroutineStubFactory(); + } + + } + +} diff --git a/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java new file mode 100644 index 00000000..eaa130bf --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/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 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; + +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor; +import io.micrometer.observation.ObservationRegistry; + +@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/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java new file mode 100644 index 00000000..ef5362b0 --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java @@ -0,0 +1,465 @@ +/* + * 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 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; + +import io.grpc.ManagedChannel; + +@ConfigurationProperties(prefix = "spring.grpc.client") +public class GrpcClientProperties implements EnvironmentAware, VirtualTargets { + + /** + * Map of channels configured by name. + */ + private final Map channels = new HashMap<>(); + + /** + * The default channel configuration to use for new channels. + */ + private final ChannelConfig defaultChannel = new ChannelConfig(); + + /** + * 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 Map getChannels() { + return this.channels; + } + + public ChannelConfig getDefaultChannel() { + return this.defaultChannel; + } + + 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"; + + /** + * The default deadline for RPCs performed on this channel. + */ + private @Nullable Duration defaultDeadline; + + /** + * The load balancing policy the channel should use. + */ + private String defaultLoadBalancingPolicy = "round_robin"; + + /** + * Whether keep alive is enabled on the channel. + */ + private boolean enableKeepAlive; + + private final Health health = new Health(); + + /** + * The duration without ongoing RPCs before going to idle mode. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration idleTimeout = Duration.ofSeconds(20); + + /** + * 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); + + /** + * 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; + + /** + * 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); + + /** + * 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<>(); + + private final Ssl ssl = new Ssl(); + + /** + * The custom User-Agent for the channel. + */ + private @Nullable String userAgent; + + public String getAddress() { + return this.address; + } + + public void setAddress(final String address) { + this.address = address; + } + + public @Nullable Duration getDefaultDeadline() { + return this.defaultDeadline; + } + + 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; + } + + public void setEnableKeepAlive(boolean enableKeepAlive) { + this.enableKeepAlive = enableKeepAlive; + } + + public Health getHealth() { + return this.health; + } + + public Duration getIdleTimeout() { + return this.idleTimeout; + } + + public void setIdleTimeout(Duration idleTimeout) { + this.idleTimeout = idleTimeout; + } + + public Duration getKeepAliveTime() { + return this.keepAliveTime; + } + + public void setKeepAliveTime(Duration keepAliveTime) { + this.keepAliveTime = keepAliveTime; + } + + public Duration getKeepAliveTimeout() { + return this.keepAliveTimeout; + } + + public void setKeepAliveTimeout(Duration keepAliveTimeout) { + this.keepAliveTimeout = keepAliveTimeout; + } + + public boolean isKeepAliveWithoutCalls() { + return this.keepAliveWithoutCalls; + } + + public void setKeepAliveWithoutCalls(boolean keepAliveWithoutCalls) { + this.keepAliveWithoutCalls = keepAliveWithoutCalls; + } + + 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)); + } + } + + public NegotiationType getNegotiationType() { + return this.negotiationType; + } + + public void setNegotiationType(NegotiationType negotiationType) { + this.negotiationType = negotiationType; + } + + public boolean isSecure() { + return this.secure; + } + + public void setSecure(boolean secure) { + this.secure = secure; + } + + public Map getServiceConfig() { + return this.serviceConfig; + } + + public Ssl getSsl() { + return this.ssl; + } + + 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. + */ + 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; + } + + /** + * Extracts the service configuration from the client properties, respecting the + * yaml lists (e.g. `retryPolicy`). + * @return the map for the `serviceConfig` property + */ + @SuppressWarnings("NullAway") + public Map extractServiceConfig() { + return ConfigurationPropertiesMapUtils.convertIntegerKeyedMapsToLists(getServiceConfig()); + } + + public static class Health { + + /** + * Whether to enable client-side health check for the channel. + */ + private boolean enabled; + + /** + * 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 + */ + void copyValuesFrom(Health other) { + this.enabled = other.enabled; + this.serviceName = other.serviceName; + } + + } + + 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 @Nullable Boolean isEnabled() { + return this.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; + } + + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; + } + + /** + * Copies the values from another instance. + * @param other instance to copy values from + */ + void copyValuesFrom(Ssl other) { + this.enabled = other.enabled; + this.bundle = other.bundle; + } + + } + + } + +} diff --git a/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfiguration.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfiguration.java new file mode 100644 index 00000000..c9307b58 --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfiguration.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 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; + +import io.grpc.Codec; +import io.grpc.Compressor; +import io.grpc.CompressorRegistry; +import io.grpc.Decompressor; +import io.grpc.DecompressorRegistry; +import io.grpc.ManagedChannelBuilder; + +/** + * The configuration that contains all codec related beans for clients. + * + * @author Andrei Lisa + */ +@Configuration(proxyBeanMethods = false) +@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) { + 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) { + 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/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java new file mode 100644 index 00000000..a0a4de1b --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/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 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; + +import io.grpc.ChannelCredentials; +import io.grpc.InsecureChannelCredentials; +import io.grpc.TlsChannelCredentials; + +/** + * Provides channel credentials using channel configuration and {@link SslBundles}. + * + * @author David Syer + */ +class NamedChannelCredentialsProvider implements ChannelCredentialsProvider { + + private final SslBundles bundles; + + private final GrpcClientProperties properties; + + NamedChannelCredentialsProvider(SslBundles bundles, GrpcClientProperties properties) { + this.bundles = bundles; + this.properties = properties; + } + + @Override + public ChannelCredentials getChannelCredentials(String path) { + ChannelConfig channel = this.properties.getChannel(path); + boolean sslEnabled = channel.getSsl().determineEnabled(); + if (!sslEnabled && channel.getNegotiationType() == NegotiationType.PLAINTEXT) { + return InsecureChannelCredentials.create(); + } + 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); + 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/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java b/spring-grpc-client-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java new file mode 100644 index 00000000..efa3388c --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/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/spring-grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 00000000..b2914ae3 --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/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.observation.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable Observations on the client.", + "defaultValue": true + } + ] +} diff --git a/spring-grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..7d3249ab --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/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/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizersTests.java b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizersTests.java new file mode 100644 index 00000000..289e9fbb --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizersTests.java @@ -0,0 +1,125 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer; + +import io.grpc.ManagedChannelBuilder; +import io.grpc.netty.NettyChannelBuilder; + +/** + * 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/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java new file mode 100644 index 00000000..537fb5b8 --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java @@ -0,0 +1,142 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +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 io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.netty.NettyChannelBuilder; + +/** + * 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/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ConfigurationPropertiesMapUtilsTests.java b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ConfigurationPropertiesMapUtilsTests.java new file mode 100644 index 00000000..1c5c063e --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ConfigurationPropertiesMapUtilsTests.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 static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link ConfigurationPropertiesMapUtils}. + */ +class ConfigurationPropertiesMapUtilsTests { + + @Test + void simpleConfigMap() { + Map map = ConfigurationPropertiesMapUtils + .convertIntegerKeyedMapsToLists(Map.of("userAgent", "foo", "maxInboundMessageSize", "1MB")); + assertThat(map).containsEntry("userAgent", "foo").containsEntry("maxInboundMessageSize", "1MB"); + } + + @Test + void listConfigMap() { + Map map = ConfigurationPropertiesMapUtils + .convertIntegerKeyedMapsToLists(Map.of("spam", Map.of("0", "foo", "1", "bar"))); + assertThat(map.get("spam")).asInstanceOf(InstanceOfAssertFactories.LIST).containsExactly("foo", "bar"); + } + + @Test + void notReallyAListConfigMap() { + Map map = ConfigurationPropertiesMapUtils + .convertIntegerKeyedMapsToLists(Map.of("spam", Map.of("0", "foo", "oops", "bar"))); + assertThat(map.get("spam")).asInstanceOf(InstanceOfAssertFactories.MAP).containsKeys("0", "oops"); + } + + @Test + void methodConfigMap() { + Map map = ConfigurationPropertiesMapUtils.convertIntegerKeyedMapsToLists(Map.of("methodConfig", + Map.of("0", + Map.of("name", Map.of("0", Map.of("service", "foo.Bar", "method", "Baz")), "waitForReady", true, + "timeout", "1s", "retryPolicy", + Map.of("maxAttempts", 5, "initialBackoff", "0.1s", "maxBackoff", "1s", + "backoffMultiplier", 2, "retryableStatusCodes", + Map.of("0", "UNAVAILABLE", "1", "RESOURCE_EXHAUSTED")))))); + + assertThat(map.get("methodConfig")).isInstanceOf(List.class); + @SuppressWarnings("unchecked") + List> methodConfigs = (List>) map.get("methodConfig"); + assertThat(methodConfigs).hasSize(1); + Map methodConfig = methodConfigs.get(0); + assertThat(methodConfig.get("name")).isInstanceOf(List.class); + @SuppressWarnings("unchecked") + List> names = (List>) methodConfig.get("name"); + assertThat(names).hasSize(1); + assertThat(names.get(0)).containsEntry("service", "foo.Bar").containsEntry("method", "Baz"); + assertThat(methodConfig).containsEntry("waitForReady", true); + assertThat(methodConfig.get("retryPolicy")).asInstanceOf(InstanceOfAssertFactories.MAP) + .containsEntry("maxAttempts", 5) + .containsEntry("maxBackoff", "1s") + .containsEntry("backoffMultiplier", 2) + .extractingByKey("retryableStatusCodes") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .contains("UNAVAILABLE", "RESOURCE_EXHAUSTED"); + } + +} diff --git a/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrationsTests.java b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrationsTests.java new file mode 100644 index 00000000..2c1f6a6f --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/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 static org.assertj.core.api.Assertions.assertThat; + +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; + +/** + * 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/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java new file mode 100644 index 00000000..709f211c --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java @@ -0,0 +1,451 @@ +/* + * 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 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; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +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.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; +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.GrpcClientFactory; +import org.springframework.grpc.client.InProcessGrpcChannelFactory; +import org.springframework.grpc.client.NettyGrpcChannelFactory; +import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory; + +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; + +/** + * 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 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 whenNoCompressorRegistryThenCompressionCustomizerIsNotConfigured() { + // 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 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 whenNoDecompressorRegistryThenDecompressionCustomizerIsNotConfigured() { + // 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(); + 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 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() + .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/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java new file mode 100644 index 00000000..9cce192d --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java @@ -0,0 +1,119 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; + +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 io.grpc.stub.AbstractStub; +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor; +import io.micrometer.observation.ObservationRegistry; + +/** + * 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/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java new file mode 100644 index 00000000..c96df2ea --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java @@ -0,0 +1,311 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +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; + +/** + * 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()).isNull(); + assertThat(channel.getSsl().determineEnabled()).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().determineEnabled()).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); + } + + @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 + 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/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java new file mode 100644 index 00000000..1990f752 --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java @@ -0,0 +1,101 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +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 io.grpc.Codec; +import io.grpc.Compressor; +import io.grpc.CompressorRegistry; +import io.grpc.Decompressor; +import io.grpc.DecompressorRegistry; + +/** + * Tests for {@link GrpcCodecConfiguration}. + * + * @author Andrei Lisa + */ +class GrpcCodecConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcCodecConfiguration.class)); + + @Test + void whenCodecNotOnClasspathThenAutoconfigurationSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcCodecConfiguration.class)); + } + + @Test + 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/spring-grpc-client-spring-boot-autoconfigure/src/test/resources/logback-test.xml b/spring-grpc-client-spring-boot-autoconfigure/src/test/resources/logback-test.xml new file mode 100644 index 00000000..b8a41480 --- /dev/null +++ b/spring-grpc-client-spring-boot-autoconfigure/src/test/resources/logback-test.xml @@ -0,0 +1,4 @@ + + + + diff --git a/spring-grpc-client-spring-boot-starter/pom.xml b/spring-grpc-client-spring-boot-starter/pom.xml new file mode 100644 index 00000000..19da8406 --- /dev/null +++ b/spring-grpc-client-spring-boot-starter/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + org.springframework.grpc + spring-grpc + 1.0.0-SNAPSHOT + + spring-grpc-client-spring-boot-starter + jar + Spring gRPC Client Spring Boot Starter + Spring gRPC Client Spring Boot Starter + + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + org.springframework.grpc + spring-grpc-client-spring-boot-autoconfigure + + + io.grpc + grpc-netty + + + io.grpc + grpc-stub + + + + diff --git a/spring-grpc-dependencies/pom.xml b/spring-grpc-dependencies/pom.xml index 31595cda..4049148d 100644 --- a/spring-grpc-dependencies/pom.xml +++ b/spring-grpc-dependencies/pom.xml @@ -48,8 +48,9 @@ + - 1.75.0 + 1.76.0 1.5.0 4.32.1 2.61.2 @@ -60,11 +61,46 @@ + + org.springframework.grpc + spring-grpc-client-spring-boot-autoconfigure + ${project.version} + + + org.springframework.grpc + spring-grpc-client-spring-boot-starter + ${project.version} + org.springframework.grpc spring-grpc-core ${project.version} + + org.springframework.grpc + spring-grpc-server-spring-boot-autoconfigure + ${project.version} + + + org.springframework.grpc + spring-grpc-server-spring-boot-starter + ${project.version} + + + org.springframework.grpc + spring-grpc-server-web-spring-boot-starter + ${project.version} + + + org.springframework.grpc + spring-grpc-spring-boot-starter + ${project.version} + + + org.springframework.grpc + spring-grpc-test-spring-boot-autoconfigure + ${project.version} + io.grpc grpc-bom diff --git a/spring-grpc-server-spring-boot-autoconfigure/pom.xml b/spring-grpc-server-spring-boot-autoconfigure/pom.xml new file mode 100644 index 00000000..780be95f --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/pom.xml @@ -0,0 +1,201 @@ + + + 4.0.0 + + org.springframework.grpc + spring-grpc + 1.0.0-SNAPSHOT + + spring-grpc-server-spring-boot-autoconfigure + jar + Spring gRPC Server Auto Configuration + Spring gRPC Server Auto Configuration + https://github.com/spring-projects/spring-grpc + + + https://github.com/spring-projects/spring-grpc + git://github.com/spring-projects/spring-grpc.git + git@github.com:spring-projects/spring-grpc.git + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + + org.springframework.boot + spring-boot + + + org.springframework.grpc + spring-grpc-core + + + + com.fasterxml.jackson.core + jackson-annotations + true + + + + org.springframework.boot + spring-boot-autoconfigure + true + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springframework.boot + spring-boot-actuator + true + + + org.springframework.boot + spring-boot-actuator-autoconfigure + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-health + true + + + org.springframework.boot + spring-boot-micrometer-observation + true + + + org.springframework.boot + spring-boot-security + true + + + org.springframework.boot + spring-boot-security-oauth2-client + true + + + org.springframework.boot + spring-boot-security-oauth2-resource-server + true + + + + io.grpc + grpc-servlet-jakarta + true + + + io.grpc + grpc-services + true + + + io.grpc + grpc-netty + true + + + io.grpc + grpc-netty-shaded + true + + + io.grpc + grpc-inprocess + true + + + io.grpc + grpc-kotlin-stub + true + + + javax.annotation + javax.annotation-api + + + + + io.micrometer + micrometer-core + true + + + io.netty + netty-transport-native-epoll + true + + + io.projectreactor + reactor-core + true + + + jakarta.servlet + jakarta.servlet-api + true + + + + org.springframework + spring-web + true + + + org.springframework.security + spring-security-config + true + + + org.springframework.security + spring-security-oauth2-client + true + + + org.springframework.security + spring-security-oauth2-resource-server + true + + + org.springframework.security + spring-security-oauth2-jose + true + + + org.springframework.security + spring-security-web + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcNativeServer.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcNativeServer.java new file mode 100644 index 00000000..51be07d1 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java new file mode 100644 index 00000000..9d47c9f0 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.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 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 + * @since 4.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Conditional(OnEnabledGrpcServerCondition.class) +public @interface ConditionalOnGrpcServerEnabled { + + /** + * Name of the gRPC service. + * @return the name of the gRPC service + */ + String value() default ""; + +} diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServletServer.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServletServer.java new file mode 100644 index 00000000..a80f45d6 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 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; + +import io.grpc.servlet.jakarta.GrpcServlet; + +/** + * {@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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnSpringGrpc.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnSpringGrpc.java new file mode 100644 index 00000000..b20b0e53 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Conditional; +import org.springframework.grpc.server.GrpcServerFactory; + +import io.grpc.BindableService; + +/** + * {@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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java new file mode 100644 index 00000000..f8778b01 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.grpc.server.DefaultGrpcServerFactory; +import org.springframework.util.unit.DataSize; + +import io.grpc.ServerBuilder; + +/** + * Helper class used to map {@link GrpcServerProperties} to + * {@link DefaultGrpcServerFactory}. + * + * @param the type of server builder + * @author Chris Bono + */ +class DefaultServerFactoryPropertyMapper> { + + private 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 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 map the property mapper + */ + void customizeKeepAlive(T serverBuilder, PropertyMapper map) { + GrpcServerProperties.KeepAlive keepAliveProps = this.properties.getKeepAlive(); + 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 map the property mapper + */ + void customizeInboundLimits(T serverBuilder, PropertyMapper map) { + map.from(this.properties.getMaxInboundMessageSize()) + .asInt(DataSize::toBytes) + .to(serverBuilder::maxInboundMessageSize); + map.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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfiguration.java new file mode 100644 index 00000000..d9e6058e --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfiguration.java @@ -0,0 +1,82 @@ +/* + * 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.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; + +import io.grpc.Codec; +import io.grpc.Compressor; +import io.grpc.CompressorRegistry; +import io.grpc.Decompressor; +import io.grpc.DecompressorRegistry; +import io.grpc.ServerBuilder; + +/** + * The configuration that contains all codec related beans for gRPC servers. + * + * @author Andrei Lisa + */ +@Configuration(proxyBeanMethods = false) +@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) { + 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) { + 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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java new file mode 100644 index 00000000..860847e6 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.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; + +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.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.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; + +import io.grpc.BindableService; +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.ServerBuilder; + +/** + * {@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) +@ConditionalOnSpringGrpc +@ConditionalOnGrpcServerEnabled +@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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java new file mode 100644 index 00000000..ac5f1d8c --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java @@ -0,0 +1,36 @@ +/* + * 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; + +/** + * 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 to be customized. + * @return the executor to use for the gRPC server + */ + Executor getExecutor(); + +} diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java new file mode 100644 index 00000000..14b88eaa --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.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; + +import java.util.List; + +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.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.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.util.unit.DataSize; + +import io.grpc.BindableService; +import io.grpc.servlet.jakarta.GrpcServlet; +import io.grpc.servlet.jakarta.ServletServerBuilder; + +/** + * {@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) +@ConditionalOnSpringGrpc +@ConditionalOnGrpcServerEnabled +@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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java new file mode 100644 index 00000000..f32b7d7b --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 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; + +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.netty.NettyServerBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; + +/** + * 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().determineEnabled()) { + 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.determineAddress(), + 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().determineEnabled()) { + 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.determineAddress(), + 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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java new file mode 100644 index 00000000..1965c1cc --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java @@ -0,0 +1,37 @@ +/* + * 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; + +/** + * 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 { + + /** + * Customize the given {@link GrpcServerFactory}. + * @param serverFactory the server factory to customize + */ + void customize(GrpcServerFactory serverFactory); + +} diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java new file mode 100644 index 00000000..c40c2728 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.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 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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.grpc.server.GlobalServerInterceptor; + +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcServerInterceptor; +import io.micrometer.core.instrument.kotlin.ObservationCoroutineContextServerInterceptor; +import io.micrometer.observation.ObservationRegistry; + +/** + * {@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") +@ConditionalOnSpringGrpc +@ConditionalOnClass({ ObservationRegistry.class, ObservationGrpcServerInterceptor.class }) +@ConditionalOnGrpcServerEnabled("observation") +@ConditionalOnBean(ObservationRegistry.class) +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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java new file mode 100644 index 00000000..a6a752f6 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java @@ -0,0 +1,444 @@ +/* + * 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 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; + +import io.grpc.TlsServerCredentials.ClientAuth; + +@ConfigurationProperties(prefix = "spring.grpc.server") +public class GrpcServerProperties { + + /** + * Server should listen to any IPv4 and IPv6 address. + */ + public static final String 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 @Nullable String address; + + /** + * Server host 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. + */ + private int port = GrpcUtils.DEFAULT_PORT; + + /** + * 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); + + /** + * 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 Health health = new Health(); + + private final Inprocess inprocess = new Inprocess(); + + private final KeepAlive keepAlive = new KeepAlive(); + + private final Ssl ssl = new Ssl(); + + public @Nullable String getAddress() { + return this.address; + } + + public void setAddress(@Nullable String address) { + this.address = address; + } + + /** + * 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() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return this.port; + } + + public void setPort(int port) { + this.port = port; + } + + 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 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 Ssl getSsl() { + return this.ssl; + } + + public static class Health { + + /** + * Whether to auto-configure Health feature on the gRPC server. + */ + private boolean enabled = true; + + private final Actuator actuator = new Actuator(); + + public boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Actuator getActuator() { + return this.actuator; + } + + } + + public static class Actuator { + + /** + * 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 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 { + + /** + * 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; + + /** + * Maximum time a connection may exist before being gracefully terminated (default + * infinite). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration maxAge; + + /** + * Maximum time for graceful connection termination (default infinite). + */ + @DurationUnit(ChronoUnit.SECONDS) + private @Nullable Duration maxAgeGrace; + + /** + * 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; + + public @Nullable Duration getTime() { + return this.time; + } + + public void setTime(@Nullable Duration time) { + this.time = time; + } + + public @Nullable Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(@Nullable Duration timeout) { + this.timeout = timeout; + } + + public @Nullable Duration getMaxIdle() { + return this.maxIdle; + } + + public void setMaxIdle(@Nullable Duration maxIdle) { + this.maxIdle = maxIdle; + } + + public @Nullable Duration getMaxAge() { + return this.maxAge; + } + + public void setMaxAge(@Nullable Duration maxAge) { + this.maxAge = maxAge; + } + + public @Nullable Duration getMaxAgeGrace() { + return this.maxAgeGrace; + } + + public void setMaxAgeGrace(@Nullable Duration maxAgeGrace) { + this.maxAgeGrace = maxAgeGrace; + } + + public @Nullable Duration getPermitTime() { + return this.permitTime; + } + + public void setPermitTime(@Nullable 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. + */ + 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 @Nullable Boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(@Nullable 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(@Nullable 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; + } + + } + +} diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.java new file mode 100644 index 00000000..a0e00928 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.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; + +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.context.annotation.Bean; + +import io.grpc.BindableService; +import io.grpc.protobuf.services.ProtoReflectionServiceV1; + +/** + * {@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) +@ConditionalOnSpringGrpc +@ConditionalOnClass({ ProtoReflectionServiceV1.class }) +@ConditionalOnGrpcServerEnabled("reflection") +@ConditionalOnBean(BindableService.class) +public final class GrpcServerReflectionAutoConfiguration { + + @Bean + BindableService serverReflection() { + return ProtoReflectionServiceV1.newInstance(); + } + +} diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessServerFactoryPropertyMapper.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessServerFactoryPropertyMapper.java new file mode 100644 index 00000000..588fb570 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 org.springframework.boot.context.properties.PropertyMapper; + +import io.grpc.inprocess.InProcessServerBuilder; + +/** + * 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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyServerFactoryPropertyMapper.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyServerFactoryPropertyMapper.java new file mode 100644 index 00000000..18c08e3b --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 org.springframework.grpc.server.NettyGrpcServerFactory; + +import io.grpc.netty.NettyServerBuilder; + +/** + * 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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerCondition.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerCondition.java new file mode 100644 index 00000000..913ed012 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnGrpcNativeServerCondition.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnGrpcNativeServerCondition.java new file mode 100644 index 00000000..dc1ff310 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 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; + +import io.grpc.servlet.jakarta.GrpcServlet; + +/** + * {@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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java new file mode 100644 index 00000000..fe74b5f0 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java @@ -0,0 +1,58 @@ +/* + * 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 org.springframework.boot.util.LambdaSafe; +import org.springframework.grpc.server.ServerBuilderCustomizer; + +import io.grpc.ServerBuilder; + +/** + * Invokes the available {@link ServerBuilderCustomizer} instances in the context for a + * given {@link ServerBuilder}. + * + * @author Chris Bono + */ +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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.java new file mode 100644 index 00000000..c3a34578 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.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.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; + +/** + * 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); + + @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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyServerFactoryPropertyMapper.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyServerFactoryPropertyMapper.java new file mode 100644 index 00000000..629f9005 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 org.springframework.grpc.server.ShadedNettyGrpcServerFactory; + +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; + +/** + * 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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java new file mode 100644 index 00000000..8cb0ed97 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java @@ -0,0 +1,55 @@ +/* + * 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 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.ConditionalOnMissingBean; +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.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 +@ConditionalOnSpringGrpc +@ConditionalOnGrpcServerEnabled("exception-handler") +@ConditionalOnBean(GrpcExceptionHandler.class) +@ConditionalOnMissingBean(GrpcExceptionHandlerInterceptor.class) +public final class GrpcExceptionHandlerAutoConfiguration { + + @GlobalServerInterceptor + @Bean + GrpcExceptionHandlerInterceptor globalExceptionHandlerInterceptor( + ObjectProvider exceptionHandler) { + return new GrpcExceptionHandlerInterceptor(new CompositeGrpcExceptionHandler( + exceptionHandler.orderedStream().toArray(GrpcExceptionHandler[]::new))); + } + +} diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/package-info.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/package-info.java new file mode 100644 index 00000000..9e2fb4a7 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapter.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapter.java new file mode 100644 index 00000000..598ed581 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 org.springframework.boot.health.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.health.actuate.endpoint.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; + +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import io.grpc.protobuf.services.HealthStatusManager; + +/** + * 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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvoker.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvoker.java new file mode 100644 index 00000000..b1780a91 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java new file mode 100644 index 00000000..07fcd661 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java @@ -0,0 +1,132 @@ +/* + * 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 org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +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.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.ConditionalOnSpringGrpc; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties; +import org.springframework.boot.health.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.health.actuate.endpoint.StatusAggregator; +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.scheduling.annotation.EnableScheduling; + +import io.grpc.BindableService; +import io.grpc.protobuf.services.HealthStatusManager; + +/** + * {@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) +@ConditionalOnSpringGrpc +@ConditionalOnClass(HealthStatusManager.class) +@ConditionalOnGrpcServerEnabled("health") +@ConditionalOnBean(BindableService.class) +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") + @ConditionalOnGrpcServerEnabled("health.actuator") + @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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java new file mode 100644 index 00000000..d7ef5d4d --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java new file mode 100644 index 00000000..5cd06d10 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java new file mode 100644 index 00000000..28f71e43 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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) { + 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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequest.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequest.java new file mode 100644 index 00000000..d3bd2953 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 org.springframework.boot.grpc.server.autoconfigure.security.GrpcServletRequest.GrpcServletRequestMatcher; +import org.springframework.boot.security.web.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; + +import reactor.core.publisher.Mono; + +/** + * 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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java new file mode 100644 index 00000000..f78f2c90 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.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.security; + +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.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; +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.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; + +import io.grpc.internal.GrpcUtil; + +/** + * {@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") +@ConditionalOnSpringGrpc +@ConditionalOnClass(ObjectPostProcessor.class) +@ConditionalOnGrpcServerEnabled +@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) + @ConditionalOnGrpcNativeServer + @Configuration(proxyBeanMethods = false) + 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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequest.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequest.java new file mode 100644 index 00000000..01ac0bd2 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 org.springframework.boot.security.web.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; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * 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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ClientAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ClientAutoConfiguration.java new file mode 100644 index 00000000..d4c64c08 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ClientAutoConfiguration.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.server.autoconfigure.security; + +import java.util.ArrayList; +import java.util.List; + +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.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") +@ConditionalOnSpringGrpc +@ConditionalOnClass(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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java new file mode 100644 index 00000000..4bf1b221 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 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.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; +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.security.AuthenticationProcessInterceptor; +import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.security.config.Customizer; +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 io.grpc.BindableService; + +/** + * {@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) +@ConditionalOnSpringGrpc +@ConditionalOnClass({ 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(Customizer.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(Customizer.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/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/package-info.java new file mode 100644 index 00000000..264c92d6 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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/spring-grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 00000000..cfafdb62 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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.observation.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/spring-grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..fc5b89e9 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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.EnvironmentPostProcessor=\ +org.springframework.boot.grpc.server.autoconfigure.ServletEnvironmentPostProcessor diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..bfb64d0c --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfigurationTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfigurationTests.java new file mode 100644 index 00000000..79f8fce0 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfigurationTests.java @@ -0,0 +1,101 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +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 io.grpc.Codec; +import io.grpc.Compressor; +import io.grpc.CompressorRegistry; +import io.grpc.Decompressor; +import io.grpc.DecompressorRegistry; + +/** + * Tests for {@link GrpcCodecConfiguration}. + * + * @author Andrei Lisa + */ +class GrpcCodecConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GrpcCodecConfiguration.class)); + + @Test + void whenCodecNotOnClasspathThenAutoconfigurationSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcCodecConfiguration.class)); + } + + @Test + 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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java new file mode 100644 index 00000000..573fb4d9 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfigurationTests.java @@ -0,0 +1,594 @@ +/* + * 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 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.BDDMockito.then; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; + +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 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; +import io.grpc.ServiceDescriptor; +import io.grpc.netty.NettyServerBuilder; + +/** + * 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 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)) + .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 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 { + + @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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.java new file mode 100644 index 00000000..38ee2825 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfigurationTests.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; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Map; + +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 org.springframework.grpc.server.GrpcServerFactory; + +import io.grpc.BindableService; +import io.grpc.ServerInterceptor; +import io.micrometer.core.instrument.binder.grpc.ObservationGrpcServerInterceptor; +import io.micrometer.observation.ObservationRegistry; + +/** + * 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 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() + .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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java new file mode 100644 index 00000000..1676ec71 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerPropertiesTests.java @@ -0,0 +1,192 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; + +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.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.util.unit.DataSize; + +/** + * 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.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)); + } + + } + + @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.determineAddress()).isEqualTo("my-server-ip:3130"); + } + + @Test + 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"); + GrpcServerProperties properties = bindProperties(map); + assertThat(properties.getAddress()).isEqualTo("my-server-ip:3130"); + } + + } + +} diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfigurationTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfigurationTests.java new file mode 100644 index 00000000..4de02592 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfigurationTests.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 static org.assertj.core.api.Assertions.assertThat; + +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.lifecycle.GrpcServerLifecycle; + +import io.grpc.BindableService; + +/** + * 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 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() + .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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServletAutoConfigurationTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServletAutoConfigurationTests.java new file mode 100644 index 00000000..a6cd10dc --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServletAutoConfigurationTests.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 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; + +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.GrpcServerFactory; +import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.util.unit.DataSize; + +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; + +/** + * 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 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)) + .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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerConditionTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerConditionTests.java new file mode 100644 index 00000000..b0321d34 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +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; + +/** + * 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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizersTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizersTests.java new file mode 100644 index 00000000..a56b5d9d --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizersTests.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.server.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import org.springframework.grpc.server.ServerBuilderCustomizer; + +import io.grpc.ServerBuilder; +import io.grpc.netty.NettyServerBuilder; + +/** + * 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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerFactoryPropertyMappersTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerFactoryPropertyMappersTests.java new file mode 100644 index 00000000..afbf15f4 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/ServerFactoryPropertyMappersTests.java @@ -0,0 +1,86 @@ +/* + * 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 static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import org.springframework.util.unit.DataSize; + +import io.grpc.ServerBuilder; + +/** + * 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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfigurationTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfigurationTests.java new file mode 100644 index 00000000..80697eac --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfigurationTests.java @@ -0,0 +1,129 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; + +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 io.grpc.BindableService; + +/** + * 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(BindableService.class)) + .run((context) -> assertThat(context).doesNotHaveBean(GrpcExceptionHandlerAutoConfiguration.class)); + } + + @Test + void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() { + 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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvokerTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvokerTests.java new file mode 100644 index 00000000..525625ef --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/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 static org.mockito.BDDMockito.atLeast; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +import java.time.Duration; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder; + +/** + * 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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterTests.java new file mode 100644 index 00000000..79756526 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterTests.java @@ -0,0 +1,156 @@ +/* + * 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 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; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +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.health.actuate.endpoint.HealthDescriptor; +import org.springframework.boot.health.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.health.actuate.endpoint.StatusAggregator; +import org.springframework.boot.health.contributor.Status; + +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import io.grpc.protobuf.services.HealthStatusManager; + +/** + * 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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java new file mode 100644 index 00000000..fe99400d --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfigurationTests.java @@ -0,0 +1,273 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; + +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.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration; +import org.springframework.boot.grpc.server.autoconfigure.health.GrpcServerHealthAutoConfiguration.ActuatorHealthAdapterConfiguration; +import org.springframework.boot.health.actuate.endpoint.HealthEndpoint; +import org.springframework.boot.health.autoconfigure.actuate.endpoint.HealthEndpointAutoConfiguration; +import org.springframework.boot.health.autoconfigure.contributor.HealthContributorAutoConfiguration; +import org.springframework.boot.health.autoconfigure.registry.HealthContributorRegistryAutoConfiguration; +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 io.grpc.BindableService; +import io.grpc.protobuf.services.HealthStatusManager; + +/** + * 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 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() + .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)); + } + + @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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequestTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequestTests.java new file mode 100644 index 00000000..5641202e --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequestTests.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.server.autoconfigure.security; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +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 io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; + +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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfigurationTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfigurationTests.java new file mode 100644 index 00000000..8fa16c71 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfigurationTests.java @@ -0,0 +1,119 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; + +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 io.grpc.BindableService; + +/** + * 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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequestTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequestTests.java new file mode 100644 index 00000000..5bd92b1a --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcServletRequestTests.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.grpc.server.autoconfigure.security; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +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 io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; + +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/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfigurationTests.java b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfigurationTests.java new file mode 100644 index 00000000..06f41f63 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfigurationTests.java @@ -0,0 +1,177 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +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.web.servlet.ServletWebSecurityAutoConfiguration; +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 io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; + +/** + * 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, + ServletWebSecurityAutoConfiguration.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, ServletWebSecurityAutoConfiguration.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, + ServletWebSecurityAutoConfiguration.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/spring-grpc-server-spring-boot-autoconfigure/src/test/resources/logback-test.xml b/spring-grpc-server-spring-boot-autoconfigure/src/test/resources/logback-test.xml new file mode 100644 index 00000000..b8a41480 --- /dev/null +++ b/spring-grpc-server-spring-boot-autoconfigure/src/test/resources/logback-test.xml @@ -0,0 +1,4 @@ + + + + diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/test/resources/org/springframework/boot/grpc/server/autoconfigure/test.jks b/spring-grpc-server-spring-boot-autoconfigure/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% + + 4.0.0 + + org.springframework.grpc + spring-grpc + 1.0.0-SNAPSHOT + + spring-grpc-server-spring-boot-starter + jar + Spring gRPC Server (Netty) Spring Boot Starter + Spring gRPC Server (Netty) Spring Boot Starter + + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + org.springframework.grpc + spring-grpc-server-spring-boot-autoconfigure + + + io.grpc + grpc-netty + + + io.grpc + grpc-services + + + + diff --git a/spring-grpc-server-web-spring-boot-starter/pom.xml b/spring-grpc-server-web-spring-boot-starter/pom.xml new file mode 100644 index 00000000..e2bf184c --- /dev/null +++ b/spring-grpc-server-web-spring-boot-starter/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + org.springframework.grpc + spring-grpc + 1.0.0-SNAPSHOT + + spring-grpc-server-web-spring-boot-starter + jar + Spring gRPC Servlet Server Spring Boot Starter + Spring gRPC Servlet Server Spring Boot Starter + + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + + + org.springframework.grpc + spring-grpc-server-spring-boot-autoconfigure + + + io.grpc + grpc-servlet-jakarta + + + + diff --git a/spring-grpc-spring-boot-starter/pom.xml b/spring-grpc-spring-boot-starter/pom.xml new file mode 100644 index 00000000..39b97deb --- /dev/null +++ b/spring-grpc-spring-boot-starter/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + org.springframework.grpc + spring-grpc + 1.0.0-SNAPSHOT + + spring-grpc-spring-boot-starter + jar + Spring gRPC Client and Server (Netty) Spring Boot Starter + Spring gRPC Client and Server (Netty) Spring Boot Starter + https://github.com/spring-projects/spring-grpc + + + https://github.com/spring-projects/spring-grpc + git://github.com/spring-projects/spring-grpc.git + git@github.com:spring-projects/spring-grpc.git + + + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + org.springframework.grpc + spring-grpc-client-spring-boot-starter + + + org.springframework.grpc + spring-grpc-server-spring-boot-starter + + + + diff --git a/spring-grpc-test-spring-boot-autoconfigure/pom.xml b/spring-grpc-test-spring-boot-autoconfigure/pom.xml new file mode 100644 index 00000000..247a7ce3 --- /dev/null +++ b/spring-grpc-test-spring-boot-autoconfigure/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + org.springframework.grpc + spring-grpc + 1.0.0-SNAPSHOT + + spring-grpc-test-spring-boot-autoconfigure + jar + Spring gRPC Test Auto Configuration + Spring gRPC Test Auto Configuration + https://github.com/spring-projects/spring-grpc + + + https://github.com/spring-projects/spring-grpc + git://github.com/spring-projects/spring-grpc.git + git@github.com:spring-projects/spring-grpc.git + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + + org.springframework.grpc + spring-grpc-client-spring-boot-autoconfigure + + + org.springframework.grpc + spring-grpc-server-spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-test-autoconfigure + + + + org.springframework.boot + spring-boot-autoconfigure + true + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + io.grpc + grpc-inprocess + true + + + io.grpc + grpc-stub + true + + + + + io.grpc + grpc-netty + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureInProcessTransport.java b/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureInProcessTransport.java new file mode 100644 index 00000000..02ac487b --- /dev/null +++ b/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/AutoConfigureInProcessTransport.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.test.autoconfigure; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.PropertyMapping; + +/** + * Annotation that can be applied to a test class to start an in-process gRPC server. All + * clients that connect to any server via the autoconfigured {@code GrpcChannelFactory} + * will be able to connect to the in-process gRPC server. + * + * @author Dave Syer + * @author Chris Bono + * @since 4.0.0 + * @see InProcessTestAutoConfiguration + */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ImportAutoConfiguration +@PropertyMapping("spring.test.grpc.inprocess") +public @interface AutoConfigureInProcessTransport { + + /** + * 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/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfiguration.java b/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfiguration.java new file mode 100644 index 00000000..f70bd70f --- /dev/null +++ b/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfiguration.java @@ -0,0 +1,163 @@ +/* + * 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.test.autoconfigure; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +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.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.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.grpc.client.ClientInterceptorsConfigurer; +import org.springframework.grpc.client.GrpcChannelFactory; +import org.springframework.grpc.client.InProcessGrpcChannelFactory; +import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.InProcessGrpcServerFactory; +import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.grpc.server.ServerServiceDefinitionFilter; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.service.GrpcServiceConfigurer; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; + +import io.grpc.BindableService; +import io.grpc.ChannelCredentials; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.AbstractStub; + +/** + * Auto-configuration for an in-process test gRPC server. + * + * @author Chris Bono + * @author Dave Syer + * @author Andrey Litvitski + * @since 4.0.0 + * @see AutoConfigureInProcessTransport + */ +@AutoConfiguration(before = { GrpcServerFactoryAutoConfiguration.class, GrpcClientAutoConfiguration.class }) +@ConditionalOnClass({ InProcessServerBuilder.class, InProcessChannelBuilder.class, InProcessGrpcServerFactory.class, + InProcessGrpcChannelFactory.class }) +@ConditionalOnBooleanProperty("spring.test.grpc.inprocess.enabled") +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) + @Order(Ordered.HIGHEST_PRECEDENCE) + TestInProcessGrpcServerFactory testInProcessGrpcServerFactory(GrpcServiceDiscoverer serviceDiscoverer, + GrpcServiceConfigurer serviceConfigurer, List> 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/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTransportContextCustomizerFactory.java b/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTransportContextCustomizerFactory.java new file mode 100644 index 00000000..bd1ccdf7 --- /dev/null +++ b/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/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.grpc.test.autoconfigure; + +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/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/LocalGrpcPort.java b/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/LocalGrpcPort.java new file mode 100644 index 00000000..e8c837b3 --- /dev/null +++ b/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/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.grpc.test.autoconfigure; + +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/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/ServerPortInfoApplicationContextInitializer.java b/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/ServerPortInfoApplicationContextInitializer.java new file mode 100644 index 00000000..b9f00f60 --- /dev/null +++ b/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/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.grpc.test.autoconfigure; + +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/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/package-info.java b/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/autoconfigure/package-info.java new file mode 100644 index 00000000..c6785eb5 --- /dev/null +++ b/spring-grpc-test-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/test/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 Spring gRPC tests. + */ +@NullMarked +package org.springframework.boot.grpc.test.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-grpc-test-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-grpc-test-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..0a6418f3 --- /dev/null +++ b/spring-grpc-test-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -0,0 +1,6 @@ +# 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/spring-grpc-test-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.grpc.test.autoconfigure.AutoConfigureInProcessTransport.imports b/spring-grpc-test-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.grpc.test.autoconfigure.AutoConfigureInProcessTransport.imports new file mode 100644 index 00000000..7abb17f2 --- /dev/null +++ b/spring-grpc-test-spring-boot-autoconfigure/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/spring-grpc-test-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfigurationTests.java b/spring-grpc-test-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfigurationTests.java new file mode 100644 index 00000000..2d19099a --- /dev/null +++ b/spring-grpc-test-spring-boot-autoconfigure/src/test/java/org/springframework/boot/grpc/test/autoconfigure/InProcessTestAutoConfigurationTests.java @@ -0,0 +1,100 @@ +/* + * 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.test.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +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 io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; + +/** + * 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"); + }); + } + +} From ed5f8de0a2e267ad21b38739de0b2e3fc51caf89 Mon Sep 17 00:00:00 2001 From: onobc Date: Thu, 6 Nov 2025 11:45:15 -0600 Subject: [PATCH 02/21] Add grpc-server and grpc-client samples back in. Signed-off-by: onobc --- pom.xml | 2 +- samples/grpc-client/build.gradle | 19 +++-- samples/grpc-client/pom.xml | 24 +++++-- .../src/main/resources/application.properties | 4 +- .../server/GrpcServerPortFileWriter.java | 12 ++-- .../sample/DefaultDeadlineSetupTests.java | 12 ++-- .../sample/GrpcClientApplicationTests.java | 9 ++- .../test/resources/META-INF/spring.factories | 2 +- .../testjars/grpcServer/application.yml | 5 +- samples/grpc-server/build.gradle | 14 ++-- samples/grpc-server/pom.xml | 27 ++++--- .../sample/GrpcClientApplicationTests.java | 2 +- .../GrpcServerHealthIntegrationTests.java | 8 +-- .../sample/GrpcServerIntegrationTests.java | 72 ++++++++++++------- samples/pom.xml | 18 ++--- 15 files changed, 143 insertions(+), 87 deletions(-) diff --git a/pom.xml b/pom.xml index c3ede736..8e2426d4 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ spring-grpc-core spring-grpc-dependencies spring-grpc-docs - + samples spring-grpc-client-spring-boot-autoconfigure diff --git a/samples/grpc-client/build.gradle b/samples/grpc-client/build.gradle index 24ae0295..10dd0faa 100644 --- a/samples/grpc-client/build.gradle +++ b/samples/grpc-client/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.5' - id 'io.spring.dependency-management' version '1.1.6' + id 'org.springframework.boot' version '4.0.0-RC1' + id 'io.spring.dependency-management' version '1.1.7' id 'com.google.protobuf' version '0.9.4' } @@ -28,13 +28,18 @@ dependencyManagement { } dependencies { - implementation 'org.springframework.grpc:spring-grpc-client-spring-boot-starter' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.grpc:spring-grpc-test' - testImplementation 'org.springframework.experimental.boot:spring-boot-testjars-maven:0.0.3' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.grpc:spring-grpc-client-spring-boot-starter' + + testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-web-server' + testImplementation 'org.springframework.experimental.boot:spring-boot-testjars-maven:0.4.0.0-SNAPSHOT' + testImplementation 'io.grpc:grpc-inprocess' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } + tasks.named('test') { useJUnitPlatform() } diff --git a/samples/grpc-client/pom.xml b/samples/grpc-client/pom.xml index 85cde8d7..63bacd8b 100644 --- a/samples/grpc-client/pom.xml +++ b/samples/grpc-client/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.5 + 4.0.0-SNAPSHOT org.springframework.grpc @@ -30,7 +30,7 @@ 17 0.0.39 - 4.31.1 + 4.32.1 1.76.0 3.9.4 1.9.18 @@ -46,6 +46,7 @@ + org.springframework.grpc @@ -54,13 +55,28 @@ org.springframework.grpc - spring-grpc-test + spring-grpc-test-spring-boot-autoconfigure + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-web-server test org.springframework.experimental.boot spring-boot-testjars-maven - 0.0.3 + 0.4.0.0-SNAPSHOT + test + + + io.grpc + grpc-inprocess test diff --git a/samples/grpc-client/src/main/resources/application.properties b/samples/grpc-client/src/main/resources/application.properties index c7409ed5..d9fa69a2 100644 --- a/samples/grpc-client/src/main/resources/application.properties +++ b/samples/grpc-client/src/main/resources/application.properties @@ -1,2 +1,4 @@ spring.application.name=grpc-client -spring.grpc.client.default-channel.address=static://0.0.0.0:9090 +spring.grpc.client.default-channel.address=static://0.0.0.0:${launched.grpc.port} + +#logging.level.org.springframework.experimental.boot.server.exec=debug diff --git a/samples/grpc-client/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerPortFileWriter.java b/samples/grpc-client/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerPortFileWriter.java index b34e2485..3f77d401 100644 --- a/samples/grpc-client/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerPortFileWriter.java +++ b/samples/grpc-client/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerPortFileWriter.java @@ -28,10 +28,14 @@ import org.springframework.util.FileCopyUtils; /** - * An {@link ApplicationListener} that saves embedded server port and management port into - * file. This application listener will be triggered whenever the server starts, and the - * file name can be overridden at runtime with a System property or environment variable - * named "PORTFILE" or "portfile". + * An {@link ApplicationListener} that saves the gRPC server port into a file. This + * application listener will be triggered whenever the gRPC server starts, and the file + * name can be overridden at runtime with a System property or environment variable named + * "PORTFILE" or "portfile". + * + * NOTE: This is currently required in order to use spring-boot-testjars as it + * expects this file to be available in order to determine the port of the dynamically + * launched gRPC server. * * @author David Liu * @author Phillip Webb diff --git a/samples/grpc-client/src/test/java/org/springframework/grpc/sample/DefaultDeadlineSetupTests.java b/samples/grpc-client/src/test/java/org/springframework/grpc/sample/DefaultDeadlineSetupTests.java index 2fc4886b..09aa3956 100644 --- a/samples/grpc-client/src/test/java/org/springframework/grpc/sample/DefaultDeadlineSetupTests.java +++ b/samples/grpc-client/src/test/java/org/springframework/grpc/sample/DefaultDeadlineSetupTests.java @@ -28,8 +28,7 @@ public class DefaultDeadlineSetupTests { @Nested - @SpringBootTest(properties = { "spring.grpc.client.default-channel.address=static://0.0.0.0:${local.grpc.port}", - "spring.grpc.client.default-channel.default-deadline=1s" }) + @SpringBootTest(properties = "spring.grpc.client.default-channel.default-deadline=1s") @DirtiesContext @EnabledIf("serverJarAvailable") class Deadline { @@ -48,11 +47,12 @@ void contextLoads() { static class ExtraConfiguration { @Bean - @DynamicProperty(name = "local.grpc.port", value = "port") + @DynamicProperty(name = "launched.grpc.port", value = "port") static CommonsExecWebServerFactoryBean grpcServer() { return CommonsExecWebServerFactoryBean.builder() .classpath(classpath -> classpath .entries(new MavenClasspathEntry("org.springframework.grpc:grpc-server-sample:1.0.0-SNAPSHOT")) + .entries(MavenClasspathEntry.springBootDependency("spring-boot-web-server")) .files("target/test-classes")); } @@ -76,8 +76,7 @@ public CommandLineRunner otherRunner(SimpleGrpc.SimpleBlockingStub stub) { } @Nested - @SpringBootTest(properties = { "spring.grpc.client.default-channel.address=static://0.0.0.0:${local.grpc.port}", - "spring.grpc.client.default-channel.default-deadline=1s" }) + @SpringBootTest(properties = "spring.grpc.client.default-channel.default-deadline=1s") @DirtiesContext @EnabledIf("serverJarAvailable") class WithoutDeadline { @@ -96,11 +95,12 @@ void contextLoads() { static class ExtraConfiguration { @Bean - @DynamicProperty(name = "local.grpc.port", value = "port") + @DynamicProperty(name = "launched.grpc.port", value = "port") static CommonsExecWebServerFactoryBean grpcServer() { return CommonsExecWebServerFactoryBean.builder() .classpath(classpath -> classpath .entries(new MavenClasspathEntry("org.springframework.grpc:grpc-server-sample:1.0.0-SNAPSHOT")) + .entries(MavenClasspathEntry.springBootDependency("spring-boot-web-server")) .files("target/test-classes")); } diff --git a/samples/grpc-client/src/test/java/org/springframework/grpc/sample/GrpcClientApplicationTests.java b/samples/grpc-client/src/test/java/org/springframework/grpc/sample/GrpcClientApplicationTests.java index ef94bb67..b3e6daa6 100644 --- a/samples/grpc-client/src/test/java/org/springframework/grpc/sample/GrpcClientApplicationTests.java +++ b/samples/grpc-client/src/test/java/org/springframework/grpc/sample/GrpcClientApplicationTests.java @@ -16,7 +16,7 @@ import org.springframework.experimental.boot.test.context.EnableDynamicProperty; import org.springframework.test.annotation.DirtiesContext; -@SpringBootTest(properties = "spring.grpc.client.default-channel.address=static://0.0.0.0:${local.grpc.port}") +@SpringBootTest @DirtiesContext @EnabledIf("serverJarAvailable") public class GrpcClientApplicationTests { @@ -38,19 +38,18 @@ void contextLoads() { static class ExtraConfiguration { @Bean - @DynamicProperty(name = "local.grpc.port", value = "port") + @DynamicProperty(name = "launched.grpc.port", value = "port") static CommonsExecWebServerFactoryBean grpcServer() { return CommonsExecWebServerFactoryBean.builder() .classpath(classpath -> classpath .entries(new MavenClasspathEntry("org.springframework.grpc:grpc-server-sample:1.0.0-SNAPSHOT")) + .entries(MavenClasspathEntry.springBootDependency("spring-boot-web-server")) .files("target/test-classes")); } @Bean static BeanDefinitionRegistryPostProcessor startup(WebServer server) { - return registry -> { - server.getPort(); - }; + return registry -> System.out.println("*** gRPC server started on port " + server.getPort()); } } diff --git a/samples/grpc-client/src/test/resources/META-INF/spring.factories b/samples/grpc-client/src/test/resources/META-INF/spring.factories index e5a2b1c8..9b66246c 100644 --- a/samples/grpc-client/src/test/resources/META-INF/spring.factories +++ b/samples/grpc-client/src/test/resources/META-INF/spring.factories @@ -1,2 +1,2 @@ org.springframework.context.ApplicationListener=\ -org.springframework.grpc.autoconfigure.server.GrpcServerPortFileWriter \ No newline at end of file +org.springframework.grpc.autoconfigure.server.GrpcServerPortFileWriter diff --git a/samples/grpc-client/src/test/resources/testjars/grpcServer/application.yml b/samples/grpc-client/src/test/resources/testjars/grpcServer/application.yml index d12f7374..067c9d6c 100644 --- a/samples/grpc-client/src/test/resources/testjars/grpcServer/application.yml +++ b/samples/grpc-client/src/test/resources/testjars/grpcServer/application.yml @@ -1,4 +1,7 @@ spring: grpc: server: - port: ${server.port} \ No newline at end of file + port: ${server.port} + +#logging.level: +# org.springframework.experimental.boot.server.exec: debug diff --git a/samples/grpc-server/build.gradle b/samples/grpc-server/build.gradle index eb6d2cf0..171acfd8 100644 --- a/samples/grpc-server/build.gradle +++ b/samples/grpc-server/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.5' - id 'io.spring.dependency-management' version '1.1.6' + id 'org.springframework.boot' version '4.0.0-RC1' + id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' } @@ -30,11 +30,13 @@ dependencyManagement { dependencies { implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'io.grpc:grpc-services' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.grpc:spring-grpc-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.grpc:grpc-inprocess' + testRuntimeOnly "io.netty:netty-transport-native-epoll::linux-x86_64" + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { diff --git a/samples/grpc-server/pom.xml b/samples/grpc-server/pom.xml index e5c1fd3f..b5422841 100644 --- a/samples/grpc-server/pom.xml +++ b/samples/grpc-server/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.5 + 4.0.0-SNAPSHOT org.springframework.grpc @@ -44,20 +44,32 @@ + org.springframework.grpc spring-grpc-spring-boot-starter - - io.grpc - grpc-services - org.springframework.boot spring-boot-starter-actuator + + org.springframework.grpc + spring-grpc-test-spring-boot-autoconfigure + test + + + org.springframework.boot + spring-boot-starter-test + test + + + io.grpc + grpc-inprocess + test + io.netty @@ -65,11 +77,6 @@ linux-x86_64 test - - org.springframework.grpc - spring-grpc-test - test - diff --git a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcClientApplicationTests.java b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcClientApplicationTests.java index e9b2d409..3fa5aef3 100644 --- a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcClientApplicationTests.java +++ b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcClientApplicationTests.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.grpc.test.autoconfigure.AutoConfigureInProcessTransport; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.ApplicationContext; @@ -15,7 +16,6 @@ import org.springframework.grpc.client.ImportGrpcClients; import org.springframework.grpc.client.SimpleStubFactory; import org.springframework.grpc.sample.proto.SimpleGrpc; -import org.springframework.grpc.test.AutoConfigureInProcessTransport; import io.grpc.stub.AbstractStub; diff --git a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerHealthIntegrationTests.java b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerHealthIntegrationTests.java index 77f6e00c..1464921a 100644 --- a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerHealthIntegrationTests.java +++ b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerHealthIntegrationTests.java @@ -26,9 +26,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.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.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; @@ -36,7 +37,6 @@ import org.springframework.grpc.sample.proto.HelloReply; import org.springframework.grpc.sample.proto.HelloRequest; import org.springframework.grpc.sample.proto.SimpleGrpc; -import org.springframework.grpc.test.AutoConfigureInProcessTransport; import org.springframework.test.annotation.DirtiesContext; import io.grpc.ManagedChannel; diff --git a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerIntegrationTests.java b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerIntegrationTests.java index 02575a71..93fcf4e1 100644 --- a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerIntegrationTests.java +++ b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerIntegrationTests.java @@ -17,19 +17,24 @@ package org.springframework.grpc.sample; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertThrows; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.concurrent.atomic.AtomicInteger; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties; +import org.springframework.boot.grpc.test.autoconfigure.AutoConfigureInProcessTransport; +import org.springframework.boot.grpc.test.autoconfigure.LocalGrpcPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.grpc.autoconfigure.server.GrpcServerProperties; import org.springframework.grpc.client.ChannelBuilderOptions; import org.springframework.grpc.client.GrpcChannelFactory; import org.springframework.grpc.sample.proto.HelloReply; @@ -37,8 +42,6 @@ import org.springframework.grpc.sample.proto.SimpleGrpc; import org.springframework.grpc.server.GlobalServerInterceptor; import org.springframework.grpc.server.GrpcServerFactory; -import org.springframework.grpc.test.AutoConfigureInProcessTransport; -import org.springframework.grpc.test.LocalGrpcPort; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; @@ -47,9 +50,11 @@ import io.grpc.Metadata; import io.grpc.ServerCall.Listener; import io.grpc.ServerInterceptor; +import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.StatusRuntimeException; import io.grpc.netty.NettyChannelBuilder; +import net.bytebuddy.asm.Advice.Thrown; /** * More detailed integration tests for {@link GrpcServerFactory gRPC server factories} and @@ -77,19 +82,27 @@ class ServerWithException { @Test void specificErrorResponse(@Autowired GrpcChannelFactory channels) { SimpleGrpc.SimpleBlockingStub client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")); - assertThat(assertThrows(StatusRuntimeException.class, - () -> client.sayHello(HelloRequest.newBuilder().setName("internal").build())) - .getStatus() - .getCode()).isEqualTo(Code.UNKNOWN); + assertThatExceptionOfType(StatusRuntimeException.class) + .isThrownBy(() -> client.sayHello(HelloRequest.newBuilder().setName("internal").build())) + .extracting(StatusRuntimeException::getStatus) + .extracting(Status::getCode) + .isEqualTo(Code.UNKNOWN); } @Test void defaultErrorResponseIsUnknown(@Autowired GrpcChannelFactory channels) { SimpleGrpc.SimpleBlockingStub client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")); - StatusRuntimeException status = assertThrows(StatusRuntimeException.class, + + assertThatExceptionOfType(StatusRuntimeException.class) + .isThrownBy(() -> client.sayHello(HelloRequest.newBuilder().setName("error").build())) + .extracting(StatusRuntimeException::getStatus) + .extracting(Status::getCode) + .isEqualTo(Code.INVALID_ARGUMENT); + + StatusRuntimeException ex = Assertions.catchThrowableOfType(StatusRuntimeException.class, () -> client.sayHello(HelloRequest.newBuilder().setName("error").build())); - assertThat(status.getStatus().getCode()).isEqualTo(Code.INVALID_ARGUMENT); - assertThat(status.getTrailers().get(Metadata.Key.of("error-code", Metadata.ASCII_STRING_MARSHALLER))) + assertThat(ex.getStatus().getCode()).isEqualTo(Code.INVALID_ARGUMENT); + assertThat(ex.getTrailers().get(Metadata.Key.of("error-code", Metadata.ASCII_STRING_MARSHALLER))) .isNotNull(); } @@ -103,10 +116,11 @@ class ServerWithExceptionInInterceptorCall { @Test void specificErrorResponse(@Autowired GrpcChannelFactory channels) { SimpleGrpc.SimpleBlockingStub client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")); - assertThat(assertThrows(StatusRuntimeException.class, - () -> client.sayHello(HelloRequest.newBuilder().setName("foo").build())) - .getStatus() - .getCode()).isEqualTo(Code.INVALID_ARGUMENT); + assertThatExceptionOfType(StatusRuntimeException.class) + .isThrownBy(() -> client.sayHello(HelloRequest.newBuilder().setName("foo").build())) + .extracting(StatusRuntimeException::getStatus) + .extracting(Status::getCode) + .isEqualTo(Code.INVALID_ARGUMENT); } @TestConfiguration @@ -143,10 +157,11 @@ class ServerWithExceptionInInterceptorListener { void specificErrorResponse(@Autowired GrpcChannelFactory channels) { TestConfig.reset(); SimpleGrpc.SimpleBlockingStub client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")); - assertThat(assertThrows(StatusRuntimeException.class, - () -> client.sayHello(HelloRequest.newBuilder().setName("foo").build())) - .getStatus() - .getCode()).isEqualTo(Code.INVALID_ARGUMENT); + assertThatExceptionOfType(StatusRuntimeException.class) + .isThrownBy(() -> client.sayHello(HelloRequest.newBuilder().setName("foo").build())) + .extracting(StatusRuntimeException::getStatus) + .extracting(Status::getCode) + .isEqualTo(Code.INVALID_ARGUMENT); assertThat(TestConfig.readyCount.get()).isEqualTo(1); assertThat(TestConfig.callCount.get()).isEqualTo(0); assertThat(TestConfig.messageCount.get()).isEqualTo(0); @@ -227,19 +242,22 @@ class ServerWithUnhandledException { @Test void specificErrorResponse(@Autowired GrpcChannelFactory channels) { SimpleGrpc.SimpleBlockingStub client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")); - assertThat(assertThrows(StatusRuntimeException.class, - () -> client.sayHello(HelloRequest.newBuilder().setName("error").build())) - .getStatus() - .getCode()).isEqualTo(Code.UNKNOWN); + + assertThatExceptionOfType(StatusRuntimeException.class) + .isThrownBy(() -> client.sayHello(HelloRequest.newBuilder().setName("error").build())) + .extracting(StatusRuntimeException::getStatus) + .extracting(Status::getCode) + .isEqualTo(Code.UNKNOWN); } @Test void defaultErrorResponseIsUnknown(@Autowired GrpcChannelFactory channels) { SimpleGrpc.SimpleBlockingStub client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")); - assertThat(assertThrows(StatusRuntimeException.class, - () -> client.sayHello(HelloRequest.newBuilder().setName("internal").build())) - .getStatus() - .getCode()).isEqualTo(Code.UNKNOWN); + assertThatExceptionOfType(StatusRuntimeException.class) + .isThrownBy(() -> client.sayHello(HelloRequest.newBuilder().setName("internal").build())) + .extracting(StatusRuntimeException::getStatus) + .extracting(Status::getCode) + .isEqualTo(Code.UNKNOWN); } } diff --git a/samples/pom.xml b/samples/pom.xml index b5470e52..7451e5a9 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -18,15 +18,15 @@ grpc-server grpc-client - grpc-secure - grpc-oauth2 - grpc-reactive - grpc-server-kotlin - grpc-server-netty-shaded - grpc-tomcat - grpc-tomcat-secure - grpc-webflux - grpc-webflux-secure + + + + + + + + + From c9b5899f492a2b6354b380a8839c87ed5b5c52d5 Mon Sep 17 00:00:00 2001 From: onobc Date: Thu, 6 Nov 2025 12:31:50 -0600 Subject: [PATCH 03/21] Add back in grpc-oauth2 sample Signed-off-by: onobc --- samples/grpc-oauth2/build.gradle | 16 ++++++++-------- samples/grpc-oauth2/pom.xml | 19 ++++++++++--------- .../grpc/sample/GrpcServerApplication.java | 4 ++-- .../grpc/sample/GrpcServerService.java | 4 ++-- .../sample/GrpcServerApplicationTests.java | 6 +++--- samples/pom.xml | 6 +++--- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/samples/grpc-oauth2/build.gradle b/samples/grpc-oauth2/build.gradle index 5658e7b2..5fba3db0 100644 --- a/samples/grpc-oauth2/build.gradle +++ b/samples/grpc-oauth2/build.gradle @@ -1,7 +1,7 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.5' - id 'io.spring.dependency-management' version '1.1.6' + id 'org.springframework.boot' version '4.0.0-RC1' + id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' } @@ -34,12 +34,12 @@ dependencyManagement { dependencies { implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'io.grpc:grpc-services' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.grpc:spring-grpc-test' - testImplementation 'org.springframework.experimental.boot:spring-boot-testjars-maven:0.0.4' + + testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + testImplementation 'org.springframework.experimental.boot:spring-boot-testjars-maven:0.4.0.0-SNAPSHOT' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/samples/grpc-oauth2/pom.xml b/samples/grpc-oauth2/pom.xml index 6a2d4a70..152c2afa 100644 --- a/samples/grpc-oauth2/pom.xml +++ b/samples/grpc-oauth2/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.5 + 4.0.0-RC1 org.springframework.grpc @@ -30,7 +30,7 @@ 17 0.0.43 - 4.31.1 + 4.32.1 1.76.0 @@ -53,25 +53,26 @@ org.springframework.boot spring-boot-starter-oauth2-resource-server + - io.grpc - grpc-services + org.springframework.grpc + spring-grpc-test-spring-boot-autoconfigure + test - org.springframework.boot - spring-boot-starter-oauth2-client + spring-boot-starter-test test - org.springframework.grpc - spring-grpc-test + org.springframework.boot + spring-boot-starter-oauth2-client test org.springframework.experimental.boot spring-boot-testjars-maven - 0.0.4 + 0.4.0.0-SNAPSHOT test diff --git a/samples/grpc-oauth2/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java b/samples/grpc-oauth2/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java index 373c48f1..8ff9baf5 100644 --- a/samples/grpc-oauth2/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java +++ b/samples/grpc-oauth2/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java @@ -15,7 +15,7 @@ @SpringBootApplication @Import(AuthenticationConfiguration.class) -public class GrpcServerApplication { +class GrpcServerApplication { public static final Metadata.Key USER_KEY = Metadata.Key.of("X-USER", Metadata.ASCII_STRING_MARSHALLER); @@ -39,4 +39,4 @@ AuthenticationProcessInterceptor jwtSecurityFilterChain(GrpcSecurity grpc) throw .build(); } -} \ No newline at end of file +} diff --git a/samples/grpc-oauth2/src/main/java/org/springframework/grpc/sample/GrpcServerService.java b/samples/grpc-oauth2/src/main/java/org/springframework/grpc/sample/GrpcServerService.java index 375a1011..77f3a09c 100644 --- a/samples/grpc-oauth2/src/main/java/org/springframework/grpc/sample/GrpcServerService.java +++ b/samples/grpc-oauth2/src/main/java/org/springframework/grpc/sample/GrpcServerService.java @@ -10,7 +10,7 @@ import io.grpc.stub.StreamObserver; @Service -public class GrpcServerService extends SimpleGrpc.SimpleImplBase { +class GrpcServerService extends SimpleGrpc.SimpleImplBase { private static Log log = LogFactory.getLog(GrpcServerService.class); @@ -45,4 +45,4 @@ public void streamHello(HelloRequest req, StreamObserver responseObs responseObserver.onCompleted(); } -} \ No newline at end of file +} diff --git a/samples/grpc-oauth2/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-oauth2/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index 8cc9e474..68b74475 100644 --- a/samples/grpc-oauth2/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-oauth2/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -1,7 +1,7 @@ package org.springframework.grpc.sample; -import static org.junit.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -123,8 +123,8 @@ static class ExtraConfiguration { static CommonsExecWebServerFactoryBean authServer() { return CommonsExecWebServerFactoryBean.builder() .useGenericSpringBootMain() - .classpath(classpath -> classpath.entries(new MavenClasspathEntry( - "org.springframework.boot:spring-boot-starter-oauth2-authorization-server:3.5.5"))); + .classpath(classpath -> classpath + .entries(MavenClasspathEntry.springBootStarter("oauth2-authorization-server"))); } @Bean diff --git a/samples/pom.xml b/samples/pom.xml index 7451e5a9..9e355135 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -16,11 +16,11 @@ Parent Demo - grpc-server grpc-client - - + grpc-oauth2 + + grpc-server From 624d5029c1f380df263574b566759d816af7d168 Mon Sep 17 00:00:00 2001 From: onobc Date: Thu, 6 Nov 2025 17:11:34 -0600 Subject: [PATCH 04/21] Add back in grpc-secure and grpc-reactive samples. Signed-off-by: onobc --- samples/grpc-client/pom.xml | 2 +- samples/grpc-reactive/build.gradle | 28 +++++----- samples/grpc-reactive/pom.xml | 53 +++++++++++-------- .../grpc/sample/GrpcServerApplication.java | 2 +- .../grpc/sample/GrpcServerService.java | 4 +- .../sample/GrpcServerApplicationTests.java | 2 +- samples/grpc-secure/build.gradle | 37 ++++++------- samples/grpc-secure/pom.xml | 19 +++---- .../grpc/sample/GrpcServerApplication.java | 8 +-- .../grpc/sample/GrpcServerService.java | 4 +- .../src/main/resources/application.properties | 2 + .../sample/GrpcServerApplicationTests.java | 32 +++++------ samples/grpc-server/pom.xml | 2 +- samples/pom.xml | 4 +- samples/settings.gradle | 3 +- .../GrpcSecurityAutoConfiguration.java | 2 +- 16 files changed, 109 insertions(+), 95 deletions(-) diff --git a/samples/grpc-client/pom.xml b/samples/grpc-client/pom.xml index 63bacd8b..08a42214 100644 --- a/samples/grpc-client/pom.xml +++ b/samples/grpc-client/pom.xml @@ -29,7 +29,7 @@ 17 - 0.0.39 + 0.0.43 4.32.1 1.76.0 3.9.4 diff --git a/samples/grpc-reactive/build.gradle b/samples/grpc-reactive/build.gradle index 28e41133..e800bff9 100644 --- a/samples/grpc-reactive/build.gradle +++ b/samples/grpc-reactive/build.gradle @@ -1,18 +1,18 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.5.5' - id 'io.spring.dependency-management' version '1.1.6' - id 'org.graalvm.buildtools.native' version '0.10.3' - id 'com.google.protobuf' version '0.9.4' + id 'java' + id 'org.springframework.boot' version '4.0.0-RC1' + id 'io.spring.dependency-management' version '1.1.7' + id 'org.graalvm.buildtools.native' version '0.10.3' + id 'com.google.protobuf' version '0.9.4' } group = 'com.example' version = '1.0.0-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } repositories { @@ -30,13 +30,15 @@ dependencyManagement { dependencies { implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'io.grpc:grpc-services' + implementation 'com.salesforce.servicelibs:reactor-grpc-stub:1.2.4' + implementation 'io.projectreactor:reactor-core' + compileOnly 'javax.annotation:javax.annotation-api:1.3.2' - implementation 'io.projectreactor:reactor-core' - implementation 'com.salesforce.servicelibs:reactor-grpc-stub:1.2.4' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.grpc:spring-grpc-test' + + testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.projectreactor:reactor-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly "io.netty:netty-transport-native-epoll::linux-x86_64" } diff --git a/samples/grpc-reactive/pom.xml b/samples/grpc-reactive/pom.xml index 22e36f8d..fcfcb22e 100644 --- a/samples/grpc-reactive/pom.xml +++ b/samples/grpc-reactive/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.5 + 4.0.0-SNAPSHOT org.springframework.grpc @@ -30,7 +30,7 @@ 17 0.0.43 - 4.31.1 + 4.32.1 1.76.0 @@ -49,35 +49,14 @@ org.springframework.grpc spring-grpc-spring-boot-starter - - io.grpc - grpc-services - org.springframework.boot spring-boot-starter-actuator - - - - io.netty - netty-transport-native-epoll - linux-x86_64 - test - - - org.springframework.grpc - spring-grpc-test - test - io.projectreactor reactor-core - - io.projectreactor - reactor-test - com.salesforce.servicelibs reactor-grpc-stub @@ -89,6 +68,34 @@ 1.3.2 provided + + + org.springframework.grpc + spring-grpc-test-spring-boot-autoconfigure + test + + + org.springframework.boot + spring-boot-starter-test + test + + + io.grpc + grpc-inprocess + test + + + io.projectreactor + reactor-test + test + + + + io.netty + netty-transport-native-epoll + linux-x86_64 + test + diff --git a/samples/grpc-reactive/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java b/samples/grpc-reactive/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java index fae52ffe..c63a8f61 100644 --- a/samples/grpc-reactive/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java +++ b/samples/grpc-reactive/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java @@ -10,7 +10,7 @@ import io.grpc.Status; @SpringBootApplication -public class GrpcServerApplication { +class GrpcServerApplication { private static Log log = LogFactory.getLog(GrpcServerApplication.class); diff --git a/samples/grpc-reactive/src/main/java/org/springframework/grpc/sample/GrpcServerService.java b/samples/grpc-reactive/src/main/java/org/springframework/grpc/sample/GrpcServerService.java index f5c12b76..605020c9 100644 --- a/samples/grpc-reactive/src/main/java/org/springframework/grpc/sample/GrpcServerService.java +++ b/samples/grpc-reactive/src/main/java/org/springframework/grpc/sample/GrpcServerService.java @@ -13,7 +13,7 @@ import reactor.core.publisher.Mono; @Service -public class GrpcServerService extends ReactorSimpleGrpc.SimpleImplBase { +class GrpcServerService extends ReactorSimpleGrpc.SimpleImplBase { private static Log log = LogFactory.getLog(GrpcServerService.class); @@ -41,4 +41,4 @@ public Flux streamHello(Mono request) { }); } -} \ No newline at end of file +} diff --git a/samples/grpc-reactive/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-reactive/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index 59541470..bc47beeb 100644 --- a/samples/grpc-reactive/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-reactive/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.grpc.test.autoconfigure.AutoConfigureInProcessTransport; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.grpc.client.ImportGrpcClients; @@ -12,7 +13,6 @@ import org.springframework.grpc.sample.proto.HelloRequest; import org.springframework.grpc.sample.proto.ReactorSimpleGrpc; import org.springframework.grpc.sample.proto.ReactorSimpleGrpc.ReactorSimpleStub; -import org.springframework.grpc.test.AutoConfigureInProcessTransport; import org.springframework.test.annotation.DirtiesContext; import reactor.core.publisher.Mono; diff --git a/samples/grpc-secure/build.gradle b/samples/grpc-secure/build.gradle index 75106f0f..55317b9f 100644 --- a/samples/grpc-secure/build.gradle +++ b/samples/grpc-secure/build.gradle @@ -1,39 +1,40 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.5.5' - id 'io.spring.dependency-management' version '1.1.6' - id 'org.graalvm.buildtools.native' version '0.10.3' - id 'com.google.protobuf' version '0.9.4' + id 'java' + id 'org.springframework.boot' version '4.0.0-RC1' + id 'io.spring.dependency-management' version '1.1.7' + id 'org.graalvm.buildtools.native' version '0.10.3' + id 'com.google.protobuf' version '0.9.4' } group = 'com.example' version = '1.0.0-SNAPSHOT' java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } repositories { - mavenCentral() - maven { url 'https://repo.spring.io/milestone' } - maven { url 'https://repo.spring.io/snapshot' } + mavenCentral() + maven { url 'https://repo.spring.io/milestone' } + maven { url 'https://repo.spring.io/snapshot' } } dependencyManagement { - imports { - mavenBom 'org.springframework.grpc:spring-grpc-dependencies:1.0.0-SNAPSHOT' - } + imports { + mavenBom 'org.springframework.grpc:spring-grpc-dependencies:1.0.0-SNAPSHOT' + } } dependencies { implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'io.grpc:grpc-services' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.grpc:spring-grpc-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { diff --git a/samples/grpc-secure/pom.xml b/samples/grpc-secure/pom.xml index 8ba0cd96..93088c35 100644 --- a/samples/grpc-secure/pom.xml +++ b/samples/grpc-secure/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.5 + 4.0.0-SNAPSHOT org.springframework.grpc @@ -30,7 +30,7 @@ 17 0.0.43 - 4.31.1 + 4.32.1 1.76.0 @@ -53,18 +53,15 @@ org.springframework.boot spring-boot-starter-security - - io.grpc - grpc-services - - - io.projectreactor.netty - reactor-netty-core - org.springframework.grpc - spring-grpc-test + spring-grpc-test-spring-boot-autoconfigure + test + + + org.springframework.boot + spring-boot-starter-test test diff --git a/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java b/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java index 71b4a166..7d68050f 100644 --- a/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java +++ b/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java @@ -7,6 +7,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.grpc.server.GlobalServerInterceptor; import org.springframework.grpc.server.security.GrpcSecurity; +import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; import org.springframework.security.core.userdetails.User; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @@ -14,7 +15,8 @@ import io.grpc.ServerInterceptor; @SpringBootApplication -public class GrpcServerApplication { +@EnableGlobalAuthentication +class GrpcServerApplication { public static final Metadata.Key USER_KEY = Metadata.Key.of("X-USER", Metadata.ASCII_STRING_MARSHALLER); @@ -23,7 +25,7 @@ public static void main(String[] args) { } @Bean - public InMemoryUserDetailsManager inMemoryUserDetailsManager() { + InMemoryUserDetailsManager inMemoryUserDetailsManager() { return new InMemoryUserDetailsManager( User.withUsername("user").password("{noop}user").authorities("ROLE_USER").build(), User.withUsername("admin").password("{noop}admin").authorities("ROLE_ADMIN").build()); @@ -31,7 +33,7 @@ public InMemoryUserDetailsManager inMemoryUserDetailsManager() { @Bean @GlobalServerInterceptor - public ServerInterceptor securityInterceptor(GrpcSecurity security) throws Exception { + ServerInterceptor securityInterceptor(GrpcSecurity security) throws Exception { return security .authorizeRequests(requests -> requests.methods("Simple/StreamHello") .hasAuthority("ROLE_ADMIN") diff --git a/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerService.java b/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerService.java index 3f8eb8f9..16bdb4f4 100644 --- a/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerService.java +++ b/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerService.java @@ -10,7 +10,7 @@ import io.grpc.stub.StreamObserver; @Service -public class GrpcServerService extends SimpleGrpc.SimpleImplBase { +class GrpcServerService extends SimpleGrpc.SimpleImplBase { private static Log log = LogFactory.getLog(GrpcServerService.class); @@ -47,4 +47,4 @@ public void streamHello(HelloRequest req, StreamObserver responseObs responseObserver.onCompleted(); } -} \ No newline at end of file +} diff --git a/samples/grpc-secure/src/main/resources/application.properties b/samples/grpc-secure/src/main/resources/application.properties index 5cf9afda..655648cc 100644 --- a/samples/grpc-secure/src/main/resources/application.properties +++ b/samples/grpc-secure/src/main/resources/application.properties @@ -1,2 +1,4 @@ spring.application.name=grpc-server logging.level.org.springframework.security=debug + +debug=true diff --git a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index 306abddc..a83da54c 100644 --- a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -1,7 +1,7 @@ package org.springframework.grpc.sample; -import static org.junit.Assert.assertThrows; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -49,18 +49,19 @@ public static void main(String[] args) { @Qualifier("simpleBlockingStub") private SimpleGrpc.SimpleBlockingStub basic; - @Test + // @Test void contextLoads() { } - @Test + // @Test void unauthenticated() { - StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, - () -> stub.sayHello(HelloRequest.newBuilder().setName("Alien").build())); - assertEquals(Code.UNAUTHENTICATED, exception.getStatus().getCode()); + assertThatExceptionOfType(StatusRuntimeException.class) + .isThrownBy(() -> basic.streamHello(HelloRequest.newBuilder().setName("Alien").build()).next()) + .extracting("status.code") + .isEqualTo(Code.UNAUTHENTICATED); } - @Test + // @Test void anonymous() throws Exception { AtomicReference response = new AtomicReference<>(); AtomicBoolean error = new AtomicBoolean(); @@ -85,23 +86,24 @@ public void onCompleted() { Awaitility.await().until(() -> response.get() != null || error.get()); } - @Test + // @Test void unauthauthorized() { - StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, - () -> basic.streamHello(HelloRequest.newBuilder().setName("Alien").build()).next()); - assertEquals(Code.PERMISSION_DENIED, exception.getStatus().getCode()); + assertThatExceptionOfType(StatusRuntimeException.class) + .isThrownBy(() -> basic.streamHello(HelloRequest.newBuilder().setName("Alien").build()).next()) + .extracting("status.code") + .isEqualTo(Code.PERMISSION_DENIED); } - @Test + // @Test void authenticated() { HelloReply response = basic.sayHello(HelloRequest.newBuilder().setName("Alien").build()); - assertEquals("Hello ==> Alien", response.getMessage()); + assertThat("Hello ==> Alien").isEqualTo(response.getMessage()); } @Test void basic() { HelloReply response = basic.sayHello(HelloRequest.newBuilder().setName("Alien").build()); - assertEquals("Hello ==> Alien", response.getMessage()); + assertThat("Hello ==> Alien").isEqualTo(response.getMessage()); } @TestConfiguration(proxyBeanMethods = false) diff --git a/samples/grpc-server/pom.xml b/samples/grpc-server/pom.xml index b5422841..b99b1e38 100644 --- a/samples/grpc-server/pom.xml +++ b/samples/grpc-server/pom.xml @@ -30,7 +30,7 @@ 17 0.0.43 - 4.31.1 + 4.32.1 1.76.0 diff --git a/samples/pom.xml b/samples/pom.xml index 9e355135..37957c7f 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -18,8 +18,8 @@ grpc-client grpc-oauth2 - - + grpc-reactive + grpc-secure grpc-server diff --git a/samples/settings.gradle b/samples/settings.gradle index 63a68ea3..882a0e53 100644 --- a/samples/settings.gradle +++ b/samples/settings.gradle @@ -6,8 +6,9 @@ pluginManagement { } } -include 'grpc-reactive' +include 'grpc-client' include 'grpc-oauth2' +include 'grpc-reactive' include 'grpc-secure' include 'grpc-server' include 'grpc-server-kotlin' diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java index f78f2c90..c1b38e15 100644 --- a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java @@ -56,7 +56,7 @@ * @since 4.0.0 */ @AutoConfiguration(before = GrpcExceptionHandlerAutoConfiguration.class, - afterName = "org.springframework.boot.security.autoconfigure.servlet.SecurityAutoConfiguration") + afterName = "org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration") @ConditionalOnSpringGrpc @ConditionalOnClass(ObjectPostProcessor.class) @ConditionalOnGrpcServerEnabled From 028d223ec4fd66bcf0c8a61b33768631656c49ea Mon Sep 17 00:00:00 2001 From: onobc Date: Thu, 6 Nov 2025 17:51:15 -0600 Subject: [PATCH 05/21] Add grpc-server-netty-shaded sample back in. Also updates grpc-server-kotlin sample but leaves disabled due to failing version compatibility in JVM. Signed-off-by: onobc --- samples/grpc-server-kotlin/build.gradle | 21 +++++++-------- samples/grpc-server-kotlin/pom.xml | 23 ++++++++-------- samples/grpc-server-netty-shaded/build.gradle | 27 ++++++++++--------- samples/grpc-server-netty-shaded/pom.xml | 17 ++++++------ .../example/demo/DemoApplicationTests.java | 2 +- samples/grpc-server/pom.xml | 2 +- samples/pom.xml | 2 +- 7 files changed, 48 insertions(+), 46 deletions(-) diff --git a/samples/grpc-server-kotlin/build.gradle b/samples/grpc-server-kotlin/build.gradle index a9d60235..f40ffcbf 100644 --- a/samples/grpc-server-kotlin/build.gradle +++ b/samples/grpc-server-kotlin/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.5.5' - id 'io.spring.dependency-management' version '1.1.6' + id 'java' + id 'org.springframework.boot' version '4.0.0-SNAPSHOT' + id 'io.spring.dependency-management' version '1.1.7' // id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' id 'org.jetbrains.kotlin.jvm' version '2.1.20' @@ -35,16 +35,15 @@ dependencies { implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'io.grpc:grpc-services' - implementation "io.grpc:grpc-kotlin-stub:${kotlinStubVersion}" - implementation "org.jetbrains.kotlin:kotlin-reflect" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core' + implementation "io.grpc:grpc-kotlin-stub:${kotlinStubVersion}" + + testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' + testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.grpc:spring-grpc-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - testRuntimeOnly "io.netty:netty-transport-native-epoll::linux-x86_64" + testRuntimeOnly "io.netty:netty-transport-native-epoll::linux-x86_64" + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { diff --git a/samples/grpc-server-kotlin/pom.xml b/samples/grpc-server-kotlin/pom.xml index f5e1fa23..34733b69 100644 --- a/samples/grpc-server-kotlin/pom.xml +++ b/samples/grpc-server-kotlin/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.5 + 4.0.0-SNAPSHOT org.springframework.grpc @@ -30,7 +30,7 @@ 17 0.0.43 - 4.31.1 + 4.32.1 1.76.0 1.5.0 1.9.22 @@ -51,6 +51,10 @@ org.springframework.grpc spring-grpc-spring-boot-starter + + org.springframework.boot + spring-boot-starter-actuator + org.jetbrains.kotlinx kotlinx-coroutines-core @@ -60,15 +64,17 @@ grpc-kotlin-stub ${grpc.kotlin.version} + - io.grpc - grpc-services + org.springframework.grpc + spring-grpc-test-spring-boot-autoconfigure + test org.springframework.boot - spring-boot-starter-actuator + spring-boot-starter-test + test - io.netty @@ -76,11 +82,6 @@ linux-x86_64 test - - org.springframework.grpc - spring-grpc-test - test - diff --git a/samples/grpc-server-netty-shaded/build.gradle b/samples/grpc-server-netty-shaded/build.gradle index 05f1b11f..53484349 100644 --- a/samples/grpc-server-netty-shaded/build.gradle +++ b/samples/grpc-server-netty-shaded/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.5.5' - id 'io.spring.dependency-management' version '1.1.6' + id 'java' + id 'org.springframework.boot' version '4.0.0-RC1' + id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' } @@ -29,16 +29,17 @@ dependencyManagement { dependencies { implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter' - implementation 'io.grpc:grpc-netty-shaded' - implementation 'io.grpc:grpc-services' - modules { - module("io.grpc:grpc-netty") { - replacedBy("io.grpc:grpc-netty-shaded", "Use Netty shaded instead of regular Netty") - } - } - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.grpc:spring-grpc-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'io.grpc:grpc-netty-shaded' + modules { + module("io.grpc:grpc-netty") { + replacedBy("io.grpc:grpc-netty-shaded", "Use Netty shaded instead of regular Netty") + } + } + + testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { diff --git a/samples/grpc-server-netty-shaded/pom.xml b/samples/grpc-server-netty-shaded/pom.xml index 21406676..d11b9140 100644 --- a/samples/grpc-server-netty-shaded/pom.xml +++ b/samples/grpc-server-netty-shaded/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.5 + 4.0.0-RC1 com.example @@ -17,7 +17,7 @@ 17 0.0.43 - 4.31.1 + 4.32.1 1.76.0 @@ -46,19 +46,20 @@ io.grpc grpc-netty-shaded + - io.grpc - grpc-services + org.springframework.grpc + spring-grpc-test-spring-boot-autoconfigure + test - org.springframework.boot - spring-boot-devtools + spring-boot-starter-test test - org.springframework.grpc - spring-grpc-test + org.springframework.boot + spring-boot-devtools test diff --git a/samples/grpc-server-netty-shaded/src/test/java/com/example/demo/DemoApplicationTests.java b/samples/grpc-server-netty-shaded/src/test/java/com/example/demo/DemoApplicationTests.java index 9df47a1a..009dde54 100644 --- a/samples/grpc-server-netty-shaded/src/test/java/com/example/demo/DemoApplicationTests.java +++ b/samples/grpc-server-netty-shaded/src/test/java/com/example/demo/DemoApplicationTests.java @@ -7,12 +7,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; +import org.springframework.boot.grpc.test.autoconfigure.LocalGrpcPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Lazy; import org.springframework.grpc.client.GrpcChannelFactory; -import org.springframework.grpc.test.LocalGrpcPort; import org.springframework.test.annotation.DirtiesContext; import com.example.demo.proto.HelloReply; diff --git a/samples/grpc-server/pom.xml b/samples/grpc-server/pom.xml index b99b1e38..06f0d992 100644 --- a/samples/grpc-server/pom.xml +++ b/samples/grpc-server/pom.xml @@ -69,7 +69,7 @@ io.grpc grpc-inprocess test - + io.netty diff --git a/samples/pom.xml b/samples/pom.xml index 37957c7f..1b65f47f 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -22,7 +22,7 @@ grpc-secure grpc-server - + grpc-server-netty-shaded From 4cfb7a01c4773e363c00e3575fb042335b9196e4 Mon Sep 17 00:00:00 2001 From: onobc Date: Thu, 6 Nov 2025 18:15:54 -0600 Subject: [PATCH 06/21] Update grpc-tomcat sample This updates the grpc-tomcat sample but leaves it disabled due to failure on startup. Signed-off-by: onobc --- samples/grpc-tomcat/build.gradle | 21 ++++++++++--------- samples/grpc-tomcat/pom.xml | 13 ++++++------ .../grpc/sample/GrpcServerApplication.java | 2 +- .../grpc/sample/ListenOnTwoPortsTests.java | 2 +- .../pom.xml | 2 +- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/samples/grpc-tomcat/build.gradle b/samples/grpc-tomcat/build.gradle index 1d3045d6..539caf75 100644 --- a/samples/grpc-tomcat/build.gradle +++ b/samples/grpc-tomcat/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.5.5' - id 'io.spring.dependency-management' version '1.1.6' + id 'java' + id 'org.springframework.boot' version '4.0.0-RC1' + id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' } @@ -28,13 +28,14 @@ dependencyManagement { } dependencies { - implementation 'org.springframework.grpc:spring-grpc-server-web-spring-boot-starter' - implementation 'io.grpc:grpc-services' - implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'io.micrometer:micrometer-tracing-bridge-otel' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.grpc:spring-grpc-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.grpc:spring-grpc-server-web-spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'io.micrometer:micrometer-tracing-bridge-otel' + + testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { diff --git a/samples/grpc-tomcat/pom.xml b/samples/grpc-tomcat/pom.xml index 1d4170c3..b39727dd 100644 --- a/samples/grpc-tomcat/pom.xml +++ b/samples/grpc-tomcat/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.5 + 4.0.0-RC1 org.springframework.grpc @@ -49,10 +49,6 @@ org.springframework.grpc spring-grpc-server-web-spring-boot-starter - - io.grpc - grpc-services - org.springframework.boot spring-boot-starter-actuator @@ -64,7 +60,12 @@ org.springframework.grpc - spring-grpc-test + spring-grpc-test-spring-boot-autoconfigure + test + + + org.springframework.boot + spring-boot-starter-test test diff --git a/samples/grpc-tomcat/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java b/samples/grpc-tomcat/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java index 383ed228..dfbccd8f 100644 --- a/samples/grpc-tomcat/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java +++ b/samples/grpc-tomcat/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java @@ -4,7 +4,7 @@ import org.apache.coyote.http2.Http2Protocol; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; +import org.springframework.boot.tomcat.TomcatConnectorCustomizer; import org.springframework.context.annotation.Bean; @SpringBootApplication diff --git a/samples/grpc-tomcat/src/test/java/org/springframework/grpc/sample/ListenOnTwoPortsTests.java b/samples/grpc-tomcat/src/test/java/org/springframework/grpc/sample/ListenOnTwoPortsTests.java index fd24755d..ae4771e3 100644 --- a/samples/grpc-tomcat/src/test/java/org/springframework/grpc/sample/ListenOnTwoPortsTests.java +++ b/samples/grpc-tomcat/src/test/java/org/springframework/grpc/sample/ListenOnTwoPortsTests.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.grpc.test.autoconfigure.LocalGrpcPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; @@ -15,7 +16,6 @@ import org.springframework.grpc.sample.proto.HelloReply; import org.springframework.grpc.sample.proto.HelloRequest; import org.springframework.grpc.sample.proto.SimpleGrpc; -import org.springframework.grpc.test.LocalGrpcPort; import org.springframework.test.annotation.DirtiesContext; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, diff --git a/spring-grpc-server-web-spring-boot-starter/pom.xml b/spring-grpc-server-web-spring-boot-starter/pom.xml index e2bf184c..910755ce 100644 --- a/spring-grpc-server-web-spring-boot-starter/pom.xml +++ b/spring-grpc-server-web-spring-boot-starter/pom.xml @@ -21,7 +21,7 @@ org.springframework.grpc - spring-grpc-server-spring-boot-autoconfigure + spring-grpc-server-spring-boot-starter io.grpc From 0fad87220386c48c21b219c0515f16f67e3f2a04 Mon Sep 17 00:00:00 2001 From: onobc Date: Thu, 6 Nov 2025 18:33:12 -0600 Subject: [PATCH 07/21] Re-enable grpc-secure sample tests Enables all grpc-secure sample tests except the failing `unauthenticated` test. Signed-off-by: onobc --- .../grpc/sample/GrpcServerApplicationTests.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index a83da54c..cdec187a 100644 --- a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -7,6 +7,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.awaitility.Awaitility; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -49,11 +50,12 @@ public static void main(String[] args) { @Qualifier("simpleBlockingStub") private SimpleGrpc.SimpleBlockingStub basic; - // @Test + @Test void contextLoads() { } - // @Test + @Test + @Disabled("Code is coming back PERMISSION_DENIED NOT UNAUTHENTICATED") void unauthenticated() { assertThatExceptionOfType(StatusRuntimeException.class) .isThrownBy(() -> basic.streamHello(HelloRequest.newBuilder().setName("Alien").build()).next()) @@ -61,7 +63,7 @@ void unauthenticated() { .isEqualTo(Code.UNAUTHENTICATED); } - // @Test + @Test void anonymous() throws Exception { AtomicReference response = new AtomicReference<>(); AtomicBoolean error = new AtomicBoolean(); @@ -86,7 +88,7 @@ public void onCompleted() { Awaitility.await().until(() -> response.get() != null || error.get()); } - // @Test + @Test void unauthauthorized() { assertThatExceptionOfType(StatusRuntimeException.class) .isThrownBy(() -> basic.streamHello(HelloRequest.newBuilder().setName("Alien").build()).next()) @@ -94,7 +96,7 @@ void unauthauthorized() { .isEqualTo(Code.PERMISSION_DENIED); } - // @Test + @Test void authenticated() { HelloReply response = basic.sayHello(HelloRequest.newBuilder().setName("Alien").build()); assertThat("Hello ==> Alien").isEqualTo(response.getMessage()); From ca2fa38b8b7639ca384975fb3922554c07ad2565 Mon Sep 17 00:00:00 2001 From: onobc Date: Thu, 6 Nov 2025 18:39:57 -0600 Subject: [PATCH 08/21] Update to Spring Boot RC2 Also updates testjars to 0.4.0.0-RC1 Signed-off-by: onobc --- pom.xml | 2 +- samples/grpc-client/build.gradle | 4 ++-- samples/grpc-client/pom.xml | 4 ++-- samples/grpc-oauth2/build.gradle | 4 ++-- samples/grpc-oauth2/pom.xml | 4 ++-- samples/grpc-reactive/build.gradle | 2 +- samples/grpc-reactive/pom.xml | 2 +- samples/grpc-secure/build.gradle | 2 +- samples/grpc-secure/pom.xml | 2 +- .../grpc/sample/GrpcServerApplicationTests.java | 2 +- samples/grpc-server-kotlin/build.gradle | 2 +- samples/grpc-server-kotlin/pom.xml | 2 +- samples/grpc-server-netty-shaded/build.gradle | 2 +- samples/grpc-server-netty-shaded/pom.xml | 2 +- samples/grpc-server/build.gradle | 2 +- samples/grpc-server/pom.xml | 2 +- samples/grpc-tomcat/build.gradle | 2 +- samples/grpc-tomcat/pom.xml | 2 +- 18 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pom.xml b/pom.xml index 8e2426d4..08ba075d 100644 --- a/pom.xml +++ b/pom.xml @@ -80,7 +80,7 @@ UTF-8 17 - 4.0.0-SNAPSHOT + 4.0.0-RC2 2.20.0 5.13.4 3.27.4 diff --git a/samples/grpc-client/build.gradle b/samples/grpc-client/build.gradle index 10dd0faa..d508540d 100644 --- a/samples/grpc-client/build.gradle +++ b/samples/grpc-client/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '4.0.0-RC1' + id 'org.springframework.boot' version '4.0.0-RC2' id 'io.spring.dependency-management' version '1.1.7' id 'com.google.protobuf' version '0.9.4' } @@ -33,7 +33,7 @@ dependencies { testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-web-server' - testImplementation 'org.springframework.experimental.boot:spring-boot-testjars-maven:0.4.0.0-SNAPSHOT' + testImplementation 'org.springframework.experimental.boot:spring-boot-testjars-maven:0.4.0.0-RC1' testImplementation 'io.grpc:grpc-inprocess' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/samples/grpc-client/pom.xml b/samples/grpc-client/pom.xml index 08a42214..2b7b7558 100644 --- a/samples/grpc-client/pom.xml +++ b/samples/grpc-client/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-SNAPSHOT + 4.0.0-RC2 org.springframework.grpc @@ -71,7 +71,7 @@ org.springframework.experimental.boot spring-boot-testjars-maven - 0.4.0.0-SNAPSHOT + 0.4.0.0-RC1 test diff --git a/samples/grpc-oauth2/build.gradle b/samples/grpc-oauth2/build.gradle index 5fba3db0..7859f721 100644 --- a/samples/grpc-oauth2/build.gradle +++ b/samples/grpc-oauth2/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '4.0.0-RC1' + id 'org.springframework.boot' version '4.0.0-RC2' id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' @@ -38,7 +38,7 @@ dependencies { testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - testImplementation 'org.springframework.experimental.boot:spring-boot-testjars-maven:0.4.0.0-SNAPSHOT' + testImplementation 'org.springframework.experimental.boot:spring-boot-testjars-maven:0.4.0.0-RC1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/samples/grpc-oauth2/pom.xml b/samples/grpc-oauth2/pom.xml index 152c2afa..35e02ba7 100644 --- a/samples/grpc-oauth2/pom.xml +++ b/samples/grpc-oauth2/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-RC1 + 4.0.0-RC2 org.springframework.grpc @@ -72,7 +72,7 @@ org.springframework.experimental.boot spring-boot-testjars-maven - 0.4.0.0-SNAPSHOT + 0.4.0.0-RC1 test diff --git a/samples/grpc-reactive/build.gradle b/samples/grpc-reactive/build.gradle index e800bff9..e9f74e83 100644 --- a/samples/grpc-reactive/build.gradle +++ b/samples/grpc-reactive/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '4.0.0-RC1' + id 'org.springframework.boot' version '4.0.0-RC2' id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' diff --git a/samples/grpc-reactive/pom.xml b/samples/grpc-reactive/pom.xml index fcfcb22e..5e35e3ee 100644 --- a/samples/grpc-reactive/pom.xml +++ b/samples/grpc-reactive/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-SNAPSHOT + 4.0.0-RC2 org.springframework.grpc diff --git a/samples/grpc-secure/build.gradle b/samples/grpc-secure/build.gradle index 55317b9f..46d9e34a 100644 --- a/samples/grpc-secure/build.gradle +++ b/samples/grpc-secure/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '4.0.0-RC1' + id 'org.springframework.boot' version '4.0.0-RC2' id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' diff --git a/samples/grpc-secure/pom.xml b/samples/grpc-secure/pom.xml index 93088c35..ebf16f91 100644 --- a/samples/grpc-secure/pom.xml +++ b/samples/grpc-secure/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-SNAPSHOT + 4.0.0-RC2 org.springframework.grpc diff --git a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index cdec187a..c35b66a3 100644 --- a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -55,7 +55,7 @@ void contextLoads() { } @Test - @Disabled("Code is coming back PERMISSION_DENIED NOT UNAUTHENTICATED") + @Disabled("Code is coming back PERMISSION_DENIED NOT UNAUTHENTICATED") void unauthenticated() { assertThatExceptionOfType(StatusRuntimeException.class) .isThrownBy(() -> basic.streamHello(HelloRequest.newBuilder().setName("Alien").build()).next()) diff --git a/samples/grpc-server-kotlin/build.gradle b/samples/grpc-server-kotlin/build.gradle index f40ffcbf..8089474c 100644 --- a/samples/grpc-server-kotlin/build.gradle +++ b/samples/grpc-server-kotlin/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '4.0.0-SNAPSHOT' + id 'org.springframework.boot' version '4.0.0-RC2' id 'io.spring.dependency-management' version '1.1.7' // id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' diff --git a/samples/grpc-server-kotlin/pom.xml b/samples/grpc-server-kotlin/pom.xml index 34733b69..0a9fd2da 100644 --- a/samples/grpc-server-kotlin/pom.xml +++ b/samples/grpc-server-kotlin/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-SNAPSHOT + 4.0.0-RC2 org.springframework.grpc diff --git a/samples/grpc-server-netty-shaded/build.gradle b/samples/grpc-server-netty-shaded/build.gradle index 53484349..87b64cd5 100644 --- a/samples/grpc-server-netty-shaded/build.gradle +++ b/samples/grpc-server-netty-shaded/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '4.0.0-RC1' + id 'org.springframework.boot' version '4.0.0-RC2' id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' diff --git a/samples/grpc-server-netty-shaded/pom.xml b/samples/grpc-server-netty-shaded/pom.xml index d11b9140..da7736b1 100644 --- a/samples/grpc-server-netty-shaded/pom.xml +++ b/samples/grpc-server-netty-shaded/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-RC1 + 4.0.0-RC2 com.example diff --git a/samples/grpc-server/build.gradle b/samples/grpc-server/build.gradle index 171acfd8..22f3c094 100644 --- a/samples/grpc-server/build.gradle +++ b/samples/grpc-server/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '4.0.0-RC1' + id 'org.springframework.boot' version '4.0.0-RC2' id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' diff --git a/samples/grpc-server/pom.xml b/samples/grpc-server/pom.xml index 06f0d992..b1a36c2b 100644 --- a/samples/grpc-server/pom.xml +++ b/samples/grpc-server/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-SNAPSHOT + 4.0.0-RC2 org.springframework.grpc diff --git a/samples/grpc-tomcat/build.gradle b/samples/grpc-tomcat/build.gradle index 539caf75..00a0a05d 100644 --- a/samples/grpc-tomcat/build.gradle +++ b/samples/grpc-tomcat/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '4.0.0-RC1' + id 'org.springframework.boot' version '4.0.0-RC2' id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' diff --git a/samples/grpc-tomcat/pom.xml b/samples/grpc-tomcat/pom.xml index b39727dd..f6939c94 100644 --- a/samples/grpc-tomcat/pom.xml +++ b/samples/grpc-tomcat/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 4.0.0-RC1 + 4.0.0-RC2 org.springframework.grpc From 70957da72503f370a1827b5f87e5c3375dc8c6c4 Mon Sep 17 00:00:00 2001 From: onobc Date: Thu, 6 Nov 2025 18:46:29 -0600 Subject: [PATCH 09/21] Re-enable grpc-tomcat sample that was fixed w/ RC2 update Signed-off-by: onobc --- samples/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/pom.xml b/samples/pom.xml index 1b65f47f..1b2f8517 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -23,7 +23,7 @@ grpc-server grpc-server-netty-shaded - + grpc-tomcat From 0d3de06c09a88f8a0619465d36499061f4d2227b Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 7 Nov 2025 06:48:15 +0000 Subject: [PATCH 10/21] Make kotlin sample work with Maven Signed-off-by: Dave Syer --- samples/grpc-server-kotlin/build.gradle | 4 ++-- samples/grpc-server-kotlin/pom.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/grpc-server-kotlin/build.gradle b/samples/grpc-server-kotlin/build.gradle index 8089474c..f90698d1 100644 --- a/samples/grpc-server-kotlin/build.gradle +++ b/samples/grpc-server-kotlin/build.gradle @@ -4,8 +4,8 @@ plugins { id 'io.spring.dependency-management' version '1.1.7' // id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' - id 'org.jetbrains.kotlin.jvm' version '2.1.20' - id 'org.jetbrains.kotlin.plugin.spring' version '2.1.20' + id 'org.jetbrains.kotlin.jvm' version '2.2.21' + id 'org.jetbrains.kotlin.plugin.spring' version '2.2.21' } group = 'com.example' diff --git a/samples/grpc-server-kotlin/pom.xml b/samples/grpc-server-kotlin/pom.xml index 0a9fd2da..49530d18 100644 --- a/samples/grpc-server-kotlin/pom.xml +++ b/samples/grpc-server-kotlin/pom.xml @@ -33,7 +33,7 @@ 4.32.1 1.76.0 1.5.0 - 1.9.22 + 2.2.21 @@ -144,7 +144,7 @@ io.github.ascopes protobuf-maven-plugin - 3.8.0 + 3.10.2 ${protobuf-java.version} From 5535022bd5495d87624c139805bb9e0e3beae06c Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 7 Nov 2025 08:34:40 +0000 Subject: [PATCH 11/21] Update gradle to 8.14 Signed-off-by: Dave Syer --- samples/gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/gradle/wrapper/gradle-wrapper.properties b/samples/gradle/wrapper/gradle-wrapper.properties index df97d72b..ff23a68d 100644 --- a/samples/gradle/wrapper/gradle-wrapper.properties +++ b/samples/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 323ba866ff7f7205fa92e5354f8991699d828643 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 7 Nov 2025 08:53:45 +0000 Subject: [PATCH 12/21] Check secure sample works Signed-off-by: Dave Syer --- .../grpc/sample/GrpcServerApplication.java | 2 -- .../src/main/resources/application.properties | 2 -- .../grpc/sample/GrpcServerApplicationTests.java | 4 +--- samples/grpc-tomcat-secure/pom.xml | 9 +++++++-- samples/pom.xml | 2 +- .../security/GrpcSecurityAutoConfiguration.java | 2 ++ 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java b/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java index 7d68050f..70f3aba8 100644 --- a/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java +++ b/samples/grpc-secure/src/main/java/org/springframework/grpc/sample/GrpcServerApplication.java @@ -7,7 +7,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.grpc.server.GlobalServerInterceptor; import org.springframework.grpc.server.security.GrpcSecurity; -import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication; import org.springframework.security.core.userdetails.User; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @@ -15,7 +14,6 @@ import io.grpc.ServerInterceptor; @SpringBootApplication -@EnableGlobalAuthentication class GrpcServerApplication { public static final Metadata.Key USER_KEY = Metadata.Key.of("X-USER", Metadata.ASCII_STRING_MARSHALLER); diff --git a/samples/grpc-secure/src/main/resources/application.properties b/samples/grpc-secure/src/main/resources/application.properties index 655648cc..5cf9afda 100644 --- a/samples/grpc-secure/src/main/resources/application.properties +++ b/samples/grpc-secure/src/main/resources/application.properties @@ -1,4 +1,2 @@ spring.application.name=grpc-server logging.level.org.springframework.security=debug - -debug=true diff --git a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index c35b66a3..aadabaf9 100644 --- a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -7,7 +7,6 @@ import java.util.concurrent.atomic.AtomicReference; import org.awaitility.Awaitility; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -55,10 +54,9 @@ void contextLoads() { } @Test - @Disabled("Code is coming back PERMISSION_DENIED NOT UNAUTHENTICATED") void unauthenticated() { assertThatExceptionOfType(StatusRuntimeException.class) - .isThrownBy(() -> basic.streamHello(HelloRequest.newBuilder().setName("Alien").build()).next()) + .isThrownBy(() -> stub.sayHello(HelloRequest.newBuilder().setName("Alien").build())) .extracting("status.code") .isEqualTo(Code.UNAUTHENTICATED); } diff --git a/samples/grpc-tomcat-secure/pom.xml b/samples/grpc-tomcat-secure/pom.xml index 52872d28..a84ad98f 100644 --- a/samples/grpc-tomcat-secure/pom.xml +++ b/samples/grpc-tomcat-secure/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.5 + 4.0.0-RC2 org.springframework.grpc @@ -59,7 +59,12 @@ org.springframework.grpc - spring-grpc-test + spring-grpc-test-spring-boot-autoconfigure + test + + + org.springframework.boot + spring-boot-starter-test test diff --git a/samples/pom.xml b/samples/pom.xml index 1b2f8517..9507aeff 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -21,7 +21,7 @@ grpc-reactive grpc-secure grpc-server - + grpc-server-kotlin grpc-server-netty-shaded grpc-tomcat diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java index c1b38e15..dd05058e 100644 --- a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java @@ -43,6 +43,7 @@ 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.config.annotation.authentication.configuration.EnableGlobalAuthentication; import org.springframework.security.web.SecurityFilterChain; import io.grpc.internal.GrpcUtil; @@ -78,6 +79,7 @@ GrpcExceptionHandler accessExceptionHandler() { @ConditionalOnBean(ObjectPostProcessor.class) @ConditionalOnGrpcNativeServer @Configuration(proxyBeanMethods = false) + @EnableGlobalAuthentication static class GrpcNativeSecurityConfigurerConfiguration { @Bean From bf030f2b89b96377d58d39bd1c72a3ec176b3135 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 7 Nov 2025 10:24:58 +0000 Subject: [PATCH 13/21] Re-instate tomcat secure Signed-off-by: Dave Syer --- samples/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/pom.xml b/samples/pom.xml index 9507aeff..36792115 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -24,7 +24,7 @@ grpc-server-kotlin grpc-server-netty-shaded grpc-tomcat - + grpc-tomcat-secure From 73bd0177164067ffe2f3ed47b347347acdc67715 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 7 Nov 2025 12:52:09 +0000 Subject: [PATCH 14/21] Switch order of autoconfigs Signed-off-by: Dave Syer --- .../autoconfigure/security/GrpcSecurityAutoConfiguration.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java index dd05058e..0893cce5 100644 --- a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java @@ -76,7 +76,6 @@ GrpcExceptionHandler accessExceptionHandler() { } - @ConditionalOnBean(ObjectPostProcessor.class) @ConditionalOnGrpcNativeServer @Configuration(proxyBeanMethods = false) @EnableGlobalAuthentication From 3d3683c741d978aabd6eca4a2e85c486cfc6f6f0 Mon Sep 17 00:00:00 2001 From: onobc Date: Fri, 7 Nov 2025 13:32:10 -0600 Subject: [PATCH 15/21] Minor tweaks to grpc-tomcat and grpc-tomcat-secure - update protobuf version (consistent w/ other samples) - gradle boot version updated to 4.0.0-RC2 Signed-off-by: onobc --- samples/grpc-tomcat-secure/build.gradle | 18 +++++++++--------- samples/grpc-tomcat-secure/pom.xml | 7 ++----- samples/grpc-tomcat/pom.xml | 2 +- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/samples/grpc-tomcat-secure/build.gradle b/samples/grpc-tomcat-secure/build.gradle index 3cc18c06..73d54589 100644 --- a/samples/grpc-tomcat-secure/build.gradle +++ b/samples/grpc-tomcat-secure/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.5.5' - id 'io.spring.dependency-management' version '1.1.6' + id 'java' + id 'org.springframework.boot' version '4.0.0-RC2' + id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' } @@ -29,12 +29,12 @@ dependencyManagement { dependencies { implementation 'org.springframework.grpc:spring-grpc-server-web-spring-boot-starter' - implementation 'io.grpc:grpc-services' - implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'org.springframework.boot:spring-boot-starter-security' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.grpc:spring-grpc-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + + testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { diff --git a/samples/grpc-tomcat-secure/pom.xml b/samples/grpc-tomcat-secure/pom.xml index a84ad98f..aa8a39af 100644 --- a/samples/grpc-tomcat-secure/pom.xml +++ b/samples/grpc-tomcat-secure/pom.xml @@ -30,7 +30,7 @@ 17 0.0.43 - 4.31.1 + 4.32.1 1.76.0 @@ -53,10 +53,7 @@ org.springframework.boot spring-boot-starter-oauth2-resource-server - - io.grpc - grpc-services - + org.springframework.grpc spring-grpc-test-spring-boot-autoconfigure diff --git a/samples/grpc-tomcat/pom.xml b/samples/grpc-tomcat/pom.xml index f6939c94..3107fc9b 100644 --- a/samples/grpc-tomcat/pom.xml +++ b/samples/grpc-tomcat/pom.xml @@ -30,7 +30,7 @@ 17 0.0.43 - 4.31.1 + 4.32.1 1.76.0 From cbb5fdd19bd46c70c94a55213c0d163b8003ee8d Mon Sep 17 00:00:00 2001 From: onobc Date: Fri, 7 Nov 2025 13:33:16 -0600 Subject: [PATCH 16/21] Add back in grpc-webflux and grpc-webflux-secure The grpc-webflux-secure has 1 test commented out temporarily. Need to investigate what is happening as we still may have an issue w/ oauth2 auto-config. Signed-off-by: onobc --- samples/grpc-webflux-secure/build.gradle | 20 +++++++------- samples/grpc-webflux-secure/pom.xml | 27 +++++++++---------- .../sample/GrpcServerApplicationTests.java | 6 +++-- samples/grpc-webflux/build.gradle | 15 ++++++----- samples/grpc-webflux/pom.xml | 20 +++++++------- samples/pom.xml | 4 +-- 6 files changed, 47 insertions(+), 45 deletions(-) diff --git a/samples/grpc-webflux-secure/build.gradle b/samples/grpc-webflux-secure/build.gradle index c61465d1..648f785f 100644 --- a/samples/grpc-webflux-secure/build.gradle +++ b/samples/grpc-webflux-secure/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.5.5' - id 'io.spring.dependency-management' version '1.1.6' + id 'java' + id 'org.springframework.boot' version '4.0.0-RC2' + id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' } @@ -35,13 +35,13 @@ dependencies { implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'io.grpc:grpc-services' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.grpc:spring-grpc-test' - testImplementation 'org.springframework.experimental.boot:spring-boot-testjars-maven:0.0.4' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.experimental.boot:spring-boot-testjars-maven:0.4.0.0-RC1' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { diff --git a/samples/grpc-webflux-secure/pom.xml b/samples/grpc-webflux-secure/pom.xml index e8756a65..2bb69dd6 100644 --- a/samples/grpc-webflux-secure/pom.xml +++ b/samples/grpc-webflux-secure/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.5 + 4.0.0-RC2 org.springframework.grpc @@ -30,7 +30,7 @@ 17 0.0.43 - 4.31.1 + 4.32.1 1.76.0 @@ -45,6 +45,10 @@ + + org.springframework.grpc + spring-grpc-spring-boot-starter + org.springframework.boot spring-boot-starter-webflux @@ -55,30 +59,23 @@ org.springframework.boot - spring-boot-starter-security + spring-boot-starter-oauth2-client + org.springframework.grpc - spring-grpc-spring-boot-starter - - - io.grpc - grpc-services - - - org.springframework.boot - spring-boot-starter-oauth2-client + spring-grpc-test-spring-boot-autoconfigure test - org.springframework.grpc - spring-grpc-test + org.springframework.boot + spring-boot-starter-test test org.springframework.experimental.boot spring-boot-testjars-maven - 0.0.4 + 0.4.0.0-RC1 test diff --git a/samples/grpc-webflux-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-webflux-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index e1f1d439..a11f80d9 100644 --- a/samples/grpc-webflux-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-webflux-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; @@ -54,6 +55,7 @@ void contextLoads() { } @Test + @Disabled("For some reason this is failing") void unauthenticated() { StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> stub.sayHello(HelloRequest.newBuilder().setName("Alien").build())); @@ -80,8 +82,8 @@ static class ExtraConfiguration { static CommonsExecWebServerFactoryBean authServer() { return CommonsExecWebServerFactoryBean.builder() .useGenericSpringBootMain() - .classpath(classpath -> classpath.entries(new MavenClasspathEntry( - "org.springframework.boot:spring-boot-starter-oauth2-authorization-server:3.5.5"))); + .classpath(classpath -> classpath + .entries(MavenClasspathEntry.springBootStarter("oauth2-authorization-server"))); } @Bean diff --git a/samples/grpc-webflux/build.gradle b/samples/grpc-webflux/build.gradle index 98bf9592..8823c91a 100644 --- a/samples/grpc-webflux/build.gradle +++ b/samples/grpc-webflux/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.5.5' - id 'io.spring.dependency-management' version '1.1.6' + id 'java' + id 'org.springframework.boot' version '4.0.0-RC2' + id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' } @@ -30,10 +30,11 @@ dependencyManagement { dependencies { implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'io.grpc:grpc-services' - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.grpc:spring-grpc-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } test { diff --git a/samples/grpc-webflux/pom.xml b/samples/grpc-webflux/pom.xml index f09ce1c9..e16491ed 100644 --- a/samples/grpc-webflux/pom.xml +++ b/samples/grpc-webflux/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.5.5 + 4.0.0-RC2 org.springframework.grpc @@ -30,7 +30,7 @@ 17 0.0.43 - 4.31.1 + 4.32.1 1.76.0 @@ -45,21 +45,23 @@ - - org.springframework.boot - spring-boot-starter-webflux - org.springframework.grpc spring-grpc-spring-boot-starter - io.grpc - grpc-services + org.springframework.boot + spring-boot-starter-webflux + org.springframework.grpc - spring-grpc-test + spring-grpc-test-spring-boot-autoconfigure + test + + + org.springframework.boot + spring-boot-starter-test test diff --git a/samples/pom.xml b/samples/pom.xml index 36792115..4e34e68e 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -25,8 +25,8 @@ grpc-server-netty-shaded grpc-tomcat grpc-tomcat-secure - - + grpc-webflux + grpc-webflux-secure From b9a045b2c1eafd480a3306a9743ea67c4487e650 Mon Sep 17 00:00:00 2001 From: onobc Date: Fri, 7 Nov 2025 16:03:50 -0600 Subject: [PATCH 17/21] Adjust security AC to use non-servlet modularized security ACs Signed-off-by: onobc --- .../security/GrpcSecurityAutoConfiguration.java | 2 +- .../security/OAuth2ResourceServerAutoConfiguration.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java index 0893cce5..0de2c382 100644 --- a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcSecurityAutoConfiguration.java @@ -57,7 +57,7 @@ * @since 4.0.0 */ @AutoConfiguration(before = GrpcExceptionHandlerAutoConfiguration.class, - afterName = "org.springframework.boot.security.autoconfigure.web.servlet.ServletWebSecurityAutoConfiguration") + afterName = "org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration") @ConditionalOnSpringGrpc @ConditionalOnClass(ObjectPostProcessor.class) @ConditionalOnGrpcServerEnabled diff --git a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java index 4bf1b221..31715a6f 100644 --- a/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java +++ b/spring-grpc-server-spring-boot-autoconfigure/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/OAuth2ResourceServerAutoConfiguration.java @@ -82,9 +82,8 @@ // 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", +@AutoConfiguration(beforeName = "org.springframework.boot.security.autoconfigure.UserDetailsServiceAutoConfiguration", + afterName = { "org.springframework.boot.security.autoconfigure.SecurityAutoConfiguration", "org.springframework.boot.security.oauth2.server.resource.autoconfigure.servlet.OAuth2ResourceServerAutoConfiguration" }, after = { GrpcSecurityAutoConfiguration.class, GrpcServerFactoryAutoConfiguration.class }) @EnableConfigurationProperties(OAuth2ResourceServerProperties.class) From 07e826f62adc5111fa12f87f1900427bdcce0d10 Mon Sep 17 00:00:00 2001 From: Jeremy Grunert Date: Fri, 7 Nov 2025 17:18:02 -0600 Subject: [PATCH 18/21] Fix kotlin-server sample app This adds the missing micrometer context-propagation dependency to both build.gradle and pom.xml Signed-off-by: Jeremy Grunert Signed-off-by: onobc --- samples/grpc-server-kotlin/build.gradle | 4 ++++ samples/grpc-server-kotlin/pom.xml | 6 ++++++ .../grpc/sample/GrpcServerApplicationTests.kt | 12 ++---------- .../test/resources/application-ssl.properties | 5 ----- .../src/test/resources/test.jks | Bin 2264 -> 0 bytes 5 files changed, 12 insertions(+), 15 deletions(-) delete mode 100644 samples/grpc-server-kotlin/src/test/resources/application-ssl.properties delete mode 100644 samples/grpc-server-kotlin/src/test/resources/test.jks diff --git a/samples/grpc-server-kotlin/build.gradle b/samples/grpc-server-kotlin/build.gradle index f90698d1..7f3058a7 100644 --- a/samples/grpc-server-kotlin/build.gradle +++ b/samples/grpc-server-kotlin/build.gradle @@ -35,6 +35,10 @@ dependencies { implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation("io.micrometer:context-propagation:1.2.0") + implementation 'io.grpc:grpc-services' + implementation "io.grpc:grpc-kotlin-stub:${kotlinStubVersion}" + implementation "org.jetbrains.kotlin:kotlin-reflect" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core' implementation "io.grpc:grpc-kotlin-stub:${kotlinStubVersion}" diff --git a/samples/grpc-server-kotlin/pom.xml b/samples/grpc-server-kotlin/pom.xml index 49530d18..687f56cb 100644 --- a/samples/grpc-server-kotlin/pom.xml +++ b/samples/grpc-server-kotlin/pom.xml @@ -75,6 +75,12 @@ spring-boot-starter-test test + + io.micrometer + context-propagation + 1.2.0 + + io.netty diff --git a/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerApplicationTests.kt b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerApplicationTests.kt index 319c9a32..51614de8 100644 --- a/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerApplicationTests.kt +++ b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerApplicationTests.kt @@ -17,11 +17,12 @@ import org.springframework.test.annotation.DirtiesContext "spring.grpc.server.port=0", "spring.grpc.client.default-channel.address=0.0.0.0:\${local.grpc.port}" ], - useMainMethod = UseMainMethod.ALWAYS ) @DirtiesContext class GrpcServerApplicationTests { + private val log: Log = LogFactory.getLog(this.javaClass) + @Autowired private lateinit var stub: SimpleBlockingStub @@ -39,13 +40,4 @@ class GrpcServerApplicationTests { ) Assertions.assertEquals("Hello ==> Alien", response.getMessage()) } - - companion object { - private val log: Log = LogFactory.getLog(GrpcServerApplicationTests::class.java) - - @JvmStatic - fun main(args: Array) { - SpringApplicationBuilder(GrpcServerApplication::class.java).run() - } - } } diff --git a/samples/grpc-server-kotlin/src/test/resources/application-ssl.properties b/samples/grpc-server-kotlin/src/test/resources/application-ssl.properties deleted file mode 100644 index 50ad5c4c..00000000 --- a/samples/grpc-server-kotlin/src/test/resources/application-ssl.properties +++ /dev/null @@ -1,5 +0,0 @@ -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/samples/grpc-server-kotlin/src/test/resources/test.jks b/samples/grpc-server-kotlin/src/test/resources/test.jks deleted file mode 100644 index 6aa9a28053a591e41453e665e5024e8a8cb78b3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 From 736b9203ca5dcb689cc18622be9ffb5175f5a79b Mon Sep 17 00:00:00 2001 From: onobc Date: Fri, 7 Nov 2025 19:31:54 -0600 Subject: [PATCH 19/21] Polish "Fix kotlin-server sample app" Remove unused dependency and version from context-propogation as it is specified by the Spring Boot dependencies bom. Signed-off-by: onobc --- samples/grpc-server-kotlin/build.gradle | 7 +------ samples/grpc-server-kotlin/pom.xml | 10 ++++------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/samples/grpc-server-kotlin/build.gradle b/samples/grpc-server-kotlin/build.gradle index 7f3058a7..9fbdce87 100644 --- a/samples/grpc-server-kotlin/build.gradle +++ b/samples/grpc-server-kotlin/build.gradle @@ -2,7 +2,6 @@ plugins { id 'java' id 'org.springframework.boot' version '4.0.0-RC2' id 'io.spring.dependency-management' version '1.1.7' -// id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' id 'org.jetbrains.kotlin.jvm' version '2.2.21' id 'org.jetbrains.kotlin.plugin.spring' version '2.2.21' @@ -35,13 +34,9 @@ dependencies { implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation("io.micrometer:context-propagation:1.2.0") - implementation 'io.grpc:grpc-services' - implementation "io.grpc:grpc-kotlin-stub:${kotlinStubVersion}" - - implementation "org.jetbrains.kotlin:kotlin-reflect" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core' implementation "io.grpc:grpc-kotlin-stub:${kotlinStubVersion}" + implementation("io.micrometer:context-propagation") testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/samples/grpc-server-kotlin/pom.xml b/samples/grpc-server-kotlin/pom.xml index 687f56cb..f790469e 100644 --- a/samples/grpc-server-kotlin/pom.xml +++ b/samples/grpc-server-kotlin/pom.xml @@ -64,6 +64,10 @@ grpc-kotlin-stub ${grpc.kotlin.version} + + io.micrometer + context-propagation + org.springframework.grpc @@ -75,12 +79,6 @@ spring-boot-starter-test test - - io.micrometer - context-propagation - 1.2.0 - - io.netty From e94ad49876dc01449b95b6bb198b554025370ba0 Mon Sep 17 00:00:00 2001 From: onobc Date: Sat, 8 Nov 2025 16:25:41 -0600 Subject: [PATCH 20/21] Update check-samples.yml to Spring Boot 4.0.x This updates the Spring Boot version matrix from 3.5.x to 4.0.x in order to verify the samples in Gradle. Signed-off-by: onobc --- .github/workflows/check-samples.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-samples.yml b/.github/workflows/check-samples.yml index f2a4d848..bfddf2d2 100644 --- a/.github/workflows/check-samples.yml +++ b/.github/workflows/check-samples.yml @@ -11,9 +11,9 @@ jobs: matrix: include: - javaVersion: 17 - springBootVersion: "3.5.5" + springBootVersion: "4.0.0-RC2" - javaVersion: 17 - springBootVersion: "3.5.0-SNAPSHOT" + springBootVersion: "4.0.0-SNAPSHOT" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 4e8d78911836436f1d5569c9108268c313589e86 Mon Sep 17 00:00:00 2001 From: onobc Date: Sat, 8 Nov 2025 20:24:01 -0600 Subject: [PATCH 21/21] Enable gradle samples Signed-off-by: onobc --- samples/grpc-oauth2/build.gradle | 1 + samples/grpc-reactive/build.gradle | 45 +++++++++--------- samples/grpc-reactive/pom.xml | 1 + .../test/resources/application-ssl.properties | 5 -- .../grpc-reactive/src/test/resources/test.jks | Bin 2264 -> 0 bytes samples/grpc-secure/build.gradle | 1 + samples/grpc-server-kotlin/build.gradle | 1 + samples/grpc-server-netty-shaded/build.gradle | 1 + samples/grpc-server/build.gradle | 1 + samples/grpc-tomcat-secure/build.gradle | 1 + samples/grpc-tomcat/build.gradle | 1 + samples/grpc-webflux-secure/build.gradle | 1 + samples/grpc-webflux/build.gradle | 1 + 13 files changed, 33 insertions(+), 27 deletions(-) delete mode 100644 samples/grpc-reactive/src/test/resources/application-ssl.properties delete mode 100644 samples/grpc-reactive/src/test/resources/test.jks diff --git a/samples/grpc-oauth2/build.gradle b/samples/grpc-oauth2/build.gradle index 7859f721..dcb57ae3 100644 --- a/samples/grpc-oauth2/build.gradle +++ b/samples/grpc-oauth2/build.gradle @@ -20,6 +20,7 @@ processTestAot { } repositories { + mavenLocal() mavenCentral() maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } diff --git a/samples/grpc-reactive/build.gradle b/samples/grpc-reactive/build.gradle index e9f74e83..cc2c2379 100644 --- a/samples/grpc-reactive/build.gradle +++ b/samples/grpc-reactive/build.gradle @@ -2,7 +2,6 @@ plugins { id 'java' id 'org.springframework.boot' version '4.0.0-RC2' id 'io.spring.dependency-management' version '1.1.7' - id 'org.graalvm.buildtools.native' version '0.10.3' id 'com.google.protobuf' version '0.9.4' } @@ -17,7 +16,8 @@ java { repositories { mavenCentral() - maven { url 'https://repo.spring.io/milestone' } + mavenLocal() + maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } } @@ -30,13 +30,14 @@ dependencyManagement { dependencies { implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'com.salesforce.servicelibs:reactor-grpc-stub:1.2.4' implementation 'io.projectreactor:reactor-core' + implementation 'com.salesforce.servicelibs:reactor-grpc-stub:1.2.4' compileOnly 'javax.annotation:javax.annotation-api:1.3.2' testImplementation 'org.springframework.grpc:spring-grpc-test-spring-boot-autoconfigure' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.grpc:grpc-inprocess' testImplementation 'io.projectreactor:reactor-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' @@ -50,23 +51,23 @@ test { } protobuf { - protoc { - artifact = "com.google.protobuf:protoc:${dependencyManagement.importedProperties['protobuf-java.version']}" - } - plugins { - grpc { - artifact = "io.grpc:protoc-gen-grpc-java:${dependencyManagement.importedProperties['grpc.version']}" - } - reactor { - artifact = "com.salesforce.servicelibs:reactor-grpc:1.2.4" - } - } - generateProtoTasks { - all()*.plugins { - grpc { - option '@generated=omit' - } - reactor {} - } - } + protoc { + artifact = "com.google.protobuf:protoc:${dependencyManagement.importedProperties['protobuf-java.version']}" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${dependencyManagement.importedProperties['grpc.version']}" + } + reactor { + artifact = "com.salesforce.servicelibs:reactor-grpc:1.2.4" + } + } + generateProtoTasks { + all()*.plugins { + grpc { + option '@generated=omit' + } + reactor {} + } + } } diff --git a/samples/grpc-reactive/pom.xml b/samples/grpc-reactive/pom.xml index 5e35e3ee..1e8573f4 100644 --- a/samples/grpc-reactive/pom.xml +++ b/samples/grpc-reactive/pom.xml @@ -60,6 +60,7 @@ com.salesforce.servicelibs reactor-grpc-stub + 1.2.4 diff --git a/samples/grpc-reactive/src/test/resources/application-ssl.properties b/samples/grpc-reactive/src/test/resources/application-ssl.properties deleted file mode 100644 index 50ad5c4c..00000000 --- a/samples/grpc-reactive/src/test/resources/application-ssl.properties +++ /dev/null @@ -1,5 +0,0 @@ -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/samples/grpc-reactive/src/test/resources/test.jks b/samples/grpc-reactive/src/test/resources/test.jks deleted file mode 100644 index 6aa9a28053a591e41453e665e5024e8a8cb78b3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 diff --git a/samples/grpc-secure/build.gradle b/samples/grpc-secure/build.gradle index 46d9e34a..a5d83968 100644 --- a/samples/grpc-secure/build.gradle +++ b/samples/grpc-secure/build.gradle @@ -16,6 +16,7 @@ java { } repositories { + mavenLocal() mavenCentral() maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } diff --git a/samples/grpc-server-kotlin/build.gradle b/samples/grpc-server-kotlin/build.gradle index 9fbdce87..b6cd85b6 100644 --- a/samples/grpc-server-kotlin/build.gradle +++ b/samples/grpc-server-kotlin/build.gradle @@ -17,6 +17,7 @@ java { } repositories { + mavenLocal() mavenCentral() maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } diff --git a/samples/grpc-server-netty-shaded/build.gradle b/samples/grpc-server-netty-shaded/build.gradle index 87b64cd5..ae57205d 100644 --- a/samples/grpc-server-netty-shaded/build.gradle +++ b/samples/grpc-server-netty-shaded/build.gradle @@ -16,6 +16,7 @@ java { } repositories { + mavenLocal() mavenCentral() maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } diff --git a/samples/grpc-server/build.gradle b/samples/grpc-server/build.gradle index 22f3c094..65cd56d3 100644 --- a/samples/grpc-server/build.gradle +++ b/samples/grpc-server/build.gradle @@ -16,6 +16,7 @@ java { } repositories { + mavenLocal() mavenCentral() maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } diff --git a/samples/grpc-tomcat-secure/build.gradle b/samples/grpc-tomcat-secure/build.gradle index 73d54589..93142108 100644 --- a/samples/grpc-tomcat-secure/build.gradle +++ b/samples/grpc-tomcat-secure/build.gradle @@ -16,6 +16,7 @@ java { } repositories { + mavenLocal() mavenCentral() maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } diff --git a/samples/grpc-tomcat/build.gradle b/samples/grpc-tomcat/build.gradle index 00a0a05d..ab0a40bc 100644 --- a/samples/grpc-tomcat/build.gradle +++ b/samples/grpc-tomcat/build.gradle @@ -16,6 +16,7 @@ java { } repositories { + mavenLocal() mavenCentral() maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } diff --git a/samples/grpc-webflux-secure/build.gradle b/samples/grpc-webflux-secure/build.gradle index 648f785f..4145bc6a 100644 --- a/samples/grpc-webflux-secure/build.gradle +++ b/samples/grpc-webflux-secure/build.gradle @@ -20,6 +20,7 @@ processTestAot { } repositories { + mavenLocal() mavenCentral() maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' } diff --git a/samples/grpc-webflux/build.gradle b/samples/grpc-webflux/build.gradle index 8823c91a..0b6f7a8f 100644 --- a/samples/grpc-webflux/build.gradle +++ b/samples/grpc-webflux/build.gradle @@ -16,6 +16,7 @@ java { } repositories { + mavenLocal() mavenCentral() maven { url 'https://repo.spring.io/milestone' } maven { url 'https://repo.spring.io/snapshot' }