diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java
index d6070017d25c..9e3a28955a36 100644
--- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java
+++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java
@@ -58,6 +58,7 @@ void documentConfigurationProperties() throws IOException {
Snippets snippets = new Snippets(this.configurationPropertyMetadata);
snippets.add("application-properties.core", "Core Properties", this::corePrefixes);
snippets.add("application-properties.cache", "Cache Properties", this::cachePrefixes);
+ snippets.add("application-properties.grpc", "gRPC Properties", this::grpcPrefixes);
snippets.add("application-properties.mail", "Mail Properties", this::mailPrefixes);
snippets.add("application-properties.json", "JSON Properties", this::jsonPrefixes);
snippets.add("application-properties.data", "Data Properties", this::dataPrefixes);
@@ -159,6 +160,10 @@ private void dataMigrationPrefixes(Config prefix) {
prefix.accept("spring.sql.init");
}
+ private void grpcPrefixes(Config prefix) {
+ prefix.accept("spring.grpc");
+ }
+
private void integrationPrefixes(Config prefix) {
prefix.accept("spring.activemq");
prefix.accept("spring.artemis");
diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml
index eac6829276e7..085130a1c007 100644
--- a/config/checkstyle/checkstyle-suppressions.xml
+++ b/config/checkstyle/checkstyle-suppressions.xml
@@ -50,6 +50,7 @@
+
diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle
index 5bf08fc75605..c205696ab8b7 100644
--- a/documentation/spring-boot-docs/build.gradle
+++ b/documentation/spring-boot-docs/build.gradle
@@ -102,6 +102,8 @@ dependencies {
implementation(project(path: ":module:spring-boot-data-redis-test"))
implementation(project(path: ":module:spring-boot-devtools"))
implementation(project(path: ":module:spring-boot-graphql-test"))
+ implementation(project(path: ":module:spring-boot-grpc-client"))
+ implementation(project(path: ":module:spring-boot-grpc-server"))
implementation(project(path: ":module:spring-boot-health"))
implementation(project(path: ":module:spring-boot-hibernate"))
implementation(project(path: ":module:spring-boot-http-converter"))
@@ -195,6 +197,7 @@ dependencies {
implementation("org.springframework.data:spring-data-r2dbc")
implementation("org.springframework.graphql:spring-graphql")
implementation("org.springframework.graphql:spring-graphql-test")
+ implementation("org.springframework.grpc:spring-grpc-core")
implementation("org.springframework.kafka:spring-kafka")
implementation("org.springframework.kafka:spring-kafka-test")
implementation("org.springframework.pulsar:spring-pulsar")
diff --git a/module/spring-boot-grpc-client/build.gradle b/module/spring-boot-grpc-client/build.gradle
new file mode 100644
index 000000000000..1f00daa289fb
--- /dev/null
+++ b/module/spring-boot-grpc-client/build.gradle
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the License);
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id "java-library"
+ id "org.springframework.boot.auto-configuration"
+ id "org.springframework.boot.configuration-properties"
+ id "org.springframework.boot.deployed"
+ id "org.springframework.boot.optional-dependencies"
+}
+
+description = "Spring Boot gRPC Client"
+
+
+dependencies {
+ api(project(":core:spring-boot"))
+ api("org.springframework.grpc:spring-grpc-core")
+
+ compileOnly("com.fasterxml.jackson.core:jackson-annotations")
+
+ optional(project(":core:spring-boot-autoconfigure"))
+ optional(project(":module:spring-boot-actuator"))
+ optional(project(":module:spring-boot-actuator-autoconfigure"))
+ optional(project(":module:spring-boot-health"))
+ optional(project(":module:spring-boot-micrometer-observation"))
+ optional(project(":module:spring-boot-security"))
+ optional(project(":module:spring-boot-security-oauth2-client"))
+ optional(project(":module:spring-boot-security-oauth2-resource-server"))
+ optional("io.grpc:grpc-servlet-jakarta")
+ optional("io.grpc:grpc-stub")
+ optional("io.grpc:grpc-netty")
+ optional("io.grpc:grpc-netty-shaded")
+ optional("io.grpc:grpc-inprocess")
+ optional("io.grpc:grpc-kotlin-stub") {
+ exclude group: "javax.annotation", module: "javax.annotation-api"
+ }
+ optional("io.micrometer:micrometer-core")
+ optional("io.netty:netty-transport-native-epoll")
+ optional("io.projectreactor:reactor-core")
+ optional("jakarta.servlet:jakarta.servlet-api")
+ optional("org.springframework:spring-web")
+ optional("org.springframework.security:spring-security-config")
+ optional("org.springframework.security:spring-security-oauth2-client")
+ optional("org.springframework.security:spring-security-oauth2-resource-server")
+ optional("org.springframework.security:spring-security-oauth2-jose")
+ optional("org.springframework.security:spring-security-web")
+
+ testCompileOnly("com.fasterxml.jackson.core:jackson-annotations")
+
+ testImplementation(project(":core:spring-boot-test"))
+ testImplementation(project(":test-support:spring-boot-test-support"))
+
+ testRuntimeOnly("ch.qos.logback:logback-classic")
+}
diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizers.java
new file mode 100644
index 000000000000..fe0eadd59146
--- /dev/null
+++ b/module/spring-boot-grpc-client/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 io.grpc.ManagedChannelBuilder;
+
+import org.springframework.boot.util.LambdaSafe;
+import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
+
+/**
+ * Invokes the available {@link GrpcChannelBuilderCustomizer} instances for a given
+ * {@link ManagedChannelBuilder}.
+ *
+ * @author Chris Bono
+ */
+class ChannelBuilderCustomizers {
+
+ private final List> customizers;
+
+ ChannelBuilderCustomizers(List extends GrpcChannelBuilderCustomizer>> customizers) {
+ this.customizers = (customizers != null) ? new ArrayList<>(customizers) : Collections.emptyList();
+ }
+
+ /**
+ * Customize the specified {@link ManagedChannelBuilder}. Locates all
+ * {@link GrpcChannelBuilderCustomizer} beans able to handle the specified instance
+ * and invoke {@link GrpcChannelBuilderCustomizer#customize} on them.
+ * @param the type of channel builder
+ * @param authority the target authority of the channel
+ * @param channelBuilder the builder to customize
+ * @return the customized builder
+ */
+ @SuppressWarnings("unchecked")
+ > T customize(String authority, T channelBuilder) {
+ LambdaSafe.callbacks(GrpcChannelBuilderCustomizer.class, this.customizers, channelBuilder)
+ .withLogger(ChannelBuilderCustomizers.class)
+ .invoke((customizer) -> customizer.customize(authority, channelBuilder));
+ return channelBuilder;
+ }
+
+}
diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientPropertiesChannelBuilderCustomizer.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientPropertiesChannelBuilderCustomizer.java
new file mode 100644
index 000000000000..280eaeed79db
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ClientPropertiesChannelBuilderCustomizer.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.client.autoconfigure;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import io.grpc.ManagedChannelBuilder;
+
+import org.springframework.boot.context.properties.PropertyMapper;
+import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.ChannelConfig;
+import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
+import org.springframework.grpc.client.interceptor.DefaultDeadlineSetupClientInterceptor;
+import org.springframework.util.unit.DataSize;
+
+/**
+ * A {@link GrpcChannelBuilderCustomizer} that maps {@link GrpcClientProperties client
+ * properties} to a channel builder.
+ *
+ * @param the type of the builder
+ * @author David Syer
+ * @author Chris Bono
+ */
+class ClientPropertiesChannelBuilderCustomizer>
+ implements GrpcChannelBuilderCustomizer {
+
+ private final GrpcClientProperties properties;
+
+ ClientPropertiesChannelBuilderCustomizer(GrpcClientProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ public void customize(String authority, T builder) {
+ ChannelConfig channel = this.properties.getChannel(authority);
+ PropertyMapper mapper = PropertyMapper.get();
+ mapper.from(channel.getUserAgent()).to(builder::userAgent);
+ if (!authority.startsWith("unix:") && !authority.startsWith("in-process:")) {
+ mapper.from(channel.getDefaultLoadBalancingPolicy()).to(builder::defaultLoadBalancingPolicy);
+ }
+ mapper.from(channel.getMaxInboundMessageSize()).asInt(DataSize::toBytes).to(builder::maxInboundMessageSize);
+ mapper.from(channel.getMaxInboundMetadataSize()).asInt(DataSize::toBytes).to(builder::maxInboundMetadataSize);
+ mapper.from(channel.getKeepAliveTime()).to(durationProperty(builder::keepAliveTime));
+ mapper.from(channel.getKeepAliveTimeout()).to(durationProperty(builder::keepAliveTimeout));
+ mapper.from(channel.getIdleTimeout()).to(durationProperty(builder::idleTimeout));
+ mapper.from(channel.isKeepAliveWithoutCalls()).to(builder::keepAliveWithoutCalls);
+ Map defaultServiceConfig = new HashMap<>(channel.getServiceConfig());
+ if (channel.getHealth().isEnabled()) {
+ String serviceNameToCheck = (channel.getHealth().getServiceName() != null)
+ ? channel.getHealth().getServiceName() : "";
+ defaultServiceConfig.put("healthCheckConfig", Map.of("serviceName", serviceNameToCheck));
+ }
+ if (!defaultServiceConfig.isEmpty()) {
+ builder.defaultServiceConfig(defaultServiceConfig);
+ }
+ if (channel.getDefaultDeadline() != null && channel.getDefaultDeadline().toMillis() > 0L) {
+ builder.intercept(new DefaultDeadlineSetupClientInterceptor(channel.getDefaultDeadline()));
+ }
+ }
+
+ Consumer durationProperty(BiConsumer setter) {
+ return (duration) -> setter.accept(duration.toNanos(), TimeUnit.NANOSECONDS);
+ }
+
+}
diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java
new file mode 100644
index 000000000000..563c388e7d4f
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.client.autoconfigure;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
+import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Primary;
+import org.springframework.grpc.client.CompositeGrpcChannelFactory;
+import org.springframework.grpc.client.GrpcChannelFactory;
+
+/**
+ * {@link EnableAutoConfiguration Auto-configuration} for a
+ * {@link CompositeGrpcChannelFactory}.
+ *
+ * @author Chris Bono
+ * @since 4.0.0
+ */
+@AutoConfiguration
+@ConditionalOnGrpcClientEnabled
+@Conditional(CompositeChannelFactoryAutoConfiguration.MultipleNonPrimaryChannelFactoriesCondition.class)
+public final class CompositeChannelFactoryAutoConfiguration {
+
+ @Bean
+ @Primary
+ CompositeGrpcChannelFactory compositeChannelFactory(ObjectProvider channelFactoriesProvider) {
+ return new CompositeGrpcChannelFactory(channelFactoriesProvider.orderedStream().toList());
+ }
+
+ static class MultipleNonPrimaryChannelFactoriesCondition extends NoneNestedConditions {
+
+ MultipleNonPrimaryChannelFactoriesCondition() {
+ super(ConfigurationPhase.REGISTER_BEAN);
+ }
+
+ @ConditionalOnMissingBean(GrpcChannelFactory.class)
+ static class NoChannelFactoryCondition {
+
+ }
+
+ @ConditionalOnSingleCandidate(GrpcChannelFactory.class)
+ static class SingleInjectableChannelFactoryCondition {
+
+ }
+
+ }
+
+}
diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientEnabled.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientEnabled.java
new file mode 100644
index 000000000000..40658fff4c9a
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/ConditionalOnGrpcClientEnabled.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.client.autoconfigure;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import io.grpc.stub.AbstractStub;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Conditional;
+
+/**
+ * {@link Conditional @Conditional} that only matches when the {@code io.grpc:grpc-stub}
+ * module is in the classpath and the {@code spring.grpc.client.enabled} property is not
+ * explicitly set to {@code false}.
+ *
+ * @author Freeman Freeman
+ * @author Chris Bono
+ * @since 4.0.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@ConditionalOnClass(AbstractStub.class)
+@ConditionalOnProperty(prefix = "spring.grpc.client", name = "enabled", matchIfMissing = true)
+public @interface ConditionalOnGrpcClientEnabled {
+
+}
diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrations.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrations.java
new file mode 100644
index 000000000000..26e4ef51b8e2
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrations.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.client.autoconfigure;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.ChannelConfig;
+import org.springframework.core.env.Environment;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.grpc.client.AbstractGrpcClientRegistrar;
+import org.springframework.grpc.client.GrpcClientFactory.GrpcClientRegistrationSpec;
+
+/**
+ * Default implementation of {@link AbstractGrpcClientRegistrar} that registers client
+ * bean definitions for the default configured gRPC channel if the
+ * {@code spring.grpc.client.default-channel} property is set.
+ *
+ * @author Dave Syer
+ * @author Chris Bono
+ */
+class DefaultGrpcClientRegistrations extends AbstractGrpcClientRegistrar {
+
+ private final Environment environment;
+
+ private final BeanFactory beanFactory;
+
+ DefaultGrpcClientRegistrations(Environment environment, BeanFactory beanFactory) {
+ this.environment = environment;
+ this.beanFactory = beanFactory;
+ }
+
+ @Override
+ protected GrpcClientRegistrationSpec[] collect(AnnotationMetadata meta) {
+ Binder binder = Binder.get(this.environment);
+ boolean hasDefaultChannel = binder.bind("spring.grpc.client.default-channel", ChannelConfig.class).isBound();
+ if (hasDefaultChannel) {
+ List packages = new ArrayList<>();
+ if (AutoConfigurationPackages.has(this.beanFactory)) {
+ packages.addAll(AutoConfigurationPackages.get(this.beanFactory));
+ }
+ GrpcClientProperties clientProperties = binder.bind("spring.grpc.client", GrpcClientProperties.class)
+ .orElseGet(GrpcClientProperties::new);
+ return new GrpcClientRegistrationSpec[] { GrpcClientRegistrationSpec.of("default")
+ .factory(clientProperties.getDefaultStubFactory())
+ .packages(packages.toArray(new String[0])) };
+ }
+ return new GrpcClientRegistrationSpec[0];
+ }
+
+}
diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryConfigurations.java
new file mode 100644
index 000000000000..2aeadd57a830
--- /dev/null
+++ b/module/spring-boot-grpc-client/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 io.grpc.Channel;
+import io.grpc.inprocess.InProcessChannelBuilder;
+import io.grpc.netty.NettyChannelBuilder;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.grpc.client.ChannelCredentialsProvider;
+import org.springframework.grpc.client.ClientInterceptorFilter;
+import org.springframework.grpc.client.ClientInterceptorsConfigurer;
+import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
+import org.springframework.grpc.client.GrpcChannelFactory;
+import org.springframework.grpc.client.InProcessGrpcChannelFactory;
+import org.springframework.grpc.client.NettyGrpcChannelFactory;
+import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory;
+
+/**
+ * Configurations for {@link GrpcChannelFactory gRPC channel factories}.
+ *
+ * @author Chris Bono
+ */
+class GrpcChannelFactoryConfigurations {
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass({ io.grpc.netty.shaded.io.netty.channel.Channel.class,
+ io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class })
+ @ConditionalOnMissingBean(value = GrpcChannelFactory.class, ignored = InProcessGrpcChannelFactory.class)
+ @ConditionalOnProperty(prefix = "spring.grpc.client.inprocess.", name = "exclusive", havingValue = "false",
+ matchIfMissing = true)
+ @EnableConfigurationProperties(GrpcClientProperties.class)
+ static class ShadedNettyChannelFactoryConfiguration {
+
+ @Bean
+ ShadedNettyGrpcChannelFactory shadedNettyGrpcChannelFactory(GrpcClientProperties properties,
+ ChannelBuilderCustomizers channelBuilderCustomizers,
+ ClientInterceptorsConfigurer interceptorsConfigurer,
+ ObjectProvider channelFactoryCustomizers,
+ ChannelCredentialsProvider credentials) {
+ List> builderCustomizers = List
+ .of(channelBuilderCustomizers::customize);
+ 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/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcChannelFactoryCustomizer.java
new file mode 100644
index 000000000000..7e640a3e7b83
--- /dev/null
+++ b/module/spring-boot-grpc-client/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/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java
new file mode 100644
index 000000000000..95895fef06aa
--- /dev/null
+++ b/module/spring-boot-grpc-client/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 io.grpc.CompressorRegistry;
+import io.grpc.DecompressorRegistry;
+import io.grpc.ManagedChannelBuilder;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.grpc.client.autoconfigure.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;
+
+@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/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java
new file mode 100644
index 000000000000..a98a9e9af435
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfiguration.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.client.autoconfigure;
+
+import io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor;
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.grpc.client.GlobalClientInterceptor;
+
+@AutoConfiguration(
+ afterName = "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration")
+@ConditionalOnGrpcClientEnabled
+@ConditionalOnClass({ ObservationRegistry.class, ObservationGrpcClientInterceptor.class })
+@ConditionalOnBean(ObservationRegistry.class)
+@ConditionalOnProperty(name = "spring.grpc.client.observation.enabled", havingValue = "true", matchIfMissing = true)
+
+public final class GrpcClientObservationAutoConfiguration {
+
+ @Bean
+ @GlobalClientInterceptor
+ @ConditionalOnMissingBean
+ ObservationGrpcClientInterceptor observationGrpcClientInterceptor(ObservationRegistry observationRegistry) {
+ return new ObservationGrpcClientInterceptor(observationRegistry);
+ }
+
+}
diff --git a/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java
new file mode 100644
index 000000000000..48bfc399266e
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientProperties.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.client.autoconfigure;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import io.grpc.ManagedChannel;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.convert.DurationUnit;
+import org.springframework.context.EnvironmentAware;
+import org.springframework.core.env.Environment;
+import org.springframework.core.env.StandardEnvironment;
+import org.springframework.grpc.client.BlockingStubFactory;
+import org.springframework.grpc.client.NegotiationType;
+import org.springframework.grpc.client.StubFactory;
+import org.springframework.grpc.client.VirtualTargets;
+import org.springframework.util.unit.DataSize;
+
+@ConfigurationProperties(prefix = "spring.grpc.client")
+public class GrpcClientProperties implements EnvironmentAware, VirtualTargets {
+
+ /**
+ * 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 extends StubFactory>> 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 extends StubFactory>> getDefaultStubFactory() {
+ return this.defaultStubFactory;
+ }
+
+ public void setDefaultStubFactory(Class extends StubFactory>> 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;
+ }
+
+ 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/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfiguration.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfiguration.java
new file mode 100644
index 000000000000..77ded0c30100
--- /dev/null
+++ b/module/spring-boot-grpc-client/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 io.grpc.Codec;
+import io.grpc.Compressor;
+import io.grpc.CompressorRegistry;
+import io.grpc.Decompressor;
+import io.grpc.DecompressorRegistry;
+import io.grpc.ManagedChannelBuilder;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * The configuration that contains all codec related beans for clients.
+ *
+ * @author Andrei Lisa
+ */
+@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/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java
new file mode 100644
index 000000000000..0e8cc2b3ca73
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/NamedChannelCredentialsProvider.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.client.autoconfigure;
+
+import javax.net.ssl.TrustManagerFactory;
+
+import io.grpc.ChannelCredentials;
+import io.grpc.InsecureChannelCredentials;
+import io.grpc.TlsChannelCredentials;
+
+import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.ChannelConfig;
+import org.springframework.boot.ssl.SslBundle;
+import org.springframework.boot.ssl.SslBundles;
+import org.springframework.grpc.client.ChannelCredentialsProvider;
+import org.springframework.grpc.client.NegotiationType;
+import org.springframework.grpc.internal.InsecureTrustManagerFactory;
+import org.springframework.util.Assert;
+
+/**
+ * Provides channel credentials using channel configuration and {@link SslBundles}.
+ *
+ * @author David Syer
+ */
+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/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java
new file mode 100644
index 000000000000..efa3388c6505
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/main/java/org/springframework/boot/grpc/client/autoconfigure/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Auto-configuration for gRPC client.
+ */
+@NullMarked
+package org.springframework.boot.grpc.client.autoconfigure;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 000000000000..b2914ae3fdc3
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,29 @@
+{
+ "groups": [],
+ "properties": [
+ {
+ "name": "spring.grpc.client.enabled",
+ "type": "java.lang.Boolean",
+ "description": "Whether to enable client autoconfiguration.",
+ "defaultValue": true
+ },
+ {
+ "name": "spring.grpc.client.inprocess.enabled",
+ "type": "java.lang.Boolean",
+ "description": "Whether to configure the in-process channel factory.",
+ "defaultValue": true
+ },
+ {
+ "name": "spring.grpc.client.inprocess.exclusive",
+ "type": "java.lang.Boolean",
+ "description": "Whether the inprocess channel factory should be the only channel factory available. When the value is true, no other channel factory will be configured.",
+ "defaultValue": true
+ },
+ {
+ "name": "spring.grpc.client.observation.enabled",
+ "type": "java.lang.Boolean",
+ "description": "Whether to enable Observations on the client.",
+ "defaultValue": true
+ }
+ ]
+}
diff --git a/module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 000000000000..7d3249ab92fc
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,3 @@
+org.springframework.boot.grpc.client.autoconfigure.CompositeChannelFactoryAutoConfiguration
+org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration
+org.springframework.boot.grpc.client.autoconfigure.GrpcClientObservationAutoConfiguration
diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizersTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizersTests.java
new file mode 100644
index 000000000000..dd089aefff49
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/ChannelBuilderCustomizersTests.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.client.autoconfigure;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.netty.NettyChannelBuilder;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link ChannelBuilderCustomizers}.
+ *
+ * @author Chris Bono
+ */
+class ChannelBuilderCustomizersTests {
+
+ private static final String DEFAULT_TARGET = "localhost";
+
+ @Test
+ void customizeWithNullCustomizersShouldDoNothing() {
+ ManagedChannelBuilder> channelBuilder = mock(ManagedChannelBuilder.class);
+ new ChannelBuilderCustomizers(null).customize(DEFAULT_TARGET, channelBuilder);
+ then(channelBuilder).shouldHaveNoInteractions();
+ }
+
+ @Test
+ void customizeSimpleChannelBuilder() {
+ ChannelBuilderCustomizers customizers = new ChannelBuilderCustomizers(
+ List.of(new SimpleChannelBuilderCustomizer()));
+ NettyChannelBuilder channelBuilder = mock(NettyChannelBuilder.class);
+ customizers.customize(DEFAULT_TARGET, channelBuilder);
+ then(channelBuilder).should().flowControlWindow(100);
+ }
+
+ @Test
+ void customizeShouldCheckGeneric() {
+ List> list = new ArrayList<>();
+ list.add(new TestCustomizer<>());
+ list.add(new TestNettyChannelBuilderCustomizer());
+ list.add(new TestShadedNettyChannelBuilderCustomizer());
+ ChannelBuilderCustomizers customizers = new ChannelBuilderCustomizers(list);
+
+ customizers.customize(DEFAULT_TARGET, mock(ManagedChannelBuilder.class));
+ assertThat(list.get(0).getCount()).isOne();
+ assertThat(list.get(1).getCount()).isZero();
+ assertThat(list.get(2).getCount()).isZero();
+
+ customizers.customize(DEFAULT_TARGET, mock(NettyChannelBuilder.class));
+ assertThat(list.get(0).getCount()).isEqualTo(2);
+ assertThat(list.get(1).getCount()).isOne();
+ assertThat(list.get(2).getCount()).isZero();
+
+ customizers.customize(DEFAULT_TARGET, mock(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class));
+ assertThat(list.get(0).getCount()).isEqualTo(3);
+ assertThat(list.get(1).getCount()).isOne();
+ assertThat(list.get(2).getCount()).isOne();
+ }
+
+ static class SimpleChannelBuilderCustomizer implements GrpcChannelBuilderCustomizer {
+
+ @Override
+ public void customize(String target, NettyChannelBuilder channelBuilder) {
+ channelBuilder.flowControlWindow(100);
+ }
+
+ }
+
+ /**
+ * Test customizer that will match any {@link GrpcChannelBuilderCustomizer}.
+ */
+ static class TestCustomizer> implements GrpcChannelBuilderCustomizer {
+
+ private int count;
+
+ @Override
+ public void customize(String target, T channelBuilder) {
+ this.count++;
+ }
+
+ int getCount() {
+ return this.count;
+ }
+
+ }
+
+ /**
+ * Test customizer that will match only {@link NettyChannelBuilder}.
+ */
+ static class TestNettyChannelBuilderCustomizer extends TestCustomizer {
+
+ }
+
+ /**
+ * Test customizer that will match only
+ * {@link io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder}.
+ */
+ static class TestShadedNettyChannelBuilderCustomizer
+ extends TestCustomizer {
+
+ }
+
+}
diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java
new file mode 100644
index 000000000000..5540405d15bc
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/CompositeChannelFactoryAutoConfigurationTests.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.client.autoconfigure;
+
+import io.grpc.inprocess.InProcessChannelBuilder;
+import io.grpc.netty.NettyChannelBuilder;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
+import org.springframework.boot.test.context.FilteredClassLoader;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.grpc.client.CompositeGrpcChannelFactory;
+import org.springframework.grpc.client.GrpcChannelFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link CompositeChannelFactoryAutoConfiguration}.
+ *
+ * @author Chris Bono
+ */
+@SuppressWarnings({ "unchecked", "rawtypes" })
+class CompositeChannelFactoryAutoConfigurationTests {
+
+ private ApplicationContextRunner contextRunnerWithoutChannelFactories() {
+ return new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class, SslAutoConfiguration.class,
+ CompositeChannelFactoryAutoConfiguration.class))
+ .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class,
+ NettyChannelBuilder.class, InProcessChannelBuilder.class));
+ }
+
+ @Test
+ void whenNoChannelFactoriesDoesNotAutoconfigureComposite() {
+ this.contextRunnerWithoutChannelFactories()
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcChannelFactory.class));
+ }
+
+ @Test
+ void whenSingleChannelFactoryDoesNotAutoconfigureComposite() {
+ GrpcChannelFactory channelFactory1 = mock();
+ this.contextRunnerWithoutChannelFactories()
+ .withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1)
+ .run((context) -> assertThat(context).hasSingleBean(GrpcChannelFactory.class)
+ .getBean(GrpcChannelFactory.class)
+ .isNotInstanceOf(CompositeGrpcChannelFactory.class)
+ .isSameAs(channelFactory1));
+ }
+
+ @Test
+ void whenMultipleChannelFactoriesWithPrimaryDoesNotAutoconfigureComposite() {
+ GrpcChannelFactory channelFactory1 = mock();
+ GrpcChannelFactory channelFactory2 = mock();
+ this.contextRunnerWithoutChannelFactories()
+ .withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1)
+ .withBean("channelFactory2", GrpcChannelFactory.class, () -> channelFactory2, (bd) -> bd.setPrimary(true))
+ .run((context) -> {
+ assertThat(context).getBeans(GrpcChannelFactory.class)
+ .containsOnlyKeys("channelFactory1", "channelFactory2");
+ assertThat(context).getBean(GrpcChannelFactory.class)
+ .isNotInstanceOf(CompositeGrpcChannelFactory.class)
+ .isSameAs(channelFactory2);
+ });
+ }
+
+ @Test
+ void whenMultipleChannelFactoriesDoesAutoconfigureComposite() {
+ GrpcChannelFactory channelFactory1 = mock();
+ GrpcChannelFactory channelFactory2 = mock();
+ this.contextRunnerWithoutChannelFactories()
+ .withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1)
+ .withBean("channelFactory2", GrpcChannelFactory.class, () -> channelFactory2)
+ .run((context) -> {
+ assertThat(context).getBeans(GrpcChannelFactory.class)
+ .containsOnlyKeys("channelFactory1", "channelFactory2", "compositeChannelFactory");
+ assertThat(context).getBean(GrpcChannelFactory.class).isInstanceOf(CompositeGrpcChannelFactory.class);
+ });
+ }
+
+ @Test
+ void compositeAutoconfiguredAsExpected() {
+ this.contextRunnerWithoutChannelFactories()
+ .withUserConfiguration(MultipleFactoriesTestConfig.class)
+ .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
+ .isInstanceOf(CompositeGrpcChannelFactory.class)
+ .extracting("channelFactories")
+ .asInstanceOf(InstanceOfAssertFactories.list(GrpcChannelFactory.class))
+ .containsExactly(MultipleFactoriesTestConfig.CHANNEL_FACTORY_BAR,
+ MultipleFactoriesTestConfig.CHANNEL_FACTORY_ZAA,
+ MultipleFactoriesTestConfig.CHANNEL_FACTORY_FOO));
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class MultipleFactoriesTestConfig {
+
+ static GrpcChannelFactory CHANNEL_FACTORY_FOO = mock();
+ static GrpcChannelFactory CHANNEL_FACTORY_BAR = mock();
+ static GrpcChannelFactory CHANNEL_FACTORY_ZAA = mock();
+
+ @Bean
+ @Order(3)
+ GrpcChannelFactory channelFactoryFoo() {
+ return CHANNEL_FACTORY_FOO;
+ }
+
+ @Bean
+ @Order(1)
+ GrpcChannelFactory channelFactoryBar() {
+ return CHANNEL_FACTORY_BAR;
+ }
+
+ @Bean
+ @Order(2)
+ GrpcChannelFactory channelFactoryZaa() {
+ return CHANNEL_FACTORY_ZAA;
+ }
+
+ }
+
+}
diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrationsTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrationsTests.java
new file mode 100644
index 000000000000..365ba6189811
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/DefaultGrpcClientRegistrationsTests.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.client.autoconfigure;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.grpc.client.BlockingStubFactory;
+import org.springframework.grpc.client.CoroutineStubFactory;
+import org.springframework.grpc.client.GrpcClientFactory.GrpcClientRegistrationSpec;
+import org.springframework.grpc.client.ReactorStubFactory;
+import org.springframework.grpc.client.StubFactory;
+import org.springframework.mock.env.MockEnvironment;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for the {@link DefaultGrpcClientRegistrations}.
+ *
+ * @author CheolHwan Ihn
+ * @author Chris Bono
+ */
+class DefaultGrpcClientRegistrationsTests {
+
+ private static void assertThatPropertiesResultInExpectedStubFactory(Map properties,
+ Class extends StubFactory>> expectedStubFactoryClass) {
+ MockEnvironment environment = new MockEnvironment();
+ environment.getPropertySources()
+ .addFirst(new MapPropertySource("defaultGrpcClientRegistrationsTests", properties));
+ try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
+ AutoConfigurationPackages.register(context, "org.springframework.boot.grpc.client.autoconfigure");
+ DefaultGrpcClientRegistrations registrations = new DefaultGrpcClientRegistrations(environment,
+ context.getBeanFactory());
+
+ GrpcClientRegistrationSpec[] specs = registrations.collect(null);
+
+ assertThat(specs).hasSize(1);
+ assertThat(specs[0].factory()).isEqualTo(expectedStubFactoryClass);
+ }
+ }
+
+ @Test
+ void withReactorStubFactory() {
+ Map properties = new HashMap<>();
+ properties.put("spring.grpc.client.default-stub-factory", ReactorStubFactory.class.getName());
+ properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090");
+ assertThatPropertiesResultInExpectedStubFactory(properties, ReactorStubFactory.class);
+ }
+
+ @Test
+ void withDefaultStubFactory() {
+ Map properties = new HashMap<>();
+ properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090");
+ assertThatPropertiesResultInExpectedStubFactory(properties, BlockingStubFactory.class);
+ }
+
+ @Test
+ void withCoroutineStubFactory() {
+ Map properties = new HashMap<>();
+ properties.put("spring.grpc.client.default-stub-factory", CoroutineStubFactory.class.getName());
+ properties.put("spring.grpc.client.default-channel.address", "static://localhost:9090");
+ assertThatPropertiesResultInExpectedStubFactory(properties, CoroutineStubFactory.class);
+ }
+
+}
diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java
new file mode 100644
index 000000000000..f109af556774
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientAutoConfigurationTests.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.client.autoconfigure;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import io.grpc.Codec;
+import io.grpc.CompressorRegistry;
+import io.grpc.DecompressorRegistry;
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.inprocess.InProcessChannelBuilder;
+import io.grpc.kotlin.AbstractCoroutineStub;
+import io.grpc.netty.NettyChannelBuilder;
+import io.grpc.stub.AbstractStub;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
+import org.springframework.boot.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 static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+
+/**
+ * Tests for {@link GrpcClientAutoConfiguration}.
+ *
+ * @author Chris Bono
+ */
+@SuppressWarnings({ "unchecked", "rawtypes" })
+class GrpcClientAutoConfigurationTests {
+
+ private ApplicationContextRunner contextRunner() {
+ return new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class, SslAutoConfiguration.class));
+ }
+
+ private ApplicationContextRunner contextRunnerWithoutInProcessChannelFactory() {
+ return this.contextRunner().withPropertyValues("spring.grpc.client.inprocess.enabled=false");
+ }
+
+ @Test
+ void whenGrpcStubNotOnClasspathThenAutoConfigurationIsSkipped() {
+ this.contextRunner()
+ .withClassLoader(new FilteredClassLoader(AbstractStub.class))
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientAutoConfiguration.class));
+ }
+
+ @Test
+ void whenGrpcKotlinIsNotOnClasspathThenAutoConfigurationIsSkipped() {
+ this.contextRunner()
+ .withClassLoader(new FilteredClassLoader(AbstractCoroutineStub.class))
+ .run((context) -> assertThat(context)
+ .doesNotHaveBean(GrpcClientAutoConfiguration.GrpcClientCoroutineStubConfiguration.class));
+ }
+
+ @Test
+ void whenClientEnabledPropertySetFalseThenAutoConfigurationIsSkipped() {
+ this.contextRunner()
+ .withPropertyValues("spring.grpc.client.enabled=false")
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientAutoConfiguration.class));
+ }
+
+ @Test
+ void whenClientEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() {
+ this.contextRunner().run((context) -> assertThat(context).hasSingleBean(GrpcClientAutoConfiguration.class));
+ }
+
+ @Test
+ void whenClientEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() {
+ this.contextRunner()
+ .withPropertyValues("spring.grpc.client.enabled=true")
+ .run((context) -> assertThat(context).hasSingleBean(GrpcClientAutoConfiguration.class));
+ }
+
+ @Test
+ void whenHasUserDefinedCredentialsProviderDoesNotAutoConfigureBean() {
+ ChannelCredentialsProvider customCredentialsProvider = mock(ChannelCredentialsProvider.class);
+ this.contextRunner()
+ .withBean("customCredentialsProvider", ChannelCredentialsProvider.class, () -> customCredentialsProvider)
+ .run((context) -> assertThat(context).getBean(ChannelCredentialsProvider.class)
+ .isSameAs(customCredentialsProvider));
+ }
+
+ @Test
+ void credentialsProviderAutoConfiguredAsExpected() {
+ this.contextRunner()
+ .run((context) -> assertThat(context).getBean(NamedChannelCredentialsProvider.class)
+ .hasFieldOrPropertyWithValue("properties", context.getBean(GrpcClientProperties.class))
+ .extracting("bundles")
+ .isInstanceOf(SslBundles.class));
+ }
+
+ @Test
+ void clientPropertiesAutoConfiguredResolvesPlaceholders() {
+ this.contextRunner()
+ .withPropertyValues("spring.grpc.client.channels.c1.address=my-server-${channelName}:8888",
+ "channelName=foo")
+ .run((context) -> assertThat(context).getBean(GrpcClientProperties.class)
+ .satisfies((properties) -> assertThat(properties.getTarget("c1")).isEqualTo("my-server-foo:8888")));
+ }
+
+ @Test
+ void clientPropertiesChannelCustomizerAutoConfiguredWithHealthAsExpected() {
+ this.contextRunner()
+ .withPropertyValues("spring.grpc.client.channels.test.health.enabled=true",
+ "spring.grpc.client.channels.test.health.service-name=my-service")
+ .run((context) -> {
+ assertThat(context).getBean("clientPropertiesChannelCustomizer", GrpcChannelBuilderCustomizer.class)
+ .isNotNull();
+ var customizer = context.getBean("clientPropertiesChannelCustomizer",
+ GrpcChannelBuilderCustomizer.class);
+ ManagedChannelBuilder> builder = Mockito.mock();
+ customizer.customize("test", builder);
+ Map healthCheckConfig = Map.of("healthCheckConfig", Map.of("serviceName", "my-service"));
+ then(builder).should().defaultServiceConfig(healthCheckConfig);
+ });
+ }
+
+ @Test
+ void clientPropertiesChannelCustomizerAutoConfiguredWithoutHealthAsExpected() {
+ this.contextRunner().run((context) -> {
+ assertThat(context).getBean("clientPropertiesChannelCustomizer", GrpcChannelBuilderCustomizer.class)
+ .isNotNull();
+ var customizer = context.getBean("clientPropertiesChannelCustomizer", GrpcChannelBuilderCustomizer.class);
+ ManagedChannelBuilder> builder = Mockito.mock();
+ customizer.customize("test", builder);
+ then(builder).should(never()).defaultServiceConfig(anyMap());
+ });
+ }
+
+ @Test
+ void 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/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java
new file mode 100644
index 000000000000..47158ea5fda0
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientObservationAutoConfigurationTests.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.client.autoconfigure;
+
+import io.grpc.stub.AbstractStub;
+import io.micrometer.core.instrument.binder.grpc.ObservationGrpcClientInterceptor;
+import io.micrometer.observation.ObservationRegistry;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.FilteredClassLoader;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.grpc.client.GlobalClientInterceptor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for the {@link GrpcClientObservationAutoConfiguration}.
+ */
+class GrpcClientObservationAutoConfigurationTests {
+
+ private final ApplicationContextRunner baseContextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(GrpcClientObservationAutoConfiguration.class));
+
+ private ApplicationContextRunner validContextRunner() {
+ return new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(GrpcClientObservationAutoConfiguration.class))
+ .withBean("observationRegistry", ObservationRegistry.class, Mockito::mock);
+ }
+
+ @Test
+ void whenObservationRegistryNotOnClasspathAutoConfigSkipped() {
+ this.validContextRunner()
+ .withClassLoader(new FilteredClassLoader(ObservationRegistry.class))
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class));
+ }
+
+ @Test
+ void whenObservationGrpcClientInterceptorNotOnClasspathAutoConfigSkipped() {
+ this.validContextRunner()
+ .withClassLoader(new FilteredClassLoader(ObservationGrpcClientInterceptor.class))
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class));
+ }
+
+ @Test
+ void whenObservationRegistryNotProvidedThenAutoConfigSkipped() {
+ this.baseContextRunner
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class));
+ }
+
+ @Test
+ void whenObservationPropertyEnabledThenAutoConfigNotSkipped() {
+ this.validContextRunner()
+ .withPropertyValues("spring.grpc.client.observation.enabled=true")
+ .run((context) -> assertThat(context).hasSingleBean(GrpcClientObservationAutoConfiguration.class));
+ }
+
+ @Test
+ void whenObservationPropertyDisabledThenAutoConfigIsSkipped() {
+ this.validContextRunner()
+ .withPropertyValues("spring.grpc.client.observation.enabled=false")
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class));
+ }
+
+ @Test
+ void whenClientEnabledPropertyNotSetThenAutoConfigNotSkipped() {
+ this.validContextRunner()
+ .run((context) -> assertThat(context).hasSingleBean(GrpcClientObservationAutoConfiguration.class));
+ }
+
+ @Test
+ void whenClientEnabledPropertySetTrueThenAutoConfigIsNotSkipped() {
+ this.validContextRunner()
+ .withPropertyValues("spring.grpc.client.enabled=true")
+ .run((context) -> assertThat(context).hasSingleBean(GrpcClientObservationAutoConfiguration.class));
+ }
+
+ @Test
+ void whenClientEnabledPropertySetFalseThenAutoConfigIsSkipped() {
+ this.validContextRunner()
+ .withPropertyValues("spring.grpc.client.enabled=false")
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class));
+ }
+
+ @Test
+ void whenGrpcStubNotOnClasspathThenAutoConfigIsSkipped() {
+ this.validContextRunner()
+ .withClassLoader(new FilteredClassLoader(AbstractStub.class))
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcClientObservationAutoConfiguration.class));
+ }
+
+ @Test
+ void whenAllConditionsAreMetThenInterceptorConfiguredAsExpected() {
+ this.validContextRunner().run((context) -> {
+ assertThat(context).hasSingleBean(ObservationGrpcClientInterceptor.class);
+ assertThat(context.getBeansWithAnnotation(GlobalClientInterceptor.class)).hasEntrySatisfying(
+ "observationGrpcClientInterceptor",
+ (bean) -> assertThat(bean.getClass().isAssignableFrom(ObservationGrpcClientInterceptor.class))
+ .isTrue());
+ });
+ }
+
+}
diff --git a/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcClientPropertiesTests.java
new file mode 100644
index 000000000000..71eddc8e8ddb
--- /dev/null
+++ b/module/spring-boot-grpc-client/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 java.time.Duration;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
+import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.ChannelConfig;
+import org.springframework.grpc.client.NegotiationType;
+import org.springframework.mock.env.MockEnvironment;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.util.unit.DataSize;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+
+/**
+ * Tests for {@link GrpcClientProperties}.
+ *
+ * @author Chris Bono
+ */
+class GrpcClientPropertiesTests {
+
+ private GrpcClientProperties bindProperties(Map map) {
+ return new Binder(new MapConfigurationPropertySource(map))
+ .bind("spring.grpc.client", GrpcClientProperties.class)
+ .get();
+ }
+
+ private GrpcClientProperties newProperties(ChannelConfig defaultChannel, Map channels) {
+ var properties = new GrpcClientProperties();
+ ReflectionTestUtils.setField(properties, "defaultChannel", defaultChannel);
+ ReflectionTestUtils.setField(properties, "channels", channels);
+ return properties;
+ }
+
+ @Nested
+ class BindPropertiesAPI {
+
+ @Test
+ void defaultChannelWithDefaultValues() {
+ this.withDefaultValues("default-channel", GrpcClientProperties::getDefaultChannel);
+ }
+
+ @Test
+ void specificChannelWithDefaultValues() {
+ this.withDefaultValues("channels.c1", (p) -> p.getChannel("c1"));
+ }
+
+ private void withDefaultValues(String channelName,
+ Function channelFromProperties) {
+ Map map = new HashMap<>();
+ // we have to at least bind one property or bind() fails
+ map.put("spring.grpc.client.%s.enable-keep-alive".formatted(channelName), "false");
+ GrpcClientProperties properties = bindProperties(map);
+ var channel = channelFromProperties.apply(properties);
+ assertThat(channel.getAddress()).isEqualTo("static://localhost:9090");
+ assertThat(channel.getDefaultLoadBalancingPolicy()).isEqualTo("round_robin");
+ assertThat(channel.getHealth().isEnabled()).isFalse();
+ assertThat(channel.getHealth().getServiceName()).isNull();
+ assertThat(channel.getNegotiationType()).isEqualTo(NegotiationType.PLAINTEXT);
+ assertThat(channel.isEnableKeepAlive()).isFalse();
+ assertThat(channel.getIdleTimeout()).isEqualTo(Duration.ofSeconds(20));
+ assertThat(channel.getKeepAliveTime()).isEqualTo(Duration.ofMinutes(5));
+ assertThat(channel.getKeepAliveTimeout()).isEqualTo(Duration.ofSeconds(20));
+ assertThat(channel.isEnableKeepAlive()).isFalse();
+ assertThat(channel.isKeepAliveWithoutCalls()).isFalse();
+ assertThat(channel.getMaxInboundMessageSize()).isEqualTo(DataSize.ofBytes(4194304));
+ assertThat(channel.getMaxInboundMetadataSize()).isEqualTo(DataSize.ofBytes(8192));
+ assertThat(channel.getUserAgent()).isNull();
+ assertThat(channel.isSecure()).isTrue();
+ assertThat(channel.getSsl().isEnabled()).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/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.java
new file mode 100644
index 000000000000..a36f0b79cb86
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/test/java/org/springframework/boot/grpc/client/autoconfigure/GrpcCodecConfigurationTests.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.client.autoconfigure;
+
+import io.grpc.Codec;
+import io.grpc.Compressor;
+import io.grpc.CompressorRegistry;
+import io.grpc.Decompressor;
+import io.grpc.DecompressorRegistry;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.FilteredClassLoader;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link GrpcCodecConfiguration}.
+ *
+ * @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/module/spring-boot-grpc-client/src/test/resources/logback-test.xml b/module/spring-boot-grpc-client/src/test/resources/logback-test.xml
new file mode 100644
index 000000000000..b8a41480d7d6
--- /dev/null
+++ b/module/spring-boot-grpc-client/src/test/resources/logback-test.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/module/spring-boot-grpc-server/build.gradle b/module/spring-boot-grpc-server/build.gradle
new file mode 100644
index 000000000000..88dce1015131
--- /dev/null
+++ b/module/spring-boot-grpc-server/build.gradle
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the License);
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id "java-library"
+ id "org.springframework.boot.auto-configuration"
+ id "org.springframework.boot.configuration-properties"
+ id "org.springframework.boot.deployed"
+ id "org.springframework.boot.optional-dependencies"
+}
+
+description = "Spring Boot gRPC Server"
+
+
+dependencies {
+ api(project(":core:spring-boot"))
+ api("org.springframework.grpc:spring-grpc-core")
+
+ compileOnly("com.fasterxml.jackson.core:jackson-annotations")
+
+ optional(project(":core:spring-boot-autoconfigure"))
+ optional(project(":module:spring-boot-actuator"))
+ optional(project(":module:spring-boot-actuator-autoconfigure"))
+ optional(project(":module:spring-boot-health"))
+ optional(project(":module:spring-boot-micrometer-observation"))
+ optional(project(":module:spring-boot-security"))
+ optional(project(":module:spring-boot-security-oauth2-client"))
+ optional(project(":module:spring-boot-security-oauth2-resource-server"))
+ optional("io.grpc:grpc-servlet-jakarta")
+ optional("io.grpc:grpc-services")
+ optional("io.grpc:grpc-netty")
+ optional("io.grpc:grpc-netty-shaded")
+ optional("io.grpc:grpc-inprocess")
+ optional("io.grpc:grpc-kotlin-stub") {
+ exclude group: "javax.annotation", module: "javax.annotation-api"
+ }
+ optional("io.micrometer:micrometer-core")
+ optional("io.netty:netty-transport-native-epoll")
+ optional("io.projectreactor:reactor-core")
+ optional("jakarta.servlet:jakarta.servlet-api")
+ optional("org.springframework:spring-web")
+ optional("org.springframework.security:spring-security-config")
+ optional("org.springframework.security:spring-security-oauth2-client")
+ optional("org.springframework.security:spring-security-oauth2-resource-server")
+ optional("org.springframework.security:spring-security-oauth2-jose")
+ optional("org.springframework.security:spring-security-web")
+
+ testCompileOnly("com.fasterxml.jackson.core:jackson-annotations")
+
+ testImplementation(project(":core:spring-boot-test"))
+ testImplementation(project(":test-support:spring-boot-test-support"))
+
+ testRuntimeOnly("ch.qos.logback:logback-classic")
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcNativeServer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcNativeServer.java
new file mode 100644
index 000000000000..51be07d1fc4c
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcNativeServer.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.Conditional;
+
+/**
+ * {@link Conditional @Conditional} that determines if the gRPC server implementation
+ * should be one of the native varieties (e.g. Netty, Shaded Netty) - i.e. not the servlet
+ * container.
+ *
+ * @author Chris Bono
+ * @author Dave Syer
+ * @since 4.0.0
+ * @see OnGrpcNativeServerCondition
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Conditional(OnGrpcNativeServerCondition.class)
+public @interface ConditionalOnGrpcNativeServer {
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServerEnabled.java
new file mode 100644
index 000000000000..9d47c9f0bdb7
--- /dev/null
+++ b/module/spring-boot-grpc-server/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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServletServer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServletServer.java
new file mode 100644
index 000000000000..02ac7ec16ebe
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnGrpcServletServer.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import io.grpc.servlet.jakarta.GrpcServlet;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.context.annotation.Conditional;
+
+/**
+ * {@link Conditional @Conditional} that determines if the Servlet container should be
+ * used to run the gRPC server. The condition matches only when the app is a servlet web
+ * application and the {@code io.grpc.servlet.jakarta.GrpcServlet} class is on the
+ * classpath and the {@code spring.grpc.server.servlet.enabled} property is not explicitly
+ * set to {@code false}.
+ *
+ * @author Chris Bono
+ * @author Dave Syer
+ * @since 4.0.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
+@ConditionalOnClass(GrpcServlet.class)
+@ConditionalOnProperty(prefix = "spring.grpc.server", name = "servlet.enabled", havingValue = "true",
+ matchIfMissing = true)
+public @interface ConditionalOnGrpcServletServer {
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnSpringGrpc.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnSpringGrpc.java
new file mode 100644
index 000000000000..0e48757af410
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ConditionalOnSpringGrpc.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import io.grpc.BindableService;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.grpc.server.GrpcServerFactory;
+
+/**
+ * {@link Conditional @Conditional} that only matches when Spring gRPC is on the classpath
+ * (i.e. {@link BindableService} and {@link GrpcServerFactory} are on the classpath).
+ *
+ * @author Freeman Freeman
+ * @author Chris Bono
+ * @since 4.0.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@ConditionalOnClass({ BindableService.class, GrpcServerFactory.class })
+public @interface ConditionalOnSpringGrpc {
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java
new file mode 100644
index 000000000000..c395653cc805
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/DefaultServerFactoryPropertyMapper.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import io.grpc.ServerBuilder;
+
+import org.springframework.boot.context.properties.PropertyMapper;
+import org.springframework.grpc.server.DefaultGrpcServerFactory;
+import org.springframework.util.unit.DataSize;
+
+/**
+ * Helper class used to map {@link GrpcServerProperties} to
+ * {@link DefaultGrpcServerFactory}.
+ *
+ * @param the type of server builder
+ * @author Chris Bono
+ */
+class DefaultServerFactoryPropertyMapper> {
+
+ 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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcCodecConfiguration.java
new file mode 100644
index 000000000000..77ef45b044b3
--- /dev/null
+++ b/module/spring-boot-grpc-server/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 io.grpc.Codec;
+import io.grpc.Compressor;
+import io.grpc.CompressorRegistry;
+import io.grpc.Decompressor;
+import io.grpc.DecompressorRegistry;
+import io.grpc.ServerBuilder;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * The configuration that contains all codec related beans for 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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java
new file mode 100644
index 000000000000..697f54af5c98
--- /dev/null
+++ b/module/spring-boot-grpc-server/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 io.grpc.BindableService;
+import io.grpc.CompressorRegistry;
+import io.grpc.DecompressorRegistry;
+import io.grpc.ServerBuilder;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.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;
+
+/**
+ * {@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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerExecutorProvider.java
new file mode 100644
index 000000000000..ac5f1d8c849e
--- /dev/null
+++ b/module/spring-boot-grpc-server/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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java
new file mode 100644
index 000000000000..76cafd55d012
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure;
+
+import java.util.List;
+
+import io.grpc.BindableService;
+import io.grpc.servlet.jakarta.GrpcServlet;
+import io.grpc.servlet.jakarta.ServletServerBuilder;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureOrder;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.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;
+
+/**
+ * {@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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java
new file mode 100644
index 000000000000..3065640510ca
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryConfigurations.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure;
+
+import java.util.List;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.TrustManagerFactory;
+
+import io.grpc.inprocess.InProcessServerBuilder;
+import io.grpc.netty.NettyServerBuilder;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.ssl.SslBundle;
+import org.springframework.boot.ssl.SslBundles;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.grpc.server.GrpcServerFactory;
+import org.springframework.grpc.server.InProcessGrpcServerFactory;
+import org.springframework.grpc.server.NettyGrpcServerFactory;
+import org.springframework.grpc.server.ServerBuilderCustomizer;
+import org.springframework.grpc.server.ServerServiceDefinitionFilter;
+import org.springframework.grpc.server.ShadedNettyGrpcServerFactory;
+import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
+import org.springframework.grpc.server.service.GrpcServiceConfigurer;
+import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
+import org.springframework.grpc.server.service.ServerInterceptorFilter;
+import org.springframework.util.Assert;
+
+/**
+ * Configurations for {@link GrpcServerFactory gRPC server factories}.
+ *
+ * @author Chris Bono
+ */
+class GrpcServerFactoryConfigurations {
+
+ private static void applyServerFactoryCustomizers(ObjectProvider customizers,
+ GrpcServerFactory factory) {
+ customizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)
+ @ConditionalOnMissingBean(value = GrpcServerFactory.class, ignored = InProcessGrpcServerFactory.class)
+ @ConditionalOnProperty(prefix = "spring.grpc.server.inprocess.", name = "exclusive", havingValue = "false",
+ matchIfMissing = true)
+ @EnableConfigurationProperties(GrpcServerProperties.class)
+ static class ShadedNettyServerFactoryConfiguration {
+
+ @Bean
+ ShadedNettyGrpcServerFactory shadedNettyGrpcServerFactory(GrpcServerProperties properties,
+ GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer,
+ ServerBuilderCustomizers serverBuilderCustomizers, SslBundles bundles,
+ ObjectProvider customizers) {
+ ShadedNettyServerFactoryPropertyMapper mapper = new ShadedNettyServerFactoryPropertyMapper(properties);
+ List> builderCustomizers = List
+ .of(mapper::customizeServerBuilder, serverBuilderCustomizers::customize);
+ KeyManagerFactory keyManager = null;
+ TrustManagerFactory trustManager = null;
+ if (properties.getSsl().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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerFactoryCustomizer.java
new file mode 100644
index 000000000000..1965c1cc10e9
--- /dev/null
+++ b/module/spring-boot-grpc-server/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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerObservationAutoConfiguration.java
new file mode 100644
index 000000000000..b0739440f393
--- /dev/null
+++ b/module/spring-boot-grpc-server/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 io.micrometer.core.instrument.binder.grpc.ObservationGrpcServerInterceptor;
+import io.micrometer.core.instrument.kotlin.ObservationCoroutineContextServerInterceptor;
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.grpc.server.GlobalServerInterceptor;
+
+/**
+ * {@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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java
new file mode 100644
index 000000000000..bfa5573fc485
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerProperties.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.grpc.TlsServerCredentials.ClientAuth;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.convert.DataSizeUnit;
+import org.springframework.boot.convert.DurationUnit;
+import org.springframework.grpc.internal.GrpcUtils;
+import org.springframework.util.unit.DataSize;
+import org.springframework.util.unit.DataUnit;
+
+@ConfigurationProperties(prefix = "spring.grpc.server")
+public class GrpcServerProperties {
+
+ /**
+ * Server should listen to any IPv4 and IPv6 address.
+ */
+ public static final String ANY_IP_ADDRESS = "*";
+
+ /**
+ * 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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/GrpcServerReflectionAutoConfiguration.java
new file mode 100644
index 000000000000..fffd3e690638
--- /dev/null
+++ b/module/spring-boot-grpc-server/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 io.grpc.BindableService;
+import io.grpc.protobuf.services.ProtoReflectionServiceV1;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * {@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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessServerFactoryPropertyMapper.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessServerFactoryPropertyMapper.java
new file mode 100644
index 000000000000..00736791cf24
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/InProcessServerFactoryPropertyMapper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure;
+
+import io.grpc.inprocess.InProcessServerBuilder;
+
+import org.springframework.boot.context.properties.PropertyMapper;
+
+/**
+ * Helper class used to map {@link GrpcServerProperties} to
+ * {@link InProcessServerBuilder}.
+ *
+ * @author Chris Bono
+ */
+class InProcessServerFactoryPropertyMapper extends DefaultServerFactoryPropertyMapper {
+
+ InProcessServerFactoryPropertyMapper(GrpcServerProperties properties) {
+ super(properties);
+ }
+
+ @Override
+ void customizeServerBuilder(InProcessServerBuilder serverBuilder) {
+ PropertyMapper mapper = PropertyMapper.get();
+ customizeInboundLimits(serverBuilder, mapper);
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyServerFactoryPropertyMapper.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyServerFactoryPropertyMapper.java
new file mode 100644
index 000000000000..53124a910ade
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/NettyServerFactoryPropertyMapper.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure;
+
+import io.grpc.netty.NettyServerBuilder;
+
+import org.springframework.grpc.server.NettyGrpcServerFactory;
+
+/**
+ * Helper class used to map {@link GrpcServerProperties} to
+ * {@link NettyGrpcServerFactory}.
+ *
+ * @author Chris Bono
+ */
+class NettyServerFactoryPropertyMapper extends DefaultServerFactoryPropertyMapper {
+
+ NettyServerFactoryPropertyMapper(GrpcServerProperties properties) {
+ super(properties);
+ }
+
+ @Override
+ void customizeServerBuilder(NettyServerBuilder nettyServerBuilder) {
+ super.customizeServerBuilder(nettyServerBuilder);
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerCondition.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerCondition.java
new file mode 100644
index 000000000000..913ed012ba5d
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnEnabledGrpcServerCondition.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure;
+
+import java.util.Map;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.boot.autoconfigure.condition.ConditionMessage;
+import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
+import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link SpringBootCondition} to check whether gRPC server/service is enabled.
+ *
+ * @author Chris Bono
+ * @see ConditionalOnGrpcServerEnabled
+ */
+class OnEnabledGrpcServerCondition extends SpringBootCondition {
+
+ private static final String SERVER_PROPERTY = "spring.grpc.server.enabled";
+
+ private static final String SERVICE_PROPERTY = "spring.grpc.server.%s.enabled";
+
+ @Override
+ public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
+ Boolean serverEnabled = context.getEnvironment().getProperty(SERVER_PROPERTY, Boolean.class);
+ if (serverEnabled != null && !serverEnabled) {
+ return new ConditionOutcome(serverEnabled,
+ ConditionMessage.forCondition(ConditionalOnGrpcServerEnabled.class)
+ .because(SERVER_PROPERTY + " is " + serverEnabled));
+ }
+ String serviceName = getServiceName(metadata);
+ if (StringUtils.hasLength(serviceName)) {
+ Boolean serviceEnabled = context.getEnvironment()
+ .getProperty(SERVICE_PROPERTY.formatted(serviceName), Boolean.class);
+ if (serviceEnabled != null) {
+ return new ConditionOutcome(serviceEnabled,
+ ConditionMessage.forCondition(ConditionalOnGrpcServerEnabled.class)
+ .because(SERVICE_PROPERTY.formatted(serviceName) + " is " + serviceEnabled));
+ }
+ }
+ return ConditionOutcome.match(ConditionMessage.forCondition(ConditionalOnGrpcServerEnabled.class)
+ .because("server and service are enabled by default"));
+ }
+
+ private static @Nullable String getServiceName(AnnotatedTypeMetadata metadata) {
+ Map attributes = metadata
+ .getAnnotationAttributes(ConditionalOnGrpcServerEnabled.class.getName());
+ if (attributes == null) {
+ return null;
+ }
+ return (String) attributes.get("value");
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnGrpcNativeServerCondition.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnGrpcNativeServerCondition.java
new file mode 100644
index 000000000000..7de51852c75e
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/OnGrpcNativeServerCondition.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure;
+
+import io.grpc.servlet.jakarta.GrpcServlet;
+
+import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.context.annotation.Conditional;
+
+/**
+ * {@link Conditional @Conditional} that determines if the gRPC server implementation
+ * should be one of the native varieties (e.g. Netty, Shaded Netty) - i.e. not the servlet
+ * container. The condition matches when the app is not a Reactive web application OR the
+ * {@code io.grpc.servlet.jakarta.GrpcServlet} class is not on the classpath OR the app is
+ * a servlet web application and the {@code io.grpc.servlet.jakarta.GrpcServlet} is on the
+ * classpath BUT the {@code spring.grpc.server.servlet.enabled} property is explicitly set
+ * to {@code false}.
+ *
+ * @author Dave Syer
+ * @author Chris Bono
+ */
+class OnGrpcNativeServerCondition extends AnyNestedCondition {
+
+ OnGrpcNativeServerCondition() {
+ super(ConfigurationPhase.PARSE_CONFIGURATION);
+ }
+
+ @ConditionalOnNotWebApplication
+ static class OnNonWebApplication {
+
+ }
+
+ @ConditionalOnMissingClass("io.grpc.servlet.jakarta.GrpcServlet")
+ static class OnGrpcServletClass {
+
+ }
+
+ @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
+ @ConditionalOnClass(GrpcServlet.class)
+ @ConditionalOnProperty(prefix = "spring.grpc.server", name = "servlet.enabled", havingValue = "false")
+ static class OnExplicitlyDisabledServlet {
+
+ }
+
+ @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
+ static class OnExplicitlyDisabledWebflux {
+
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServerBuilderCustomizers.java
new file mode 100644
index 000000000000..7af249e3fae7
--- /dev/null
+++ b/module/spring-boot-grpc-server/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 io.grpc.ServerBuilder;
+
+import org.springframework.boot.util.LambdaSafe;
+import org.springframework.grpc.server.ServerBuilderCustomizer;
+
+/**
+ * Invokes the available {@link ServerBuilderCustomizer} instances in the context for a
+ * given {@link ServerBuilder}.
+ *
+ * @author Chris Bono
+ */
+class ServerBuilderCustomizers {
+
+ private final List> customizers;
+
+ ServerBuilderCustomizers(List extends ServerBuilderCustomizer>> customizers) {
+ this.customizers = (customizers != null) ? new ArrayList<>(customizers) : Collections.emptyList();
+ }
+
+ /**
+ * Customize the specified {@link ServerBuilder}. Locates all
+ * {@link ServerBuilderCustomizer} beans able to handle the specified instance and
+ * invoke {@link ServerBuilderCustomizer#customize} on them.
+ * @param the type of server builder
+ * @param serverBuilder the builder to customize
+ * @return the customized builder
+ */
+ @SuppressWarnings("unchecked")
+ > T customize(T serverBuilder) {
+ LambdaSafe.callbacks(ServerBuilderCustomizer.class, this.customizers, serverBuilder)
+ .withLogger(ServerBuilderCustomizers.class)
+ .invoke((customizer) -> customizer.customize(serverBuilder));
+ return serverBuilder;
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ServletEnvironmentPostProcessor.java
new file mode 100644
index 000000000000..c3a345780c92
--- /dev/null
+++ b/module/spring-boot-grpc-server/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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyServerFactoryPropertyMapper.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyServerFactoryPropertyMapper.java
new file mode 100644
index 000000000000..3b9c0fbab3f5
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/ShadedNettyServerFactoryPropertyMapper.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure;
+
+import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
+
+import org.springframework.grpc.server.ShadedNettyGrpcServerFactory;
+
+/**
+ * Helper class used to map {@link GrpcServerProperties} to
+ * {@link ShadedNettyGrpcServerFactory}.
+ *
+ * @author Chris Bono
+ */
+class ShadedNettyServerFactoryPropertyMapper extends DefaultServerFactoryPropertyMapper {
+
+ ShadedNettyServerFactoryPropertyMapper(GrpcServerProperties properties) {
+ super(properties);
+ }
+
+ @Override
+ void customizeServerBuilder(NettyServerBuilder nettyServerBuilder) {
+ super.customizeServerBuilder(nettyServerBuilder);
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/GrpcExceptionHandlerAutoConfiguration.java
new file mode 100644
index 000000000000..8cb0ed97c774
--- /dev/null
+++ b/module/spring-boot-grpc-server/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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/package-info.java
new file mode 100644
index 000000000000..9e2fb4a769ac
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/exception/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Auto-configuration for gRPC server exception handling.
+ */
+@NullMarked
+package org.springframework.boot.grpc.server.autoconfigure.exception;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapter.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapter.java
new file mode 100644
index 000000000000..ae0df28c28e1
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapter.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure.health;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import io.grpc.health.v1.HealthCheckResponse.ServingStatus;
+import io.grpc.protobuf.services.HealthStatusManager;
+
+import org.springframework.boot.actuate.health.HealthEndpoint;
+import org.springframework.boot.actuate.health.StatusAggregator;
+import org.springframework.boot.health.contributor.HealthIndicator;
+import org.springframework.boot.health.contributor.Status;
+import org.springframework.core.log.LogAccessor;
+import org.springframework.util.Assert;
+
+/**
+ * Adapts {@link HealthIndicator Actuator health indicators} into gRPC health checks by
+ * periodically invoking {@link HealthEndpoint health endpoints} and updating the health
+ * status in gRPC {@link HealthStatusManager}.
+ *
+ * @author Chris Bono
+ * @since 4.0.0
+ */
+public class ActuatorHealthAdapter {
+
+ private static final String INVALID_INDICATOR_MSG = "Unable to determine health for '%s' - check that your configured health-indicator-paths point to available indicators";
+
+ private final LogAccessor logger = new LogAccessor(getClass());
+
+ private final HealthStatusManager healthStatusManager;
+
+ private final HealthEndpoint healthEndpoint;
+
+ private final StatusAggregator statusAggregator;
+
+ private final boolean updateOverallHealth;
+
+ private final List healthIndicatorPaths;
+
+ protected ActuatorHealthAdapter(HealthStatusManager healthStatusManager, HealthEndpoint healthEndpoint,
+ StatusAggregator statusAggregator, boolean updateOverallHealth, List healthIndicatorPaths) {
+ this.healthStatusManager = healthStatusManager;
+ this.healthEndpoint = healthEndpoint;
+ this.statusAggregator = statusAggregator;
+ this.updateOverallHealth = updateOverallHealth;
+ Assert.notEmpty(healthIndicatorPaths, () -> "at least one health indicator path is required");
+ this.healthIndicatorPaths = healthIndicatorPaths;
+ }
+
+ protected void updateHealthStatus() {
+ var individualStatuses = this.updateIndicatorsHealthStatus();
+ if (this.updateOverallHealth) {
+ this.updateOverallHealthStatus(individualStatuses);
+ }
+ }
+
+ protected Set updateIndicatorsHealthStatus() {
+ Set statuses = new HashSet<>();
+ this.healthIndicatorPaths.forEach((healthIndicatorPath) -> {
+ var healthComponent = this.healthEndpoint.healthForPath(healthIndicatorPath.split("/"));
+ if (healthComponent == null) {
+ this.logger.warn(() -> INVALID_INDICATOR_MSG.formatted(healthIndicatorPath));
+ }
+ else {
+ this.logger.trace(() -> "Actuator returned '%s' for indicator '%s'".formatted(healthComponent,
+ healthIndicatorPath));
+ var actuatorStatus = healthComponent.getStatus();
+ var grpcStatus = toServingStatus(actuatorStatus.getCode());
+ this.healthStatusManager.setStatus(healthIndicatorPath, grpcStatus);
+ this.logger.trace(() -> "Updated gRPC health status to '%s' for service '%s'".formatted(grpcStatus,
+ healthIndicatorPath));
+ statuses.add(actuatorStatus);
+ }
+ });
+ return statuses;
+ }
+
+ protected void updateOverallHealthStatus(Set individualStatuses) {
+ var overallActuatorStatus = this.statusAggregator.getAggregateStatus(individualStatuses);
+ var overallGrpcStatus = toServingStatus(overallActuatorStatus.getCode());
+ this.logger.trace(() -> "Actuator aggregate status '%s' for overall health".formatted(overallActuatorStatus));
+ this.healthStatusManager.setStatus("", overallGrpcStatus);
+ this.logger.trace(() -> "Updated overall gRPC health status to '%s'".formatted(overallGrpcStatus));
+ }
+
+ protected ServingStatus toServingStatus(String actuatorHealthStatusCode) {
+ return switch (actuatorHealthStatusCode) {
+ case "UP" -> ServingStatus.SERVING;
+ case "DOWN" -> ServingStatus.NOT_SERVING;
+ case "OUT_OF_SERVICE" -> ServingStatus.NOT_SERVING;
+ case "UNKNOWN" -> ServingStatus.UNKNOWN;
+ default -> ServingStatus.UNKNOWN;
+ };
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvoker.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvoker.java
new file mode 100644
index 000000000000..b1780a911c1e
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/ActuatorHealthAdapterInvoker.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure.health;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder;
+import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
+
+/**
+ * Periodically invokes the {@link ActuatorHealthAdapter} in the background.
+ *
+ * @author Chris Bono
+ */
+class ActuatorHealthAdapterInvoker implements InitializingBean, DisposableBean {
+
+ private final ActuatorHealthAdapter healthAdapter;
+
+ private final SimpleAsyncTaskScheduler taskScheduler;
+
+ private final Duration updateInitialDelay;
+
+ private final Duration updateFixedRate;
+
+ ActuatorHealthAdapterInvoker(ActuatorHealthAdapter healthAdapter, SimpleAsyncTaskSchedulerBuilder schedulerBuilder,
+ Duration updateInitialDelay, Duration updateFixedRate) {
+ this.healthAdapter = healthAdapter;
+ this.taskScheduler = schedulerBuilder.threadNamePrefix("healthAdapter-").build();
+ this.updateInitialDelay = updateInitialDelay;
+ this.updateFixedRate = updateFixedRate;
+ }
+
+ @Override
+ public void afterPropertiesSet() {
+ this.taskScheduler.scheduleAtFixedRate(this::updateHealthStatus, Instant.now().plus(this.updateInitialDelay),
+ this.updateFixedRate);
+ }
+
+ @Override
+ public void destroy() {
+ this.taskScheduler.close();
+ }
+
+ void updateHealthStatus() {
+ this.healthAdapter.updateHealthStatus();
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/GrpcServerHealthAutoConfiguration.java
new file mode 100644
index 000000000000..c94454e6aed0
--- /dev/null
+++ b/module/spring-boot-grpc-server/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 io.grpc.BindableService;
+import io.grpc.protobuf.services.HealthStatusManager;
+
+import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
+import org.springframework.boot.actuate.health.HealthEndpoint;
+import org.springframework.boot.actuate.health.StatusAggregator;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionMessage;
+import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.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.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;
+
+/**
+ * {@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/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java
new file mode 100644
index 000000000000..d7ef5d4d129f
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/health/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Auto-configuration for gRPC server health adapter.
+ */
+@NullMarked
+package org.springframework.boot.grpc.server.autoconfigure.health;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java
new file mode 100644
index 000000000000..5cd06d1019bb
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Auto-configuration for gRPC server.
+ */
+
+@NullMarked
+package org.springframework.boot.grpc.server.autoconfigure;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java
new file mode 100644
index 000000000000..13358ea45623
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcDisableCsrfHttpConfigurer.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure.security;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
+import org.springframework.security.web.csrf.CsrfFilter;
+import org.springframework.security.web.util.matcher.AndRequestMatcher;
+import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
+
+/**
+ * A custom {@link AbstractHttpConfigurer} that disables CSRF protection for gRPC
+ * requests.
+ *
+ * This configurer checks the application context to determine if CSRF protection should
+ * be disabled for gRPC requests based on the property
+ * {@code spring.grpc.server.security.csrf.enabled}. By default, CSRF protection is
+ * disabled unless explicitly enabled in the application properties.
+ *
+ *
+ * @author Dave Syer
+ * @since 4.0.0
+ * @see AbstractHttpConfigurer
+ * @see HttpSecurity
+ */
+public class GrpcDisableCsrfHttpConfigurer extends AbstractHttpConfigurer {
+
+ @Override
+ public void init(HttpSecurity http) throws Exception {
+ ApplicationContext context = http.getSharedObject(ApplicationContext.class);
+ if (context != null && context.getBeanNamesForType(GrpcServiceDiscoverer.class).length == 1
+ && isServletEnabledAndCsrfDisabled(context) && isCsrfConfigurerPresent(http)) {
+ http.csrf(this::disable);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private boolean isCsrfConfigurerPresent(HttpSecurity http) {
+ return http.getConfigurer(CsrfConfigurer.class) != null;
+ }
+
+ private void disable(CsrfConfigurer csrf) {
+ csrf.requireCsrfProtectionMatcher(new AndRequestMatcher(CsrfFilter.DEFAULT_CSRF_MATCHER,
+ new NegatedRequestMatcher(GrpcServletRequest.all())));
+ }
+
+ private boolean isServletEnabledAndCsrfDisabled(ApplicationContext context) {
+ return context.getEnvironment().getProperty("spring.grpc.server.servlet.enabled", Boolean.class, true)
+ && !context.getEnvironment()
+ .getProperty("spring.grpc.server.security.csrf.enabled", Boolean.class, false);
+ }
+
+}
diff --git a/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequest.java b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequest.java
new file mode 100644
index 000000000000..83172c6e3904
--- /dev/null
+++ b/module/spring-boot-grpc-server/src/main/java/org/springframework/boot/grpc/server/autoconfigure/security/GrpcReactiveRequest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2012-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.grpc.server.autoconfigure.security;
+
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.boot.grpc.server.autoconfigure.security.GrpcServletRequest.GrpcServletRequestMatcher;
+import org.springframework.boot.security.reactive.ApplicationContextServerWebExchangeMatcher;
+import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
+import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * Factory for a request matcher used to match against resource locations for gRPC
+ * services.
+ *
+ * @author Dave Syer
+ * @since 4.0.0
+ */
+public final class GrpcReactiveRequest {
+
+ private GrpcReactiveRequest() {
+ }
+
+ /**
+ * Returns a matcher that includes all gRPC services from the application context. The
+ * {@link GrpcReactiveRequestMatcher#excluding(String...) excluding} method can be
+ * used to remove specific services by name if required. For example:
+ *
+ *