From 11c658bfba3e1ecab7117d0a907088cde0dfd9a9 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Tue, 1 Oct 2024 13:31:42 -0500 Subject: [PATCH] Use explicit server factory impls This commit breaks the DefaultGrpcServerFactory into concrete implementations which helps with type-safety of different builders. Also, an aggregate customizers helper is added to allow the server builder customizers to be type specific and properly applied as such. Tests were also enhanced to cover newly added components. --- .../grpc/server/BaseGrpcServerFactory.java | 122 ++++++++ .../grpc/server/NettyGrpcServerFactory.java | 66 +++++ .../grpc/server/ServerBuilderCustomizer.java | 5 +- .../server/ShadedNettyGrpcServerFactory.java | 66 +++++ .../BaseServerFactoryPropertyMapper.java | 57 ++++ .../server/GrpcServerAutoConfiguration.java | 24 +- .../GrpcServerFactoryConfigurations.java | 107 +++++++ .../server/GrpcServerProperties.java | 54 +++- .../NettyServerFactoryPropertyMapper.java | 37 +++ .../server/ServerBuilderCustomizers.java | 58 ++++ ...hadedNettyServerFactoryPropertyMapper.java | 38 +++ .../GrpcServerAutoConfigurationTests.java | 276 ++++++++++++++++++ .../server/ServerBuilderCustomizersTests.java | 124 ++++++++ .../GrpcServerAutoConfigurationTests.java | 145 --------- 14 files changed, 1015 insertions(+), 164 deletions(-) create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/server/BaseGrpcServerFactory.java create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/server/NettyGrpcServerFactory.java create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/server/ShadedNettyGrpcServerFactory.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/BaseServerFactoryPropertyMapper.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryConfigurations.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/NettyServerFactoryPropertyMapper.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/ServerBuilderCustomizers.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/ShadedNettyServerFactoryPropertyMapper.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfigurationTests.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/ServerBuilderCustomizersTests.java delete mode 100644 spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/server/GrpcServerAutoConfigurationTests.java 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> 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; - } - - } - -}