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; - } - - } - -}