Add Spring gRPC server support

Add support for Spring gRPC server applications.

Closes gh-49044

Co-authored-by: Phillip Webb <phil.webb@broadcom.com>
This commit is contained in:
Chris Bono
2026-03-19 15:12:30 -07:00
committed by Phillip Webb
co-authored by Phillip Webb
parent a052376237
commit e61bb6df5b
58 changed files with 4098 additions and 0 deletions
@@ -110,6 +110,7 @@ dependencies {
implementation(project(path: ":module:spring-boot-data-redis-test"))
implementation(project(path: ":module:spring-boot-devtools"))
implementation(project(path: ":module:spring-boot-graphql-test"))
implementation(project(path: ":module:spring-boot-grpc-server"))
implementation(project(path: ":module:spring-boot-health"))
implementation(project(path: ":module:spring-boot-hibernate"))
implementation(project(path: ":module:spring-boot-http-converter"))
@@ -97,6 +97,9 @@ dependencies {
api(project(":module:spring-boot-graphql")) {
transitive = false
}
api(project(":module:spring-boot-grpc-server")) {
transitive = false
}
api(project(":module:spring-boot-groovy-templates")) {
transitive = false
}
@@ -0,0 +1,52 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id "java-library"
id "org.springframework.boot.auto-configuration"
id "org.springframework.boot.configuration-properties"
id "org.springframework.boot.deployed"
id "org.springframework.boot.optional-dependencies"
}
description = "Spring Boot gRPC Server"
dependencies {
api(project(":core:spring-boot"))
api("org.springframework.grpc:spring-grpc-core")
optional(project(":core:spring-boot-autoconfigure"))
optional("io.grpc:grpc-servlet-jakarta")
optional("io.grpc:grpc-services")
optional("io.grpc:grpc-netty")
optional("io.grpc:grpc-netty-shaded")
optional("io.grpc:grpc-inprocess")
optional("io.grpc:grpc-kotlin-stub") {
exclude group: "javax.annotation", module: "javax.annotation-api"
}
optional("jakarta.servlet:jakarta.servlet-api")
testImplementation(project(":core:spring-boot-test"))
testImplementation(project(":test-support:spring-boot-test-support"))
testImplementation(testFixtures(project(":core:spring-boot-autoconfigure")))
testImplementation("org.springframework:spring-web")
testRuntimeOnly("ch.qos.logback:logback-classic")
}
tasks.named("compileTestJava") {
options.nullability.checking = "tests"
}
@@ -0,0 +1,116 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import io.grpc.ServerServiceDefinition;
import io.grpc.ServiceDescriptor;
import io.grpc.servlet.jakarta.GrpcServlet;
import io.grpc.servlet.jakarta.ServletServerBuilder;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRegistration.Dynamic;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.web.servlet.DynamicRegistrationBean;
import org.springframework.core.log.LogMessage;
import org.springframework.grpc.server.service.GrpcServiceConfigurer;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
import org.springframework.grpc.server.service.GrpcServiceSpec;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* {@link DynamicRegistrationBean} that can be used to register a {@link GrpcServlet}.
*
* @author David Syer
* @author Chris Bono
* @author Toshiaki Maki
* @author Phillip Webb
* @since 4.0.0
*/
public class GrpcServletRegistration extends DynamicRegistrationBean<Dynamic> {
private static Log logger = LogFactory.getLog(GrpcServletRegistration.class);
private final GrpcServlet servlet;
private final String[] urlMappings;
/**
* Create a new {@link GrpcServletRegistration} instance.
* @param serviceDiscoverer the gRPC service discoverer
* @param serviceConfigurer the gRPC service configurer
*/
public GrpcServletRegistration(GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer) {
this(serviceDiscoverer, serviceConfigurer, null);
}
/**
* Create a new {@link GrpcServletRegistration} instance.
* @param serviceDiscoverer the gRPC service discoverer
* @param serviceConfigurer the gRPC service configurer
* @param serverBuilderCustomizer an optional customizer to configure the
* {@link ServletServerBuilder}
*/
public GrpcServletRegistration(GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer,
@Nullable Consumer<ServletServerBuilder> serverBuilderCustomizer) {
Assert.notNull(serviceDiscoverer, "'serviceDiscoverer' must not be null");
Assert.notNull(serviceConfigurer, "'serviceConfigurer' must not be null");
ServletServerBuilder builder = new ServletServerBuilder();
List<String> urlMappings = new ArrayList<>();
for (GrpcServiceSpec spec : serviceDiscoverer.findServices()) {
ServiceDescriptor descriptor = spec.service().bindService().getServiceDescriptor();
logger.info(LogMessage.format("Registering servlet gRPC service: %s", descriptor.getName()));
urlMappings.add("/" + descriptor.getName() + "/*");
ServerServiceDefinition definition = serviceConfigurer.configure(spec, null);
builder.addService(definition);
}
if (serverBuilderCustomizer != null) {
serverBuilderCustomizer.accept(builder);
}
this.servlet = builder.buildServlet();
this.urlMappings = urlMappings.toArray(String[]::new);
}
@Override
protected Dynamic addRegistration(String description, ServletContext servletContext) {
return servletContext.addServlet(getName(), this.servlet);
}
@Override
protected void configure(Dynamic registration) {
super.configure(registration);
if (!ObjectUtils.isEmpty(this.urlMappings)) {
registration.addMapping(this.urlMappings);
}
}
@Override
protected String getDescription() {
return getName();
}
private String getName() {
return getOrDeduceName(this.servlet);
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.context.annotation.Conditional;
/**
* {@link Conditional @Conditional} that matches when
* {@code spring.grpc.server.factory.enabled} is {@code true} or missing.
*
* @author Phillip Webb
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@ConditionalOnBooleanProperty(name = "spring.grpc.server.factory.enabled", matchIfMissing = true)
@interface ConditionalOnGrpcServerFactoryEnabled {
}
@@ -0,0 +1,47 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.grpc.server.GrpcServletRegistration;
import org.springframework.context.annotation.Conditional;
import org.springframework.grpc.server.GrpcServerFactory;
import org.springframework.grpc.server.InProcessGrpcServerFactory;
/**
* {@link Conditional @Conditional} that matches when no network gRPC server is found.
* Concretely:
* <ul>
* <li>There are no {@link GrpcServletRegistration} beans.</li>
* <li>There are no {@link GrpcServerFactory} beans (ignoring
* {@link InProcessGrpcServerFactory} beans)</li>
* </ul>
*
* @author Phillip Webb
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(MissingNetworkGrpcServerCondition.class)
@interface ConditionalOnMissingNetworkGrpcServer {
}
@@ -0,0 +1,107 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.util.List;
import io.grpc.BindableService;
import io.grpc.CompressorRegistry;
import io.grpc.DecompressorRegistry;
import io.grpc.Grpc;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.grpc.server.GlobalServerInterceptor;
import org.springframework.grpc.server.GrpcServerFactory;
import org.springframework.grpc.server.ServerBuilderCustomizer;
import org.springframework.grpc.server.exception.CompositeGrpcExceptionHandler;
import org.springframework.grpc.server.exception.GrpcExceptionHandler;
import org.springframework.grpc.server.exception.GrpcExceptionHandlerInterceptor;
import org.springframework.grpc.server.exception.ReactiveStubBeanDefinitionRegistrar;
import org.springframework.grpc.server.service.DefaultGrpcServiceConfigurer;
import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer;
import org.springframework.grpc.server.service.GrpcServiceConfigurer;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring gRPC server-side
* components.
*
* @author David Syer
* @author Chris Bono
* @author Phillip Webb
* @since 4.1.0
*/
@AutoConfiguration
@ConditionalOnClass({ GrpcServerFactory.class, Grpc.class })
@ConditionalOnBean(BindableService.class)
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
@EnableConfigurationProperties(GrpcServerProperties.class)
@Import({ GrpcServerCodecConfiguration.class, ServletGrpcServerConfiguration.class,
ShadedNettyGrpcServerConfiguration.class, NettyGrpcServerConfiguration.class,
InProcessGrpcServerConfiguration.class })
public final class GrpcServerAutoConfiguration {
@Bean
GrpcServerBuilderCustomizers grpcServerBuilderCustomizers(GrpcServerProperties grpcServerProperties,
ObjectProvider<CompressorRegistry> compressorRegistry,
ObjectProvider<DecompressorRegistry> decompressorRegistry,
ObjectProvider<GrpcServerExecutorProvider> executorProvider,
ObjectProvider<ServerBuilderCustomizer<?>> customizers) {
return new GrpcServerBuilderCustomizers(grpcServerProperties, compressorRegistry, decompressorRegistry,
executorProvider, customizers);
}
@Bean
@ConditionalOnMissingBean(GrpcServiceConfigurer.class)
DefaultGrpcServiceConfigurer grpcServiceConfigurer(ApplicationContext applicationContext) {
return new DefaultGrpcServiceConfigurer(applicationContext);
}
@Bean
@ConditionalOnMissingBean(GrpcServiceDiscoverer.class)
DefaultGrpcServiceDiscoverer grpcServiceDiscoverer(ApplicationContext applicationContext) {
return new DefaultGrpcServiceDiscoverer(applicationContext);
}
@Bean
@GlobalServerInterceptor
@ConditionalOnMissingBean
GrpcExceptionHandlerInterceptor globalExceptionHandlerInterceptor(List<GrpcExceptionHandler> exceptionHandlers) {
CompositeGrpcExceptionHandler compositeHandler = new CompositeGrpcExceptionHandler(
exceptionHandlers.toArray(GrpcExceptionHandler[]::new));
return new GrpcExceptionHandlerInterceptor(compositeHandler);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "com.salesforce.reactivegrpc.common.Function")
@Import(ReactiveStubBeanDefinitionRegistrar.class)
static class ReactiveStubConfiguration {
}
}
@@ -0,0 +1,89 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import io.grpc.CompressorRegistry;
import io.grpc.DecompressorRegistry;
import io.grpc.ServerBuilder;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.util.LambdaSafe;
import org.springframework.grpc.server.ServerBuilderCustomizer;
/**
* Invokes the customizations to a {@link ServerBuilder} based on the provided beans.
*
* @author Chris Bono
* @author Phillip Webb
*/
class GrpcServerBuilderCustomizers {
private final List<ServerBuilderCustomizer<?>> customizers;
GrpcServerBuilderCustomizers(GrpcServerProperties grpcServerProperties,
ObjectProvider<CompressorRegistry> compressorRegistry,
ObjectProvider<DecompressorRegistry> decompressorRegistry,
ObjectProvider<GrpcServerExecutorProvider> executorProvider,
ObjectProvider<ServerBuilderCustomizer<?>> customizers) {
this(grpcServerProperties, compressorRegistry.getIfAvailable(), decompressorRegistry.getIfAvailable(),
executorProvider.getIfAvailable(), customizers.orderedStream().toList());
}
GrpcServerBuilderCustomizers(List<? extends ServerBuilderCustomizer<?>> customizers) {
this(null, null, null, null, customizers);
}
GrpcServerBuilderCustomizers(@Nullable GrpcServerProperties grpcServerProperties,
@Nullable CompressorRegistry compressorRegistry, @Nullable DecompressorRegistry decompressorRegistry,
@Nullable GrpcServerExecutorProvider executorProvider,
List<? extends ServerBuilderCustomizer<?>> customizers) {
List<ServerBuilderCustomizer<?>> all = new ArrayList<>();
addCustomizer(all, compressorRegistry, ServerBuilder::compressorRegistry);
addCustomizer(all, decompressorRegistry, ServerBuilder::decompressorRegistry);
addCustomizer(all, executorProvider, (builder, bean) -> builder.executor(bean.getExecutor()));
if (grpcServerProperties != null) {
all.add(new PropertiesServerBuilderCustomizer<>(grpcServerProperties));
}
all.addAll(customizers);
this.customizers = List.copyOf(all);
}
private static <B extends ServerBuilder<B>, T> void addCustomizer(List<ServerBuilderCustomizer<?>> customizers,
@Nullable T bean, BiConsumer<B, T> action) {
if (bean != null) {
ServerBuilderCustomizer<B> customizer = (builder) -> action.accept(builder, bean);
customizers.add(customizer);
}
}
<T extends ServerBuilder<T>> List<ServerBuilderCustomizer<T>> forFactory() {
return List.of(this::apply);
}
@SuppressWarnings("unchecked")
<T extends ServerBuilder<?>> void apply(T builder) {
LambdaSafe.callbacks(ServerBuilderCustomizer.class, this.customizers, builder)
.withLogger(GrpcServerBuilderCustomizers.class)
.invoke((customizer) -> customizer.customize(builder));
}
}
@@ -0,0 +1,80 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.util.List;
import io.grpc.Compressor;
import io.grpc.CompressorRegistry;
import io.grpc.Decompressor;
import io.grpc.DecompressorRegistry;
import io.grpc.ServerBuilder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* The configuration that contains all gRPC codec related beans.
*
* @author Andrei Lisa
*/
@Configuration(proxyBeanMethods = false)
class GrpcServerCodecConfiguration {
/**
* The compressor registry that is set on the
* {@link ServerBuilder#compressorRegistry(CompressorRegistry) server builder} .
* @param compressors the compressors to use on the registry
* @return a new {@link CompressorRegistry#newEmptyInstance() registry} with the
* specified compressors or the {@link CompressorRegistry#getDefaultInstance() default
* registry} if no custom compressors are available in the application context.
*/
@Bean
@ConditionalOnMissingBean
CompressorRegistry grpcCompressorRegistry(List<Compressor> compressors) {
if (compressors.isEmpty()) {
return CompressorRegistry.getDefaultInstance();
}
CompressorRegistry registry = CompressorRegistry.newEmptyInstance();
compressors.forEach(registry::register);
return registry;
}
/**
* The decompressor registry that is set on the
* {@link ServerBuilder#decompressorRegistry(DecompressorRegistry) server builder}.
* @param decompressors the decompressors to use on the registry
* @return a new {@link DecompressorRegistry#emptyInstance() registry} with the
* specified decompressors or the {@link DecompressorRegistry#getDefaultInstance()
* default registry} if no custom decompressors are available in the application
* context.
*/
@Bean
@ConditionalOnMissingBean
DecompressorRegistry grpcDecompressorRegistry(List<Decompressor> decompressors) {
if (decompressors.isEmpty()) {
return DecompressorRegistry.getDefaultInstance();
}
DecompressorRegistry registry = DecompressorRegistry.emptyInstance();
for (Decompressor decompressor : decompressors) {
registry = registry.with(decompressor, false);
}
return registry;
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.util.concurrent.Executor;
/**
* Strategy interface to determine the {@link Executor} to use for the gRPC server.
*
* @author Chris Bono
* @since 4.1.0
*/
@FunctionalInterface
public interface GrpcServerExecutorProvider {
/**
* Returns a {@link Executor} for the gRPC server, if it needs to be customized.
* @return the executor to use for the gRPC server
*/
Executor getExecutor();
}
@@ -0,0 +1,37 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import org.springframework.grpc.server.GrpcServerFactory;
/**
* Callback interface that can be implemented by beans wishing to customize the
* {@link GrpcServerFactory server factory} before it is fully initialized.
*
* @author Chris Bono
* @since 4.1.0
*/
@FunctionalInterface
public interface GrpcServerFactoryCustomizer {
/**
* Customize the given {@link GrpcServerFactory}.
* @param serverFactory the server factory to customize
*/
void customize(GrpcServerFactory serverFactory);
}
@@ -0,0 +1,452 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.net.InetAddress;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import io.grpc.TlsServerCredentials.ClientAuth;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DataSizeUnit;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;
/**
* {@link ConfigurationProperties Properties} for Spring gRPC servers.
*
* @author Chris Bono
* @author Phillip Webb
* @since 4.1.0
*/
@ConfigurationProperties("spring.grpc.server")
public class GrpcServerProperties {
/**
* Port on which the gRPC server should listen. Use '0' to bind to a dynamic port.
*/
private @Nullable Integer port;
/**
* Network address to which the gRPC server should bind.
*/
private @Nullable InetAddress address;
private final Shutdown shutdown = new Shutdown();
private final Inbound inbound = new Inbound();
private final Inprocess inprocess = new Inprocess();
private final Keepalive keepalive = new Keepalive();
private final Ssl ssl = new Ssl();
private final Netty netty = new Netty();
public @Nullable Integer getPort() {
return this.port;
}
public void setPort(@Nullable Integer port) {
this.port = port;
}
public @Nullable InetAddress getAddress() {
return this.address;
}
public void setAddress(@Nullable InetAddress address) {
this.address = address;
}
public Shutdown getShutdown() {
return this.shutdown;
}
public Inbound getInbound() {
return this.inbound;
}
public Inprocess getInprocess() {
return this.inprocess;
}
public Keepalive getKeepalive() {
return this.keepalive;
}
public Ssl getSsl() {
return this.ssl;
}
public Netty getNetty() {
return this.netty;
}
/**
* Server shutdown properties.
*/
public static class 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 gracePeriod = Duration.ofSeconds(30);
public Duration getGracePeriod() {
return this.gracePeriod;
}
public void setGracePeriod(Duration gracePeriod) {
this.gracePeriod = gracePeriod;
}
}
/**
* In-bound properties.
*/
public static class Inbound {
private final Message message = new Message();
private final Metadata metadata = new Metadata();
public Message getMessage() {
return this.message;
}
public Metadata getMetadata() {
return this.metadata;
}
/**
* In-bound message properties.
*/
public static class Message {
/**
* Maximum message size allowed to be received by the server (default 4MiB).
*/
@DataSizeUnit(DataUnit.BYTES)
private DataSize maxSize = DataSize.ofBytes(4194304);
public DataSize getMaxSize() {
return this.maxSize;
}
public void setMaxSize(DataSize maxSize) {
this.maxSize = maxSize;
}
}
/**
* In-bound metadata properties.
*/
public static class Metadata {
/**
* Maximum metadata size allowed to be received by the server (default 8KiB).
*/
@DataSizeUnit(DataUnit.BYTES)
private DataSize maxSize = DataSize.ofBytes(8192);
public DataSize getMaxSize() {
return this.maxSize;
}
public void setMaxSize(DataSize maxSize) {
this.maxSize = maxSize;
}
}
}
/**
* In-process gRPC properties.
*/
public static class Inprocess {
/**
* The name of the in-process server or null to not start the in-process server.
*/
private @Nullable String name;
public @Nullable String getName() {
return this.name;
}
public void setName(@Nullable String name) {
this.name = name;
}
}
/**
* Keep-alive properties.
*/
public static class Keepalive {
/**
* Duration without read activity before sending a keep alive ping (default 2h).
*/
@DurationUnit(ChronoUnit.SECONDS)
private @Nullable Duration time = Duration.ofHours(2);
/**
* Maximum time to wait for read activity after sending a keep alive ping. If
* sender does not receive an acknowledgment within this time, it will close the
* connection (default 20s).
*/
@DurationUnit(ChronoUnit.SECONDS)
private @Nullable Duration timeout = Duration.ofSeconds(20);
private final Permit permit = new Permit();
private final Connection connection = new Connection();
public @Nullable Duration getTime() {
return this.time;
}
public void setTime(@Nullable Duration time) {
this.time = time;
}
public @Nullable Duration getTimeout() {
return this.timeout;
}
public void setTimeout(@Nullable Duration timeout) {
this.timeout = timeout;
}
public Permit getPermit() {
return this.permit;
}
public Connection getConnection() {
return this.connection;
}
/**
* Keep-alive permit properties.
*/
public static class Permit {
/**
* Maximum keep-alive time clients are permitted to configure (default 5m).
*/
@DurationUnit(ChronoUnit.SECONDS)
private @Nullable Duration time = Duration.ofMinutes(5);
/**
* Whether clients are permitted to send keep alive pings when there are no
* outstanding RPCs on the connection (default false).
*/
private boolean withoutCalls;
public @Nullable Duration getTime() {
return this.time;
}
public void setTime(@Nullable Duration time) {
this.time = time;
}
public boolean isWithoutCalls() {
return this.withoutCalls;
}
public void setWithoutCalls(boolean withoutCalls) {
this.withoutCalls = withoutCalls;
}
}
/**
* Keep-alive connection properties.
*/
public static class Connection {
/**
* Maximum time a connection can remain idle before being gracefully
* terminated (default infinite).
*/
@DurationUnit(ChronoUnit.SECONDS)
private @Nullable Duration maxIdleTime;
/**
* Maximum time a connection may exist before being gracefully terminated
* (default infinite).
*/
@DurationUnit(ChronoUnit.SECONDS)
private @Nullable Duration maxAge;
/**
* Maximum time for graceful connection termination (default infinite).
*/
@DurationUnit(ChronoUnit.SECONDS)
private @Nullable Duration gracePeriod = Duration.ofSeconds(30);
public @Nullable Duration getMaxIdleTime() {
return this.maxIdleTime;
}
public void setMaxIdleTime(@Nullable Duration maxIdleTime) {
this.maxIdleTime = maxIdleTime;
}
public @Nullable Duration getMaxAge() {
return this.maxAge;
}
public void setMaxAge(@Nullable Duration maxAge) {
this.maxAge = maxAge;
}
public @Nullable Duration getGracePeriod() {
return this.gracePeriod;
}
public void setGracePeriod(@Nullable Duration gracePeriod) {
this.gracePeriod = gracePeriod;
}
}
}
/**
* SSL properties.
*/
public static class Ssl {
/**
* Whether to enable SSL support.
*/
private @Nullable Boolean enabled;
/**
* Client authentication mode.
*/
private ClientAuth clientAuth = ClientAuth.NONE;
/**
* SSL bundle name. Should match a bundle configured in spring.ssl.bundle.
*/
private @Nullable String bundle;
/**
* Flag to indicate that client authentication is secure (i.e. certificates are
* checked). Do not set this to false in production.
*/
private boolean secure = true;
public @Nullable Boolean getEnabled() {
return this.enabled;
}
public void setEnabled(@Nullable Boolean enabled) {
this.enabled = enabled;
}
public @Nullable String getBundle() {
return this.bundle;
}
public void setBundle(@Nullable String bundle) {
this.bundle = bundle;
}
public void setClientAuth(ClientAuth clientAuth) {
this.clientAuth = clientAuth;
}
public ClientAuth getClientAuth() {
return this.clientAuth;
}
public void setSecure(boolean secure) {
this.secure = secure;
}
public boolean isSecure() {
return this.secure;
}
}
/**
* Netty server properties.
*/
public static class Netty {
/**
* Transport mechanism used for Netty and Netty Shaded servers. If not specified
* will the appropriate transport will be picked based on the
* 'deomain-socket-path' or 'address/port'.
*/
private @Nullable Transport transport;
/**
* Path of the domain socket that should be used.
*/
private @Nullable String domainSocketPath;
public @Nullable Transport getTransport() {
return this.transport;
}
public void setTransport(@Nullable Transport transport) {
this.transport = transport;
}
public @Nullable String getDomainSocketPath() {
return this.domainSocketPath;
}
public void setDomainSocketPath(@Nullable String domainSocketPath) {
this.domainSocketPath = domainSocketPath;
}
public enum Transport {
/**
* TCP transport.
*/
TCP,
/**
* Domain socket transport.
*/
DOMAIN_SOCKET
}
}
}
@@ -0,0 +1,58 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import io.grpc.BindableService;
import io.grpc.protobuf.services.ProtoReflectionServiceV1;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.server.GrpcServerFactory;
/**
* {@link EnableAutoConfiguration Auto-configuration} for gRPC server services.
*
* @author Haris Zujo
* @author Dave Syer
* @author Chris Bono
* @author Andrey Litvitski
* @since 4.1.0
*/
@AutoConfiguration(before = GrpcServerAutoConfiguration.class)
@ConditionalOnClass({ GrpcServerFactory.class, io.grpc.Grpc.class })
@ConditionalOnBooleanProperty(name = "spring.grpc.server.enabled", matchIfMissing = true)
public final class GrpcServerServicesAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ProtoReflectionServiceV1.class)
@ConditionalOnBean(BindableService.class)
@ConditionalOnBooleanProperty(name = "spring.grpc.server.reflection.enabled", matchIfMissing = true)
static class GrpcServerReflectionServiceConfiguration {
@Bean
BindableService grpcServerReflectionService() {
return ProtoReflectionServiceV1.newInstance();
}
}
}
@@ -0,0 +1,71 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.server.InProcessGrpcServerFactory;
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
import org.springframework.grpc.server.service.GrpcServiceConfigurer;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
import org.springframework.util.Assert;
/**
* {@link Configuration @Configuration} for an in-process gRPC server.
*
* @author David Syer
* @author Chris Bono
* @author Toshiaki Maki
* @author Phillip Webb
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(InProcessGrpcServerFactory.class)
@ConditionalOnProperty("spring.grpc.server.inprocess.name")
@ConditionalOnGrpcServerFactoryEnabled
class InProcessGrpcServerConfiguration {
@Bean
InProcessGrpcServerFactory inProcessGrpcServerFactory(GrpcServerProperties properties,
GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer,
GrpcServerBuilderCustomizers grpcServerBuilderCustomizers,
ObjectProvider<GrpcServerFactoryCustomizer> customizers) {
String inProcessName = properties.getInprocess().getName();
Assert.state(inProcessName != null, "No inprocess name provided");
InProcessGrpcServerFactory factory = new InProcessGrpcServerFactory(inProcessName,
grpcServerBuilderCustomizers.forFactory());
customizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
serviceDiscoverer.findServices()
.stream()
.map((spec) -> serviceConfigurer.configure(spec, factory))
.forEach(factory::addService);
return factory;
}
@Bean
@ConditionalOnBean(InProcessGrpcServerFactory.class)
@ConditionalOnMissingBean(name = "inProcessGrpcServerLifecycle")
GrpcServerLifecycle inProcessGrpcServerLifecycle(InProcessGrpcServerFactory factory,
GrpcServerProperties properties, ApplicationEventPublisher eventPublisher) {
return new GrpcServerLifecycle(factory, properties.getShutdown().getGracePeriod(), eventPublisher);
}
}
@@ -0,0 +1,47 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.grpc.server.GrpcServletRegistration;
import org.springframework.context.annotation.Condition;
import org.springframework.grpc.server.GrpcServerFactory;
import org.springframework.grpc.server.InProcessGrpcServerFactory;
/**
* {@link Condition} that matches when no network gRPC server is found.
*
* @author Phillip Webb
*/
class MissingNetworkGrpcServerCondition extends AllNestedConditions {
MissingNetworkGrpcServerCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnMissingBean(GrpcServletRegistration.class)
static class MissingGrpcServletRegistrationBean {
}
@ConditionalOnMissingBean(value = GrpcServerFactory.class, ignored = InProcessGrpcServerFactory.class)
static class MissingGrpcFactoryBean {
}
}
@@ -0,0 +1,94 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.net.InetAddress;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties.Netty.Transport;
import org.springframework.grpc.internal.GrpcUtils;
import org.springframework.grpc.server.GrpcServerFactory;
import org.springframework.util.StringUtils;
/**
* Address {@link GrpcServerFactory} address.
*
* @author Phillip Webb
* @param transport the transport to use
* @param address the bind address
* @param port the listen port
* @param domainSocketPath the domain socket path
*/
record NettyAddress(@Nullable Transport transport, @Nullable InetAddress address, @Nullable Integer port,
@Nullable String domainSocketPath) {
@Override
public final String toString() {
Transport transport = (this.transport != null) ? this.transport : deduceTransport();
return switch (transport) {
case TCP -> tcpAddress();
case DOMAIN_SOCKET -> domainSocketAddress();
};
}
private Transport deduceTransport() {
MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> {
entries.put("spring.grpc.server.address", this.address);
entries.put("spring.grpc.server.netty.domain-socket-path", this.domainSocketPath);
});
MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> {
entries.put("spring.grpc.server.port", this.port);
entries.put("spring.grpc.server.netty.domain-socket-path", this.domainSocketPath);
});
if (this.address != null || this.port != null) {
return Transport.TCP;
}
if (this.domainSocketPath != null) {
return Transport.DOMAIN_SOCKET;
}
return Transport.TCP;
}
private String tcpAddress() {
String address = (this.address != null) ? toString(this.address) : GrpcUtils.ANY_IP_ADDRESS;
int port = (this.port != null) ? this.port : GrpcUtils.DEFAULT_PORT;
return address + ":" + port;
}
private String domainSocketAddress() {
if (!StringUtils.hasText(this.domainSocketPath)) {
throw new InvalidConfigurationPropertyValueException("spring.grpc.server.netty.domain-socket-path",
this.domainSocketPath,
"A path is required when spring.grpc.server.netty.transport is set to 'domain-socket'");
}
return "unix:" + this.domainSocketPath;
}
private static String toString(InetAddress address) {
String hostName = address.getHostName();
return (hostName != null) ? hostName : address.getHostAddress();
}
static NettyAddress fromProperties(GrpcServerProperties properties) {
return new NettyAddress(properties.getNetty().getTransport(), properties.getAddress(), properties.getPort(),
properties.getNetty().getDomainSocketPath());
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import io.grpc.netty.NettyServerBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.server.NettyGrpcServerFactory;
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
import org.springframework.grpc.server.service.GrpcServiceConfigurer;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
/**
* {@link Configuration @Configuration} for a Netty gRPC server.
*
* @author David Syer
* @author Chris Bono
* @author Toshiaki Maki
* @author Phillip Webb
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(NettyServerBuilder.class)
@ConditionalOnMissingNetworkGrpcServer
@ConditionalOnGrpcServerFactoryEnabled
class NettyGrpcServerConfiguration {
@Bean
NettyGrpcServerFactory nettyGrpcServerFactory(GrpcServerProperties properties,
GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer,
GrpcServerBuilderCustomizers grpcServerBuilderCustomizers, SslBundles bundles,
ObjectProvider<GrpcServerFactoryCustomizer> customizers) {
NettyAddress address = NettyAddress.fromProperties(properties);
ServerCredentials credentials = ServerCredentials.get(properties.getSsl(), bundles,
InsecureTrustManagerFactory.INSTANCE);
NettyGrpcServerFactory factory = new NettyGrpcServerFactory(address.toString(),
grpcServerBuilderCustomizers.forFactory(), credentials.keyManagerFactory(),
credentials.trustManagerFactory(), credentials.clientAuth());
customizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
serviceDiscoverer.findServices()
.stream()
.map((spec) -> serviceConfigurer.configure(spec, factory))
.forEach(factory::addService);
return factory;
}
@Bean
@ConditionalOnMissingBean(name = "nettyGrpcServerLifecycle")
GrpcServerLifecycle nettyGrpcServerLifecycle(NettyGrpcServerFactory factory, GrpcServerProperties properties,
ApplicationEventPublisher eventPublisher) {
return new GrpcServerLifecycle(factory, properties.getShutdown().getGracePeriod(), eventPublisher);
}
}
@@ -0,0 +1,89 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import io.grpc.ManagedChannelBuilder;
import io.grpc.ServerBuilder;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties.Inbound;
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties.Keepalive;
import org.springframework.grpc.server.ServerBuilderCustomizer;
import org.springframework.util.ClassUtils;
import org.springframework.util.unit.DataSize;
/**
* {@link ServerBuilderCustomizer} that maps {@link GrpcServerProperties} to a
* {@link ManagedChannelBuilder}.
*
* @param <T> the type of server builder
* @param properties the properties to map
* @author Chris Bono
* @author Phillip Webb
*/
record PropertiesServerBuilderCustomizer<T extends ServerBuilder<T>>(
GrpcServerProperties properties) implements ServerBuilderCustomizer<T> {
@Override
public void customize(T builder) {
mapInboundProperties(this.properties.getInbound(), builder);
if (supportsKeepAliveProperties(builder)) {
mapKeepaliveProperties(this.properties.getKeepalive(), builder);
}
}
private void mapInboundProperties(Inbound properties, T builder) {
PropertyMapper map = PropertyMapper.get();
map.from(properties.getMessage()::getMaxSize).asInt(DataSize::toBytes).to(builder::maxInboundMessageSize);
map.from(properties.getMetadata()::getMaxSize).asInt(DataSize::toBytes).to(builder::maxInboundMetadataSize);
}
private void mapKeepaliveProperties(Keepalive properties, T builder) {
PropertyMapper map = PropertyMapper.get();
map.from(properties::getTime).to(durationProperty(builder::keepAliveTime));
map.from(properties::getTimeout).to(durationProperty(builder::keepAliveTimeout));
map.from(properties.getConnection()::getMaxIdleTime).to(durationProperty(builder::maxConnectionIdle));
map.from(properties.getConnection()::getMaxAge).to(durationProperty(builder::maxConnectionAge));
map.from(properties.getConnection()::getGracePeriod).to(durationProperty(builder::maxConnectionAgeGrace));
map.from(properties.getPermit()::getTime).to(durationProperty(builder::permitKeepAliveTime));
map.from(properties.getPermit()::isWithoutCalls).to(builder::permitKeepAliveWithoutCalls);
}
private Consumer<Duration> durationProperty(BiConsumer<Long, TimeUnit> setter) {
return (duration) -> setter.accept(duration.toNanos(), TimeUnit.NANOSECONDS);
}
private boolean supportsKeepAliveProperties(T builder) {
return !isInstance("io.grpc.inprocess.InProcessServerBuilder", builder)
&& !isInstance("io.grpc.servlet.jakarta.ServletServerBuilder", builder);
}
private boolean isInstance(String className, T builder) {
try {
return ClassUtils.forName(className, builder.getClass().getClassLoader()).isInstance(builder);
}
catch (ClassNotFoundException | LinkageError ex) {
return false;
}
}
}
@@ -0,0 +1,66 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import io.grpc.TlsServerCredentials.ClientAuth;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.ssl.SslManagerBundle;
import org.springframework.util.Assert;
/**
* Server credential details to use with gRPC servers.
*
* @author Chris Bono
* @author Phillip Webb
* @param keyManagerFactory the key manager factory to use or {@code null}
* @param trustManagerFactory the trust manager factory to use or {@code null}
* @param clientAuth the client auth to use
*/
record ServerCredentials(@Nullable KeyManagerFactory keyManagerFactory,
@Nullable TrustManagerFactory trustManagerFactory, ClientAuth clientAuth) {
/**
* Return the credentials to use based on the given properties.
* @param properties the SSL properties
* @param bundles the SSL bundles
* @param insecureTrustManagerFactory the trust manager factory to use for insecure
* connections
* @return the server credentials to use
*/
static ServerCredentials get(GrpcServerProperties.Ssl properties, SslBundles bundles,
TrustManagerFactory insecureTrustManagerFactory) {
Boolean enabled = properties.getEnabled();
String bundle = properties.getBundle();
ClientAuth clientAuth = properties.getClientAuth();
if (Boolean.FALSE.equals(enabled) || (enabled == null && bundle == null)) {
return new ServerCredentials(null, null, clientAuth);
}
Assert.state(bundle != null,
() -> "SSL bundle-name is requested when 'spring.grpc.server.ssl.enabled' is true");
SslManagerBundle managers = bundles.getBundle(bundle).getManagers();
KeyManagerFactory keyManagerFactory = managers.getKeyManagerFactory();
TrustManagerFactory trustManagerFactory = (!properties.isSecure()) ? insecureTrustManagerFactory
: managers.getTrustManagerFactory();
return new ServerCredentials(keyManagerFactory, trustManagerFactory, clientAuth);
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import io.grpc.servlet.jakarta.GrpcServlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.grpc.server.GrpcServletRegistration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.server.service.GrpcServiceConfigurer;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
/**
* {@link Configuration @Configuration} for a Servlet gRPC server.
*
* @author David Syer
* @author Chris Bono
* @author Toshiaki Maki
* @author Phillip Webb
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(GrpcServlet.class)
@ConditionalOnMissingNetworkGrpcServer
@ConditionalOnBooleanProperty(name = "spring.grpc.server.servlet.enabled", matchIfMissing = true)
class ServletGrpcServerConfiguration {
@Bean
GrpcServletRegistration grpcServletRegistration(GrpcServiceDiscoverer serviceDiscoverer,
GrpcServiceConfigurer serviceConfigurer, GrpcServerBuilderCustomizers grpcServerBuilderCustomizers) {
return new GrpcServletRegistration(serviceDiscoverer, serviceConfigurer, grpcServerBuilderCustomizers::apply);
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.server.ShadedNettyGrpcServerFactory;
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
import org.springframework.grpc.server.service.GrpcServiceConfigurer;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
/**
* {@link Configuration @Configuration} for a Shaded Netty gRPC server.
*
* @author David Syer
* @author Chris Bono
* @author Toshiaki Maki
* @author Phillip Webb
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(NettyServerBuilder.class)
@ConditionalOnMissingNetworkGrpcServer
@ConditionalOnGrpcServerFactoryEnabled
class ShadedNettyGrpcServerConfiguration {
@Bean
ShadedNettyGrpcServerFactory shadedNettyGrpcServerFactory(GrpcServerProperties properties,
GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer,
GrpcServerBuilderCustomizers grpcServerBuilderCustomizers, SslBundles bundles,
ObjectProvider<GrpcServerFactoryCustomizer> customizers) {
NettyAddress address = NettyAddress.fromProperties(properties);
ServerCredentials serverCredentials = ServerCredentials.get(properties.getSsl(), bundles,
InsecureTrustManagerFactory.INSTANCE);
ShadedNettyGrpcServerFactory factory = new ShadedNettyGrpcServerFactory(address.toString(),
grpcServerBuilderCustomizers.forFactory(), serverCredentials.keyManagerFactory(),
serverCredentials.trustManagerFactory(), serverCredentials.clientAuth());
customizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
serviceDiscoverer.findServices()
.stream()
.map((spec) -> serviceConfigurer.configure(spec, factory))
.forEach(factory::addService);
return factory;
}
@Bean
@ConditionalOnMissingBean(name = "shadedNettyGrpcServerLifecycle")
GrpcServerLifecycle shadedNettyGrpcServerLifecycle(ShadedNettyGrpcServerFactory factory,
GrpcServerProperties properties, ApplicationEventPublisher eventPublisher) {
return new GrpcServerLifecycle(factory, properties.getShutdown().getGracePeriod(), eventPublisher);
}
}
@@ -0,0 +1,23 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Auto-configuration for gRPC server.
*/
@NullMarked
package org.springframework.boot.grpc.server.autoconfigure;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,23 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Spring gRPC server support classes.
*/
@NullMarked
package org.springframework.boot.grpc.server;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,29 @@
{
"groups": [],
"properties": [
{
"name": "spring.grpc.server.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable gRPC server auto-configuration.",
"defaultValue": true
},
{
"name": "spring.grpc.server.factory.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable gRPC server factory bean auto-configuration.",
"defaultValue": true
},
{
"name": "spring.grpc.server.reflection.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable Reflection on the gRPC server.",
"defaultValue": true
},
{
"name": "spring.grpc.server.servlet.enabled",
"type": "java.lang.Boolean",
"description": "Whether to use a servlet server in a servlet-based web application. When the value is false, a native gRPC server will be created as long as one is available, and it will listen on its own port. Should only be needed if the GrpcServlet is on the classpath",
"defaultValue": true
}
]
}
@@ -0,0 +1,2 @@
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer=\
org.springframework.boot.grpc.server.autoconfigure.security.GrpcDisableCsrfHttpConfigurer
@@ -0,0 +1,2 @@
org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration
org.springframework.boot.grpc.server.autoconfigure.GrpcServerServicesAutoConfiguration
@@ -0,0 +1,152 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import io.grpc.BindableService;
import io.grpc.ServerInterceptor;
import io.grpc.ServerServiceDefinition;
import io.grpc.servlet.jakarta.GrpcServlet;
import io.grpc.servlet.jakarta.ServletServerBuilder;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRegistration.Dynamic;
import org.junit.jupiter.api.Test;
import org.springframework.grpc.server.service.GrpcServiceConfigurer;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
import org.springframework.grpc.server.service.GrpcServiceInfo;
import org.springframework.grpc.server.service.GrpcServiceSpec;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
/**
* Tests for {@link GrpcServletRegistration}.
*
* @author Phillip Webb
*/
class GrpcServletRegistrationTests {
private final GrpcServiceConfigurer serviceConfigurer = mock();
private final GrpcServiceDiscoverer serviceDiscoverer = mock();
@Test
@SuppressWarnings("NullAway") // Test null check
void createWhenServiceDiscovererIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new GrpcServletRegistration(null, this.serviceConfigurer))
.withMessage("'serviceDiscoverer' must not be null");
}
@Test
@SuppressWarnings("NullAway") // Test null check
void createWhenServiceConfigurerIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new GrpcServletRegistration(this.serviceDiscoverer, null))
.withMessage("'serviceConfigurer' must not be null");
}
@Test
void createWhenServerBuilderCustomizerIsNullDoesNotApplyCustomization() {
assertThatNoException()
.isThrownBy(() -> new GrpcServletRegistration(this.serviceDiscoverer, this.serviceConfigurer, null));
}
@Test
void createWhenServerBuilderCustomizerIsNotNullAppliesCustomization() {
Consumer<ServletServerBuilder> serverBuilderCustomizer = mock();
new GrpcServletRegistration(this.serviceDiscoverer, this.serviceConfigurer, serverBuilderCustomizer);
then(serverBuilderCustomizer).should().accept(any(ServletServerBuilder.class));
}
@Test
void addRegistrationAddsBuiltServlet() {
GrpcServletRegistration registration = new GrpcServletRegistration(this.serviceDiscoverer,
this.serviceConfigurer);
ServletContext servletContext = mock();
Dynamic result = mock();
given(servletContext.addServlet(eq("grpcServlet"), any(GrpcServlet.class))).willReturn(result);
assertThat(registration.addRegistration("test", servletContext)).isEqualTo(result);
}
@Test
void onStartupWhenHasServicesRegistersAndAddsUrlMappingsBasedOnDescriptorName() throws ServletException {
BindableService service1 = mock(BindableService.class);
ServerServiceDefinition serviceDefinition1 = ServerServiceDefinition.builder("s1").build();
given(service1.bindService()).willReturn(serviceDefinition1);
GrpcServiceInfo info1 = new GrpcServiceInfo(emptyServiceInterceptors(), new String[0], false);
BindableService service2 = mock(BindableService.class);
ServerServiceDefinition serviceDefinition2 = ServerServiceDefinition.builder("s2").build();
given(service2.bindService()).willReturn(serviceDefinition2);
GrpcServiceInfo info2 = new GrpcServiceInfo(emptyServiceInterceptors(), new String[0], false);
List<GrpcServiceSpec> specs = new ArrayList<>();
specs.add(new GrpcServiceSpec(service1, info1));
specs.add(new GrpcServiceSpec(service2, info2));
given(this.serviceDiscoverer.findServices()).willReturn(specs);
given(this.serviceConfigurer.configure(any(GrpcServiceSpec.class), eq(null))).willAnswer((invocation) -> {
GrpcServiceSpec spec = invocation.getArgument(0, GrpcServiceSpec.class);
return spec.service().bindService();
});
GrpcServletRegistration registration = new GrpcServletRegistration(this.serviceDiscoverer,
this.serviceConfigurer);
ServletContext servletContext = mock(ServletContext.class);
Dynamic result = mock(Dynamic.class);
given(servletContext.addServlet(eq("grpcServlet"), any(GrpcServlet.class))).willReturn(result);
registration.onStartup(servletContext);
then(result).should().addMapping("/s1/*", "/s2/*");
}
@Test
void onStartupWhenHasNoServicesDoesNotAddUrlMappings() throws ServletException {
given(this.serviceDiscoverer.findServices()).willReturn(Collections.emptyList());
given(this.serviceConfigurer.configure(any(GrpcServiceSpec.class), eq(null))).willAnswer((invocation) -> {
GrpcServiceSpec spec = invocation.getArgument(0, GrpcServiceSpec.class);
return spec.service().bindService();
});
GrpcServletRegistration registration = new GrpcServletRegistration(this.serviceDiscoverer,
this.serviceConfigurer);
ServletContext servletContext = mock(ServletContext.class);
Dynamic result = mock(Dynamic.class);
given(servletContext.addServlet(eq("grpcServlet"), any(GrpcServlet.class))).willReturn(result);
registration.onStartup(servletContext);
then(result).should(never()).addMapping();
}
@Test
void getDescriptionReturnsDeducedServletName() {
GrpcServletRegistration registration = new GrpcServletRegistration(this.serviceDiscoverer,
this.serviceConfigurer);
assertThat(registration.getDescription()).isEqualTo("grpcServlet");
}
@SuppressWarnings("unchecked")
private Class<? extends ServerInterceptor>[] emptyServiceInterceptors() {
return (Class<? extends ServerInterceptor>[]) new Class<?>[] {};
}
}
@@ -0,0 +1,465 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.util.concurrent.atomic.AtomicReference;
import io.grpc.BindableService;
import io.grpc.ServerServiceDefinition;
import io.grpc.ServiceDescriptor;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.internal.GrpcUtil;
import io.grpc.netty.NettyServerBuilder;
import io.grpc.servlet.jakarta.GrpcServlet;
import io.grpc.servlet.jakarta.ServletServerBuilder;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.grpc.server.GrpcServletRegistration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.ApplicationContextAssertProvider;
import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.server.GrpcServerFactory;
import org.springframework.grpc.server.InProcessGrpcServerFactory;
import org.springframework.grpc.server.NettyGrpcServerFactory;
import org.springframework.grpc.server.ServerBuilderCustomizer;
import org.springframework.grpc.server.ShadedNettyGrpcServerFactory;
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer;
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
import org.springframework.util.unit.DataSize;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests fir {@link GrpcServerAutoConfiguration}.
*
* @author Chris Bono
* @author Andrey Litvitski
* @author Phillip Webb
*/
class GrpcServerAutoConfigurationTests {
private static final AutoConfigurations autoConfigurations = AutoConfigurations
.of(GrpcServerAutoConfiguration.class, SslAutoConfiguration.class);
private final BindableService service = mock();
private final ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build();
private ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(autoConfigurations)
.with(this::noOpLifecycleBeans)
.with(this::serviceBean);
@BeforeEach
void setup() {
given(this.service.bindService()).willReturn(this.serviceDefinition);
}
@Test
void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() {
this.contextRunner.withClassLoader(new FilteredClassLoader(BindableService.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class));
}
@Test
void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() {
this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerFactory.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class));
}
@Test
void whenNoBindableServicesRegisteredAutoConfigurationIsSkipped() {
new ApplicationContextRunner().withConfiguration(autoConfigurations)
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class));
}
@Test
void whenServerEnabledPropertySetFalseThenAutoConfigurationIsSkipped() {
this.contextRunner.withPropertyValues("spring.grpc.server.enabled=false")
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerAutoConfiguration.class));
}
@Test
void whenServerEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GrpcServerAutoConfiguration.class));
}
@Test
void whenServerEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() {
this.contextRunner.withPropertyValues("spring.grpc.server.enabled=true")
.run((context) -> assertThat(context).hasSingleBean(GrpcServerAutoConfiguration.class));
}
@Test
void whenHasUserDefinedGrpcServiceDiscovererDoesNotAutoConfigureBean() {
GrpcServiceDiscoverer customGrpcServiceDiscoverer = mock(GrpcServiceDiscoverer.class);
new ApplicationContextRunner().withConfiguration(autoConfigurations)
.with(this::noOpLifecycleBeans)
.withBean("customGrpcServiceDiscoverer", GrpcServiceDiscoverer.class, () -> customGrpcServiceDiscoverer)
.withPropertyValues("spring.grpc.server.port=0")
.run((context) -> assertThat(context).getBean(GrpcServiceDiscoverer.class)
.isSameAs(customGrpcServiceDiscoverer));
}
@Test
void grpcServiceDiscovererAutoConfiguredAsExpected() {
new ApplicationContextRunner().withConfiguration(autoConfigurations)
.with(this::serviceBean)
.run((context) -> assertThat(context).getBean(GrpcServiceDiscoverer.class)
.isInstanceOf(DefaultGrpcServiceDiscoverer.class));
}
@Test
void serverBuilderCustomizersAutoConfiguredAsExpected() {
this.contextRunner.withUserConfiguration(ServerBuilderCustomizersConfig.class)
.run((context) -> assertThat(context).getBean(GrpcServerBuilderCustomizers.class)
.extracting("customizers", InstanceOfAssertFactories.list(ServerBuilderCustomizer.class))
.contains(ServerBuilderCustomizersConfig.bar, ServerBuilderCustomizersConfig.foo));
}
@Test
void customizersAreAppliedToNettyServer() {
AtomicReference<NettyServerBuilder> applied = new AtomicReference<>();
ServerBuilderCustomizer<NettyServerBuilder> customizer = applied::set;
this.contextRunner.withBean(ServerBuilderCustomizer.class, () -> customizer)
.withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class))
.run((context) -> {
context.getBean(GrpcServerFactory.class).createServer();
assertThat(applied.get()).isInstanceOf(NettyServerBuilder.class);
});
}
@Test
void customizersAreAppliedToShadedNettyServer() {
AtomicReference<io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder> applied = new AtomicReference<>();
ServerBuilderCustomizer<io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder> customizer = applied::set;
this.contextRunner.withBean(ServerBuilderCustomizer.class, () -> customizer)
.withClassLoader(new FilteredClassLoader(NettyServerBuilder.class))
.run((context) -> {
context.getBean(GrpcServerFactory.class).createServer();
assertThat(applied.get()).isInstanceOf(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class);
});
}
@Test
void customizersAreAppliedToInProcessServer() {
AtomicReference<InProcessServerBuilder> applied = new AtomicReference<>();
ServerBuilderCustomizer<InProcessServerBuilder> customizer = applied::set;
this.contextRunner.withBean(ServerBuilderCustomizer.class, () -> customizer)
.withClassLoader(new FilteredClassLoader(NettyServerBuilder.class,
io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class))
.withPropertyValues("spring.grpc.server.inprocess.name=test")
.run((context) -> {
context.getBean(GrpcServerFactory.class).createServer();
assertThat(applied.get()).isInstanceOf(InProcessServerBuilder.class);
});
}
@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 userDefinedServerFactoryWithInProcessServerFactory() {
GrpcServerFactory customServerFactory = mock(GrpcServerFactory.class);
this.contextRunner.withPropertyValues("spring.grpc.server.inprocess.name=foo")
.withBean("customServerFactory", GrpcServerFactory.class, () -> customServerFactory)
.run((context) -> assertThat(context).getBeans(GrpcServerFactory.class)
.containsOnlyKeys("customServerFactory", "inProcessGrpcServerFactory"));
}
@Test
void whenShadedAndNonShadedNettyOnClasspathShadedNettyFactoryIsAutoConfigured() {
this.contextRunner.run((context) -> assertThat(context).getBean(GrpcServerFactory.class)
.isInstanceOf(ShadedNettyGrpcServerFactory.class));
}
@Test
void shadedNettyFactoryWithInProcessServerFactory() {
this.contextRunner.withPropertyValues("spring.grpc.server.inprocess.name=foo")
.run((context) -> assertThat(context).getBeans(GrpcServerFactory.class)
.containsOnlyKeys("shadedNettyGrpcServerFactory", "inProcessGrpcServerFactory"));
}
@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 nonShadedNettyFactoryWithInProcessServerFactory() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class))
.withPropertyValues("spring.grpc.server.inprocess.name=foo")
.run((context) -> assertThat(context).getBeans(GrpcServerFactory.class)
.containsOnlyKeys("nettyGrpcServerFactory", "inProcessGrpcServerFactory"));
}
@Test
void whenShadedNettyAndNettyNotOnClasspathNoServerFactoryIsAutoConfigured() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(NettyServerBuilder.class,
io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerFactory.class));
}
@Test
void noServerFactoryWithInProcessServerFactory() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(NettyServerBuilder.class,
io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class))
.withPropertyValues("spring.grpc.server.inprocess.name=foo")
.run((context) -> assertThat(context).getBean(GrpcServerFactory.class)
.isInstanceOf(InProcessGrpcServerFactory.class));
}
@Test
void shadedNettyServerFactoryAutoConfiguredWithCustomLifecycle() {
GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class);
new ApplicationContextRunner().withConfiguration(autoConfigurations)
.with(this::serviceBean)
.withBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle)
.run((context) -> {
assertThat(context).getBean(GrpcServerFactory.class).isInstanceOf(ShadedNettyGrpcServerFactory.class);
assertThat(context).getBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class)
.isSameAs(customServerLifecycle);
});
}
@Test
void nettyServerFactoryAutoConfiguredWithCustomLifecycle() {
GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class);
new ApplicationContextRunner().withConfiguration(autoConfigurations)
.with(this::serviceBean)
.withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class))
.withBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle)
.run((context) -> {
assertThat(context).getBean(GrpcServerFactory.class).isInstanceOf(NettyGrpcServerFactory.class);
assertThat(context).getBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class)
.isSameAs(customServerLifecycle);
});
}
@Test
void inProcessServerFactoryAutoConfiguredWithCustomLifecycle() {
GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class);
new ApplicationContextRunner().withConfiguration(autoConfigurations)
.with(this::serviceBean)
.withPropertyValues("spring.grpc.server.inprocess.name=foo")
.withClassLoader(new FilteredClassLoader(NettyServerBuilder.class,
io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class))
.withBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle)
.run((context) -> {
assertThat(context).getBean(GrpcServerFactory.class).isInstanceOf(InProcessGrpcServerFactory.class);
assertThat(context).getBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class)
.isSameAs(customServerLifecycle);
});
}
@Test
void shadedNettyServerFactoryAutoConfiguredAsExpected() {
this.contextRunner.withPropertyValues("spring.grpc.server.address=192.168.0.1", "spring.grpc.server.port=6160")
.run(assertThatServerIsConfigured(ShadedNettyGrpcServerFactory.class, "192.168.0.1:6160",
"shadedNettyGrpcServerLifecycle"));
}
@Test
void nettyServerFactoryAutoConfiguredAsExpected() {
this.contextRunner.withPropertyValues("spring.grpc.server.address=192.168.0.1", "spring.grpc.server.port=6160")
.withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class))
.run(assertThatServerIsConfigured(NettyGrpcServerFactory.class, "192.168.0.1:6160",
"nettyGrpcServerLifecycle"));
}
@Test
void serverFactoryAutoConfiguredInWebAppWhenServletDisabled() {
new WebApplicationContextRunner().withConfiguration(autoConfigurations)
.with(this::noOpLifecycleBeans)
.with(this::serviceBean)
.withPropertyValues("spring.grpc.server.address=192.168.0.1")
.withPropertyValues("spring.grpc.server.port=6160")
.withPropertyValues("spring.grpc.server.servlet.enabled=false")
.run(assertThatServerIsConfigured(ShadedNettyGrpcServerFactory.class, "192.168.0.1:6160",
"shadedNettyGrpcServerLifecycle"));
}
@Test
void inProcessServerFactoryAutoConfiguredAsExpected() {
this.contextRunner.withPropertyValues("spring.grpc.server.inprocess.name=foo")
.withClassLoader(new FilteredClassLoader(NettyServerBuilder.class,
io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class))
.run(assertThatServerIsConfigured(InProcessGrpcServerFactory.class, "foo", "inProcessGrpcServerLifecycle"));
}
@Test
void nettyServerFactoryAutoConfiguredWithSsl() {
this.contextRunner.withPropertyValues("spring.grpc.server.address=192.168.0.1", "spring.grpc.server.port=6160",
"spring.grpc.server.ssl.bundle=ssltest",
"spring.ssl.bundle.jks.ssltest.keystore.location=classpath:org/springframework/boot/grpc/server/autoconfigure/test.jks",
"spring.ssl.bundle.jks.ssltest.keystore.password=secret",
"spring.ssl.bundle.jks.ssltest.key.password=password")
.withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class))
.run(assertThatServerIsConfigured(NettyGrpcServerFactory.class, "192.168.0.1:6160",
"nettyGrpcServerLifecycle"));
}
private ContextConsumer<? super ApplicationContextAssertProvider<?>> assertThatServerIsConfigured(
Class<?> expectedServerFactoryType, String expectedAddress, String expectedLifecycleBeanName) {
return (context) -> {
assertThat(context).getBean(GrpcServerFactory.class)
.isInstanceOf(expectedServerFactoryType)
.hasFieldOrPropertyWithValue("address", expectedAddress)
.extracting("serviceList", InstanceOfAssertFactories.list(ServerServiceDefinition.class))
.singleElement()
.extracting(ServerServiceDefinition::getServiceDescriptor)
.extracting(ServiceDescriptor::getName)
.isEqualTo("my-service");
assertThat(context).getBean(expectedLifecycleBeanName, GrpcServerLifecycle.class).isNotNull();
};
}
private <R extends AbstractApplicationContextRunner<R, C, A>, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider<C>> R serviceBean(
R contextRunner) {
return contextRunner.withBean(BindableService.class, () -> this.service);
}
private <R extends AbstractApplicationContextRunner<R, C, A>, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider<C>> R noOpLifecycleBeans(
R contextRunner) {
return contextRunner.withBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock)
.withBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock)
.withBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock);
}
@Nested
class ServletServerAutoConfigurationTests {
private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(autoConfigurations)
.with(GrpcServerAutoConfigurationTests.this::serviceBean);
@Test
void whenGrpcNotOnClasspathAutoConfigurationIsSkipped() {
this.contextRunner.withClassLoader(new FilteredClassLoader(BindableService.class))
.run((context) -> assertThat(context).doesNotHaveBean(ServletGrpcServerConfiguration.class)
.doesNotHaveBean(ServletRegistrationBean.class));
}
@Test
void whenSpringGrpcNotOnClasspathAutoConfigurationIsSkipped() {
this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerFactory.class))
.run((context) -> assertThat(context).doesNotHaveBean(ServletGrpcServerConfiguration.class));
}
@Test
void whenNoBindableServicesRegisteredAutoConfigurationIsSkipped() {
new WebApplicationContextRunner().withConfiguration(autoConfigurations)
.run((context) -> assertThat(context).doesNotHaveBean(ServletGrpcServerConfiguration.class)
.doesNotHaveBean(ServletRegistrationBean.class));
}
@Test
void whenGrpcServletNotOnClasspathAutoConfigurationIsSkipped() {
this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServlet.class))
.withPropertyValues("spring.grpc.server.port=0")
.run((context) -> assertThat(context).doesNotHaveBean(ServletGrpcServerConfiguration.class)
.doesNotHaveBean(ServletRegistrationBean.class));
}
@Test
void whenWebApplicationServletIsAutoConfigured() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GrpcServletRegistration.class));
}
@Test
void whenServerBuilderCustomizerIsRegistered() {
ServerBuilderCustomizer<ServletServerBuilder> customizer = mock();
this.contextRunner.withBean(ServerBuilderCustomizer.class, () -> customizer)
.run((context) -> then(customizer).should().customize(any(ServletServerBuilder.class)));
}
@Test
void whenMaxInboundMessageSizeIsSetThenItIsUsed() {
this.contextRunner.withPropertyValues("spring.grpc.server.inbound.message.max-size=10KB")
.run((context) -> assertThat(context).getBean(GrpcServletRegistration.class)
.hasFieldOrPropertyWithValue("servlet.servletAdapter.maxInboundMessageSize",
Math.toIntExact(DataSize.ofKilobytes(10).toBytes())));
}
@Test
void whenMaxInboundMessageSizeIsNotSetThenDefaultIsUsed() {
this.contextRunner.run((context) -> assertThat(context).getBean(GrpcServletRegistration.class)
.hasFieldOrPropertyWithValue("servlet.servletAdapter.maxInboundMessageSize",
GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE));
}
@Test
void customizersAreAppliedToServletServer() {
AtomicReference<ServletServerBuilder> applied = new AtomicReference<>();
ServerBuilderCustomizer<ServletServerBuilder> customizer = applied::set;
this.contextRunner.withBean(ServerBuilderCustomizer.class, () -> customizer)
.run((context) -> assertThat(applied.get()).isInstanceOf(ServletServerBuilder.class));
}
}
@Configuration(proxyBeanMethods = false)
static class ServerBuilderCustomizersConfig {
static ServerBuilderCustomizer<?> foo = mock();
static ServerBuilderCustomizer<?> bar = mock();
@Bean
@Order(200)
ServerBuilderCustomizer<?> customizerFoo() {
return foo;
}
@Bean
@Order(100)
ServerBuilderCustomizer<?> customizerBar() {
return bar;
}
}
}
@@ -0,0 +1,155 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import io.grpc.CompressorRegistry;
import io.grpc.DecompressorRegistry;
import io.grpc.ServerBuilder;
import io.grpc.netty.NettyServerBuilder;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.springframework.grpc.server.ServerBuilderCustomizer;
import org.springframework.util.unit.DataSize;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GrpcServerBuilderCustomizers}.
*
* @author Chris Bono
* @author Phillip Webb
*/
class GrpcServerBuilderCustomizersTests {
@Test
void applyWhenEmptyCustomizersDoesNothing() {
ServerBuilder<?> serverBuilder = mock(ServerBuilder.class);
new GrpcServerBuilderCustomizers(Collections.emptyList()).apply(serverBuilder);
then(serverBuilder).shouldHaveNoInteractions();
}
@Test
void applyWhenSimpleCustomizer() {
GrpcServerBuilderCustomizers customizers = new GrpcServerBuilderCustomizers(
List.of(new SimpleServerBuilderCustomizer()));
NettyServerBuilder serverBuilder = mock(NettyServerBuilder.class);
customizers.apply(serverBuilder);
then(serverBuilder).should().maxConnectionAge(100L, TimeUnit.SECONDS);
}
@Test
void applyWhenGenericCustomizersRespectsGeneric() {
List<TestCustomizer<?>> list = new ArrayList<>();
list.add(new TestCustomizer<>());
list.add(new TestNettyServerBuilderCustomizer());
list.add(new TestShadedNettyServerBuilderCustomizer());
GrpcServerBuilderCustomizers customizers = new GrpcServerBuilderCustomizers(list);
customizers.apply(mock(ServerBuilder.class));
assertThat(list.get(0).getCount()).isOne();
assertThat(list.get(1).getCount()).isZero();
assertThat(list.get(2).getCount()).isZero();
customizers.apply(mock(NettyServerBuilder.class));
assertThat(list.get(0).getCount()).isEqualTo(2);
assertThat(list.get(1).getCount()).isOne();
assertThat(list.get(2).getCount()).isZero();
customizers.apply(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();
}
@Test
void applyWhenHasInjectedBeans() {
CompressorRegistry compressorRegistry = mock();
DecompressorRegistry decompressorRegistry = mock();
GrpcServerExecutorProvider executorProvider = mock();
Executor executor = mock();
given(executorProvider.getExecutor()).willReturn(executor);
GrpcServerProperties properties = new GrpcServerProperties();
properties.getInbound().getMessage().setMaxSize(DataSize.ofMegabytes(10));
GrpcServerBuilderCustomizers customizers = new GrpcServerBuilderCustomizers(properties, compressorRegistry,
decompressorRegistry, executorProvider, List.of(new SimpleServerBuilderCustomizer()));
NettyServerBuilder serverBuilder = mock(NettyServerBuilder.class);
customizers.apply(serverBuilder);
InOrder ordered = Mockito.inOrder(serverBuilder);
then(serverBuilder).should(ordered).compressorRegistry(compressorRegistry);
then(serverBuilder).should(ordered).decompressorRegistry(decompressorRegistry);
then(serverBuilder).should().executor(executor);
then(serverBuilder).should(ordered).maxConnectionAge(100L, TimeUnit.SECONDS);
}
/**
* Test customizer that will match {@link NettyServerBuilder} and apply a simple
* customization.
*/
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}.
*
* @param <T> the builder type
*/
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> {
}
}
@@ -0,0 +1,92 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import io.grpc.Compressor;
import io.grpc.CompressorRegistry;
import io.grpc.Decompressor;
import io.grpc.DecompressorRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GrpcServerCodecConfiguration}.
*
* @author Andrei Lisa
*/
class GrpcServerCodecConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GrpcServerCodecConfiguration.class));
@Test
void compressorRegistryWhenHasUserDefinedRegistryDoesNotAutoConfigureBean() {
CompressorRegistry customRegistry = mock();
this.contextRunner.withBean("customCompressorRegistry", CompressorRegistry.class, () -> customRegistry)
.run((context) -> assertThat(context).getBean(CompressorRegistry.class).isSameAs(customRegistry));
}
@Test
void compressorRegistryWhenNoCompressorsAutoConfiguresDefaultInstance() {
this.contextRunner.run((context) -> assertThat(context).getBean(CompressorRegistry.class)
.isSameAs(CompressorRegistry.getDefaultInstance()));
}
@Test
void compressorRegistryWhenHasCompressorsAutoConfiguresNewInstance() {
Compressor compressor = mock();
given(compressor.getMessageEncoding()).willReturn("foo");
this.contextRunner.withBean(Compressor.class, () -> compressor).run((context) -> {
assertThat(context).hasSingleBean(CompressorRegistry.class);
CompressorRegistry registry = context.getBean(CompressorRegistry.class);
assertThat(registry).isNotSameAs(CompressorRegistry.getDefaultInstance());
assertThat(registry.lookupCompressor("foo")).isSameAs(compressor);
});
}
@Test
void decompressorRegistryWhenHasUserDefinedRegistryDoesNotAutoConfigureBean() {
DecompressorRegistry customRegistry = mock();
this.contextRunner.withBean("customDecompressorRegistry", DecompressorRegistry.class, () -> customRegistry)
.run((context) -> assertThat(context).getBean(DecompressorRegistry.class).isSameAs(customRegistry));
}
@Test
void decompressorRegistryWhenNoDecompressorsAutoConfiguresDefaultInstance() {
this.contextRunner.run((context) -> assertThat(context).getBean(DecompressorRegistry.class)
.isSameAs(DecompressorRegistry.getDefaultInstance()));
}
@Test
void decompressorRegistryWhenHasDecompressorsAutoConfiguresNewInstance() {
Decompressor decompressor = mock();
given(decompressor.getMessageEncoding()).willReturn("foo");
this.contextRunner.withBean(Decompressor.class, () -> decompressor).run((context) -> {
assertThat(context).hasSingleBean(DecompressorRegistry.class);
DecompressorRegistry registry = context.getBean(DecompressorRegistry.class);
assertThat(registry).isNotSameAs(DecompressorRegistry.getDefaultInstance());
assertThat(registry.lookupDecompressor("foo")).isSameAs(decompressor);
});
}
}
@@ -0,0 +1,169 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.net.InetAddress;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import io.grpc.TlsServerCredentials.ClientAuth;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.util.unit.DataSize;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GrpcServerProperties}.
*
* @author Chris Bono
* @author Phillip Webb
*/
class GrpcServerPropertiesTests {
private GrpcServerProperties bindProperties(Map<String, String> map) {
return new Binder(new MapConfigurationPropertySource(map))
.bind("spring.grpc.server", GrpcServerProperties.class)
.get();
}
@Test
void bind() throws Exception {
Map<String, String> map = new HashMap<>();
map.put("spring.grpc.server.address", "192.168.0.1");
GrpcServerProperties properties = bindProperties(map);
assertThat(properties.getAddress()).isEqualTo(InetAddress.getByName("192.168.0.1"));
}
@Test
void defaultAddressIsNull() {
assertThat(new GrpcServerProperties().getAddress()).isNull();
}
@Nested
class ShutdownProperties {
@Test
void bind() {
Map<String, String> map = new HashMap<>();
map.put("spring.grpc.server.shutdown.grace-period", "10m");
GrpcServerProperties properties = bindProperties(map);
assertThat(properties.getShutdown().getGracePeriod()).isEqualTo(Duration.ofMinutes(10));
}
@Test
void bindWithoutUnits() {
Map<String, String> map = new HashMap<>();
map.put("spring.grpc.server.shutdown.grace-period", "10");
GrpcServerProperties properties = bindProperties(map);
assertThat(properties.getShutdown().getGracePeriod()).isEqualTo(Duration.ofSeconds(10));
}
}
@Nested
class InboundProperties {
@Test
void bind() {
Map<String, String> map = new HashMap<>();
map.put("spring.grpc.server.inbound.message.max-size", "20MB");
map.put("spring.grpc.server.inbound.metadata.max-size", "1MB");
GrpcServerProperties properties = bindProperties(map);
assertThat(properties.getInbound().getMessage().getMaxSize()).isEqualTo(DataSize.ofMegabytes(20));
assertThat(properties.getInbound().getMetadata().getMaxSize()).isEqualTo(DataSize.ofMegabytes(1));
}
@Test
void bindWithoutUnits() {
Map<String, String> map = new HashMap<>();
map.put("spring.grpc.server.inbound.message.max-size", "1048576");
map.put("spring.grpc.server.inbound.metadata.max-size", "1024");
GrpcServerProperties properties = bindProperties(map);
assertThat(properties.getInbound().getMessage().getMaxSize()).isEqualTo(DataSize.ofMegabytes(1));
assertThat(properties.getInbound().getMetadata().getMaxSize()).isEqualTo(DataSize.ofKilobytes(1));
}
}
@Nested
class KeepAliveProperties {
@Test
void bind() {
Map<String, String> map = new HashMap<>();
map.put("spring.grpc.server.keepalive.time", "45m");
map.put("spring.grpc.server.keepalive.timeout", "40s");
map.put("spring.grpc.server.keepalive.permit.time", "33s");
map.put("spring.grpc.server.keepalive.permit.without-calls", "true");
map.put("spring.grpc.server.keepalive.connection.max-idle-time", "1h");
map.put("spring.grpc.server.keepalive.connection.max-age", "3h");
map.put("spring.grpc.server.keepalive.connection.grace-period", "21s");
GrpcServerProperties.Keepalive properties = bindProperties(map).getKeepalive();
assertThatPropertiesSetAsExpected(properties);
}
@Test
void bindWithoutUnits() {
Map<String, String> map = new HashMap<>();
map.put("spring.grpc.server.keepalive.time", "2700");
map.put("spring.grpc.server.keepalive.timeout", "40");
map.put("spring.grpc.server.keepalive.permit.time", "33");
map.put("spring.grpc.server.keepalive.permit.without-calls", "true");
map.put("spring.grpc.server.keepalive.connection.max-idle-time", "3600");
map.put("spring.grpc.server.keepalive.connection.max-age", "10800");
map.put("spring.grpc.server.keepalive.connection.grace-period", "21");
GrpcServerProperties.Keepalive properties = bindProperties(map).getKeepalive();
assertThatPropertiesSetAsExpected(properties);
}
private void assertThatPropertiesSetAsExpected(GrpcServerProperties.Keepalive properties) {
assertThat(properties.getTime()).isEqualTo(Duration.ofMinutes(45));
assertThat(properties.getTimeout()).isEqualTo(Duration.ofSeconds(40));
assertThat(properties.getPermit().getTime()).isEqualTo(Duration.ofSeconds(33));
assertThat(properties.getPermit().isWithoutCalls()).isTrue();
assertThat(properties.getConnection().getMaxIdleTime()).isEqualTo(Duration.ofHours(1));
assertThat(properties.getConnection().getMaxAge()).isEqualTo(Duration.ofHours(3));
assertThat(properties.getConnection().getGracePeriod()).isEqualTo(Duration.ofSeconds(21));
}
}
@Nested
class SslProperties {
@Test
void bind() {
Map<String, String> map = new HashMap<>();
map.put("spring.grpc.server.ssl.enabled", "true");
map.put("spring.grpc.server.ssl.client-auth", "require");
map.put("spring.grpc.server.ssl.bundle", "test");
map.put("spring.grpc.server.ssl.secure", "false");
GrpcServerProperties.Ssl properties = bindProperties(map).getSsl();
assertThat(properties.getEnabled()).isTrue();
assertThat(properties.getClientAuth()).isEqualTo(ClientAuth.REQUIRE);
assertThat(properties.getBundle()).isEqualTo("test");
assertThat(properties.isSecure()).isFalse();
}
}
}
@@ -0,0 +1,104 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import io.grpc.BindableService;
import io.grpc.protobuf.services.ProtoReflectionServiceV1;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerServicesAutoConfiguration.GrpcServerReflectionServiceConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GrpcServerServicesAutoConfiguration}.
*
* @author Haris Zujo
* @author Chris Bono
* @author Andrey Litvitski
* @author Phillip Webb
*/
class GrpcServerServicesAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GrpcServerServicesAutoConfiguration.class))
.withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock)
.withBean(BindableService.class, Mockito::mock);
@Nested
class GrpcServerReflectionServiceConfigurationTests {
private final ApplicationContextRunner contextRunner = GrpcServerServicesAutoConfigurationTests.this.contextRunner;
@Test
void whenAutoConfigurationIsNotSkippedCreatesReflectionServiceBean() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(GrpcServerReflectionServiceConfiguration.class);
assertThat(context).hasBean("grpcServerReflectionService");
});
}
@Test
void whenGrpcServicesNotOnClasspathAutoConfigurationIsSkipped() {
this.contextRunner.withClassLoader(new FilteredClassLoader(ProtoReflectionServiceV1.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerReflectionServiceConfiguration.class));
}
@Test
void whenNoBindableServiceDefinedAutoConfigurationIsSkipped() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GrpcServerServicesAutoConfiguration.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcServerReflectionServiceConfiguration.class));
}
@Test
void whenReflectionEnabledPropertyIsTrueAutoConfigurationIsNotSkipped() {
this.contextRunner.withPropertyValues("spring.grpc.server.reflection.enabled=true")
.run((context) -> assertThat(context).hasBean("grpcServerReflectionService"));
}
@Test
void whenReflectionEnabledPropertyIsFalseAutoConfigurationIsSkipped() {
this.contextRunner.withPropertyValues("spring.grpc.server.reflection.enabled=false").run((context) -> {
assertThat(context).doesNotHaveBean("grpcServerReflectionService");
assertThat(context).doesNotHaveBean(GrpcServerReflectionServiceConfiguration.class);
});
}
@Test
void whenServerEnabledPropertyIsTrueAutoConfigurationIsNotSkipped() {
this.contextRunner.withPropertyValues("spring.grpc.server.enabled=true")
.run((context) -> assertThat(context).hasBean("grpcServerReflectionService"));
}
@Test
void whenServerEnabledPropertyIsFalseAutoConfigurationIsSkipped() {
this.contextRunner.withPropertyValues("spring.grpc.server.enabled=false").run((context) -> {
assertThat(context).doesNotHaveBean("grpcServerReflectionService");
assertThat(context).doesNotHaveBean(GrpcServerReflectionServiceConfiguration.class);
});
}
}
}
@@ -0,0 +1,115 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.net.InetAddress;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException;
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties.Netty.Transport;
import org.springframework.grpc.internal.GrpcUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link NettyAddress}.
*
* @author Phillip Webb
*/
class NettyAddressTests {
@Test
void whenNoTransportAndNoAddressOrPortOrDomainSocketPathBindsToDefault() {
NettyAddress address = new NettyAddress(null, null, null, null);
assertThat(address).hasToString(GrpcUtils.ANY_IP_ADDRESS + ":" + GrpcUtils.DEFAULT_PORT);
}
@Test
void whenNoTransportAndOnlyPortBindsToAllAddressesUsingPort() {
NettyAddress address = new NettyAddress(null, null, 1234, null);
assertThat(address).hasToString(GrpcUtils.ANY_IP_ADDRESS + ":1234");
}
@Test
void whenNoTransportAndOnlyAddressBindsToAddressUsingPort9090() throws Exception {
InetAddress inetAddress = InetAddress.getByName("localhost");
NettyAddress address = new NettyAddress(null, inetAddress, null, null);
assertThat(address).hasToString("localhost:" + GrpcUtils.DEFAULT_PORT);
}
@Test
void whenNoTransportAndOnlyAddressWithoutNameBindsToAddressUsingPort9090() throws Exception {
InetAddress inetAddress = InetAddress.getByName("192.168.1.0");
NettyAddress address = new NettyAddress(null, inetAddress, null, null);
assertThat(address).hasToString("192.168.1.0:" + GrpcUtils.DEFAULT_PORT);
}
@Test
void whenNoTransportAndOnlyDomainSocketPathBindsToDomainSocket() {
NettyAddress address = new NettyAddress(null, null, null, "/ds");
assertThat(address).hasToString("unix:/ds");
}
@Test
void whenNoTransportAndPortAndDomainSocketPathThrowsException() {
NettyAddress address = new NettyAddress(null, null, 1234, "/ds");
assertThatExceptionOfType(MutuallyExclusiveConfigurationPropertiesException.class)
.isThrownBy(() -> address.toString())
.withMessage(
"The configuration properties 'spring.grpc.server.port, spring.grpc.server.netty.domain-socket-path' "
+ "are mutually exclusive and 'spring.grpc.server.port, spring.grpc.server.netty.domain-socket-path' "
+ "have been configured together");
}
@Test
void whenNoTransportAndAddressAndDomainSocketPathThrowsException() throws Exception {
InetAddress inetAddress = InetAddress.getByName("192.168.1.0");
NettyAddress address = new NettyAddress(null, inetAddress, null, "/ds");
assertThatExceptionOfType(MutuallyExclusiveConfigurationPropertiesException.class)
.isThrownBy(() -> address.toString())
.withMessage(
"The configuration properties 'spring.grpc.server.address, spring.grpc.server.netty.domain-socket-path' "
+ "are mutually exclusive and 'spring.grpc.server.address, spring.grpc.server.netty.domain-socket-path' "
+ "have been configured together");
}
@Test
void whenTcpTransportBindsToTcp() throws Exception {
InetAddress inetAddress = InetAddress.getByName("192.168.1.0");
NettyAddress address = new NettyAddress(Transport.TCP, inetAddress, 1234, "/ds");
assertThat(address).hasToString("192.168.1.0:1234");
}
@Test
void whenDomainSocketTransportAndNoDomainPathThrowsException() {
NettyAddress address = new NettyAddress(Transport.DOMAIN_SOCKET, null, null, "");
assertThatExceptionOfType(InvalidConfigurationPropertyValueException.class).isThrownBy(() -> address.toString())
.withMessage("Property spring.grpc.server.netty.domain-socket-path with value '' is invalid: "
+ "A path is required when spring.grpc.server.netty.transport is set to 'domain-socket'");
}
@Test
void whenDomainSocketTransportAndDomainPathBindsToDomainPath() throws Exception {
InetAddress inetAddress = InetAddress.getByName("192.168.1.0");
NettyAddress address = new NettyAddress(Transport.DOMAIN_SOCKET, inetAddress, 1234, "/ds");
assertThat(address).hasToString("unix:/ds");
}
}
@@ -0,0 +1,98 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import io.grpc.ServerBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.netty.NettyServerBuilder;
import io.grpc.servlet.jakarta.ServletServerBuilder;
import org.junit.jupiter.api.Test;
import org.mockito.verification.VerificationMode;
import org.springframework.util.unit.DataSize;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
/**
* Tests for {@link PropertiesServerBuilderCustomizer}.
*
* @author Chris Bono
* @author Phillip Webb
*/
class PropertiesServerBuilderCustomizerTests {
@Test
void customizeWhenNettyServerBuilder() {
NettyServerBuilder builder = mock();
PropertiesServerBuilderCustomizer<NettyServerBuilder> customizer = new PropertiesServerBuilderCustomizer<>(
getProperties());
customizer.customize(builder);
assertExpectedMapping(builder, atLeastOnce());
}
@Test
void customizeWhenInProcessServerBuilder() {
InProcessServerBuilder builder = mock();
PropertiesServerBuilderCustomizer<InProcessServerBuilder> customizer = new PropertiesServerBuilderCustomizer<>(
getProperties());
customizer.customize(builder);
assertExpectedMapping(builder, never());
}
@Test
void customizerWhenServletServerBuilder() {
ServletServerBuilder builder = mock();
PropertiesServerBuilderCustomizer<ServletServerBuilder> customizer = new PropertiesServerBuilderCustomizer<>(
getProperties());
customizer.customize(builder);
assertExpectedMapping(builder, never());
}
private GrpcServerProperties getProperties() {
GrpcServerProperties properties = new GrpcServerProperties();
properties.getInbound().getMessage().setMaxSize(DataSize.ofMegabytes(333));
properties.getInbound().getMetadata().setMaxSize(DataSize.ofKilobytes(111));
properties.getKeepalive().setTime(Duration.ofHours(1));
properties.getKeepalive().setTimeout(Duration.ofSeconds(10));
properties.getKeepalive().getConnection().setMaxIdleTime(Duration.ofHours(2));
properties.getKeepalive().getConnection().setMaxAge(Duration.ofHours(3));
properties.getKeepalive().getConnection().setGracePeriod(Duration.ofSeconds(45));
properties.getKeepalive().getPermit().setTime(Duration.ofMinutes(7));
properties.getKeepalive().getPermit().setWithoutCalls(true);
return properties;
}
private void assertExpectedMapping(ServerBuilder<?> builder, VerificationMode keepAliveMode) {
then(builder).should().maxInboundMessageSize(Math.toIntExact(DataSize.ofMegabytes(333).toBytes()));
then(builder).should().maxInboundMetadataSize(Math.toIntExact(DataSize.ofKilobytes(111).toBytes()));
then(builder).should(keepAliveMode).keepAliveTime(Duration.ofHours(1).toNanos(), TimeUnit.NANOSECONDS);
then(builder).should(keepAliveMode).keepAliveTimeout(Duration.ofSeconds(10).toNanos(), TimeUnit.NANOSECONDS);
then(builder).should(keepAliveMode).maxConnectionIdle(Duration.ofHours(2).toNanos(), TimeUnit.NANOSECONDS);
then(builder).should(keepAliveMode).maxConnectionAge(Duration.ofHours(3).toNanos(), TimeUnit.NANOSECONDS);
then(builder).should(keepAliveMode)
.maxConnectionAgeGrace(Duration.ofSeconds(45).toNanos(), TimeUnit.NANOSECONDS);
then(builder).should(keepAliveMode).permitKeepAliveTime(Duration.ofMinutes(7).toNanos(), TimeUnit.NANOSECONDS);
then(builder).should(keepAliveMode).permitKeepAliveWithoutCalls(true);
}
}
@@ -0,0 +1,138 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.grpc.server.autoconfigure;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import io.grpc.TlsServerCredentials.ClientAuth;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.grpc.server.autoconfigure.GrpcServerProperties.Ssl;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.ssl.SslManagerBundle;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ServerCredentials}.
*
* @author Phillip Webb
*/
class ServerCredentialsTests {
private final TrustManagerFactory insecureTrustManagerFactory = mock();
private final SslBundles bundles = mock();
private final TrustManagerFactory bundleTrustManagerFactory = mock();
private final KeyManagerFactory bundleKeyManagerFactory = mock();
ServerCredentialsTests() {
SslBundle bundle = mock();
SslManagerBundle managers = mock();
given(this.bundles.getBundle("test")).willReturn(bundle);
given(bundle.getManagers()).willReturn(managers);
given(managers.getTrustManagerFactory()).willReturn(this.bundleTrustManagerFactory);
given(managers.getKeyManagerFactory()).willReturn(this.bundleKeyManagerFactory);
}
@Test
void getWhenNotEnabledAndNoBundleReturnsNullManagers() {
ServerCredentials credentials = get((properties) -> {
});
assertThat(credentials.keyManagerFactory()).isNull();
assertThat(credentials.trustManagerFactory()).isNull();
assertThat(credentials.clientAuth()).isEqualTo(ClientAuth.NONE);
}
@Test
void getWhenDisabledReturnsNullManagers() {
ServerCredentials credentials = get((properties) -> {
properties.put("spring.grpc.server.ssl.enabled", "false");
properties.put("spring.grpc.server.ssl.client-auth", "require");
});
assertThat(credentials.keyManagerFactory()).isNull();
assertThat(credentials.trustManagerFactory()).isNull();
assertThat(credentials.clientAuth()).isEqualTo(ClientAuth.REQUIRE);
}
@Test
void getWhenEnabledTrueAndNoBundleNameThrowsException() {
assertThatIllegalStateException().isThrownBy(() -> get((properties) -> {
properties.put("spring.grpc.server.ssl.enabled", "true");
properties.put("spring.grpc.server.ssl.client-auth", "require");
})).withMessage("SSL bundle-name is requested when 'spring.grpc.server.ssl.enabled' is true");
}
@Test
void getWhenHasBundleName() {
ServerCredentials credentials = get((properties) -> {
properties.put("spring.grpc.server.ssl.bundle", "test");
properties.put("spring.grpc.server.ssl.client-auth", "require");
});
assertThat(credentials.keyManagerFactory()).isEqualTo(this.bundleKeyManagerFactory);
assertThat(credentials.trustManagerFactory()).isEqualTo(this.bundleTrustManagerFactory);
assertThat(credentials.clientAuth()).isEqualTo(ClientAuth.REQUIRE);
}
@Test
void getWhenHasBundleNameAndEnabled() {
ServerCredentials credentials = get((properties) -> {
properties.put("spring.grpc.server.ssl.enabled", "true");
properties.put("spring.grpc.server.ssl.bundle", "test");
properties.put("spring.grpc.server.ssl.client-auth", "require");
});
assertThat(credentials.keyManagerFactory()).isEqualTo(this.bundleKeyManagerFactory);
assertThat(credentials.trustManagerFactory()).isEqualTo(this.bundleTrustManagerFactory);
assertThat(credentials.clientAuth()).isEqualTo(ClientAuth.REQUIRE);
}
@Test
void getWhenHasBundleNameAndSecureFalse() {
ServerCredentials credentials = get((properties) -> {
properties.put("spring.grpc.server.ssl.enabled", "true");
properties.put("spring.grpc.server.ssl.bundle", "test");
properties.put("spring.grpc.server.ssl.secure", "false");
});
assertThat(credentials.keyManagerFactory()).isEqualTo(this.bundleKeyManagerFactory);
assertThat(credentials.trustManagerFactory()).isEqualTo(this.insecureTrustManagerFactory);
assertThat(credentials.clientAuth()).isEqualTo(ClientAuth.NONE);
}
private ServerCredentials get(Consumer<Map<String, String>> properties) {
Map<String, String> map = new HashMap<>();
properties.accept(map);
Ssl ssl = new Binder(new MapConfigurationPropertySource(map))
.bind("spring.grpc.server", GrpcServerProperties.class)
.orElseGet(GrpcServerProperties::new)
.getSsl();
return ServerCredentials.get(ssl, this.bundles, this.insecureTrustManagerFactory);
}
}
@@ -2163,6 +2163,7 @@ bom {
"spring-boot-freemarker",
"spring-boot-graphql",
"spring-boot-graphql-test",
"spring-boot-grpc-server",
"spring-boot-groovy-templates",
"spring-boot-gson",
"spring-boot-h2console",
@@ -2293,6 +2294,7 @@ bom {
"spring-boot-starter-freemarker-test",
"spring-boot-starter-graphql",
"spring-boot-starter-graphql-test",
"spring-boot-starter-grpc-server",
"spring-boot-starter-groovy-templates",
"spring-boot-starter-groovy-templates-test",
"spring-boot-starter-gson",
+5
View File
@@ -120,6 +120,7 @@ include "module:spring-boot-flyway"
include "module:spring-boot-freemarker"
include "module:spring-boot-graphql"
include "module:spring-boot-graphql-test"
include "module:spring-boot-grpc-server"
include "module:spring-boot-groovy-templates"
include "module:spring-boot-gson"
include "module:spring-boot-h2console"
@@ -268,6 +269,7 @@ include "starter:spring-boot-starter-freemarker"
include "starter:spring-boot-starter-freemarker-test"
include "starter:spring-boot-starter-graphql"
include "starter:spring-boot-starter-graphql-test"
include "starter:spring-boot-starter-grpc-server"
include "starter:spring-boot-starter-groovy-templates"
include "starter:spring-boot-starter-groovy-templates-test"
include "starter:spring-boot-starter-gson"
@@ -421,6 +423,9 @@ include ":smoke-test:spring-boot-smoke-test-data-rest"
include ":smoke-test:spring-boot-smoke-test-devtools"
include ":smoke-test:spring-boot-smoke-test-flyway"
include ":smoke-test:spring-boot-smoke-test-graphql"
include ":smoke-test:spring-boot-smoke-test-grpc-server"
include ":smoke-test:spring-boot-smoke-test-grpc-server-netty-shaded"
include ":smoke-test:spring-boot-smoke-test-grpc-server-servlet"
include ":smoke-test:spring-boot-smoke-test-hateoas"
include ":smoke-test:spring-boot-smoke-test-hibernate"
include ":smoke-test:spring-boot-smoke-test-integration"
@@ -0,0 +1,67 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id "java"
id "com.google.protobuf" version "${protobufGradlePluginVersion}"
id "org.springframework.boot.docker-test"
}
description = "Spring Boot gRPC server netty shaded smoke test"
dependencies {
implementation(project(":starter:spring-boot-starter-grpc-server")) {
exclude(group: "io.grpc", module: "grpc-netty-shaded")
}
implementation("io.grpc:grpc-netty-shaded")
dockerTestImplementation(project(":starter:spring-boot-starter-test"))
dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter")
}
def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom")
def grpcJava = dependenciesBom.getLibrary("Grpc Java")
def protobufJava = dependenciesBom.getLibrary("Protobuf Java")
tasks.named("compileTestJava") {
options.nullability.checking = "tests"
}
nullability {
requireExplicitNullMarking = false
}
configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all {
extendsFrom(configurations.dependencyManagement)
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protobufJava.version}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}"
}
}
generateProtoTasks {
all()*.plugins {
grpc {
option '@generated=omit'
}
}
}
}
@@ -0,0 +1,76 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.grpcservernettyshaded;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.startupcheck.IndefiniteWaitOneShotStartupCheckStrategy;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import smoketest.grpcservernettyshaded.SampleGrpcServerNettyShadedApplicationTests.GrpcServerStartedEventListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Import;
import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for Spring gRPC with a shaded netty server.
*
* @author Phillip Webb
*/
@SpringBootTest(properties = "spring.grpc.server.port=0")
@Testcontainers(disabledWithoutDocker = true)
@Import(GrpcServerStartedEventListener.class)
class SampleGrpcServerNettyShadedApplicationTests {
@Autowired
private GrpcServerStartedEventListener startedEventListener;
@Test
@SuppressWarnings("resource")
void test() {
String address = "host.docker.internal:" + this.startedEventListener.getPort();
try (GenericContainer<?> container = new GenericContainer<>(
DockerImageName.parse("fullstorydev/grpcurl:v1.9.3"))
.withCommand("-d", "{\"name\": \"spring\"}", "--plaintext", address, "HelloWorld/SayHello")
.withStartupCheckStrategy(new IndefiniteWaitOneShotStartupCheckStrategy())) {
container.start();
assertThat(container.getLogs()).contains("\"message\": \"Hello 'spring'\"");
}
}
static class GrpcServerStartedEventListener implements ApplicationListener<GrpcServerStartedEvent> {
private int port;
@Override
public void onApplicationEvent(GrpcServerStartedEvent event) {
this.port = event.getPort();
}
int getPort() {
return this.port;
}
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.grpcservernettyshaded;
import io.grpc.stub.StreamObserver;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import smoketest.grpcservernettyshaded.proto.HelloReply;
import smoketest.grpcservernettyshaded.proto.HelloRequest;
import smoketest.grpcservernettyshaded.proto.HelloWorldGrpc;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
@Service
public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase {
private static Log logger = LogFactory.getLog(HelloWorldService.class);
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String name = request.getName();
logger.info("sayHello " + name);
Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name);
Assert.state(!name.startsWith("internal"), "Internal error");
String message = "Hello '%s'".formatted(name);
HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
@Override
public void streamHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String name = request.getName();
logger.info("streamHello " + name);
int count = 0;
while (count < 10) {
String message = "Hello(" + count + ") '%s'".formatted(name);
HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
responseObserver.onNext(reply);
count++;
try {
Thread.sleep(100L);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
responseObserver.onError(ex);
return;
}
}
responseObserver.onCompleted();
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.grpcservernettyshaded;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleGrpcServerNettyShadedApplication {
public static void main(String[] args) {
SpringApplication.run(SampleGrpcServerNettyShadedApplication.class, args);
}
}
@@ -0,0 +1,20 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@NullMarked
package smoketest.grpcservernettyshaded;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "smoketest.grpcservernettyshaded.proto";
option java_multiple_files = true;
service HelloWorld {
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc StreamHello(HelloRequest) returns (stream HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
@@ -0,0 +1,69 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id "java"
id "com.google.protobuf" version "${protobufGradlePluginVersion}"
id "org.springframework.boot.docker-test"
}
description = "Spring Boot gRPC server servlet smoke test"
dependencies {
implementation(project(":starter:spring-boot-starter-grpc-server")) {
exclude(group: "io.grpc", module: "grpc-netty-shaded")
}
implementation(project(":starter:spring-boot-starter-tomcat"))
implementation("io.grpc:grpc-servlet-jakarta")
dockerTestImplementation(project(":starter:spring-boot-starter-test"))
dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter")
}
def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom")
def grpcJava = dependenciesBom.getLibrary("Grpc Java")
def protobufJava = dependenciesBom.getLibrary("Protobuf Java")
tasks.named("compileTestJava") {
options.nullability.checking = "tests"
}
nullability {
requireExplicitNullMarking = false
}
configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all {
extendsFrom(configurations.dependencyManagement)
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protobufJava.version}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}"
}
}
generateProtoTasks {
all()*.plugins {
grpc {
option '@generated=omit'
}
}
}
}
@@ -0,0 +1,57 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.grpcserverservlet;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.startupcheck.IndefiniteWaitOneShotStartupCheckStrategy;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for Spring gRPC with a servlet server.
*
* @author Phillip Webb
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers(disabledWithoutDocker = true)
class SampleGrpcServerServletApplicationTests {
@LocalServerPort
private String localServerPort;
@Test
@SuppressWarnings("resource")
void test() {
String address = "host.docker.internal:" + this.localServerPort;
try (GenericContainer<?> container = new GenericContainer<>(
DockerImageName.parse("fullstorydev/grpcurl:v1.9.3"))
.withCommand("-d", "{\"name\": \"spring\"}", "--plaintext", address, "HelloWorld/SayHello")
.withStartupCheckStrategy(new IndefiniteWaitOneShotStartupCheckStrategy())) {
container.start();
assertThat(container.getLogs()).contains("\"message\": \"Hello 'spring'\"");
}
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.grpcserverservlet;
import io.grpc.stub.StreamObserver;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import smoketest.grpcserverservlet.proto.HelloReply;
import smoketest.grpcserverservlet.proto.HelloRequest;
import smoketest.grpcserverservlet.proto.HelloWorldGrpc;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
@Service
public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase {
private static Log logger = LogFactory.getLog(HelloWorldService.class);
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String name = request.getName();
logger.info("sayHello " + name);
Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name);
Assert.state(!name.startsWith("internal"), "Internal error");
String message = "Hello '%s'".formatted(name);
HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
@Override
public void streamHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String name = request.getName();
logger.info("streamHello " + name);
int count = 0;
while (count < 10) {
String message = "Hello(" + count + ") '%s'".formatted(name);
HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
responseObserver.onNext(reply);
count++;
try {
Thread.sleep(100L);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
responseObserver.onError(ex);
return;
}
}
responseObserver.onCompleted();
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.grpcserverservlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleGrpcServerServletApplication {
public static void main(String[] args) {
SpringApplication.run(SampleGrpcServerServletApplication.class, args);
}
}
@@ -0,0 +1,20 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@NullMarked
package smoketest.grpcserverservlet;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "smoketest.grpcserverservlet.proto";
option java_multiple_files = true;
service HelloWorld {
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc StreamHello(HelloRequest) returns (stream HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
@@ -0,0 +1 @@
server.http2.enabled=true
@@ -0,0 +1,64 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id "java"
id "com.google.protobuf" version "${protobufGradlePluginVersion}"
id "org.springframework.boot.docker-test"
}
description = "Spring Boot gRPC server smoke test"
dependencies {
implementation(project(":starter:spring-boot-starter-grpc-server"))
dockerTestImplementation(project(":starter:spring-boot-starter-test"))
dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter")
}
def dependenciesBom = project(":platform:spring-boot-dependencies").extensions.getByName("bom")
def grpcJava = dependenciesBom.getLibrary("Grpc Java")
def protobufJava = dependenciesBom.getLibrary("Protobuf Java")
tasks.named("compileTestJava") {
options.nullability.checking = "tests"
}
nullability {
requireExplicitNullMarking = false
}
configurations.named { it.startsWith("protobufToolsLocator_") || it.toLowerCase().endsWith("protopath") }.all {
extendsFrom(configurations.dependencyManagement)
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protobufJava.version}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcJava.version}"
}
}
generateProtoTasks {
all()*.plugins {
grpc {
option '@generated=omit'
}
}
}
}
@@ -0,0 +1,76 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.grpcserver;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.startupcheck.IndefiniteWaitOneShotStartupCheckStrategy;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import smoketest.grpcserver.SampleGrpcServerApplicationTests.GrpcServerStartedEventListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Import;
import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for the default Spring gRPC netty server.
*
* @author Phillip Webb
*/
@SpringBootTest(properties = "spring.grpc.server.port=0")
@Testcontainers(disabledWithoutDocker = true)
@Import(GrpcServerStartedEventListener.class)
class SampleGrpcServerApplicationTests {
@Autowired
private GrpcServerStartedEventListener startedEventListener;
@Test
@SuppressWarnings("resource")
void test() {
String address = "host.docker.internal:" + this.startedEventListener.getPort();
try (GenericContainer<?> container = new GenericContainer<>(
DockerImageName.parse("fullstorydev/grpcurl:v1.9.3"))
.withCommand("-d", "{\"name\": \"spring\"}", "--plaintext", address, "HelloWorld/SayHello")
.withStartupCheckStrategy(new IndefiniteWaitOneShotStartupCheckStrategy())) {
container.start();
assertThat(container.getLogs()).contains("\"message\": \"Hello 'spring'\"");
}
}
static class GrpcServerStartedEventListener implements ApplicationListener<GrpcServerStartedEvent> {
private int port;
@Override
public void onApplicationEvent(GrpcServerStartedEvent event) {
this.port = event.getPort();
}
int getPort() {
return this.port;
}
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.grpcserver;
import io.grpc.stub.StreamObserver;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import smoketest.grpcserver.proto.HelloReply;
import smoketest.grpcserver.proto.HelloRequest;
import smoketest.grpcserver.proto.HelloWorldGrpc;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
@Service
public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase {
private static Log logger = LogFactory.getLog(HelloWorldService.class);
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String name = request.getName();
logger.info("sayHello " + name);
Assert.isTrue(!name.startsWith("error"), () -> "Bad name: " + name);
Assert.state(!name.startsWith("internal"), "Internal error");
String message = "Hello '%s'".formatted(name);
HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
@Override
public void streamHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String name = request.getName();
logger.info("streamHello " + name);
int count = 0;
while (count < 10) {
String message = "Hello(" + count + ") '%s'".formatted(name);
HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
responseObserver.onNext(reply);
count++;
try {
Thread.sleep(100L);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
responseObserver.onError(ex);
return;
}
}
responseObserver.onCompleted();
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.grpcserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleGrpcServerApplication {
public static void main(String[] args) {
SpringApplication.run(SampleGrpcServerApplication.class, args);
}
}
@@ -0,0 +1,20 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@NullMarked
package smoketest.grpcserver;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "smoketest.grpcserver.proto";
option java_multiple_files = true;
service HelloWorld {
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc StreamHello(HelloRequest) returns (stream HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
@@ -0,0 +1,28 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id "org.springframework.boot.starter"
}
description = "Starter for using Spring gRPC server"
dependencies {
api(project(":starter:spring-boot-starter"))
api(project(":module:spring-boot-grpc-server"))
api("io.grpc:grpc-netty")
api("io.grpc:grpc-services")
}