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.
This commit is contained in:
Chris Bono
2024-10-01 13:31:42 -05:00
committed by Dave Syer
parent a430720dbf
commit 11c658bfba
14 changed files with 1015 additions and 164 deletions

View File

@@ -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}.
* <p>
* The server builder implementation is discovered via Java's SPI mechanism.
*
* @author David Syer
* @author Chris Bono
* @param <T> the type of server builder
* @see ServerProvider#provider()
*/
public class BaseGrpcServerFactory<T extends ServerBuilder<T>> implements GrpcServerFactory {
// VisibleForSubclass
protected final Log logger = LogFactory.getLog(getClass());
private final List<ServerServiceDefinition> serviceList = Lists.newLinkedList();
private final String address;
private final int port;
private final List<ServerBuilderCustomizer<T>> serverBuilderCustomizers;
public BaseGrpcServerFactory(String address, int port, List<ServerBuilderCustomizer<T>> 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.
* <p>
* 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<ServerServiceDefinition> 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<ServerServiceDefinition> serviceDefinitions) {
Set<String> 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);
});
}
}

View File

@@ -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<NettyServerBuilder> {
private static final String ANY_IP_ADDRESS = "*";
public NettyGrpcServerFactory(String address, int port,
List<ServerBuilderCustomizer<NettyServerBuilder>> 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();
}
}

View File

@@ -21,14 +21,15 @@ import io.grpc.ServerBuilder;
* Callback interface that can be used to customize a {@link ServerBuilder}.
*
* @author Chris Bono
* @param <T> the type of server builder
*/
@FunctionalInterface
public interface ServerBuilderCustomizer {
public interface ServerBuilderCustomizer<T extends ServerBuilder<T>> {
/**
* Callback to customize a {@link ServerBuilder} instance.
* @param serverBuilder the builder to customize
*/
void customize(ServerBuilder<?> serverBuilder);
void customize(T serverBuilder);
}

View File

@@ -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<NettyServerBuilder> {
private static final String ANY_IP_ADDRESS = "*";
public ShadedNettyGrpcServerFactory(String address, int port,
List<ServerBuilderCustomizer<NettyServerBuilder>> 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();
}
}

View File

@@ -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 <T> the type of server builder
*/
class BaseServerFactoryPropertyMapper<T extends ServerBuilder<T>> {
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<Duration> durationProperty(BiConsumer<Long, TimeUnit> setter) {
return (duration) -> setter.accept(duration.toNanos(), TimeUnit.NANOSECONDS);
}
}

View File

@@ -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<BindableService> grpcServicesProvider,
ObjectProvider<ServerBuilderCustomizer> 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<ServerBuilderCustomizer<?>> customizers) {
return new ServerBuilderCustomizers(customizers.orderedStream().toList());
}
}

View File

@@ -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<BindableService> grpcServicesProvider,
ServerBuilderCustomizers serverBuilderCustomizers) {
ShadedNettyServerFactoryPropertyMapper mapper = new ShadedNettyServerFactoryPropertyMapper(properties);
List<ServerBuilderCustomizer<io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder>> 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<BindableService> grpcServicesProvider,
ServerBuilderCustomizers serverBuilderCustomizers) {
NettyServerFactoryPropertyMapper mapper = new NettyServerFactoryPropertyMapper(properties);
List<ServerBuilderCustomizer<NettyServerBuilder>> 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
<T extends ServerBuilder<T>> BaseGrpcServerFactory<T> serviceProviderGrpcServerFactory(
GrpcServerProperties properties, ObjectProvider<BindableService> grpcServicesProvider,
ServerBuilderCustomizers serverBuilderCustomizers) {
BaseServerFactoryPropertyMapper<T> mapper = new BaseServerFactoryPropertyMapper<>(properties);
List<ServerBuilderCustomizer<T>> builderCustomizers = List.of(mapper::customizeServerBuilder,
serverBuilderCustomizers::customize);
BaseGrpcServerFactory<T> factory = new BaseGrpcServerFactory<>(properties.getAddress(),
properties.getPort(), builderCustomizers);
grpcServicesProvider.orderedStream().map(BindableService::bindService).forEach(factory::addService);
return factory;
}
}
}

View File

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

View File

@@ -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<NettyServerBuilder> {
NettyServerFactoryPropertyMapper(GrpcServerProperties properties) {
super(properties);
}
@Override
void customizeServerBuilder(NettyServerBuilder nettyServerBuilder) {
super.customizeServerBuilder(nettyServerBuilder);
}
}

View File

@@ -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<ServerBuilderCustomizer<?>> customizers;
ServerBuilderCustomizers(List<? extends ServerBuilderCustomizer<?>> customizers) {
this.customizers = (customizers != null) ? new ArrayList<>(customizers) : Collections.emptyList();
}
/**
* Customize the specified {@link ServerBuilder}. Locates all
* {@link ServerBuilderCustomizer} beans able to handle the specified instance and
* invoke {@link ServerBuilderCustomizer#customize} on them.
* @param <T> the type of server builder
* @param serverBuilder the builder to customize
* @return the customized builder
*/
@SuppressWarnings("unchecked")
<T extends ServerBuilder<?>> T customize(T serverBuilder) {
LambdaSafe.callbacks(ServerBuilderCustomizer.class, this.customizers, serverBuilder)
.withLogger(ServerBuilderCustomizers.class)
.invoke((customizer) -> customizer.customize(serverBuilder));
return serverBuilder;
}
}

View File

@@ -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<NettyServerBuilder> {
ShadedNettyServerFactoryPropertyMapper(GrpcServerProperties properties) {
super(properties);
}
@Override
void customizeServerBuilder(NettyServerBuilder nettyServerBuilder) {
super.customizeServerBuilder(nettyServerBuilder);
}
}

View File

@@ -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<ServerBuilder> serverBuilderForPort = Mockito.mockStatic(ServerBuilder.class)) {
serverBuilderForPort.when(() -> ServerBuilder.forPort(anyInt()))
.thenAnswer((Answer<NettyServerBuilder>) 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
<T extends ServerBuilder<T>> void baseServerFactoryAutoConfiguredWithCustomizers() {
ServerBuilder<T> 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 <T extends ServerBuilder<T>> void serverFactoryAutoConfiguredWithCustomizers(
ApplicationContextRunner contextRunner, ServerBuilder<T> mockServerBuilder,
Class<?> expectedServerFactoryType) {
ServerBuilderCustomizer<T> customizer1 = (serverBuilder) -> serverBuilder.keepAliveTime(40L, TimeUnit.SECONDS);
ServerBuilderCustomizer<T> 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;
}
}
}

View File

@@ -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<TestCustomizer<?>> 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<NettyServerBuilder> {
@Override
public void customize(NettyServerBuilder serverBuilder) {
serverBuilder.maxConnectionAge(100, TimeUnit.SECONDS);
}
}
/**
* Test customizer that will match all {@link ServerBuilderCustomizer}.
*/
static class TestCustomizer<T extends ServerBuilder<T>> implements ServerBuilderCustomizer<T> {
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<NettyServerBuilder> {
}
/**
* Test customizer that will match only
* {@link io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder}.
*/
static class TestShadedNettyServerBuilderCustomizer
extends TestCustomizer<io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder> {
}
}

View File

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