diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/BaseGrpcServerFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/BaseGrpcServerFactory.java
new file mode 100644
index 0000000..196e3cc
--- /dev/null
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/BaseGrpcServerFactory.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.grpc.server;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import com.google.common.collect.Lists;
+import io.grpc.Server;
+import io.grpc.ServerBuilder;
+import io.grpc.ServerProvider;
+import io.grpc.ServerServiceDefinition;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Base implementation for {@link GrpcServerFactory gRPC service factories}.
+ *
+ * The server builder implementation is discovered via Java's SPI mechanism.
+ *
+ * @author David Syer
+ * @author Chris Bono
+ * @param the type of server builder
+ * @see ServerProvider#provider()
+ */
+public class BaseGrpcServerFactory> implements GrpcServerFactory {
+
+ // VisibleForSubclass
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ private final List serviceList = Lists.newLinkedList();
+
+ private final String address;
+
+ private final int port;
+
+ private final List> serverBuilderCustomizers;
+
+ public BaseGrpcServerFactory(String address, int port, List> serverBuilderCustomizers) {
+ this.address = address;
+ this.port = port;
+ this.serverBuilderCustomizers = Objects.requireNonNull(serverBuilderCustomizers, "serverBuilderCustomizers");
+ }
+
+ protected String getAddress() {
+ return this.address;
+ }
+
+ protected int getPort() {
+ return this.port;
+ }
+
+ @Override
+ public Server createServer() {
+ T builder = newServerBuilder();
+ configure(builder, this.serviceList);
+ return builder.build();
+ }
+
+ @Override
+ public void addService(ServerServiceDefinition service) {
+ this.serviceList.add(service);
+ }
+
+ /**
+ * Creates a new server builder.
+ * @return The newly created server builder.
+ */
+ @SuppressWarnings("unchecked")
+ protected T newServerBuilder() {
+ return (T) ServerBuilder.forPort(port);
+ }
+
+ /**
+ * Configures the server builder by adding service definitions and applying
+ * customizers to the builder.
+ *
+ * Subclasses can override this to add features that are not yet supported by this
+ * library.
+ * @param builder the server builder to configure
+ * @param serviceDefinitions the service definitions to add to the builder
+ */
+ protected void configure(T builder, List serviceDefinitions) {
+ configureServices(builder, serviceDefinitions);
+ this.serverBuilderCustomizers.forEach((c) -> c.customize(builder));
+ }
+
+ /**
+ * Configure the services to be served by the server.
+ * @param builder the server builder to add the services to
+ * @param serviceDefinitions the service definitions to configure and add to the
+ * builder
+ */
+ protected void configureServices(T builder, List serviceDefinitions) {
+ Set serviceNames = new LinkedHashSet<>();
+ serviceDefinitions.forEach((service) -> {
+ String serviceName = service.getServiceDescriptor().getName();
+ if (!serviceNames.add(serviceName)) {
+ throw new IllegalStateException("Found duplicate service implementation: " + serviceName);
+ }
+ logger.info("Registered gRPC service: " + serviceName);
+ builder.addService(service);
+ });
+ }
+
+}
diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/NettyGrpcServerFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/NettyGrpcServerFactory.java
new file mode 100644
index 0000000..2dad602
--- /dev/null
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/NettyGrpcServerFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.grpc.server;
+
+import java.net.InetSocketAddress;
+import java.util.List;
+
+import com.google.common.net.InetAddresses;
+import io.grpc.netty.NettyServerBuilder;
+import io.netty.channel.epoll.EpollEventLoopGroup;
+import io.netty.channel.epoll.EpollServerDomainSocketChannel;
+import io.netty.channel.unix.DomainSocketAddress;
+
+/**
+ * {@link GrpcServerFactory} that can be used to create a Netty-based gRPC server.
+ *
+ * @author David Syer
+ * @author Chris Bono
+ */
+public class NettyGrpcServerFactory extends BaseGrpcServerFactory {
+
+ private static final String ANY_IP_ADDRESS = "*";
+
+ public NettyGrpcServerFactory(String address, int port,
+ List> serverBuilderCustomizers) {
+ super(address, port, serverBuilderCustomizers);
+ }
+
+ /**
+ * Creates a new server builder.
+ * @return The newly created server builder.
+ */
+ protected NettyServerBuilder newServerBuilder() {
+ String address = getAddress();
+ int port = getPort();
+ if (address != null) {
+ if (address.startsWith("unix:")) {
+ String path = address.substring(5);
+ return NettyServerBuilder.forAddress(new DomainSocketAddress(path))
+ .channelType(EpollServerDomainSocketChannel.class)
+ .bossEventLoopGroup(new EpollEventLoopGroup(1))
+ .workerEventLoopGroup(new EpollEventLoopGroup());
+ }
+ if (!ANY_IP_ADDRESS.equals(address)) {
+ return NettyServerBuilder.forAddress(new InetSocketAddress(InetAddresses.forString(address), port));
+ }
+ // TODO: Add more support for address resolution
+ }
+ return super.newServerBuilder();
+ }
+
+}
diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServerBuilderCustomizer.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServerBuilderCustomizer.java
index d1ba610..d7e64f4 100644
--- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServerBuilderCustomizer.java
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServerBuilderCustomizer.java
@@ -21,14 +21,15 @@ import io.grpc.ServerBuilder;
* Callback interface that can be used to customize a {@link ServerBuilder}.
*
* @author Chris Bono
+ * @param the type of server builder
*/
@FunctionalInterface
-public interface ServerBuilderCustomizer {
+public interface ServerBuilderCustomizer> {
/**
* Callback to customize a {@link ServerBuilder} instance.
* @param serverBuilder the builder to customize
*/
- void customize(ServerBuilder> serverBuilder);
+ void customize(T serverBuilder);
}
diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/ShadedNettyGrpcServerFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ShadedNettyGrpcServerFactory.java
new file mode 100644
index 0000000..29d3c02
--- /dev/null
+++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ShadedNettyGrpcServerFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.grpc.server;
+
+import java.net.InetSocketAddress;
+import java.util.List;
+
+import com.google.common.net.InetAddresses;
+import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
+import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup;
+import io.grpc.netty.shaded.io.netty.channel.epoll.EpollServerDomainSocketChannel;
+import io.grpc.netty.shaded.io.netty.channel.unix.DomainSocketAddress;
+
+/**
+ * {@link GrpcServerFactory} that can be used to create a shaded Netty-based gRPC server.
+ *
+ * @author David Syer
+ * @author Chris Bono
+ */
+public class ShadedNettyGrpcServerFactory extends BaseGrpcServerFactory {
+
+ private static final String ANY_IP_ADDRESS = "*";
+
+ public ShadedNettyGrpcServerFactory(String address, int port,
+ List> serverBuilderCustomizers) {
+ super(address, port, serverBuilderCustomizers);
+ }
+
+ /**
+ * Creates a new server builder.
+ * @return The newly created server builder.
+ */
+ protected NettyServerBuilder newServerBuilder() {
+ String address = getAddress();
+ int port = getPort();
+ if (address != null) {
+ if (address.startsWith("unix:")) {
+ String path = address.substring(5);
+ return NettyServerBuilder.forAddress(new DomainSocketAddress(path))
+ .channelType(EpollServerDomainSocketChannel.class)
+ .bossEventLoopGroup(new EpollEventLoopGroup(1))
+ .workerEventLoopGroup(new EpollEventLoopGroup());
+ }
+ if (!ANY_IP_ADDRESS.equals(address)) {
+ return NettyServerBuilder.forAddress(new InetSocketAddress(InetAddresses.forString(address), port));
+ }
+ // TODO: Add more support for address resolution
+ }
+ return super.newServerBuilder();
+ }
+
+}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/BaseServerFactoryPropertyMapper.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/BaseServerFactoryPropertyMapper.java
new file mode 100644
index 0000000..f865c93
--- /dev/null
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/BaseServerFactoryPropertyMapper.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.grpc.autoconfigure.server;
+
+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;
+
+/**
+ * Helper class used to map {@link GrpcServerProperties} to various {@link ServerBuilder}.
+ *
+ * @author Chris Bono
+ * @param the type of server builder
+ */
+class BaseServerFactoryPropertyMapper> {
+
+ final GrpcServerProperties properties;
+
+ BaseServerFactoryPropertyMapper(GrpcServerProperties properties) {
+ this.properties = properties;
+ }
+
+ /**
+ * Maps the properties to the server builder.
+ * @param serverBuilder the builder
+ */
+ void customizeServerBuilder(T serverBuilder) {
+ PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
+ GrpcServerProperties.KeepAlive keepAlive = this.properties.getKeepAlive();
+ map.from(keepAlive.getTime()).to(durationProperty(serverBuilder::keepAliveTime));
+ map.from(keepAlive.getTimeout()).to(durationProperty(serverBuilder::keepAliveTimeout));
+ }
+
+ Consumer durationProperty(BiConsumer setter) {
+ return (duration) -> setter.accept(duration.toNanos(), TimeUnit.NANOSECONDS);
+ }
+
+}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfiguration.java
index 08bb4b6..7a547de 100644
--- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfiguration.java
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfiguration.java
@@ -19,6 +19,7 @@ import io.grpc.BindableService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -26,7 +27,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
-import org.springframework.grpc.server.DefaultGrpcServerFactory;
+import org.springframework.context.annotation.Import;
+import org.springframework.core.Ordered;
import org.springframework.grpc.server.GrpcServerFactory;
import org.springframework.grpc.server.ServerBuilderCustomizer;
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
@@ -41,9 +43,13 @@ import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
* @author Chris Bono
*/
@AutoConfiguration
+@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(BindableService.class)
@ConditionalOnBean(BindableService.class)
@EnableConfigurationProperties(GrpcServerProperties.class)
+@Import({ GrpcServerFactoryConfigurations.ShadedNettyServerFactoryConfiguration.class,
+ GrpcServerFactoryConfigurations.NettyServerFactoryConfiguration.class,
+ GrpcServerFactoryConfigurations.ServiceProviderServerFactoryConfiguration.class })
public class GrpcServerAutoConfiguration {
private final GrpcServerProperties properties;
@@ -52,20 +58,16 @@ public class GrpcServerAutoConfiguration {
this.properties = properties;
}
- @ConditionalOnMissingBean(GrpcServerFactory.class)
- @Bean
- DefaultGrpcServerFactory> defaultGrpcServerFactory(ObjectProvider grpcServicesProvider,
- ObjectProvider builderCustomizersProvider) {
- DefaultGrpcServerFactory> factory = new DefaultGrpcServerFactory<>(this.properties.getAddress(),
- this.properties.getPort(), builderCustomizersProvider.orderedStream().toList());
- grpcServicesProvider.orderedStream().map(BindableService::bindService).forEach(factory::addService);
- return factory;
- }
-
@ConditionalOnMissingBean
@Bean
GrpcServerLifecycle grpcServerLifecycle(GrpcServerFactory factory, ApplicationEventPublisher eventPublisher) {
return new GrpcServerLifecycle(factory, this.properties.getShutdownGracePeriod(), eventPublisher);
}
+ @ConditionalOnMissingBean
+ @Bean
+ ServerBuilderCustomizers serverBuilderCustomizers(ObjectProvider> customizers) {
+ return new ServerBuilderCustomizers(customizers.orderedStream().toList());
+ }
+
}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryConfigurations.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryConfigurations.java
new file mode 100644
index 0000000..65990fb
--- /dev/null
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryConfigurations.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.grpc.autoconfigure.server;
+
+import java.util.List;
+
+import io.grpc.BindableService;
+import io.grpc.ServerBuilder;
+import io.grpc.netty.NettyServerBuilder;
+
+import org.springframework.beans.factory.ObjectProvider;
+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.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.grpc.server.BaseGrpcServerFactory;
+import org.springframework.grpc.server.GrpcServerFactory;
+import org.springframework.grpc.server.NettyGrpcServerFactory;
+import org.springframework.grpc.server.ServerBuilderCustomizer;
+import org.springframework.grpc.server.ShadedNettyGrpcServerFactory;
+
+/**
+ * Configurations for {@link GrpcServerFactory gRPC server factories}.
+ *
+ * @author Chris Bono
+ */
+class GrpcServerFactoryConfigurations {
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)
+ @ConditionalOnMissingBean(GrpcServerFactory.class)
+ @EnableConfigurationProperties(GrpcServerProperties.class)
+ static class ShadedNettyServerFactoryConfiguration {
+
+ @Bean
+ ShadedNettyGrpcServerFactory shadedNettyGrpcServerFactory(GrpcServerProperties properties,
+ ObjectProvider grpcServicesProvider,
+ ServerBuilderCustomizers serverBuilderCustomizers) {
+ ShadedNettyServerFactoryPropertyMapper mapper = new ShadedNettyServerFactoryPropertyMapper(properties);
+ List> builderCustomizers = List
+ .of(mapper::customizeServerBuilder, serverBuilderCustomizers::customize);
+ ShadedNettyGrpcServerFactory factory = new ShadedNettyGrpcServerFactory(properties.getAddress(),
+ properties.getPort(), builderCustomizers);
+ grpcServicesProvider.orderedStream().map(BindableService::bindService).forEach(factory::addService);
+ return factory;
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(NettyServerBuilder.class)
+ @ConditionalOnMissingBean(GrpcServerFactory.class)
+ @EnableConfigurationProperties(GrpcServerProperties.class)
+ static class NettyServerFactoryConfiguration {
+
+ @Bean
+ NettyGrpcServerFactory nettyGrpcServerFactory(GrpcServerProperties properties,
+ ObjectProvider grpcServicesProvider,
+ ServerBuilderCustomizers serverBuilderCustomizers) {
+ NettyServerFactoryPropertyMapper mapper = new NettyServerFactoryPropertyMapper(properties);
+ List> builderCustomizers = List
+ .of(mapper::customizeServerBuilder, serverBuilderCustomizers::customize);
+ NettyGrpcServerFactory factory = new NettyGrpcServerFactory(properties.getAddress(), properties.getPort(),
+ builderCustomizers);
+ grpcServicesProvider.orderedStream().map(BindableService::bindService).forEach(factory::addService);
+ return factory;
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(ServerBuilder.class)
+ @ConditionalOnMissingBean(GrpcServerFactory.class)
+ @EnableConfigurationProperties(GrpcServerProperties.class)
+ static class ServiceProviderServerFactoryConfiguration {
+
+ @Bean
+ > BaseGrpcServerFactory serviceProviderGrpcServerFactory(
+ GrpcServerProperties properties, ObjectProvider grpcServicesProvider,
+ ServerBuilderCustomizers serverBuilderCustomizers) {
+ BaseServerFactoryPropertyMapper mapper = new BaseServerFactoryPropertyMapper<>(properties);
+ List> builderCustomizers = List.of(mapper::customizeServerBuilder,
+ serverBuilderCustomizers::customize);
+ BaseGrpcServerFactory factory = new BaseGrpcServerFactory<>(properties.getAddress(),
+ properties.getPort(), builderCustomizers);
+ grpcServicesProvider.orderedStream().map(BindableService::bindService).forEach(factory::addService);
+ return factory;
+ }
+
+ }
+
+}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerProperties.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerProperties.java
index 388f499..7341276 100644
--- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerProperties.java
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerProperties.java
@@ -26,19 +26,23 @@ public class GrpcServerProperties {
private String address = "*";
+ /**
+ * Server port to listen on. When the value is 0, a random available port is selected.
+ * When the value is -1, the inter-process server is disabled (for example if you only
+ * want to use the in-process server).
+ */
private int port = 9090;
/**
- * The time to wait for the server to gracefully shutdown (completing all requests
- * after the server started to shutdown). If set to a negative value, the server waits
- * forever. If set to {@code 0} the server will force shutdown immediately. Defaults
- * to {@code 30s}.
- * @param shutdownGracePeriod The time to wait for a graceful shutdown.
- * @return The time to wait for a graceful shutdown.
+ * 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.of(30, ChronoUnit.SECONDS);
+ private final KeepAlive keepAlive = new KeepAlive();
+
public String getAddress() {
return address;
}
@@ -63,4 +67,42 @@ public class GrpcServerProperties {
this.shutdownGracePeriod = shutdownGracePeriod;
}
+ public KeepAlive getKeepAlive() {
+ return this.keepAlive;
+ }
+
+ public static class KeepAlive {
+
+ /**
+ * Duration without read activity before sending a keep alive ping (default 2h).
+ */
+ @DurationUnit(ChronoUnit.SECONDS)
+ private Duration time = Duration.of(2, ChronoUnit.HOURS);
+
+ /**
+ * 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 Duration timeout = Duration.of(20, ChronoUnit.SECONDS);
+
+ public Duration getTime() {
+ return time;
+ }
+
+ public void setTime(Duration time) {
+ this.time = time;
+ }
+
+ public Duration getTimeout() {
+ return timeout;
+ }
+
+ public void setTimeout(Duration timeout) {
+ this.timeout = timeout;
+ }
+
+ }
+
}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/NettyServerFactoryPropertyMapper.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/NettyServerFactoryPropertyMapper.java
new file mode 100644
index 0000000..cd7a24a
--- /dev/null
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/NettyServerFactoryPropertyMapper.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.grpc.autoconfigure.server;
+
+import io.grpc.netty.NettyServerBuilder;
+
+/**
+ * Helper class used to map {@link GrpcServerProperties} to {@link NettyServerBuilder}.
+ *
+ * @author Chris Bono
+ */
+class NettyServerFactoryPropertyMapper extends BaseServerFactoryPropertyMapper {
+
+ NettyServerFactoryPropertyMapper(GrpcServerProperties properties) {
+ super(properties);
+ }
+
+ @Override
+ void customizeServerBuilder(NettyServerBuilder nettyServerBuilder) {
+ super.customizeServerBuilder(nettyServerBuilder);
+ }
+
+}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/ServerBuilderCustomizers.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/ServerBuilderCustomizers.java
new file mode 100644
index 0000000..3226cb9
--- /dev/null
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/ServerBuilderCustomizers.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2012-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.grpc.autoconfigure.server;
+
+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/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/ShadedNettyServerFactoryPropertyMapper.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/ShadedNettyServerFactoryPropertyMapper.java
new file mode 100644
index 0000000..012d96a
--- /dev/null
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/ShadedNettyServerFactoryPropertyMapper.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.grpc.autoconfigure.server;
+
+import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
+
+/**
+ * Helper class used to map {@link GrpcServerProperties} to the shaded
+ * {@link NettyServerBuilder}.
+ *
+ * @author Chris Bono
+ */
+class ShadedNettyServerFactoryPropertyMapper extends BaseServerFactoryPropertyMapper {
+
+ ShadedNettyServerFactoryPropertyMapper(GrpcServerProperties properties) {
+ super(properties);
+ }
+
+ @Override
+ void customizeServerBuilder(NettyServerBuilder nettyServerBuilder) {
+ super.customizeServerBuilder(nettyServerBuilder);
+ }
+
+}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfigurationTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfigurationTests.java
new file mode 100644
index 0000000..5001911
--- /dev/null
+++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfigurationTests.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.grpc.autoconfigure.server;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import io.grpc.BindableService;
+import io.grpc.ServerBuilder;
+import io.grpc.ServerServiceDefinition;
+import io.grpc.ServiceDescriptor;
+import io.grpc.netty.NettyServerBuilder;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.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.server.BaseGrpcServerFactory;
+import org.springframework.grpc.server.GrpcServerFactory;
+import org.springframework.grpc.server.NettyGrpcServerFactory;
+import org.springframework.grpc.server.ServerBuilderCustomizer;
+import org.springframework.grpc.server.ShadedNettyGrpcServerFactory;
+import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link GrpcServerAutoConfiguration}.
+ *
+ * @author Chris Bono
+ */
+class GrpcServerAutoConfigurationTests {
+
+ private ApplicationContextRunner contextRunner() {
+ BindableService service = mock();
+ ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build();
+ when(service.bindService()).thenReturn(serviceDefinition);
+ // NOTE: we use noop server lifecycle to avoid startup
+ return new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class))
+ .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock)
+ .withBean(BindableService.class, () -> service);
+ }
+
+ private ApplicationContextRunner contextRunnerWithLifecyle() {
+ BindableService service = mock();
+ ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build();
+ when(service.bindService()).thenReturn(serviceDefinition);
+ // NOTE: we use noop server lifecycle to avoid startup
+ return new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class))
+ .withBean(BindableService.class, () -> service);
+ }
+
+ @Test
+ void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() {
+ this.contextRunner()
+ .withClassLoader(new FilteredClassLoader(BindableService.class))
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class));
+ }
+
+ @Test
+ void whenNoBindableServicesRegisteredAutoConfigurationIsSkipped() {
+ new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class))
+ .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class));
+ }
+
+ @Test
+ void whenHasUserDefinedServerLifecycleDoesNotAutoConfigureBean() {
+ GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class);
+ this.contextRunnerWithLifecyle()
+ .withBean("customServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle)
+ .run((context) -> assertThat(context).getBean(GrpcServerLifecycle.class).isSameAs(customServerLifecycle));
+ }
+
+ @Test
+ void serverLifecycleAutoConfiguredAsExpected() {
+ this.contextRunnerWithLifecyle()
+ .run((context) -> assertThat(context).getBean(GrpcServerLifecycle.class)
+ .hasFieldOrPropertyWithValue("factory", context.getBean(GrpcServerFactory.class)));
+ }
+
+ @Test
+ void whenHasUserDefinedServerBuilderCustomizersDoesNotAutoConfigureBean() {
+ ServerBuilderCustomizers customCustomizers = mock(ServerBuilderCustomizers.class);
+ this.contextRunner()
+ .withBean("customCustomizers", ServerBuilderCustomizers.class, () -> customCustomizers)
+ .run((context) -> assertThat(context).getBean(ServerBuilderCustomizers.class).isSameAs(customCustomizers));
+ }
+
+ @Test
+ void serverBuilderCustomizersAutoConfiguredAsExpected() {
+ this.contextRunner()
+ .withUserConfiguration(ServerBuilderCustomizersConfig.class)
+ .run((context) -> assertThat(context).getBean(ServerBuilderCustomizers.class)
+ .extracting("customizers", InstanceOfAssertFactories.list(ServerBuilderCustomizer.class))
+ .containsExactly(ServerBuilderCustomizersConfig.CUSTOMIZER_BAR,
+ ServerBuilderCustomizersConfig.CUSTOMIZER_FOO));
+ }
+
+ @Test
+ void whenHasUserDefinedServerFactoryDoesNotAutoConfigureBean() {
+ GrpcServerFactory customServerFactory = mock(GrpcServerFactory.class);
+ this.contextRunner()
+ .withBean("customServerFactory", GrpcServerFactory.class, () -> customServerFactory)
+ .run((context) -> assertThat(context).getBean(GrpcServerFactory.class).isSameAs(customServerFactory));
+ }
+
+ @Test
+ void whenShadedAndNonShadedNettyOnClasspathShadedNettyFactoryIsAutoConfigured() {
+ this.contextRunner()
+ .run((context) -> assertThat(context).getBean(GrpcServerFactory.class)
+ .isInstanceOf(ShadedNettyGrpcServerFactory.class));
+ }
+
+ @Test
+ void whenOnlyNonShadedNettyOnClasspathNonShadedNettyFactoryIsAutoConfigured() {
+ this.contextRunner()
+ .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class))
+ .run((context) -> assertThat(context).getBean(GrpcServerFactory.class)
+ .isInstanceOf(NettyGrpcServerFactory.class));
+ }
+
+ @Test
+ void whenNeitherShadedNorNonShadedNettyOnClasspathBaseServerFactoryIsAutoConfigured() {
+ this.contextRunner()
+ .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class,
+ io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class))
+ .run((context) -> assertThat(context).getBean(GrpcServerFactory.class)
+ .isInstanceOf(BaseGrpcServerFactory.class));
+ }
+
+ @Test
+ void shadedNettyServerFactoryAutoConfiguredAsExpected() {
+ serverFactoryAutoConfiguredAsExpected(this.contextRunner(), ShadedNettyGrpcServerFactory.class);
+ }
+
+ @Test
+ void nettyServerFactoryAutoConfiguredAsExpected() {
+ serverFactoryAutoConfiguredAsExpected(this.contextRunner()
+ .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)),
+ NettyGrpcServerFactory.class);
+ }
+
+ @Test
+ void baseServerFactoryAutoConfiguredAsExpected() {
+ serverFactoryAutoConfiguredAsExpected(this.contextRunner()
+ .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class,
+ io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)),
+ BaseGrpcServerFactory.class);
+ }
+
+ private void serverFactoryAutoConfiguredAsExpected(ApplicationContextRunner contextRunner,
+ Class> expectedServerFactoryType) {
+ contextRunner.withPropertyValues("spring.grpc.server.address=myhost", "spring.grpc.server.port=6160")
+ .run((context) -> assertThat(context).getBean(GrpcServerFactory.class)
+ .isInstanceOf(expectedServerFactoryType)
+ .hasFieldOrPropertyWithValue("address", "myhost")
+ .hasFieldOrPropertyWithValue("port", 6160)
+ .extracting("serviceList", InstanceOfAssertFactories.list(ServerServiceDefinition.class))
+ .singleElement()
+ .extracting(ServerServiceDefinition::getServiceDescriptor)
+ .extracting(ServiceDescriptor::getName)
+ .isEqualTo("my-service"));
+ }
+
+ @Test
+ void shadedNettyServerFactoryAutoConfiguredWithCustomizers() {
+ io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder builder = mock();
+ serverFactoryAutoConfiguredWithCustomizers(this.contextRunnerWithLifecyle(), builder,
+ ShadedNettyGrpcServerFactory.class);
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Test
+ void nettyServerFactoryAutoConfiguredWithCustomizers() {
+ // FilteredClassLoader hides the class from the auto-configuration but not from
+ // the Java SPI
+ // used by ServerBuilder.forPort(int) which by default returns shaded Netty. This
+ // results in
+ // class cast exception when NettyGrpcServerFactory is expecting a non-shaded
+ // server builder.
+ // We static mock the builder to return non-shaded Netty - which would happen in
+ // real world.
+ try (MockedStatic serverBuilderForPort = Mockito.mockStatic(ServerBuilder.class)) {
+ serverBuilderForPort.when(() -> ServerBuilder.forPort(anyInt()))
+ .thenAnswer((Answer) invocation -> NettyServerBuilder
+ .forPort(invocation.getArgument(0)));
+ NettyServerBuilder builder = mock();
+ serverFactoryAutoConfiguredWithCustomizers(this.contextRunnerWithLifecyle()
+ .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)),
+ builder, NettyGrpcServerFactory.class);
+ }
+ }
+
+ @Test
+ > void baseServerFactoryAutoConfiguredWithCustomizers() {
+ ServerBuilder builder = mock();
+ serverFactoryAutoConfiguredWithCustomizers(
+ this.contextRunnerWithLifecyle()
+ .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class,
+ io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)),
+ builder, BaseGrpcServerFactory.class);
+ }
+
+ @SuppressWarnings("unchecked")
+ private > void serverFactoryAutoConfiguredWithCustomizers(
+ ApplicationContextRunner contextRunner, ServerBuilder mockServerBuilder,
+ Class> expectedServerFactoryType) {
+ ServerBuilderCustomizer customizer1 = (serverBuilder) -> serverBuilder.keepAliveTime(40L, TimeUnit.SECONDS);
+ ServerBuilderCustomizer customizer2 = (serverBuilder) -> serverBuilder.keepAliveTime(50L, TimeUnit.SECONDS);
+ ServerBuilderCustomizers customizers = new ServerBuilderCustomizers(List.of(customizer1, customizer2));
+ contextRunner.withPropertyValues("spring.grpc.server.port=0", "spring.grpc.server.keep-alive.time=30s")
+ .withBean("serverBuilderCustomizers", ServerBuilderCustomizers.class, () -> customizers)
+ .run((context) -> assertThat(context).getBean(GrpcServerFactory.class)
+ .isInstanceOf(expectedServerFactoryType)
+ .extracting("serverBuilderCustomizers", InstanceOfAssertFactories.list(ServerBuilderCustomizer.class))
+ .satisfies((allCustomizers) -> {
+ allCustomizers.forEach((c) -> c.customize(mockServerBuilder));
+ InOrder ordered = inOrder(mockServerBuilder);
+ ordered.verify(mockServerBuilder)
+ .keepAliveTime(Duration.ofSeconds(30L).toNanos(), TimeUnit.NANOSECONDS);
+ ordered.verify(mockServerBuilder).keepAliveTime(40L, TimeUnit.SECONDS);
+ ordered.verify(mockServerBuilder).keepAliveTime(50L, TimeUnit.SECONDS);
+ }));
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class ServerBuilderCustomizersConfig {
+
+ static ServerBuilderCustomizer> CUSTOMIZER_FOO = mock();
+
+ static ServerBuilderCustomizer> CUSTOMIZER_BAR = mock();
+
+ @Bean
+ @Order(200)
+ ServerBuilderCustomizer> customizerFoo() {
+ return CUSTOMIZER_FOO;
+ }
+
+ @Bean
+ @Order(100)
+ ServerBuilderCustomizer> customizerBar() {
+ return CUSTOMIZER_BAR;
+ }
+
+ }
+
+}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/ServerBuilderCustomizersTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/ServerBuilderCustomizersTests.java
new file mode 100644
index 0000000..f69bc68
--- /dev/null
+++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/ServerBuilderCustomizersTests.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.grpc.autoconfigure.server;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import io.grpc.ServerBuilder;
+import io.grpc.netty.NettyServerBuilder;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.grpc.server.ServerBuilderCustomizer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link ServerBuilderCustomizers}.
+ *
+ * @author Chris Bono
+ */
+class ServerBuilderCustomizersTests {
+
+ @Test
+ void customizeWithNullCustomizersShouldDoNothing() {
+ ServerBuilder> serverBuilder = mock(ServerBuilder.class);
+ new ServerBuilderCustomizers(null).customize(serverBuilder);
+ then(serverBuilder).shouldHaveNoInteractions();
+ }
+
+ @Test
+ // @SuppressWarnings({ "unchecked", "rawtypes" })
+ void customizeSimpleServerBuilder() {
+ ServerBuilderCustomizers customizers = new ServerBuilderCustomizers(
+ List.of(new SimpleServerBuilderCustomizer()));
+ NettyServerBuilder serverBuilder = mock(NettyServerBuilder.class);
+ customizers.customize(serverBuilder);
+ then(serverBuilder).should().maxConnectionAge(100L, TimeUnit.SECONDS);
+ }
+
+ @Test
+ void customizeShouldCheckGeneric() {
+ List> list = new ArrayList<>();
+ list.add(new TestCustomizer<>());
+ list.add(new TestNettyServerBuilderCustomizer());
+ list.add(new TestShadedNettyServerBuilderCustomizer());
+ ServerBuilderCustomizers customizers = new ServerBuilderCustomizers(list);
+
+ customizers.customize(mock(ServerBuilder.class));
+ assertThat(list.get(0).getCount()).isOne();
+ assertThat(list.get(1).getCount()).isZero();
+ assertThat(list.get(2).getCount()).isZero();
+
+ customizers.customize(mock(NettyServerBuilder.class));
+ assertThat(list.get(0).getCount()).isEqualTo(2);
+ assertThat(list.get(1).getCount()).isOne();
+ assertThat(list.get(2).getCount()).isZero();
+
+ customizers.customize(mock(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class));
+ assertThat(list.get(0).getCount()).isEqualTo(3);
+ assertThat(list.get(1).getCount()).isOne();
+ assertThat(list.get(2).getCount()).isOne();
+ }
+
+ static class SimpleServerBuilderCustomizer implements ServerBuilderCustomizer {
+
+ @Override
+ public void customize(NettyServerBuilder serverBuilder) {
+ serverBuilder.maxConnectionAge(100, TimeUnit.SECONDS);
+ }
+
+ }
+
+ /**
+ * Test customizer that will match all {@link ServerBuilderCustomizer}.
+ */
+ static class TestCustomizer> implements ServerBuilderCustomizer {
+
+ private int count;
+
+ @Override
+ public void customize(T serverBuilder) {
+ this.count++;
+ }
+
+ int getCount() {
+ return this.count;
+ }
+
+ }
+
+ /**
+ * Test customizer that will match only {@link NettyServerBuilder}.
+ */
+ static class TestNettyServerBuilderCustomizer extends TestCustomizer {
+
+ }
+
+ /**
+ * Test customizer that will match only
+ * {@link io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder}.
+ */
+ static class TestShadedNettyServerBuilderCustomizer
+ extends TestCustomizer {
+
+ }
+
+}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/server/GrpcServerAutoConfigurationTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/server/GrpcServerAutoConfigurationTests.java
deleted file mode 100644
index 1c25780..0000000
--- a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/server/GrpcServerAutoConfigurationTests.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright 2023-2024 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.grpc.server;
-
-import java.util.List;
-
-import io.grpc.BindableService;
-import io.grpc.ServerServiceDefinition;
-import io.grpc.ServiceDescriptor;
-import org.assertj.core.api.InstanceOfAssertFactories;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-
-import org.springframework.boot.autoconfigure.AutoConfigurations;
-import org.springframework.boot.test.context.FilteredClassLoader;
-import org.springframework.boot.test.context.runner.ApplicationContextRunner;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.core.annotation.Order;
-import org.springframework.grpc.autoconfigure.server.GrpcServerAutoConfiguration;
-import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-/**
- * Tests for {@link GrpcServerAutoConfiguration}.
- *
- * @author Chris Bono
- */
-class GrpcServerAutoConfigurationTests {
-
- private ApplicationContextRunner validContextRunner() {
- BindableService service = mock();
- ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build();
- when(service.bindService()).thenReturn(serviceDefinition);
- return new ApplicationContextRunner()
- .withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class))
- .withBean(BindableService.class, () -> service);
- }
-
- @Test
- void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() {
- this.validContextRunner()
- .withClassLoader(new FilteredClassLoader(BindableService.class))
- .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class));
- }
-
- @Test
- void whenNoBindableServicesRegisteredAutoConfigurationIsSkipped() {
- new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class))
- .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class));
- }
-
- @Test
- void whenHasUserDefinedServerFactoryDoesNotAutoConfigureBean() {
- // NOTE: we use noop server lifecycle to avoid startup
- GrpcServerFactory customServerFactory = mock(GrpcServerFactory.class);
- this.validContextRunner()
- .withBean("customServerFactory", GrpcServerFactory.class, () -> customServerFactory)
- .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock)
- .run((context) -> assertThat(context).getBean(GrpcServerFactory.class).isSameAs(customServerFactory));
- }
-
- @Test
- void whenHasUserDefinedServerLifecycleDoesNotAutoConfigureBean() {
- GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class);
- this.validContextRunner()
- .withBean("customServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle)
- .run((context) -> assertThat(context).getBean(GrpcServerLifecycle.class).isSameAs(customServerLifecycle));
- }
-
- @Test
- void serverFactoryAutoConfiguredAsExpected() {
- // NOTE: we use noop server lifecycle to avoid startup
- this.validContextRunner()
- .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock)
- .withPropertyValues("spring.grpc.server.address=myhost", "spring.grpc.server.port=6160")
- .run((context) -> assertThat(context).getBean(DefaultGrpcServerFactory.class)
- .hasFieldOrPropertyWithValue("address", "myhost")
- .hasFieldOrPropertyWithValue("port", 6160)
- .hasFieldOrPropertyWithValue("serverBuilderCustomizers", List.of())
- .extracting("serviceList", InstanceOfAssertFactories.list(ServerServiceDefinition.class))
- .singleElement()
- .extracting(ServerServiceDefinition::getServiceDescriptor)
- .extracting(ServiceDescriptor::getName)
- .isEqualTo("my-service"));
- }
-
- @Test
- void serverFactoryAutoConfiguredWithCustomizers() {
- this.validContextRunner()
- .withUserConfiguration(ServerFactoryCustomizersConfig.class)
- .run((context) -> assertThat(context).getBean(DefaultGrpcServerFactory.class)
- .extracting("serverBuilderCustomizers", InstanceOfAssertFactories.list(ServerBuilderCustomizer.class))
- .containsExactly(ServerFactoryCustomizersConfig.CUSTOMIZER_BAR,
- ServerFactoryCustomizersConfig.CUSTOMIZER_FOO));
- }
-
- @Test
- void serverLifecycleAutoConfiguredAsExpected() {
- this.validContextRunner()
- .run((context) -> assertThat(context).getBean(GrpcServerLifecycle.class)
- .hasFieldOrPropertyWithValue("factory", context.getBean(DefaultGrpcServerFactory.class)));
- }
-
- @Configuration(proxyBeanMethods = false)
- static class ServerFactoryCustomizersConfig {
-
- static ServerBuilderCustomizer CUSTOMIZER_FOO = (serverBuilder) -> {
- };
-
- static ServerBuilderCustomizer CUSTOMIZER_BAR = (serverBuilder) -> {
- };
-
- @Bean
- @Order(200)
- ServerBuilderCustomizer customizerFoo() {
- return CUSTOMIZER_FOO;
- }
-
- @Bean
- @Order(100)
- ServerBuilderCustomizer customizerBar() {
- return CUSTOMIZER_BAR;
- }
-
- }
-
-}