Add Spring gRPC client support

Add support for Spring gRPC client applications.

Closes gh-49045

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 e61bb6df5b
commit c35b21adb4
41 changed files with 3343 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-client"))
implementation(project(path: ":module:spring-boot-grpc-server"))
implementation(project(path: ":module:spring-boot-health"))
implementation(project(path: ":module:spring-boot-hibernate"))
@@ -97,6 +97,9 @@ dependencies {
api(project(":module:spring-boot-graphql")) {
transitive = false
}
api(project(":module:spring-boot-grpc-client")) {
transitive = false
}
api(project(":module:spring-boot-grpc-server")) {
transitive = false
}
@@ -0,0 +1,53 @@
/*
* 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 Client"
dependencies {
api(project(":core:spring-boot"))
api("org.springframework.grpc:spring-grpc-core")
optional(project(":core:spring-boot-autoconfigure"))
optional(project(":module:spring-boot-micrometer-observation"))
optional("io.grpc:grpc-stub")
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("io.grpc:grpc-xds") {
exclude group: "javax.annotation", module: "javax.annotation-api"
}
testImplementation(project(":core:spring-boot-test"))
testImplementation(project(":test-support:spring-boot-test-support"))
testImplementation("org.yaml:snakeyaml")
testRuntimeOnly("ch.qos.logback:logback-classic")
}
tasks.named("compileTestJava") {
options.nullability.checking = "tests"
}
@@ -0,0 +1,73 @@
/*
* 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.client.autoconfigure;
import io.grpc.stub.AbstractStub;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Primary;
import org.springframework.grpc.client.CompositeGrpcChannelFactory;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
import org.springframework.grpc.client.GrpcChannelFactory;
/**
* {@link EnableAutoConfiguration Auto-configuration} for a
* {@link CompositeGrpcChannelFactory}.
*
* @author Chris Bono
* @since 4.1.0
*/
@AutoConfiguration
@ConditionalOnClass({ AbstractStub.class, GrpcChannelBuilderCustomizer.class })
@ConditionalOnProperty(name = "spring.grpc.client.enabled", matchIfMissing = true)
@Conditional(CompositeChannelFactoryAutoConfiguration.MultipleNonPrimaryChannelFactoriesCondition.class)
public final class CompositeChannelFactoryAutoConfiguration {
@Bean
@Primary
CompositeGrpcChannelFactory compositeChannelFactory(ObjectProvider<GrpcChannelFactory> channelFactoriesProvider) {
return new CompositeGrpcChannelFactory(channelFactoriesProvider.orderedStream().toList());
}
static class MultipleNonPrimaryChannelFactoriesCondition extends NoneNestedConditions {
MultipleNonPrimaryChannelFactoriesCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnMissingBean(GrpcChannelFactory.class)
static class NoChannelFactoryCondition {
}
@ConditionalOnSingleCandidate(GrpcChannelFactory.class)
static class SingleInjectableChannelFactoryCondition {
}
}
}
@@ -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.client.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.client.channelfactory.enabled} is {@code true} or missing.
*
* @author Phillip Webb
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@ConditionalOnBooleanProperty(name = "spring.grpc.client.channelfactory.enabled", matchIfMissing = true)
@interface ConditionalOnGrpcClientChannelFactoryEnabled {
}
@@ -0,0 +1,113 @@
/*
* 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.client.autoconfigure;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import io.grpc.CompressorRegistry;
import io.grpc.DecompressorRegistry;
import io.grpc.ManagedChannelBuilder;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.util.LambdaSafe;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
/**
* Invokes the customizations to a {@link ManagedChannelBuilder} based on the provided
* beans.
*
* @author Chris Bono
* @author Phillip Webb
*/
@Order(Ordered.LOWEST_PRECEDENCE)
class GrpcChannelBuilderCustomizers {
private final List<GrpcChannelBuilderCustomizer<?>> customizers;
GrpcChannelBuilderCustomizers(GrpcClientProperties grpcClientProperties,
ObjectProvider<CompressorRegistry> compressorRegistry,
ObjectProvider<DecompressorRegistry> decompressorRegistry,
ObjectProvider<GrpcChannelBuilderCustomizer<?>> customizers,
ObjectProvider<GrpcClientDefaultServiceConfigCustomizer> defaultServiceConfigCustomizers) {
this(grpcClientProperties, compressorRegistry.getIfAvailable(), decompressorRegistry.getIfAvailable(),
customizers.orderedStream().toList(), defaultServiceConfigCustomizers.orderedStream().toList());
}
GrpcChannelBuilderCustomizers(List<? extends GrpcChannelBuilderCustomizer<?>> customizers) {
this(null, null, null, customizers, Collections.emptyList());
}
GrpcChannelBuilderCustomizers(@Nullable GrpcClientProperties grpcClientProperties,
@Nullable CompressorRegistry compressorRegistry, @Nullable DecompressorRegistry decompressorRegistry,
List<? extends GrpcChannelBuilderCustomizer<?>> customizers,
List<? extends GrpcClientDefaultServiceConfigCustomizer> defaultServiceConfigCustomizers) {
List<GrpcChannelBuilderCustomizer<?>> all = new ArrayList<>();
addCustomizer(all, compressorRegistry, ManagedChannelBuilder::compressorRegistry);
addCustomizer(all, decompressorRegistry, ManagedChannelBuilder::decompressorRegistry);
if (grpcClientProperties != null) {
all.add(new PropertiesGrpcChannelBuilderCustomizer<>(grpcClientProperties));
}
all.addAll(customizers);
all.add(customizeDefaultServiceConfig(grpcClientProperties, defaultServiceConfigCustomizers));
this.customizers = List.copyOf(all);
}
private static <B extends ManagedChannelBuilder<B>, T> void addCustomizer(
List<GrpcChannelBuilderCustomizer<?>> customizers, @Nullable T bean, BiConsumer<B, T> action) {
if (bean != null) {
GrpcChannelBuilderCustomizer<B> customizer = (target, builder) -> action.accept(builder, bean);
customizers.add(customizer);
}
}
private <B extends ManagedChannelBuilder<B>> GrpcChannelBuilderCustomizer<B> customizeDefaultServiceConfig(
@Nullable GrpcClientProperties properties,
List<? extends GrpcClientDefaultServiceConfigCustomizer> customizers) {
PropertiesGrpcClientDefaultServiceConfigCustomizer propertiesCustomizer = (properties != null)
? new PropertiesGrpcClientDefaultServiceConfigCustomizer(properties) : null;
return (target, builder) -> {
Map<String, Object> defaultServiceConfig = new LinkedHashMap<>();
if (propertiesCustomizer != null) {
propertiesCustomizer.customize(target, defaultServiceConfig);
}
customizers.forEach((customizer) -> customizer.customize(target, defaultServiceConfig));
if (!defaultServiceConfig.isEmpty()) {
builder.defaultServiceConfig(defaultServiceConfig);
}
};
}
<T extends ManagedChannelBuilder<T>> List<GrpcChannelBuilderCustomizer<T>> forFactory() {
return List.of(this::apply);
}
@SuppressWarnings("unchecked")
<T extends ManagedChannelBuilder<?>> void apply(String target, T builder) {
LambdaSafe.callbacks(GrpcChannelBuilderCustomizer.class, this.customizers, builder)
.withLogger(GrpcChannelBuilderCustomizers.class)
.invoke((customizer) -> customizer.customize(target, builder));
}
}
@@ -0,0 +1,38 @@
/*
* 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.client.autoconfigure;
import org.springframework.grpc.client.GrpcChannelFactory;
/**
* Callback interface that can be implemented by beans wishing to customize the
* {@link GrpcChannelFactory} before it is fully initialized, in particular to tune its
* configuration.
*
* @author Chris Bono
* @since 4.1.0
*/
@FunctionalInterface
public interface GrpcChannelFactoryCustomizer {
/**
* Customize the given {@link GrpcChannelFactory}.
* @param factory the factory to customize
*/
void customize(GrpcChannelFactory factory);
}
@@ -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.client.autoconfigure;
import io.grpc.CompressorRegistry;
import io.grpc.DecompressorRegistry;
import io.grpc.ManagedChannelBuilder;
import io.grpc.stub.AbstractStub;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.ssl.SslBundles;
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.client.ChannelCredentialsProvider;
import org.springframework.grpc.client.ClientInterceptorsConfigurer;
import org.springframework.grpc.client.CoroutineStubFactory;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
/**
* {@link EnableAutoConfiguration Auto-configuration} for gRPC clients.
*
* @author Dave Syer
* @author Chris Bono
* @author Phillip Webb
* @since 4.1.0
*/
@AutoConfiguration(before = CompositeChannelFactoryAutoConfiguration.class)
@ConditionalOnClass({ AbstractStub.class, GrpcChannelBuilderCustomizer.class })
@ConditionalOnProperty(name = "spring.grpc.client.enabled", matchIfMissing = true)
@EnableConfigurationProperties(GrpcClientProperties.class)
@Import({ GrpcClientCodecConfiguration.class, ShadedNettyGrpcClientConfiguration.class,
NettyGrpcClientConfiguration.class, InProcessGrpcClientConfiguration.class })
public final class GrpcClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
ClientInterceptorsConfigurer grpcClientInterceptorsConfigurer(ApplicationContext applicationContext) {
return new ClientInterceptorsConfigurer(applicationContext);
}
@Bean
@ConditionalOnMissingBean(ChannelCredentialsProvider.class)
PropertiesChannelCredentialsProvider grpcChannelCredentialsProvider(SslBundles bundles,
GrpcClientProperties properties) {
return new PropertiesChannelCredentialsProvider(properties, bundles);
}
@Bean
<T extends ManagedChannelBuilder<T>> PropertiesGrpcChannelBuilderCustomizer<T> grpcClientPropertiesChannelCustomizer(
GrpcClientProperties properties) {
return new PropertiesGrpcChannelBuilderCustomizer<>(properties);
}
@Bean
GrpcChannelBuilderCustomizers grpcDefaultServicesChannelBuilderCustomizer(GrpcClientProperties grpcClientProperties,
ObjectProvider<CompressorRegistry> compressorRegistry,
ObjectProvider<DecompressorRegistry> decompressorRegistry,
ObjectProvider<GrpcChannelBuilderCustomizer<?>> customizers,
ObjectProvider<GrpcClientDefaultServiceConfigCustomizer> defaultServiceConfigCustomizers) {
return new GrpcChannelBuilderCustomizers(grpcClientProperties, compressorRegistry, decompressorRegistry,
customizers, defaultServiceConfigCustomizers);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = "io.grpc.kotlin.AbstractCoroutineStub")
static class GrpcClientCoroutineStubConfiguration {
@Bean
@ConditionalOnMissingBean
CoroutineStubFactory coroutineStubFactory() {
return new CoroutineStubFactory();
}
}
}
@@ -0,0 +1,83 @@
/*
* 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.client.autoconfigure;
import java.util.List;
import io.grpc.Codec;
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.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* The configuration that contains all codec related beans for clients.
*
* @author Andrei Lisa
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Codec.class)
class GrpcClientCodecConfiguration {
/**
* 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,49 @@
/*
* 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.client.autoconfigure;
import java.util.Map;
import io.grpc.ManagedChannelBuilder;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
import org.springframework.grpc.client.VirtualTargets;
/**
* Callback interface that can be used to customize the default service config of the
* {@link GrpcChannelBuilderCustomizer}.
* <p>
* This customizer should be used instead of calling
* {@link ManagedChannelBuilder#defaultServiceConfig(Map)} from a
* {@link GrpcChannelBuilderCustomizer} since it allows multiple customizers to update the
* same default service config, rather than having a "last wins" outcome.
*
* @author Phillip Webb
* @since 4.1.0
* @see GrpcChannelBuilderCustomizer
*/
@FunctionalInterface
public interface GrpcClientDefaultServiceConfigCustomizer {
/**
* Customize the given default service config.
* @param target the target (which may be a {@link VirtualTargets virtual target}).
* @param defaultServiceConfig the default service config to customize
*/
void customize(String target, Map<String, Object> defaultServiceConfig);
}
@@ -0,0 +1,369 @@
/*
* 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.client.autoconfigure;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.LinkedHashMap;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.Name;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.util.unit.DataSize;
/**
* Configuration properties for gRPC clients.
*
* @author Chris Bono
* @author Phillip Webb
* @since 4.1.0
*/
@ConfigurationProperties("spring.grpc.client")
public class GrpcClientProperties {
/**
* Map of channel configured by name.
*/
private final Map<String, Channel> channel = new LinkedHashMap<>();
public Map<String, Channel> getChannel() {
return this.channel;
}
/**
* Channel Properties.
*/
public static class Channel {
static final String DEFAULT_TARGET = "static://localhost:9090";
/**
* The channel target address.
*/
private String target = DEFAULT_TARGET;
/**
* The custom User-Agent for the channel.
*/
private @Nullable String userAgent;
/**
* Bypass certificate validation for easier testing (so the remote certificate
* could be anonymous). Should not be set in production.
*/
private boolean bypassCertificateValidation;
private final Inbound inbound = new Inbound();
@Name("default")
private final Default defaultProperties = new Default();
private final Idle idle = new Idle();
private final Keepalive keepalive = new Keepalive();
private final Ssl ssl = new Ssl();
private final Health health = new Health();
public String getTarget() {
return this.target;
}
public void setTarget(String target) {
this.target = target;
}
public @Nullable String getUserAgent() {
return this.userAgent;
}
public void setUserAgent(@Nullable String userAgent) {
this.userAgent = userAgent;
}
public boolean isBypassCertificateValidation() {
return this.bypassCertificateValidation;
}
public void setBypassCertificateValidation(boolean bypassCertificateValidation) {
this.bypassCertificateValidation = bypassCertificateValidation;
}
public Inbound getInbound() {
return this.inbound;
}
public Default getDefault() {
return this.defaultProperties;
}
public Idle getIdle() {
return this.idle;
}
public Keepalive getKeepalive() {
return this.keepalive;
}
public Ssl getSsl() {
return this.ssl;
}
public Health getHealth() {
return this.health;
}
/**
* 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 channel. Set to '-1'
* to use the highest possible limit (not recommended).
*/
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 channel. Set to
* '-1' to use the highest possible limit (not recommended).
*/
private DataSize maxSize = DataSize.ofBytes(8192);
public DataSize getMaxSize() {
return this.maxSize;
}
public void setMaxSize(DataSize maxSize) {
this.maxSize = maxSize;
}
}
}
/**
* Properties for client defaults.
*/
public static class Default {
/**
* The default deadline for RPCs performed on this channel.
*/
private @Nullable Duration deadline;
/**
* The load balancing policy the channel should use.
*/
private String loadBalancingPolicy = "round_robin";
public @Nullable Duration getDeadline() {
return this.deadline;
}
public void setDeadline(@Nullable Duration deadline) {
this.deadline = deadline;
}
public String getLoadBalancingPolicy() {
return this.loadBalancingPolicy;
}
public void setLoadBalancingPolicy(String loadBalancingPolicy) {
this.loadBalancingPolicy = loadBalancingPolicy;
}
}
/**
* Idle properties.
*/
public static class Idle {
/**
* The duration without ongoing RPCs before going to idle mode.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration timeout = Duration.ofSeconds(20);
public Duration getTimeout() {
return this.timeout;
}
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
}
/**
* Keep-alive properties.
*/
public static class Keepalive {
/**
* The delay before sending a keepAlive. Note that shorter intervals increase
* the network burden for the server and this value can not be lower than
* 'permitKeepAliveTime' on the server.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration time = Duration.ofMinutes(5);
/**
* The default timeout for a keepAlives ping request.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration timeout = Duration.ofSeconds(20);
/**
* Whether a keepAlive will be performed when there are no outstanding RPC on
* a connection.
*/
private boolean withoutCalls;
public Duration getTime() {
return this.time;
}
public void setTime(Duration time) {
this.time = time;
}
public Duration getTimeout() {
return this.timeout;
}
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
public boolean isWithoutCalls() {
return this.withoutCalls;
}
public void setWithoutCalls(boolean withoutCalls) {
this.withoutCalls = withoutCalls;
}
}
/**
* Health properties.
*/
public static class Health {
/**
* Whether to enable client-side health check for the channel.
*/
private boolean enabled;
/**
* Name of the service to check health on.
*/
private @Nullable String serviceName;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public @Nullable String getServiceName() {
return this.serviceName;
}
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
}
/**
* SSL properties.
*/
public static class Ssl {
/**
* Whether to enable SSL support. Enabled automatically if "bundle" is
* provided unless specified otherwise.
*/
private @Nullable Boolean enabled;
/**
* SSL bundle name.
*/
private @Nullable String bundle;
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;
}
}
}
}
@@ -0,0 +1,56 @@
/*
* 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.client.autoconfigure;
import io.grpc.inprocess.InProcessChannelBuilder;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.client.ClientInterceptorFilter;
import org.springframework.grpc.client.ClientInterceptorsConfigurer;
import org.springframework.grpc.client.InProcessGrpcChannelFactory;
/**
* {@link Configuration @Configuration} for an in-process gRPC client.
*
* @author Chris Bono
* @author Phillip Webb
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(InProcessChannelBuilder.class)
@ConditionalOnMissingBean(InProcessGrpcChannelFactory.class)
@ConditionalOnGrpcClientChannelFactoryEnabled
@ConditionalOnProperty(name = "spring.grpc.client.inprocess.enabled", havingValue = "true", matchIfMissing = true)
class InProcessGrpcClientConfiguration {
@Bean
InProcessGrpcChannelFactory inProcessGrpcChannelFactory(GrpcChannelBuilderCustomizers grpcChannelBuilderCustomizers,
ClientInterceptorsConfigurer interceptorsConfigurer,
ObjectProvider<ClientInterceptorFilter> interceptorFilter,
ObjectProvider<GrpcChannelFactoryCustomizer> channelFactoryCustomizers) {
InProcessGrpcChannelFactory factory = new InProcessGrpcChannelFactory(
grpcChannelBuilderCustomizers.forFactory(), interceptorsConfigurer);
interceptorFilter.ifAvailable(factory::setInterceptorFilter);
channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
return factory;
}
}
@@ -0,0 +1,60 @@
/*
* 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.client.autoconfigure;
import io.grpc.Channel;
import io.grpc.netty.NettyChannelBuilder;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.grpc.client.ChannelCredentialsProvider;
import org.springframework.grpc.client.ClientInterceptorsConfigurer;
import org.springframework.grpc.client.GrpcChannelFactory;
import org.springframework.grpc.client.InProcessGrpcChannelFactory;
import org.springframework.grpc.client.NettyGrpcChannelFactory;
/**
* {@link Configuration @Configuration} for a Netty gRPC client.
*
* @author Chris Bono
* @author Phillip Webb
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Channel.class, NettyChannelBuilder.class })
@ConditionalOnMissingBean(value = GrpcChannelFactory.class, ignored = InProcessGrpcChannelFactory.class)
@ConditionalOnGrpcClientChannelFactoryEnabled
class NettyGrpcClientConfiguration {
@Bean
NettyGrpcChannelFactory nettyGrpcChannelFactory(Environment environment, GrpcClientProperties properties,
GrpcChannelBuilderCustomizers grpcChannelBuilderCustomizers,
ClientInterceptorsConfigurer interceptorsConfigurer,
ObjectProvider<GrpcChannelFactoryCustomizer> channelFactoryCustomizers,
ChannelCredentialsProvider credentials) {
NettyGrpcChannelFactory factory = new NettyGrpcChannelFactory(grpcChannelBuilderCustomizers.forFactory(),
interceptorsConfigurer);
factory.setCredentialsProvider(credentials);
factory.setVirtualTargets(new PropertiesVirtualTargets(environment, properties));
channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
return factory;
}
}
@@ -0,0 +1,65 @@
/*
* 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.client.autoconfigure;
import io.grpc.ChannelCredentials;
import io.grpc.InsecureChannelCredentials;
import io.grpc.TlsChannelCredentials;
import io.grpc.TlsChannelCredentials.Builder;
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel.Ssl;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.grpc.client.ChannelCredentialsProvider;
import org.springframework.grpc.internal.InsecureTrustManagerFactory;
/**
* {@link ChannelCredentialsProvider} backed by {@link GrpcClientProperties}.
*
* @param properties the client properties
* @param bundles the SSL bundles
* @author David Syer
* @author Phillip Webb
*/
record PropertiesChannelCredentialsProvider(GrpcClientProperties properties,
SslBundles bundles) implements ChannelCredentialsProvider {
@Override
public ChannelCredentials getChannelCredentials(String target) {
Channel channel = this.properties.getChannel().get(target);
channel = (channel != null) ? channel : this.properties.getChannel().get("default");
if (channel == null || isInsecure(channel.getSsl())) {
return InsecureChannelCredentials.create();
}
Builder builder = TlsChannelCredentials.newBuilder();
if (channel.getSsl().getBundle() != null) {
SslBundle bundle = this.bundles.getBundle(channel.getSsl().getBundle());
builder.trustManager(bundle.getManagers().getTrustManagerFactory().getTrustManagers());
builder.keyManager(bundle.getManagers().getKeyManagerFactory().getKeyManagers());
}
if (channel.isBypassCertificateValidation()) {
builder.trustManager(InsecureTrustManagerFactory.INSTANCE.getTrustManagers());
}
return builder.build();
}
private boolean isInsecure(Ssl ssl) {
return Boolean.FALSE.equals(ssl.getEnabled()) || (ssl.getBundle() == null && ssl.getEnabled() == null);
}
}
@@ -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.client.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 org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
import org.springframework.grpc.client.interceptor.DefaultDeadlineSetupClientInterceptor;
import org.springframework.util.Assert;
import org.springframework.util.unit.DataSize;
/**
* {@link GrpcChannelBuilderCustomizer} that maps {@link GrpcClientProperties} to a
* {@link ManagedChannelBuilder}.
*
* @param <T> the type of the builder
* @param properties the properties to map
* @author David Syer
* @author Chris Bono
* @author Phillip Webb
*/
record PropertiesGrpcChannelBuilderCustomizer<T extends ManagedChannelBuilder<T>>(
GrpcClientProperties properties) implements GrpcChannelBuilderCustomizer<T> {
private static final Channel STOCK_DEFAULT_CHANNEL = new Channel();
@Override
public void customize(String target, T builder) {
Channel channel = getChannel(target);
PropertyMapper map = PropertyMapper.get();
map.from(channel::getUserAgent).to(builder::userAgent);
map.from(channel.getInbound().getMessage()::getMaxSize).asInt(this::maxSize).to(builder::maxInboundMessageSize);
map.from(channel.getInbound().getMetadata()::getMaxSize)
.asInt(this::maxSize)
.to(builder::maxInboundMetadataSize);
map.from(channel.getDefault()::getDeadline)
.when((deadline) -> deadline.toMillis() > 0L)
.as(DefaultDeadlineSetupClientInterceptor::new)
.to(builder::intercept);
map.from(channel.getDefault()::getLoadBalancingPolicy)
.when((policy) -> supportsLoadBalancing(target, channel))
.to(builder::defaultLoadBalancingPolicy);
map.from(channel.getIdle()::getTimeout).to(durationProperty(builder::idleTimeout));
map.from(channel.getKeepalive()::getTime).to(durationProperty(builder::keepAliveTime));
map.from(channel.getKeepalive()::getTimeout).to(durationProperty(builder::keepAliveTimeout));
map.from(channel.getKeepalive()::isWithoutCalls).to(builder::keepAliveWithoutCalls);
}
private Channel getChannel(String target) {
Channel channel = this.properties.getChannel().get(target);
channel = (channel != null) ? channel : this.properties.getChannel().get("default");
return (channel != null) ? channel : STOCK_DEFAULT_CHANNEL;
}
private boolean supportsLoadBalancing(String target, Channel channel) {
return !(isUnixOrInProcessTarget(target) || isUnixOrInProcessTarget(channel.getTarget()));
}
private boolean isUnixOrInProcessTarget(String target) {
return target.startsWith("unix:") || target.startsWith("in-process:");
}
Consumer<Duration> durationProperty(BiConsumer<Long, TimeUnit> setter) {
return (duration) -> setter.accept(duration.toNanos(), TimeUnit.NANOSECONDS);
}
private int maxSize(DataSize maxSize) {
long bytes = maxSize.toBytes();
Assert.state(bytes >= 0 || bytes == -1, () -> "Unsupported max size value " + maxSize);
return (bytes >= 0 && bytes <= Integer.MAX_VALUE) ? (int) bytes : Integer.MAX_VALUE;
}
}
@@ -0,0 +1,44 @@
/*
* 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.client.autoconfigure;
import java.util.Map;
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
/**
* {@link GrpcClientDefaultServiceConfigCustomizer} to apply {@link GrpcClientProperties}.
*
* @author Chris Bono
* @author Phillip Webb
* @param properties the client properties
*/
record PropertiesGrpcClientDefaultServiceConfigCustomizer(
GrpcClientProperties properties) implements GrpcClientDefaultServiceConfigCustomizer {
@Override
public void customize(String target, Map<String, Object> defaultServiceConfig) {
Channel channel = this.properties.getChannel().get(target);
channel = (channel != null) ? channel : this.properties.getChannel().get("default");
if (channel != null && channel.getHealth().isEnabled()) {
String serviceName = channel.getHealth().getServiceName();
Map<String, String> healthCheckConfig = Map.of("serviceName", (serviceName != null) ? serviceName : "");
defaultServiceConfig.put("healthCheckConfig", healthCheckConfig);
}
}
}
@@ -0,0 +1,59 @@
/*
* 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.client.autoconfigure;
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
import org.springframework.core.env.PropertyResolver;
import org.springframework.grpc.client.VirtualTargets;
/**
* {@link VirtualTargets} supporting named channels from {@link GrpcClientProperties} and
* directly specified targets (which may include property placeholders).
*
* @param propertyResolver the property resolver
* @param properties the client properties
* @author Chris Bono
* @author Phillip Webb
*/
record PropertiesVirtualTargets(PropertyResolver propertyResolver,
GrpcClientProperties properties) implements VirtualTargets {
@Override
public String getTarget(String target) {
Channel channel = this.properties.getChannel().get(target);
if (channel != null) {
return clean(this.propertyResolver.resolvePlaceholders(channel.getTarget()));
}
if ("default".equals(target)) {
return clean(Channel.DEFAULT_TARGET);
}
target = this.propertyResolver.resolvePlaceholders(target);
if (target.contains(":/") || target.startsWith("unix:")) {
return clean(target);
}
return target;
}
private String clean(String target) {
if (target.startsWith("static:") || target.startsWith("tcp:")) {
String withoutScheme = target.substring(target.indexOf(":") + 1);
return withoutScheme.replaceFirst("/*", "");
}
return target;
}
}
@@ -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.client.autoconfigure;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.grpc.client.ChannelCredentialsProvider;
import org.springframework.grpc.client.ClientInterceptorsConfigurer;
import org.springframework.grpc.client.GrpcChannelFactory;
import org.springframework.grpc.client.InProcessGrpcChannelFactory;
import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory;
/**
* {@link Configuration @Configuration} for a Shaded Netty gRPC client.
*
* @author Chris Bono
* @author Phillip Webb
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ io.grpc.netty.shaded.io.netty.channel.Channel.class,
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class })
@ConditionalOnMissingBean(value = GrpcChannelFactory.class, ignored = InProcessGrpcChannelFactory.class)
@ConditionalOnGrpcClientChannelFactoryEnabled
class ShadedNettyGrpcClientConfiguration {
@Bean
ShadedNettyGrpcChannelFactory shadedNettyGrpcChannelFactory(Environment environment,
GrpcClientProperties properties, GrpcChannelBuilderCustomizers grpcChannelBuilderCustomizers,
ClientInterceptorsConfigurer interceptorsConfigurer,
ObjectProvider<GrpcChannelFactoryCustomizer> channelFactoryCustomizers,
ChannelCredentialsProvider credentials) {
ShadedNettyGrpcChannelFactory factory = new ShadedNettyGrpcChannelFactory(
grpcChannelBuilderCustomizers.forFactory(), interceptorsConfigurer);
factory.setCredentialsProvider(credentials);
factory.setVirtualTargets(new PropertiesVirtualTargets(environment, properties));
channelFactoryCustomizers.orderedStream().forEach((customizer) -> customizer.customize(factory));
return factory;
}
}
@@ -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 clients.
*/
@NullMarked
package org.springframework.boot.grpc.client.autoconfigure;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,23 @@
{
"groups": [],
"properties": [
{
"name": "spring.grpc.client.channelfactory.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable gRPC channel factory bean auto-configuration.",
"defaultValue": true
},
{
"name": "spring.grpc.client.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable gRPC client auto-configuration.",
"defaultValue": true
},
{
"name": "spring.grpc.client.inprocess.enabled",
"type": "java.lang.Boolean",
"description": "Whether to configure the in-process channel factory.",
"defaultValue": true
}
]
}
@@ -0,0 +1,2 @@
org.springframework.boot.grpc.client.autoconfigure.CompositeChannelFactoryAutoConfiguration
org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration
@@ -0,0 +1,140 @@
/*
* 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.client.autoconfigure;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.netty.NettyChannelBuilder;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.grpc.client.CompositeGrpcChannelFactory;
import org.springframework.grpc.client.GrpcChannelFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link CompositeChannelFactoryAutoConfiguration}.
*
* @author Chris Bono
*/
class CompositeChannelFactoryAutoConfigurationTests {
private ApplicationContextRunner contextRunnerWithoutChannelFactories() {
return new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class, SslAutoConfiguration.class,
CompositeChannelFactoryAutoConfiguration.class))
.withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class,
NettyChannelBuilder.class, InProcessChannelBuilder.class));
}
@Test
void whenNoChannelFactoriesDoesNotAutoconfigureComposite() {
this.contextRunnerWithoutChannelFactories()
.run((context) -> assertThat(context).doesNotHaveBean(GrpcChannelFactory.class));
}
@Test
void whenSingleChannelFactoryDoesNotAutoconfigureComposite() {
GrpcChannelFactory channelFactory1 = mock();
this.contextRunnerWithoutChannelFactories()
.withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1)
.run((context) -> assertThat(context).hasSingleBean(GrpcChannelFactory.class)
.getBean(GrpcChannelFactory.class)
.isNotInstanceOf(CompositeGrpcChannelFactory.class)
.isSameAs(channelFactory1));
}
@Test
void whenMultipleChannelFactoriesWithPrimaryDoesNotAutoconfigureComposite() {
GrpcChannelFactory channelFactory1 = mock();
GrpcChannelFactory channelFactory2 = mock();
this.contextRunnerWithoutChannelFactories()
.withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1)
.withBean("channelFactory2", GrpcChannelFactory.class, () -> channelFactory2, (bd) -> bd.setPrimary(true))
.run((context) -> {
assertThat(context).getBeans(GrpcChannelFactory.class)
.containsOnlyKeys("channelFactory1", "channelFactory2");
assertThat(context).getBean(GrpcChannelFactory.class)
.isNotInstanceOf(CompositeGrpcChannelFactory.class)
.isSameAs(channelFactory2);
});
}
@Test
void whenMultipleChannelFactoriesDoesAutoconfigureComposite() {
GrpcChannelFactory channelFactory1 = mock();
GrpcChannelFactory channelFactory2 = mock();
this.contextRunnerWithoutChannelFactories()
.withBean("channelFactory1", GrpcChannelFactory.class, () -> channelFactory1)
.withBean("channelFactory2", GrpcChannelFactory.class, () -> channelFactory2)
.run((context) -> {
assertThat(context).getBeans(GrpcChannelFactory.class)
.containsOnlyKeys("channelFactory1", "channelFactory2", "compositeChannelFactory");
assertThat(context).getBean(GrpcChannelFactory.class).isInstanceOf(CompositeGrpcChannelFactory.class);
});
}
@Test
void compositeAutoconfiguredAsExpected() {
this.contextRunnerWithoutChannelFactories()
.withUserConfiguration(MultipleFactoriesTestConfig.class)
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
.isInstanceOf(CompositeGrpcChannelFactory.class)
.extracting("channelFactories")
.asInstanceOf(InstanceOfAssertFactories.list(GrpcChannelFactory.class))
.containsExactly(MultipleFactoriesTestConfig.CHANNEL_FACTORY_BAR,
MultipleFactoriesTestConfig.CHANNEL_FACTORY_BAZ,
MultipleFactoriesTestConfig.CHANNEL_FACTORY_FOO));
}
@Configuration(proxyBeanMethods = false)
static class MultipleFactoriesTestConfig {
static GrpcChannelFactory CHANNEL_FACTORY_FOO = mock();
static GrpcChannelFactory CHANNEL_FACTORY_BAR = mock();
static GrpcChannelFactory CHANNEL_FACTORY_BAZ = mock();
@Bean
@Order(3)
GrpcChannelFactory channelFactoryFoo() {
return CHANNEL_FACTORY_FOO;
}
@Bean
@Order(1)
GrpcChannelFactory channelFactoryBar() {
return CHANNEL_FACTORY_BAR;
}
@Bean
@Order(2)
GrpcChannelFactory channelFactoryBaz() {
return CHANNEL_FACTORY_BAZ;
}
}
}
@@ -0,0 +1,242 @@
/*
* 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.client.autoconfigure;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import io.grpc.CompressorRegistry;
import io.grpc.DecompressorRegistry;
import io.grpc.ManagedChannelBuilder;
import io.grpc.netty.NettyChannelBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
/**
* Tests for {@link GrpcChannelBuilderCustomizers}.
*
* @author Chris Bono
* @author Phillip Webb
*/
class GrpcChannelBuilderCustomizersTests {
@Test
void applyWhenHasProperties() {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channel = new Channel();
channel.setUserAgent("spring-boot");
properties.getChannel().put("target", channel);
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
Collections.emptyList(), Collections.emptyList());
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
customizers.apply("target", builder);
then(builder).should().userAgent("spring-boot");
}
@Test
void applyWhenHasCompressorRegistry() {
CompressorRegistry compressorRegistry = mock();
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(null, compressorRegistry, null,
Collections.emptyList(), Collections.emptyList());
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
customizers.apply("target", builder);
then(builder).should().compressorRegistry(compressorRegistry);
}
@Test
void applyWhenHasDecompressorRegistry() {
DecompressorRegistry decompressorRegistry = mock();
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(null, null, decompressorRegistry,
Collections.emptyList(), Collections.emptyList());
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
customizers.apply("target", builder);
then(builder).should().decompressorRegistry(decompressorRegistry);
}
@Test
void applyWhenEmptyCustomizersDoesNothing() {
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
new GrpcChannelBuilderCustomizers(Collections.emptyList()).apply("target", builder);
then(builder).shouldHaveNoInteractions();
}
@Test
void applyWhenSimpleChannelBuilder() {
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(
List.of(new SimpleChannelBuilderCustomizer()));
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
customizers.apply("target", builder);
then(builder).should().flowControlWindow(100);
}
@Test
@SuppressWarnings("unchecked")
void applyWhenGenericCustomizersRespectsGeneric() {
List<TestCustomizer<?>> list = new ArrayList<>();
list.add(new TestCustomizer<>());
list.add(new TestNettyChannelBuilderCustomizer());
list.add(new TestShadedNettyChannelBuilderCustomizer());
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(list);
customizers.apply("target", mock(ManagedChannelBuilder.class));
assertThat(list.get(0).getCount()).isOne();
assertThat(list.get(1).getCount()).isZero();
assertThat(list.get(2).getCount()).isZero();
customizers.apply("target", mock(NettyChannelBuilder.class));
assertThat(list.get(0).getCount()).isEqualTo(2);
assertThat(list.get(1).getCount()).isOne();
assertThat(list.get(2).getCount()).isZero();
customizers.apply("target", mock(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class));
assertThat(list.get(0).getCount()).isEqualTo(3);
assertThat(list.get(1).getCount()).isOne();
assertThat(list.get(2).getCount()).isOne();
}
@Test
void applyWhenHasGrpcClientDefaultServiceConfigCustomizers() {
GrpcClientDefaultServiceConfigCustomizer defaultConfigCustomizer1 = (target, defaultServiceConfig) -> {
defaultServiceConfig.put("c", "v1");
defaultServiceConfig.put("c1", "v1");
};
GrpcClientDefaultServiceConfigCustomizer defaultConfigCustomizer2 = (target, defaultServiceConfig) -> {
defaultServiceConfig.put("c", "v2");
defaultServiceConfig.put("c2", "v2");
};
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(null, null, null,
Collections.emptyList(), List.of(defaultConfigCustomizer1, defaultConfigCustomizer2));
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
customizers.apply("target", builder);
Map<String, Object> expected = new LinkedHashMap<>();
expected.put("c", "v2");
expected.put("c1", "v1");
expected.put("c2", "v2");
then(builder).should().defaultServiceConfig(expected);
}
@Test
void applyWhenHasChannelHealthAddsHealthServiceConfig() {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channel = new Channel();
channel.getHealth().setEnabled(true);
channel.getHealth().setServiceName("testservice");
properties.getChannel().put("target", channel);
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
Collections.emptyList(), Collections.emptyList());
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
customizers.apply("target", builder);
Map<String, Object> expected = new LinkedHashMap<>();
expected.put("healthCheckConfig", Map.of("serviceName", "testservice"));
then(builder).should().defaultServiceConfig(expected);
}
@Test
void applyWhenHasDefaultHealthAddsHealthServiceConfig() {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channel = new Channel();
channel.getHealth().setEnabled(true);
channel.getHealth().setServiceName("testdefaultservice");
properties.getChannel().put("default", channel);
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
Collections.emptyList(), Collections.emptyList());
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
customizers.apply("target", builder);
Map<String, Object> expected = new LinkedHashMap<>();
expected.put("healthCheckConfig", Map.of("serviceName", "testdefaultservice"));
then(builder).should().defaultServiceConfig(expected);
}
@Test
void applyWhenHealthEnabledAndNoServiceNameAddsHealthConfig() {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channel = new Channel();
channel.getHealth().setEnabled(true);
properties.getChannel().put("target", channel);
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
Collections.emptyList(), Collections.emptyList());
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
customizers.apply("target", builder);
Map<String, Object> expected = new LinkedHashMap<>();
expected.put("healthCheckConfig", Map.of("serviceName", ""));
then(builder).should().defaultServiceConfig(expected);
}
@Test
void applyWhenNoCustomizersOrHealthDoesSetDefaultServiceConfig() {
GrpcClientProperties properties = new GrpcClientProperties();
GrpcChannelBuilderCustomizers customizers = new GrpcChannelBuilderCustomizers(properties, null, null,
Collections.emptyList(), Collections.emptyList());
NettyChannelBuilder builder = mock(NettyChannelBuilder.class);
customizers.apply("target", builder);
then(builder).should(never()).defaultServiceConfig(any());
}
static class SimpleChannelBuilderCustomizer implements GrpcChannelBuilderCustomizer<NettyChannelBuilder> {
@Override
public void customize(String target, NettyChannelBuilder channelBuilder) {
channelBuilder.flowControlWindow(100);
}
}
/**
* Test customizer that will match any {@link GrpcChannelBuilderCustomizer}.
*
* @param <T> the builder type
*/
static class TestCustomizer<T extends ManagedChannelBuilder<T>> implements GrpcChannelBuilderCustomizer<T> {
private int count;
@Override
public void customize(String targetOrChannelName, T channelBuilder) {
this.count++;
}
int getCount() {
return this.count;
}
}
/**
* Test customizer that will match only {@link NettyChannelBuilder}.
*/
static class TestNettyChannelBuilderCustomizer extends TestCustomizer<NettyChannelBuilder> {
}
/**
* Test customizer that will match only
* {@link io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder}.
*/
static class TestShadedNettyChannelBuilderCustomizer
extends TestCustomizer<io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder> {
}
}
@@ -0,0 +1,396 @@
/*
* 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.client.autoconfigure;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import io.grpc.Codec;
import io.grpc.CompressorRegistry;
import io.grpc.DecompressorRegistry;
import io.grpc.ManagedChannelBuilder;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.kotlin.AbstractCoroutineStub;
import io.grpc.netty.NettyChannelBuilder;
import io.grpc.stub.AbstractStub;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration.GrpcClientCoroutineStubConfiguration;
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
import org.springframework.boot.grpc.client.autoconfigure.test.scan.DummyBlockingGrpc;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.grpc.client.ChannelCredentialsProvider;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
import org.springframework.grpc.client.GrpcChannelFactory;
import org.springframework.grpc.client.InProcessGrpcChannelFactory;
import org.springframework.grpc.client.NettyGrpcChannelFactory;
import org.springframework.grpc.client.ShadedNettyGrpcChannelFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
/**
* Tests for {@link GrpcClientAutoConfiguration}.
*
* @author Chris Bono
* @author Phillip Webb
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
class GrpcClientAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class, SslAutoConfiguration.class));
private final ApplicationContextRunner contextRunnerWithoutInProcessChannelFactory = this.contextRunner
.withPropertyValues("spring.grpc.client.inprocess.enabled=false");
@Test
void whenGrpcStubNotOnClasspathThenAutoConfigurationIsSkipped() {
this.contextRunner.withClassLoader(new FilteredClassLoader(AbstractStub.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientAutoConfiguration.class));
}
@Test
void whenGrpcKotlinIsNotOnClasspathThenAutoConfigurationIsSkipped() {
this.contextRunner.withClassLoader(new FilteredClassLoader(AbstractCoroutineStub.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientCoroutineStubConfiguration.class));
}
@Test
void whenClientEnabledPropertySetFalseThenAutoConfigurationIsSkipped() {
this.contextRunner.withPropertyValues("spring.grpc.client.enabled=false")
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientAutoConfiguration.class));
}
@Test
void whenClientEnabledPropertyNotSetThenAutoConfigurationIsNotSkipped() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GrpcClientAutoConfiguration.class));
}
@Test
void whenClientEnabledPropertySetTrueThenAutoConfigurationIsNotSkipped() {
this.contextRunner.withPropertyValues("spring.grpc.client.enabled=true")
.run((context) -> assertThat(context).hasSingleBean(GrpcClientAutoConfiguration.class));
}
@Test
void whenHasUserDefinedCredentialsProviderDoesNotAutoConfigureBean() {
ChannelCredentialsProvider customCredentialsProvider = mock(ChannelCredentialsProvider.class);
this.contextRunner
.withBean("customCredentialsProvider", ChannelCredentialsProvider.class, () -> customCredentialsProvider)
.run((context) -> assertThat(context).getBean(ChannelCredentialsProvider.class)
.isSameAs(customCredentialsProvider));
}
@Test
void credentialsProviderAutoConfiguredAsExpected() {
this.contextRunner.run((context) -> assertThat(context).getBean(PropertiesChannelCredentialsProvider.class)
.hasFieldOrPropertyWithValue("properties", context.getBean(GrpcClientProperties.class))
.extracting("bundles")
.isInstanceOf(SslBundles.class));
}
@Test
void clientPropertiesAutoConfiguredResolvesPlaceholders() {
this.contextRunner
.withPropertyValues("spring.grpc.client.channel.c1.target=my-server-${channelName}:8888", "channelName=foo")
.run((context) -> assertThat(context).getBean(GrpcClientProperties.class).satisfies((properties) -> {
Channel channel = properties.getChannel().get("c1");
assertThat(channel).isNotNull();
assertThat(channel.getTarget()).isEqualTo("my-server-foo:8888");
}));
}
@Test
void clientPropertiesChannelCustomizerAutoConfiguredWithHealthAsExpected() {
this.contextRunner
.withPropertyValues("spring.grpc.client.channel.test.health.enabled=true",
"spring.grpc.client.channel.test.health.service-name=my-service")
.run((context) -> {
GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class);
ManagedChannelBuilder<?> builder = Mockito.mock();
customizers.apply("test", builder);
Map<String, ?> healthCheckConfig = Map.of("healthCheckConfig", Map.of("serviceName", "my-service"));
then(builder).should().defaultServiceConfig(healthCheckConfig);
});
}
@Test
void clientPropertiesChannelCustomizerAutoConfiguredWithoutHealthAsExpected() {
this.contextRunner.run((context) -> {
GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class);
ManagedChannelBuilder<?> builder = Mockito.mock();
customizers.apply("test", builder);
then(builder).should(never()).defaultServiceConfig(anyMap());
});
}
@Test
void compressionCustomizerAutoConfiguredAsExpected() {
this.contextRunner.run((context) -> {
GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class);
CompressorRegistry compressorRegistry = context.getBean(CompressorRegistry.class);
ManagedChannelBuilder<?> builder = Mockito.mock();
customizers.apply("testChannel", builder);
then(builder).should().compressorRegistry(compressorRegistry);
});
}
@Test
void compressionCustomizerWhenNoRegistrry() {
// Codec class guards the imported GrpcCodecConfiguration to hide registry
this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class)).run((context) -> {
GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class);
ManagedChannelBuilder<?> builder = Mockito.mock();
customizers.apply("testChannel", builder);
then(builder).should(never()).compressorRegistry(any());
});
}
@Test
void decompressionCustomizerAutoConfiguredAsExpected() {
this.contextRunner.run((context) -> {
GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class);
DecompressorRegistry decompressorRegistry = context.getBean(DecompressorRegistry.class);
ManagedChannelBuilder<?> builder = Mockito.mock();
customizers.apply("testChannel", builder);
then(builder).should().decompressorRegistry(decompressorRegistry);
});
}
@Test
void whenNoDecompressorRegistryThenDecompressionCustomizerIsNotConfigured() {
// Codec class guards the imported GrpcCodecConfiguration to hide registry
this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class)).run((context) -> {
GrpcChannelBuilderCustomizers customizers = context.getBean(GrpcChannelBuilderCustomizers.class);
ManagedChannelBuilder<?> builder = Mockito.mock();
customizers.apply("testChannel", builder);
then(builder).should(never()).compressorRegistry(any());
});
}
@Test
void whenInProcessEnabledPropNotSetDoesAutoconfigureInProcess() {
this.contextRunner.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
.containsKey("inProcessGrpcChannelFactory"));
}
@Test
void whenInProcessEnabledPropSetToTrueDoesAutoconfigureInProcess() {
this.contextRunner.withPropertyValues("spring.grpc.client.inprocess.enabled=true")
.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
.containsKey("inProcessGrpcChannelFactory"));
}
@Test
void whenInProcessEnabledPropSetToFalseDoesNotAutoconfigureInProcess() {
this.contextRunner.withPropertyValues("spring.grpc.client.inprocess.enabled=false")
.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
.doesNotContainKey("inProcessGrpcChannelFactory"));
}
@Test
void whenInProcessIsNotOnClasspathDoesNotAutoconfigureInProcess() {
this.contextRunner.withClassLoader(new FilteredClassLoader(InProcessChannelBuilder.class))
.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
.doesNotContainKey("inProcessGrpcChannelFactory"));
}
@Test
void whenHasUserDefinedInProcessChannelFactoryDoesNotAutoConfigureBean() {
InProcessGrpcChannelFactory customChannelFactory = mock();
this.contextRunner
.withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class,
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
.withBean("customChannelFactory", InProcessGrpcChannelFactory.class, () -> customChannelFactory)
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class).isSameAs(customChannelFactory));
}
@Test
void whenHasUserDefinedChannelFactoryDoesNotAutoConfigureNettyOrShadedNetty() {
GrpcChannelFactory customChannelFactory = mock();
this.contextRunnerWithoutInProcessChannelFactory
.withBean("customChannelFactory", GrpcChannelFactory.class, () -> customChannelFactory)
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class).isSameAs(customChannelFactory));
}
@Test
void userDefinedChannelFactoryWithInProcessChannelFactory() {
GrpcChannelFactory customChannelFactory = mock();
this.contextRunner.withBean("customChannelFactory", GrpcChannelFactory.class, () -> customChannelFactory)
.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
.containsOnlyKeys("customChannelFactory", "inProcessGrpcChannelFactory"));
}
@Test
void whenShadedAndNonShadedNettyOnClasspathShadedNettyFactoryIsAutoConfigured() {
this.contextRunnerWithoutInProcessChannelFactory
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
.isInstanceOf(ShadedNettyGrpcChannelFactory.class));
}
@Test
void shadedNettyWithInProcessChannelFactory() {
this.contextRunner.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
.containsOnlyKeys("shadedNettyGrpcChannelFactory", "inProcessGrpcChannelFactory"));
}
@Test
void whenOnlyNonShadedNettyOnClasspathNonShadedNettyFactoryIsAutoConfigured() {
this.contextRunnerWithoutInProcessChannelFactory
.withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
.isInstanceOf(NettyGrpcChannelFactory.class));
}
@Test
void nonShadedNettyWithInProcessChannelFactory() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
.run((context) -> assertThat(context).getBeans(GrpcChannelFactory.class)
.containsOnlyKeys("nettyGrpcChannelFactory", "inProcessGrpcChannelFactory"));
}
@Test
void whenShadedNettyAndNettyNotOnClasspathNoChannelFactoryIsAutoConfigured() {
this.contextRunnerWithoutInProcessChannelFactory
.withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class,
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcChannelFactory.class));
}
@Test
void noChannelFactoryWithInProcessChannelFactory() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class,
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
.isInstanceOf(InProcessGrpcChannelFactory.class));
}
@Test
void shadedNettyChannelFactoryAutoConfiguredAsExpected() {
this.contextRunnerWithoutInProcessChannelFactory
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
.isInstanceOf(ShadedNettyGrpcChannelFactory.class)
.hasFieldOrPropertyWithValue("credentials", context.getBean(PropertiesChannelCredentialsProvider.class))
.extracting("targets")
.isInstanceOf(PropertiesVirtualTargets.class));
}
@Test
void nettyChannelFactoryAutoConfiguredAsExpected() {
this.contextRunnerWithoutInProcessChannelFactory
.withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
.isInstanceOf(NettyGrpcChannelFactory.class)
.hasFieldOrPropertyWithValue("credentials", context.getBean(PropertiesChannelCredentialsProvider.class))
.extracting("targets")
.isInstanceOf(PropertiesVirtualTargets.class));
}
@Test
void inProcessChannelFactoryAutoConfiguredAsExpected() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class,
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
.isInstanceOf(InProcessGrpcChannelFactory.class)
.extracting("credentials")
.isSameAs(ChannelCredentialsProvider.INSECURE));
}
@Test
void shadedNettyChannelFactoryAutoConfiguredWithCustomizers() {
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder builder = mock();
channelFactoryAutoConfiguredWithCustomizers(this.contextRunnerWithoutInProcessChannelFactory, builder,
ShadedNettyGrpcChannelFactory.class);
}
@Test
void nettyChannelFactoryAutoConfiguredWithCustomizers() {
NettyChannelBuilder builder = mock();
channelFactoryAutoConfiguredWithCustomizers(
this.contextRunnerWithoutInProcessChannelFactory.withClassLoader(
new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)),
builder, NettyGrpcChannelFactory.class);
}
@Test
void inProcessChannelFactoryAutoConfiguredWithCustomizers() {
InProcessChannelBuilder builder = mock();
channelFactoryAutoConfiguredWithCustomizers(
this.contextRunner.withClassLoader(new FilteredClassLoader(NettyChannelBuilder.class,
io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)),
builder, InProcessGrpcChannelFactory.class);
}
private <T extends ManagedChannelBuilder<T>> void channelFactoryAutoConfiguredWithCustomizers(
ApplicationContextRunner contextRunner, ManagedChannelBuilder<T> mockChannelBuilder,
Class<?> expectedChannelFactoryType) {
contextRunner.withUserConfiguration(ChannelBuilderCustomizersConfig.class)
.run((context) -> assertThat(context).getBean(GrpcChannelFactory.class)
.isInstanceOf(expectedChannelFactoryType)
.extracting("globalCustomizers", InstanceOfAssertFactories.list(GrpcChannelBuilderCustomizer.class))
.satisfies((allCustomizers) -> {
allCustomizers.forEach((c) -> c.customize("channel1", mockChannelBuilder));
InOrder ordered = inOrder(mockChannelBuilder);
ordered.verify(mockChannelBuilder).keepAliveTime(40L, TimeUnit.SECONDS);
ordered.verify(mockChannelBuilder).keepAliveTime(50L, TimeUnit.SECONDS);
}));
}
@Configuration(proxyBeanMethods = false)
@AutoConfigurationPackage(basePackageClasses = DummyBlockingGrpc.class)
static class AutoConfigurePackagesConfiguration {
}
@Configuration(proxyBeanMethods = false)
static class ChannelBuilderCustomizersConfig {
@Bean
@Order(100)
<T extends ManagedChannelBuilder<T>> GrpcChannelBuilderCustomizer<T> customizerOne() {
return (target, builder) -> builder.keepAliveTime(40L, TimeUnit.SECONDS);
}
@Bean
@Order(200)
<T extends ManagedChannelBuilder<T>> GrpcChannelBuilderCustomizer<T> customizerTwo() {
return (target, builder) -> builder.keepAliveTime(50L, TimeUnit.SECONDS);
}
}
}
@@ -0,0 +1,158 @@
/*
* 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.client.autoconfigure;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.unit.DataSize;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GrpcClientProperties}.
*
* @author Chris Bono
* @author Phillip Webb
*/
class GrpcClientPropertiesTests {
@Test
@WithResource(name = "client.yaml", content = """
channel:
a:
target: static://my-server:8888
b:
user-agent: me""")
void defaultValues() throws Exception {
GrpcClientProperties properties = bind();
Channel channelA = properties.getChannel().get("a");
assertThat(channelA).isNotNull();
assertThat(channelA.getUserAgent()).isNull();
assertThat(channelA.isBypassCertificateValidation()).isFalse();
assertThat(channelA.getInbound().getMessage().getMaxSize()).isEqualTo(DataSize.ofBytes(4194304));
assertThat(channelA.getInbound().getMetadata().getMaxSize()).isEqualTo(DataSize.ofBytes(8192));
assertThat(channelA.getDefault().getDeadline()).isNull();
assertThat(channelA.getIdle().getTimeout()).isEqualTo(Duration.ofSeconds(20));
assertThat(channelA.getKeepalive().getTime()).isEqualTo(Duration.ofMinutes(5));
assertThat(channelA.getKeepalive().getTimeout()).isEqualTo(Duration.ofSeconds(20));
assertThat(channelA.getKeepalive().isWithoutCalls()).isFalse();
assertThat(channelA.getSsl().getEnabled()).isNull();
assertThat(channelA.getSsl().getBundle()).isNull();
Channel channelB = properties.getChannel().get("b");
assertThat(channelB).isNotNull();
assertThat(channelB.getTarget()).isEqualTo("static://localhost:9090");
}
@Test
@WithResource(name = "client.yaml", content = """
channel:
test:
target: static://my-server:8888
user-agent: me
bypass-certificate-validation: true
inbound:
message:
max-size: 200MB
metadata:
max-size: 1GB
default:
deadline: 1s
load-balancing-policy: pick_first
idle:
timeout: 1m
keepalive:
time: 200s
timeout: 60000ms
without-calls: true
ssl:
enabled: true
bundle: my-bundle
health:
enabled: true
service-name: my-service""")
void specificProperties() throws Exception {
GrpcClientProperties properties = bind();
Channel channel = properties.getChannel().get("test");
assertThat(channel).isNotNull();
assertThat(channel.getTarget()).isEqualTo("static://my-server:8888");
assertThat(channel.getUserAgent()).isEqualTo("me");
assertThat(channel.isBypassCertificateValidation()).isTrue();
assertThat(channel.getInbound().getMessage().getMaxSize()).isEqualTo(DataSize.ofMegabytes(200));
assertThat(channel.getInbound().getMetadata().getMaxSize()).isEqualTo(DataSize.ofGigabytes(1));
assertThat(channel.getDefault().getDeadline()).isEqualTo(Duration.ofSeconds(1));
assertThat(channel.getIdle().getTimeout()).isEqualTo(Duration.ofMinutes(1));
assertThat(channel.getKeepalive().getTime()).isEqualTo(Duration.ofSeconds(200));
assertThat(channel.getKeepalive().getTimeout()).isEqualTo(Duration.ofMillis(60000));
assertThat(channel.getKeepalive().isWithoutCalls()).isTrue();
assertThat(channel.getSsl().getEnabled()).isTrue();
assertThat(channel.getSsl().getBundle()).isEqualTo("my-bundle");
}
@Test
@WithResource(name = "client.yaml", content = """
channel:
test:
idle:
timeout: 1
keepalive:
time: 60
timeout: 5""")
void withoutKeepAliveUnitsSpecified() throws Exception {
GrpcClientProperties properties = bind();
Channel channel = properties.getChannel().get("test");
assertThat(channel).isNotNull();
assertThat(channel.getIdle().getTimeout()).isEqualTo(Duration.ofSeconds(1));
assertThat(channel.getKeepalive().getTime()).isEqualTo(Duration.ofSeconds(60));
assertThat(channel.getKeepalive().getTimeout()).isEqualTo(Duration.ofSeconds(5));
}
@Test
@WithResource(name = "client.yaml", content = """
channel:
test:
inbound:
message:
max-size: 1000
metadata:
max-size: 256""")
void withoutInboundSizeUnitsSpecified() throws Exception {
GrpcClientProperties properties = bind();
Channel channel = properties.getChannel().get("test");
assertThat(channel).isNotNull();
assertThat(channel.getInbound().getMessage().getMaxSize()).isEqualTo(DataSize.ofBytes(1000));
assertThat(channel.getInbound().getMetadata().getMaxSize()).isEqualTo(DataSize.ofBytes(256));
}
private GrpcClientProperties bind() throws Exception {
StandardEnvironment environment = new StandardEnvironment();
new YamlPropertySourceLoader().load("client.yaml", new ClassPathResource("client.yaml"))
.forEach(environment.getPropertySources()::addLast);
return Binder.get(environment)
.bind("", Bindable.of(GrpcClientProperties.class))
.orElseGet(GrpcClientProperties::new);
}
}
@@ -0,0 +1,100 @@
/*
* 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.client.autoconfigure;
import io.grpc.Codec;
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.FilteredClassLoader;
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 GrpcClientCodecConfiguration}.
*
* @author Andrei Lisa
*/
class GrpcCodecConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GrpcClientCodecConfiguration.class));
@Test
void whenCodecNotOnClasspathThenAutoconfigurationSkipped() {
this.contextRunner.withClassLoader(new FilteredClassLoader(Codec.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcClientCodecConfiguration.class));
}
@Test
void whenHasCustomCompressorRegistryDoesNotAutoConfigureBean() {
CompressorRegistry customRegistry = mock();
this.contextRunner.withBean("customCompressorRegistry", CompressorRegistry.class, () -> customRegistry)
.run((context) -> assertThat(context).getBean(CompressorRegistry.class).isSameAs(customRegistry));
}
@Test
void compressorRegistryAutoConfiguredAsExpected() {
this.contextRunner.run((context) -> assertThat(context).getBean(CompressorRegistry.class)
.isSameAs(CompressorRegistry.getDefaultInstance()));
}
@Test
void whenCustomCompressorsThenCompressorRegistryIsNewInstance() {
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 whenHasCustomDecompressorRegistryDoesNotAutoConfigureBean() {
DecompressorRegistry customRegistry = mock();
this.contextRunner.withBean("customDecompressorRegistry", DecompressorRegistry.class, () -> customRegistry)
.run((context) -> assertThat(context).getBean(DecompressorRegistry.class).isSameAs(customRegistry));
}
@Test
void decompressorRegistryAutoConfiguredAsExpected() {
this.contextRunner.run((context) -> assertThat(context).getBean(DecompressorRegistry.class)
.isSameAs(DecompressorRegistry.getDefaultInstance()));
}
@Test
void whenCustomDecompressorsThenDecompressorRegistryIsNewInstance() {
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,179 @@
/*
* 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.client.autoconfigure;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedTrustManager;
import io.grpc.ChannelCredentials;
import io.grpc.InsecureChannelCredentials;
import io.grpc.TlsChannelCredentials;
import org.junit.jupiter.api.Test;
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.ssl.SslManagerBundle;
import org.springframework.grpc.client.ChannelCredentialsProvider;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link PropertiesChannelCredentialsProvider}.
*
* @author Phillip Webb
*/
class PropertiesChannelCredentialsProviderTests {
private final TrustManager[] trustManagers = { mock() };
private final KeyManager[] keyManagers = { mock() };
@Test
void getChannelCredentialsWhenTargetMatchesChannel() {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channelProperties = new Channel();
channelProperties.getSsl().setEnabled(true);
properties.getChannel().put("test", channelProperties);
SslBundles sslBundles = new DefaultSslBundleRegistry();
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test");
assertThat(credentials.getTrustManagers()).isNull();
assertThat(credentials.getKeyManagers()).isNull();
}
@Test
void getChannelCredentialsWhenTargetDoesNotMatchChannelAndHasDefault() {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channelProperties = new Channel();
channelProperties.getSsl().setEnabled(true);
properties.getChannel().put("default", channelProperties);
SslBundles sslBundles = new DefaultSslBundleRegistry();
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test");
assertThat(credentials.getTrustManagers()).isNull();
assertThat(credentials.getKeyManagers()).isNull();
}
@Test
void getChannelCredentialsWhenTargetDoesNotMatchChannelAndHasNoDefaultUsesInsecure() {
GrpcClientProperties properties = new GrpcClientProperties();
SslBundles sslBundles = new DefaultSslBundleRegistry();
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
ChannelCredentials credentials = provider.getChannelCredentials("test");
assertThat(credentials).isInstanceOf(InsecureChannelCredentials.class);
}
@Test
void getChannelCredentialsWhenSslExplictlyDisabled() {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channelProperties = new Channel();
channelProperties.getSsl().setEnabled(false);
properties.getChannel().put("test", channelProperties);
SslBundles sslBundles = new DefaultSslBundleRegistry();
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
ChannelCredentials credentials = provider.getChannelCredentials("test");
assertThat(credentials).isInstanceOf(InsecureChannelCredentials.class);
}
@Test
void getChannelCredentialsWhenSslExplictlyEnabledAndNoBundle() {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channelProperties = new Channel();
channelProperties.getSsl().setEnabled(true);
properties.getChannel().put("test", channelProperties);
SslBundles sslBundles = new DefaultSslBundleRegistry();
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test");
assertThat(credentials.getTrustManagers()).isNull();
assertThat(credentials.getKeyManagers()).isNull();
}
@Test
void getChannelCredentialsWhenNoSslEnabledSetButHasBundle() {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channelProperties = new Channel();
channelProperties.getSsl().setBundle("test");
properties.getChannel().put("test", channelProperties);
SslBundles sslBundles = new DefaultSslBundleRegistry("test", mockBundle());
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test");
assertThat(credentials.getTrustManagers()).containsExactly(this.trustManagers);
assertThat(credentials.getKeyManagers()).containsExactly(this.keyManagers);
}
@Test
void getChannelCredentialsWhenNoSslEnabledSetAndNoBundle() {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channelProperties = new Channel();
properties.getChannel().put("test", channelProperties);
SslBundles sslBundles = new DefaultSslBundleRegistry();
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
ChannelCredentials credentials = provider.getChannelCredentials("test");
assertThat(credentials).isInstanceOf(InsecureChannelCredentials.class);
}
@Test
void getChannelCredentialsWhenSslEnabledAndHasBundle() {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channelProperties = new Channel();
channelProperties.getSsl().setEnabled(true);
channelProperties.getSsl().setBundle("test");
properties.getChannel().put("test", channelProperties);
SslBundles sslBundles = new DefaultSslBundleRegistry("test", mockBundle());
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test");
assertThat(credentials.getTrustManagers()).containsExactly(this.trustManagers);
assertThat(credentials.getKeyManagers()).containsExactly(this.keyManagers);
}
@Test
void getChannelCredentialsWhenBypassCertificateValidation() {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channelProperties = new Channel();
channelProperties.setBypassCertificateValidation(true);
channelProperties.getSsl().setBundle("test");
properties.getChannel().put("test", channelProperties);
SslBundles sslBundles = new DefaultSslBundleRegistry("test", mockBundle());
ChannelCredentialsProvider provider = new PropertiesChannelCredentialsProvider(properties, sslBundles);
TlsChannelCredentials credentials = (TlsChannelCredentials) provider.getChannelCredentials("test");
TrustManager trustManager = credentials.getTrustManagers().get(0);
assertThat(trustManager.getClass().getName()).contains("InsecureTrustManager");
assertThat(((X509ExtendedTrustManager) trustManager).getAcceptedIssuers()).isEmpty();
assertThat(credentials.getKeyManagers()).containsExactly(this.keyManagers);
}
private SslBundle mockBundle() {
SslBundle bundle = mock();
SslManagerBundle managerBundle = mock();
TrustManagerFactory trustManagerFactory = mock();
KeyManagerFactory keyManagerFactory = mock();
given(bundle.getManagers()).willReturn(managerBundle);
given(managerBundle.getTrustManagerFactory()).willReturn(trustManagerFactory);
given(managerBundle.getKeyManagerFactory()).willReturn(keyManagerFactory);
given(trustManagerFactory.getTrustManagers()).willReturn(this.trustManagers);
given(keyManagerFactory.getKeyManagers()).willReturn(this.keyManagers);
return bundle;
}
}
@@ -0,0 +1,209 @@
/*
* 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.client.autoconfigure;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import io.grpc.ClientInterceptor;
import io.grpc.ManagedChannelBuilder;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
import org.springframework.grpc.client.interceptor.DefaultDeadlineSetupClientInterceptor;
import org.springframework.util.unit.DataSize;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
/**
* Tests for {@link PropertiesGrpcChannelBuilderCustomizer}.
*
* @author Phillip Webb
*/
class PropertiesGrpcChannelBuilderCustomizerTests {
@Test
<T extends ManagedChannelBuilder<T>> void customizeWhenHasMatchingChannel() {
GrpcClientProperties properties = new GrpcClientProperties();
properties.getChannel().put("test", createTestChannelProperties());
GrpcChannelBuilderCustomizer<T> customizer = new PropertiesGrpcChannelBuilderCustomizer<>(properties);
T builder = mock();
customizer.customize("test", builder);
assertMapped(builder);
}
@Test
<T extends ManagedChannelBuilder<T>> void customizeWhenHasDefaultChannel() {
GrpcClientProperties properties = new GrpcClientProperties();
properties.getChannel().put("default", createTestChannelProperties());
GrpcChannelBuilderCustomizer<T> customizer = new PropertiesGrpcChannelBuilderCustomizer<>(properties);
T builder = mock();
customizer.customize("test", builder);
assertMapped(builder);
}
@Test
<T extends ManagedChannelBuilder<T>> void customizeWhenHasNoMatchAndNoDefault() {
GrpcClientProperties properties = new GrpcClientProperties();
properties.getChannel().put("other", createTestChannelProperties());
GrpcChannelBuilderCustomizer<T> customizer = new PropertiesGrpcChannelBuilderCustomizer<>(properties);
T builder = mock();
customizer.customize("test", builder);
assertMappedStockDefaults(builder);
}
@Test
<T extends ManagedChannelBuilder<T>> void customizeWhenChannelDoesNotSupportLoadBalancingDoesNotMapDefaultLoadBalancer() {
assertNoLoadBalancerMappedBasedOnChannel("unix:test");
assertNoLoadBalancerMappedBasedOnChannel("in-process:test");
}
private <T extends ManagedChannelBuilder<T>> void assertNoLoadBalancerMappedBasedOnChannel(String target) {
T builder = getBuilder((channelProperties) -> {
channelProperties.setTarget(target);
channelProperties.getDefault().setLoadBalancingPolicy("testlbp");
});
then(builder).should(never()).defaultLoadBalancingPolicy(any());
}
@Test
<T extends ManagedChannelBuilder<T>> void customizeWhenTargetDoesNotSupportLoadBalancingDoesNotMapDefaultLoadBalancer() {
GrpcChannelBuilderCustomizer<?> customizer = getCustomizer(
(channelProperties) -> channelProperties.setTarget("static://localhost:1234"));
assertNoLoadBalancerMappedBasedOnTarget(customizer, "unix:test");
assertNoLoadBalancerMappedBasedOnTarget(customizer, "in-process:test");
}
private <T extends ManagedChannelBuilder<T>> void assertNoLoadBalancerMappedBasedOnTarget(
GrpcChannelBuilderCustomizer<T> customizer, String target) {
T builder = mock();
customizer.customize(target, builder);
then(builder).should(never()).defaultLoadBalancingPolicy(any());
}
@Test
<T extends ManagedChannelBuilder<T>> void customizeWhenMaxMessageSizeIsMinusOneUsesMaxValue() {
T builder = getBuilder(
(channelProperties) -> channelProperties.getInbound().getMessage().setMaxSize(DataSize.ofBytes(-1)));
then(builder).should().maxInboundMessageSize(Integer.MAX_VALUE);
}
@Test
<T extends ManagedChannelBuilder<T>> void customizeWhenMaxMessageSizeIsTooBigUsesMaxValue() {
T builder = getBuilder((channelProperties) -> channelProperties.getInbound()
.getMessage()
.setMaxSize(DataSize.ofBytes((long) Integer.MAX_VALUE + 100)));
then(builder).should().maxInboundMessageSize(Integer.MAX_VALUE);
}
@Test
<T extends ManagedChannelBuilder<T>> void customizeWhenMaxMessageSizeIsNegativeAndNotMinusOneThrowsException() {
assertThatIllegalStateException().isThrownBy(() -> getBuilder(
(channelProperties) -> channelProperties.getInbound().getMessage().setMaxSize(DataSize.ofBytes(-2))))
.withMessage("Unsupported max size value -2B");
}
@Test
<T extends ManagedChannelBuilder<T>> void customizeWhenMaxMetadataSizeIsMinusOneUsesMaxValue() {
T builder = getBuilder(
(channelProperties) -> channelProperties.getInbound().getMetadata().setMaxSize(DataSize.ofBytes(-1)));
then(builder).should().maxInboundMetadataSize(Integer.MAX_VALUE);
}
@Test
<T extends ManagedChannelBuilder<T>> void customizeWhenMaxMetadataSizeIsTooBigUsesMaxValue() {
T builder = getBuilder((channelProperties) -> channelProperties.getInbound()
.getMetadata()
.setMaxSize(DataSize.ofBytes((long) Integer.MAX_VALUE + 100)));
then(builder).should().maxInboundMetadataSize(Integer.MAX_VALUE);
}
@Test
<T extends ManagedChannelBuilder<T>> void customizeWhenMaxMetadataSizeIsNegativeAndNotMinusOneThrowsException() {
assertThatIllegalStateException().isThrownBy(() -> getBuilder(
(channelProperties) -> channelProperties.getInbound().getMetadata().setMaxSize(DataSize.ofBytes(-2))))
.withMessage("Unsupported max size value -2B");
}
private <T extends ManagedChannelBuilder<T>> T getBuilder(Consumer<Channel> setup) {
GrpcChannelBuilderCustomizer<T> customizer = getCustomizer(setup);
T builder = mock();
customizer.customize("test", builder);
return builder;
}
private <T extends ManagedChannelBuilder<T>> GrpcChannelBuilderCustomizer<T> getCustomizer(
Consumer<Channel> setup) {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channelProperties = new Channel();
setup.accept(channelProperties);
properties.getChannel().put("test", channelProperties);
GrpcChannelBuilderCustomizer<T> customizer = new PropertiesGrpcChannelBuilderCustomizer<>(properties);
return customizer;
}
private Channel createTestChannelProperties() {
Channel properties = new Channel();
properties.setUserAgent("testua");
properties.getInbound().getMessage().setMaxSize(DataSize.ofBytes(10));
properties.getInbound().getMetadata().setMaxSize(DataSize.ofBytes(20));
properties.getDefault().setDeadline(Duration.ofMinutes(5));
properties.getDefault().setLoadBalancingPolicy("testlbp");
properties.getIdle().setTimeout(Duration.ofMinutes(6));
properties.getKeepalive().setTime(Duration.ofMinutes(7));
properties.getKeepalive().setTimeout(Duration.ofMinutes(8));
properties.getKeepalive().setWithoutCalls(true);
return properties;
}
private <T extends ManagedChannelBuilder<T>> void assertMapped(T builder) {
then(builder).should().userAgent("testua");
then(builder).should().maxInboundMessageSize(10);
then(builder).should().maxInboundMetadataSize(20);
ArgumentCaptor<ClientInterceptor[]> interceptors = ArgumentCaptor.captor();
then(builder).should().intercept(interceptors.capture());
ClientInterceptor interceptor = interceptors.getValue()[0];
assertThat(interceptor).isInstanceOf(DefaultDeadlineSetupClientInterceptor.class)
.extracting("defaultDeadline")
.isEqualTo(Duration.ofMinutes(5));
then(builder).should().defaultLoadBalancingPolicy("testlbp");
then(builder).should().idleTimeout(360000000000L, TimeUnit.NANOSECONDS);
then(builder).should().keepAliveTime(420000000000L, TimeUnit.NANOSECONDS);
then(builder).should().keepAliveTimeout(480000000000L, TimeUnit.NANOSECONDS);
then(builder).should().keepAliveWithoutCalls(true);
}
private <T extends ManagedChannelBuilder<T>> void assertMappedStockDefaults(T builder) {
then(builder).should().maxInboundMessageSize(4194304);
then(builder).should().maxInboundMetadataSize(8192);
then(builder).should().defaultLoadBalancingPolicy("round_robin");
then(builder).should().idleTimeout(20000000000L, TimeUnit.NANOSECONDS);
then(builder).should().keepAliveTime(300000000000L, TimeUnit.NANOSECONDS);
then(builder).should().keepAliveTimeout(20000000000L, TimeUnit.NANOSECONDS);
then(builder).should().keepAliveWithoutCalls(false);
then(builder).shouldHaveNoMoreInteractions();
}
}
@@ -0,0 +1,129 @@
/*
* 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.client.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.grpc.client.autoconfigure.GrpcClientProperties.Channel;
import org.springframework.grpc.client.VirtualTargets;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PropertiesVirtualTargets}.
*
* @author Chris Bono
* @author Phillip Webb
*/
class PropertiesVirtualTargetsTests {
@Test
void getTargetWhenHasMatchingChannel() {
GrpcClientProperties properties = createProperties("test", "my-server:8888");
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
assertThat(targets.getTarget("test")).isEqualTo("my-server:8888");
}
@Test
void getTargetWhenDefaultAndDefaultChannelDefined() {
GrpcClientProperties properties = createProperties("default", "my-server:8888");
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
assertThat(targets.getTarget("default")).isEqualTo("my-server:8888");
}
@Test
void getTargetWhenDefaultAndNoDefaultChannelDefined() {
GrpcClientProperties properties = new GrpcClientProperties();
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
assertThat(targets.getTarget("default")).isEqualTo("localhost:9090");
}
@Test
void getTargetWhenChannelHasStaticTargetReturnsStrippedTarget() {
GrpcClientProperties properties = createProperties("test", "static://my-server:8888");
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
assertThat(targets.getTarget("test")).isEqualTo("my-server:8888");
}
@Test
void getTargetWhenChannelHasTcpTargetReturnsStrippedTarget() {
GrpcClientProperties properties = createProperties("test", "tcp://my-server:8888");
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
assertThat(targets.getTarget("test")).isEqualTo("my-server:8888");
}
@Test
void getTargetWhenChannelHasOtherUrlTarget() {
GrpcClientProperties properties = createProperties("test", "foo://my-server:8888");
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
assertThat(targets.getTarget("test")).isEqualTo("foo://my-server:8888");
}
@Test
void getTargetWhenStaticReturnsStripped() {
GrpcClientProperties properties = new GrpcClientProperties();
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
assertThat(targets.getTarget("static://my-server:8888")).isEqualTo("my-server:8888");
}
@Test
void getTargetWhenTcpReturnsStripped() {
GrpcClientProperties properties = new GrpcClientProperties();
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
assertThat(targets.getTarget("tcp://my-server:8888")).isEqualTo("my-server:8888");
}
@Test
void getTargetWhenUnixUrlDoesNotPrependStatic() {
GrpcClientProperties properties = new GrpcClientProperties();
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
assertThat(targets.getTarget("foo://bar")).isEqualTo("foo://bar");
}
@Test
void getTargetWhenUrlReturnsAsIs() {
GrpcClientProperties properties = new GrpcClientProperties();
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
assertThat(targets.getTarget("unix:foo")).isEqualTo("unix:foo");
}
@Test
void getTargetUrlWhenHasColonWithoutSlashReturnsAsIs() {
GrpcClientProperties properties = new GrpcClientProperties();
VirtualTargets targets = new PropertiesVirtualTargets(new MockEnvironment(), properties);
assertThat(targets.getTarget("localhost:123/bar")).isEqualTo("localhost:123/bar");
}
@Test
void getTargetWhenNotChannelNameResolvesPlaceholders() {
GrpcClientProperties properties = new GrpcClientProperties();
MockEnvironment environment = new MockEnvironment();
environment.setProperty("channelName", "foo");
VirtualTargets targets = new PropertiesVirtualTargets(environment, properties);
assertThat(targets.getTarget("my-server-${channelName}:8888")).isEqualTo("my-server-foo:8888");
}
private GrpcClientProperties createProperties(String name, String target) {
GrpcClientProperties properties = new GrpcClientProperties();
Channel channel = new Channel();
channel.setTarget(target);
properties.getChannel().put(name, channel);
return properties;
}
}
@@ -0,0 +1,46 @@
/*
* 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.client.autoconfigure.test.scan;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.stub.AbstractBlockingStub;
import io.grpc.stub.AbstractStub.StubFactory;
public final class DummyBlockingGrpc {
private DummyBlockingGrpc() {
}
public static DummyBlockingStub newBlockingStub(io.grpc.Channel channel) {
return AbstractBlockingStub.newStub((StubFactory<DummyBlockingStub>) DummyBlockingStub::new, channel);
}
public static class DummyBlockingStub extends AbstractBlockingStub<DummyBlockingStub> {
protected DummyBlockingStub(Channel channel, CallOptions callOptions) {
super(channel, callOptions);
}
@Override
protected DummyBlockingStub build(Channel channel, CallOptions callOptions) {
return new DummyBlockingStub(channel, callOptions);
}
}
}
@@ -0,0 +1,46 @@
/*
* 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.client.autoconfigure.test.scan;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.stub.AbstractBlockingStub;
import io.grpc.stub.AbstractStub.StubFactory;
public final class DummyBlockingV2Grpc {
private DummyBlockingV2Grpc() {
}
public static DummyBlockingV2Stub newBlockingV2Stub(io.grpc.Channel channel) {
return AbstractBlockingStub.newStub((StubFactory<DummyBlockingV2Stub>) DummyBlockingV2Stub::new, channel);
}
public static class DummyBlockingV2Stub extends AbstractBlockingStub<DummyBlockingV2Stub> {
protected DummyBlockingV2Stub(Channel channel, CallOptions callOptions) {
super(channel, callOptions);
}
@Override
protected DummyBlockingV2Stub build(Channel channel, CallOptions callOptions) {
return new DummyBlockingV2Stub(channel, callOptions);
}
}
}
@@ -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 org.springframework.boot.grpc.client.autoconfigure.test.scan;
import org.jspecify.annotations.NullMarked;
@@ -2163,6 +2163,7 @@ bom {
"spring-boot-freemarker",
"spring-boot-graphql",
"spring-boot-graphql-test",
"spring-boot-grpc-client",
"spring-boot-grpc-server",
"spring-boot-groovy-templates",
"spring-boot-gson",
@@ -2294,6 +2295,7 @@ bom {
"spring-boot-starter-freemarker-test",
"spring-boot-starter-graphql",
"spring-boot-starter-graphql-test",
"spring-boot-starter-grpc-client",
"spring-boot-starter-grpc-server",
"spring-boot-starter-groovy-templates",
"spring-boot-starter-groovy-templates-test",
+3
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-client"
include "module:spring-boot-grpc-server"
include "module:spring-boot-groovy-templates"
include "module:spring-boot-gson"
@@ -269,6 +270,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-client"
include "starter:spring-boot-starter-grpc-server"
include "starter:spring-boot-starter-groovy-templates"
include "starter:spring-boot-starter-groovy-templates-test"
@@ -423,6 +425,7 @@ 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-client"
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"
@@ -0,0 +1,63 @@
/*
* 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 client smoke test"
dependencies {
implementation(project(":starter:spring-boot-starter-grpc-client"))
testImplementation(project(":starter:spring-boot-starter-test"))
}
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,46 @@
/*
* 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.grpcclient;
import smoketest.grpcclient.proto.HelloReply;
import smoketest.grpcclient.proto.HelloRequest;
import smoketest.grpcclient.proto.HelloWorldGrpc.HelloWorldBlockingStub;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.grpc.client.ImportGrpcClients;
@SpringBootApplication
@ImportGrpcClients(types = HelloWorldBlockingStub.class)
public class SampleGrpcClientApplication {
@Bean
ApplicationRunner applicationRunner(HelloWorldBlockingStub hello) {
return (args) -> {
HelloRequest request = HelloRequest.newBuilder().setName("Spring").build();
HelloReply reply = hello.sayHello(request);
System.out.println(">>> " + reply.getMessage());
};
}
public static void main(String[] args) {
SpringApplication.run(SampleGrpcClientApplication.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.grpcclient;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,17 @@
syntax = "proto3";
option java_package = "smoketest.grpcclient.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,6 @@
spring:
grpc:
client:
channel:
default:
target: "static://localhost:9090"
@@ -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 smoketest.grpcclient;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import io.grpc.stub.StreamObserver;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import smoketest.grpcclient.SampleGrpcClientApplicationTests.MockServerInitializer;
import smoketest.grpcclient.proto.HelloReply;
import smoketest.grpcclient.proto.HelloRequest;
import smoketest.grpcclient.proto.HelloWorldGrpc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.MapPropertySource;
import org.springframework.grpc.server.NettyGrpcServerFactory;
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
import org.springframework.test.context.ContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@ContextConfiguration(initializers = MockServerInitializer.class)
@ExtendWith(OutputCaptureExtension.class)
class SampleGrpcClientApplicationTests {
@Test
void applicationRunsAndCallsGrpcServer(CapturedOutput output) {
assertThat(output).contains(">>> Hello 'Spring'");
}
static class MockServerInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
NettyGrpcServerFactory serverFactory = new NettyGrpcServerFactory("*:0", Collections.emptyList(), null,
null, null);
HelloWorldService helloWorldService = new HelloWorldService();
serverFactory.addService(helloWorldService.bindService());
GrpcServerLifecycle lifecycle = new GrpcServerLifecycle(serverFactory, Duration.ofSeconds(30),
applicationContext);
lifecycle.start();
String target = "static://localhost:%s".formatted(lifecycle.getPort());
applicationContext.getEnvironment()
.getPropertySources()
.addFirst(new MapPropertySource("grpc", Map.of("spring.grpc.client.channel.default.target", target)));
applicationContext.getBeanFactory().registerSingleton("grpcServerLifecyce", lifecycle);
}
}
static class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase {
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setMessage("Hello '%s'".formatted(request.getName())).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
@Override
public void streamHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
throw new IllegalStateException();
}
}
}
@@ -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 client"
dependencies {
api(project(":starter:spring-boot-starter"))
api(project(":module:spring-boot-grpc-client"))
api("io.grpc:grpc-netty")
api("io.grpc:grpc-stub")
}