From a087eac7764a22b9d89ee766283ddb0f17272e08 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Sat, 7 Jun 2025 14:02:54 -0500 Subject: [PATCH] Add support for in-process outside of tests This commit allows the usage of inprocess server and channel factories outside of tests. Resolves #144 Signed-off-by: Chris Bono --- .../sample/GrpcServerApplicationTests.java | 2 +- spring-grpc-core/pom.xml | 5 + .../client/CompositeGrpcChannelFactory.java | 62 +++++++ .../client/DefaultGrpcChannelFactory.java | 12 ++ .../grpc/client/GrpcChannelFactory.java | 9 + .../client/InProcessGrpcChannelFactory.java | 62 +++++++ .../server}/InProcessGrpcServerFactory.java | 12 +- .../CompositeGrpcChannelFactoryTests.java | 112 ++++++++++++ .../grpc/client/GrpcChannelFactoryTests.java | 55 ++++++ spring-grpc-spring-boot-autoconfigure/pom.xml | 5 + ...positeChannelFactoryAutoConfiguration.java | 67 +++++++ .../GrpcChannelFactoryConfigurations.java | 34 +++- .../client/GrpcClientAutoConfiguration.java | 5 +- .../server/GrpcServerAutoConfiguration.java | 7 - .../GrpcServerFactoryAutoConfiguration.java | 9 +- .../GrpcServerFactoryConfigurations.java | 59 ++++++- .../server/GrpcServerProperties.java | 37 ++++ .../InProcessServerFactoryPropertyMapper.java | 41 +++++ ...itional-spring-configuration-metadata.json | 12 ++ ...ot.autoconfigure.AutoConfiguration.imports | 3 +- ...eChannelFactoryAutoConfigurationTests.java | 144 ++++++++++++++++ .../GrpcClientAutoConfigurationTests.java | 150 +++++++++++++--- .../GrpcServerAutoConfigurationTests.java | 163 +++++++++++++----- .../test/InProcessGrpcChannelFactory.java | 38 ---- ...essGrpcServerFactoryAutoConfiguration.java | 60 ------- .../test/InProcessTestAutoConfiguration.java | 131 ++++++++++++++ ...cessTransportEnvironmentPostProcessor.java | 4 +- ...itional-spring-configuration-metadata.json | 4 +- ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../InProcessTestAutoConfigurationTests.java | 96 +++++++++++ 30 files changed, 1215 insertions(+), 188 deletions(-) create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/client/CompositeGrpcChannelFactory.java create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/client/InProcessGrpcChannelFactory.java rename {spring-grpc-test/src/main/java/org/springframework/grpc/test => spring-grpc-core/src/main/java/org/springframework/grpc/server}/InProcessGrpcServerFactory.java (82%) create mode 100644 spring-grpc-core/src/test/java/org/springframework/grpc/client/CompositeGrpcChannelFactoryTests.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/CompositeChannelFactoryAutoConfiguration.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/InProcessServerFactoryPropertyMapper.java create mode 100644 spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/CompositeChannelFactoryAutoConfigurationTests.java delete mode 100644 spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcChannelFactory.java delete mode 100644 spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactoryAutoConfiguration.java create mode 100644 spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessTestAutoConfiguration.java create mode 100644 spring-grpc-test/src/test/java/org/springframework/grpc/test/InProcessTestAutoConfigurationTests.java diff --git a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index 975d659..701fc01 100644 --- a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -29,7 +29,7 @@ import io.grpc.reflection.v1.ServerReflectionRequest; import io.grpc.reflection.v1.ServerReflectionResponse; import io.grpc.stub.StreamObserver; -@SpringBootTest(properties = { "debug=true", "spring.grpc.server.port=0", +@SpringBootTest(properties = { "spring.grpc.server.port=0", "spring.grpc.client.default-channel.address=static://0.0.0.0:${local.grpc.port}" }) @DirtiesContext public class GrpcServerApplicationTests { diff --git a/spring-grpc-core/pom.xml b/spring-grpc-core/pom.xml index 352192f..5d9e715 100644 --- a/spring-grpc-core/pom.xml +++ b/spring-grpc-core/pom.xml @@ -71,6 +71,11 @@ netty-transport-native-epoll true + + io.grpc + grpc-inprocess + true + io.grpc grpc-protobuf diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/CompositeGrpcChannelFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/CompositeGrpcChannelFactory.java new file mode 100644 index 0000000..aaa8a6c --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/CompositeGrpcChannelFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.grpc.client; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.util.Assert; + +import io.grpc.ManagedChannel; + +/** + * A composite {@link GrpcChannelFactory} that combines a list of channel factories. + *

+ * The composite delegates channel creation to the first composed factory that supports + * the given target string. + * + * @author Chris Bono + */ +public class CompositeGrpcChannelFactory implements GrpcChannelFactory { + + private List channelFactories = new ArrayList<>(); + + /** + * Creates a new CompositeGrpcChannelFactory with the given factories. + * @param channelFactories the channel factories + */ + public CompositeGrpcChannelFactory(List channelFactories) { + Assert.notEmpty(channelFactories, "composite channel factory requires at least one channel factory"); + this.channelFactories.addAll(channelFactories); + } + + @Override + public boolean supports(String target) { + return this.channelFactories.stream().anyMatch((cf) -> cf.supports(target)); + } + + @Override + public ManagedChannel createChannel(final String target, ChannelBuilderOptions options) { + return this.channelFactories.stream() + .filter((cf) -> cf.supports(target)) + .findFirst() + .orElseThrow( + () -> new IllegalStateException("No grpc channel factory found that supports target : " + target)) + .createChannel(target, options); + } + +} diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/DefaultGrpcChannelFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/DefaultGrpcChannelFactory.java index 502f0ba..0ed19c0 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/DefaultGrpcChannelFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/DefaultGrpcChannelFactory.java @@ -70,6 +70,18 @@ public class DefaultGrpcChannelFactory> this.interceptorsConfigurer = interceptorsConfigurer; } + /** + * Whether this factory supports the given target string. The target can be either a + * valid nameresolver-compliant URI, an authority string as described in + * {@link Grpc#newChannelBuilder(String, ChannelCredentials)}. + * @param target the target string as described in method javadocs + * @return true unless the target begins with 'in-process:' + */ + @Override + public boolean supports(String target) { + return !target.startsWith("in-process:"); + } + public void setVirtualTargets(VirtualTargets targets) { this.targets = targets; } diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcChannelFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcChannelFactory.java index f2702d5..7f44bdb 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcChannelFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcChannelFactory.java @@ -29,6 +29,15 @@ import io.grpc.ManagedChannel; */ public interface GrpcChannelFactory { + /** + * Whether this factory supports the given target string. The target can be either a + * valid nameresolver-compliant URI, an authority string as described in + * {@link Grpc#newChannelBuilder(String, ChannelCredentials)}. + * @param target the target string as described in method javadocs + * @return whether this factory supports the given target string + */ + boolean supports(String target); + /** * Creates a {@link ManagedChannel} for the given target string. The target can be * either a valid nameresolver-compliant URI, an authority string as described in diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/InProcessGrpcChannelFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/InProcessGrpcChannelFactory.java new file mode 100644 index 0000000..715d35e --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/InProcessGrpcChannelFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.grpc.client; + +import java.util.List; + +import io.grpc.ChannelCredentials; +import io.grpc.Grpc; +import io.grpc.inprocess.InProcessChannelBuilder; + +/** + * {@link GrpcChannelFactory} that creates in-process gRPC channels. + * + * @author Chris Bono + */ +public class InProcessGrpcChannelFactory extends DefaultGrpcChannelFactory { + + /** + * Construct an in-process channel factory instance and sets the + * {@link #setVirtualTargets virtualTargets} to the identity function so that the + * exact passed in target string is used as the target of the channel factory. + * @param globalCustomizers the global customizers to apply to all created channels + * @param interceptorsConfigurer configures the client interceptors on the created + * channels + */ + public InProcessGrpcChannelFactory(List> globalCustomizers, + ClientInterceptorsConfigurer interceptorsConfigurer) { + super(globalCustomizers, interceptorsConfigurer); + setVirtualTargets((p) -> p); + } + + /** + * Whether this factory supports the given target string. The target can be either a + * valid nameresolver-compliant URI, an authority string as described in + * {@link Grpc#newChannelBuilder(String, ChannelCredentials)}. + * @param target the target string as described in method javadocs + * @return true if the target begins with 'in-process:' + */ + @Override + public boolean supports(String target) { + return target.startsWith("in-process:"); + } + + @Override + protected InProcessChannelBuilder newChannelBuilder(String target, ChannelCredentials creds) { + return InProcessChannelBuilder.forName(target.substring(11)); + } + +} diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/InProcessGrpcServerFactory.java similarity index 82% rename from spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactory.java rename to spring-grpc-core/src/main/java/org/springframework/grpc/server/InProcessGrpcServerFactory.java index 71bf8e6..cdbc4ca 100644 --- a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/InProcessGrpcServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-2024 the original author or authors. + * Copyright 2024-2025 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. @@ -13,15 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.grpc.test; +package org.springframework.grpc.server; import java.util.List; -import org.springframework.grpc.server.DefaultGrpcServerFactory; -import org.springframework.grpc.server.ServerBuilderCustomizer; - import io.grpc.inprocess.InProcessServerBuilder; +/** + * {@link GrpcServerFactory} that can be used to create an in-process gRPC server. + * + * @author Chris Bono + */ public class InProcessGrpcServerFactory extends DefaultGrpcServerFactory { public InProcessGrpcServerFactory(String address, diff --git a/spring-grpc-core/src/test/java/org/springframework/grpc/client/CompositeGrpcChannelFactoryTests.java b/spring-grpc-core/src/test/java/org/springframework/grpc/client/CompositeGrpcChannelFactoryTests.java new file mode 100644 index 0000000..dc69e03 --- /dev/null +++ b/spring-grpc-core/src/test/java/org/springframework/grpc/client/CompositeGrpcChannelFactoryTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.grpc.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.grpc.ManagedChannel; + +/** + * Unit tests for the {@link CompositeGrpcChannelFactory}. + */ +@SuppressWarnings({ "unchecked", "rawtypes" }) +class CompositeGrpcChannelFactoryTests { + + private TestChannelFactory fooChannelFactory; + + private TestChannelFactory barChannelFactory; + + private CompositeGrpcChannelFactory compositeChannelFactory; + + @BeforeEach + void prepareFactories() { + this.fooChannelFactory = new TestChannelFactory("foo"); + this.barChannelFactory = new TestChannelFactory("bar"); + this.compositeChannelFactory = new CompositeGrpcChannelFactory(List.of(fooChannelFactory, barChannelFactory)); + } + + @Test + void atLeastOneChannelFactoryRequired() { + assertThatIllegalArgumentException().isThrownBy(() -> new CompositeGrpcChannelFactory(null)) + .withMessage("composite channel factory requires at least one channel factory"); + assertThatIllegalArgumentException().isThrownBy(() -> new CompositeGrpcChannelFactory(List.of())) + .withMessage("composite channel factory requires at least one channel factory"); + } + + @Test + void supportsDependsOnSupportsOfComposedFactories() { + assertThat(compositeChannelFactory.supports("foo")).isTrue(); + assertThat(compositeChannelFactory.supports("bar")).isTrue(); + assertThat(compositeChannelFactory.supports("zaa")).isFalse(); + } + + @Test + void firstComposedChannelFactorySupportsTarget() { + assertThat(compositeChannelFactory.createChannel("foo")).isNotNull(); + assertThat(fooChannelFactory.getActualTarget()).isEqualTo("foo"); + assertThat(barChannelFactory.getActualTarget()).isNull(); + } + + @Test + void secondComposedChannelFactorySupportsTarget() { + assertThat(compositeChannelFactory.createChannel("bar")).isNotNull(); + assertThat(fooChannelFactory.getActualTarget()).isNull(); + assertThat(barChannelFactory.getActualTarget()).isEqualTo("bar"); + } + + @Test + void noComposedChannelFactorySupportsTarget() { + assertThatIllegalStateException().isThrownBy(() -> compositeChannelFactory.createChannel("zaa")) + .withMessage("No grpc channel factory found that supports target : zaa"); + assertThat(fooChannelFactory.getActualTarget()).isNull(); + assertThat(barChannelFactory.getActualTarget()).isNull(); + } + + static class TestChannelFactory implements GrpcChannelFactory { + + private String expectedTarget; + + private String actualTarget; + + TestChannelFactory(String expectedTarget) { + this.expectedTarget = expectedTarget; + } + + public boolean supports(String target) { + return target.equals(this.expectedTarget); + } + + @Override + public ManagedChannel createChannel(String target, ChannelBuilderOptions options) { + this.actualTarget = target; + return mock(); + } + + String getActualTarget() { + return this.actualTarget; + } + + } + +} diff --git a/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcChannelFactoryTests.java b/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcChannelFactoryTests.java index 718a935..6e5dc5d 100644 --- a/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcChannelFactoryTests.java +++ b/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcChannelFactoryTests.java @@ -34,6 +34,7 @@ import org.mockito.ArgumentMatchers; import io.grpc.ClientInterceptor; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; +import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.netty.NettyChannelBuilder; /** @@ -159,6 +160,60 @@ class GrpcChannelFactoryTests { .isInstanceOf(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class))); } + @Test + void inProcessChannelFactoryUsesInProcessChannelBuilder() { + var channelName = "in-process:foo"; + var customizer1 = mock(GrpcChannelBuilderCustomizer.class); + var channelFactory = new InProcessGrpcChannelFactory(List.of(), mock()); + channel = channelFactory.createChannel(channelName, + ChannelBuilderOptions.defaults().withCustomizer(customizer1)); + assertThat(channel).isNotNull(); + verify(customizer1).customize(anyString(), + ArgumentMatchers + .assertArg((builder) -> assertThat(builder).isInstanceOf(InProcessChannelBuilder.class) + .extracting("managedChannelImplBuilder.target") + .isEqualTo("directaddress:///foo"))); + // NOTE: the impl target ending in foo proves the original target was stripped + // of 'in-process:' prefix + } + + } + + @Nested + class SupportsApiTests { + + @Test + void defaultSupportsEverythingExceptInProcess() { + var channelFactory = new DefaultGrpcChannelFactory(List.of(), mock()); + assertThat(channelFactory.supports("foo")).isTrue(); + assertThat(channelFactory.supports("static:127.0.0.1")).isTrue(); + assertThat(channelFactory.supports("in-process:foo")).isFalse(); + } + + @Test + void nettySupportsEverythingExceptInProcess() { + var channelFactory = new NettyGrpcChannelFactory(List.of(), mock()); + assertThat(channelFactory.supports("foo")).isTrue(); + assertThat(channelFactory.supports("static:127.0.0.1")).isTrue(); + assertThat(channelFactory.supports("in-process:foo")).isFalse(); + } + + @Test + void shadedNettySupportsEverythingExceptInProcess() { + var channelFactory = new ShadedNettyGrpcChannelFactory(List.of(), mock()); + assertThat(channelFactory.supports("foo")).isTrue(); + assertThat(channelFactory.supports("static:127.0.0.1")).isTrue(); + assertThat(channelFactory.supports("in-process:foo")).isFalse(); + } + + @Test + void inProcessSupportsOnlyInProcess() { + var channelFactory = new InProcessGrpcChannelFactory(List.of(), mock()); + assertThat(channelFactory.supports("foo")).isFalse(); + assertThat(channelFactory.supports("static:127.0.0.1")).isFalse(); + assertThat(channelFactory.supports("in-process:foo")).isTrue(); + } + } } diff --git a/spring-grpc-spring-boot-autoconfigure/pom.xml b/spring-grpc-spring-boot-autoconfigure/pom.xml index 1eecb68..53c02da 100644 --- a/spring-grpc-spring-boot-autoconfigure/pom.xml +++ b/spring-grpc-spring-boot-autoconfigure/pom.xml @@ -93,6 +93,11 @@ netty-transport-native-epoll true + + io.grpc + grpc-inprocess + true + org.springframework.boot spring-boot diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/CompositeChannelFactoryAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/CompositeChannelFactoryAutoConfiguration.java new file mode 100644 index 0000000..95f434f --- /dev/null +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/CompositeChannelFactoryAutoConfiguration.java @@ -0,0 +1,67 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.grpc.autoconfigure.client; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +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.GrpcChannelFactory; + +import static org.springframework.grpc.autoconfigure.client.CompositeChannelFactoryAutoConfiguration.*; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for a + * {@link CompositeGrpcChannelFactory}. + * + * @author Chris Bono + */ +@AutoConfiguration +@Conditional(MultipleNonPrimaryChannelFactoriesCondition.class) +class CompositeChannelFactoryAutoConfiguration { + + @Bean + @Primary + CompositeGrpcChannelFactory compositeChannelFactory(ObjectProvider 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 { + + } + + } + +} diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcChannelFactoryConfigurations.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcChannelFactoryConfigurations.java index 970c3f7..699279a 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcChannelFactoryConfigurations.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcChannelFactoryConfigurations.java @@ -20,6 +20,7 @@ import java.util.List; 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.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -27,10 +28,13 @@ import org.springframework.grpc.client.ChannelCredentialsProvider; import org.springframework.grpc.client.ClientInterceptorsConfigurer; 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 io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.netty.NettyChannelBuilder; +import io.netty.channel.Channel; /** * Configurations for {@link GrpcChannelFactory gRPC channel factories}. @@ -40,8 +44,11 @@ import io.grpc.netty.NettyChannelBuilder; class GrpcChannelFactoryConfigurations { @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class) - @ConditionalOnMissingBean(GrpcChannelFactory.class) + @ConditionalOnClass(value = { 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) + @ConditionalOnProperty(prefix = "spring.grpc.client.inprocess.", name = "exclusive", havingValue = "false", + matchIfMissing = true) @EnableConfigurationProperties(GrpcClientProperties.class) static class ShadedNettyChannelFactoryConfiguration { @@ -60,8 +67,10 @@ class GrpcChannelFactoryConfigurations { } @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(NettyChannelBuilder.class) - @ConditionalOnMissingBean(GrpcChannelFactory.class) + @ConditionalOnClass(value = { Channel.class, NettyChannelBuilder.class }) + @ConditionalOnMissingBean(value = GrpcChannelFactory.class, ignored = InProcessGrpcChannelFactory.class) + @ConditionalOnProperty(prefix = "spring.grpc.client.inprocess.", name = "exclusive", havingValue = "false", + matchIfMissing = true) @EnableConfigurationProperties(GrpcClientProperties.class) static class NettyChannelFactoryConfiguration { @@ -79,4 +88,21 @@ class GrpcChannelFactoryConfigurations { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(InProcessChannelBuilder.class) + @ConditionalOnMissingBean(InProcessGrpcChannelFactory.class) + @ConditionalOnProperty(prefix = "spring.grpc.client.inprocess", name = "enabled", havingValue = "true", + matchIfMissing = true) + static class InProcessChannelFactoryConfiguration { + + @Bean + InProcessGrpcChannelFactory inProcessGrpcChannelFactory(ChannelBuilderCustomizers channelBuilderCustomizers, + ClientInterceptorsConfigurer interceptorsConfigurer) { + List> inProcessBuilderCustomizers = List + .of(channelBuilderCustomizers::customize); + return new InProcessGrpcChannelFactory(inProcessBuilderCustomizers, interceptorsConfigurer); + } + + } + } diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfiguration.java index dc8595f..8e771a8 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfiguration.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfiguration.java @@ -34,12 +34,13 @@ import io.grpc.CompressorRegistry; import io.grpc.DecompressorRegistry; import io.grpc.ManagedChannelBuilder; -@AutoConfiguration +@AutoConfiguration(before = CompositeChannelFactoryAutoConfiguration.class) @ConditionalOnGrpcClientEnabled @EnableConfigurationProperties(GrpcClientProperties.class) @Import({ GrpcCodecConfiguration.class, ClientInterceptorsConfiguration.class, GrpcChannelFactoryConfigurations.ShadedNettyChannelFactoryConfiguration.class, - GrpcChannelFactoryConfigurations.NettyChannelFactoryConfiguration.class, ClientScanConfiguration.class }) + GrpcChannelFactoryConfigurations.NettyChannelFactoryConfiguration.class, + GrpcChannelFactoryConfigurations.InProcessChannelFactoryConfiguration.class, ClientScanConfiguration.class }) public class GrpcClientAutoConfiguration { @Bean diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfiguration.java index a4e5cbc..2f18114 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfiguration.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfiguration.java @@ -64,13 +64,6 @@ public class GrpcServerAutoConfiguration { this.properties = properties; } - @ConditionalOnBean(GrpcServerFactory.class) - @ConditionalOnMissingBean - @Bean - GrpcServerLifecycle grpcServerLifecycle(GrpcServerFactory factory, ApplicationEventPublisher eventPublisher) { - return new GrpcServerLifecycle(factory, this.properties.getShutdownGracePeriod(), eventPublisher); - } - @ConditionalOnMissingBean @Bean ServerBuilderCustomizers serverBuilderCustomizers(ObjectProvider> customizers) { diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryAutoConfiguration.java index 88cc642..a3a84c0 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryAutoConfiguration.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryAutoConfiguration.java @@ -67,7 +67,8 @@ public class GrpcServerFactoryAutoConfiguration { @Configuration(proxyBeanMethods = false) @Import({ GrpcServerFactoryConfigurations.ShadedNettyServerFactoryConfiguration.class, - GrpcServerFactoryConfigurations.NettyServerFactoryConfiguration.class }) + GrpcServerFactoryConfigurations.NettyServerFactoryConfiguration.class, + GrpcServerFactoryConfigurations.InProcessServerFactoryConfiguration.class }) static class NettyServerFactoryConfiguration { } @@ -103,6 +104,12 @@ public class GrpcServerFactoryAutoConfiguration { return servlet; } + @Configuration(proxyBeanMethods = false) + @Import(GrpcServerFactoryConfigurations.InProcessServerFactoryConfiguration.class) + static class InProcessConfiguration { + + } + } public static class OnGrpcServletCondition extends AllNestedConditions { diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryConfigurations.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryConfigurations.java index 1e7ce9e..9789743 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryConfigurations.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryConfigurations.java @@ -21,19 +21,25 @@ import java.util.List; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 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.SslBundle; import org.springframework.boot.ssl.SslBundles; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.InProcessGrpcServerFactory; import org.springframework.grpc.server.NettyGrpcServerFactory; import org.springframework.grpc.server.ServerBuilderCustomizer; import org.springframework.grpc.server.ShadedNettyGrpcServerFactory; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.netty.NettyServerBuilder; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; @@ -46,7 +52,9 @@ class GrpcServerFactoryConfigurations { @Configuration(proxyBeanMethods = false) @ConditionalOnClass(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class) - @ConditionalOnMissingBean(GrpcServerFactory.class) + @ConditionalOnMissingBean(value = GrpcServerFactory.class, ignored = InProcessGrpcServerFactory.class) + @ConditionalOnProperty(prefix = "spring.grpc.server.inprocess.", name = "exclusive", havingValue = "false", + matchIfMissing = true) @EnableConfigurationProperties(GrpcServerProperties.class) static class ShadedNettyServerFactoryConfiguration { @@ -71,11 +79,21 @@ class GrpcServerFactoryConfigurations { return factory; } + @ConditionalOnBean(ShadedNettyGrpcServerFactory.class) + @ConditionalOnMissingBean(name = "shadedNettyGrpcServerLifecycle") + @Bean + GrpcServerLifecycle shadedNettyGrpcServerLifecycle(ShadedNettyGrpcServerFactory factory, + GrpcServerProperties properties, ApplicationEventPublisher eventPublisher) { + return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod(), eventPublisher); + } + } @Configuration(proxyBeanMethods = false) @ConditionalOnClass(NettyServerBuilder.class) - @ConditionalOnMissingBean(GrpcServerFactory.class) + @ConditionalOnMissingBean(value = GrpcServerFactory.class, ignored = InProcessGrpcServerFactory.class) + @ConditionalOnProperty(prefix = "spring.grpc.server.inprocess.", name = "exclusive", havingValue = "false", + matchIfMissing = true) @EnableConfigurationProperties(GrpcServerProperties.class) static class NettyServerFactoryConfiguration { @@ -100,6 +118,43 @@ class GrpcServerFactoryConfigurations { return factory; } + @ConditionalOnBean(NettyGrpcServerFactory.class) + @ConditionalOnMissingBean(name = "nettyGrpcServerLifecycle") + @Bean + GrpcServerLifecycle nettyGrpcServerLifecycle(NettyGrpcServerFactory factory, GrpcServerProperties properties, + ApplicationEventPublisher eventPublisher) { + return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod(), eventPublisher); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(InProcessGrpcServerFactory.class) + @ConditionalOnMissingBean(InProcessGrpcServerFactory.class) + @ConditionalOnProperty(prefix = "spring.grpc.server.inprocess", name = "name") + @EnableConfigurationProperties(GrpcServerProperties.class) + static class InProcessServerFactoryConfiguration { + + @Bean + InProcessGrpcServerFactory inProcessGrpcServerFactory(GrpcServerProperties properties, + GrpcServiceDiscoverer grpcServicesDiscoverer, ServerBuilderCustomizers serverBuilderCustomizers) { + var mapper = new InProcessServerFactoryPropertyMapper(properties); + List> builderCustomizers = List + .of(mapper::customizeServerBuilder, serverBuilderCustomizers::customize); + InProcessGrpcServerFactory factory = new InProcessGrpcServerFactory(properties.getInprocess().getName(), + builderCustomizers); + grpcServicesDiscoverer.findServices().forEach(factory::addService); + return factory; + } + + @ConditionalOnBean(InProcessGrpcServerFactory.class) + @ConditionalOnMissingBean(name = "inProcessGrpcServerLifecycle") + @Bean + GrpcServerLifecycle inProcessGrpcServerLifecycle(InProcessGrpcServerFactory factory, + GrpcServerProperties properties, ApplicationEventPublisher eventPublisher) { + return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod(), eventPublisher); + } + } } diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerProperties.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerProperties.java index 7e84ec2..71e8cad 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerProperties.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerProperties.java @@ -72,6 +72,8 @@ public class GrpcServerProperties { private final KeepAlive keepAlive = new KeepAlive(); + private final Inprocess inprocess = new Inprocess(); + /** * The address to bind to. could be a host:port combination or a pseudo URL like * static://host:port. Can not be set if host or port are set independently. @@ -143,6 +145,10 @@ public class GrpcServerProperties { return this.keepAlive; } + public Inprocess getInprocess() { + return this.inprocess; + } + public static class Health { /** @@ -416,4 +422,35 @@ public class GrpcServerProperties { } + public static class Inprocess { + + /** + * The name of the in-process server or null to not start the in-process server. + */ + private String name; + + /** + * Whether the inprocess server factory should be the only server factory + * available. When the value is true no other server factory will be configured. + */ + private Boolean exclusive; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Boolean getExclusive() { + return exclusive; + } + + public void setExclusive(Boolean exclusive) { + this.exclusive = exclusive; + } + + } + } diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/InProcessServerFactoryPropertyMapper.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/InProcessServerFactoryPropertyMapper.java new file mode 100644 index 0000000..0469151 --- /dev/null +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/InProcessServerFactoryPropertyMapper.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.grpc.autoconfigure.server; + +import io.grpc.inprocess.InProcessServerBuilder; + +import org.springframework.boot.context.properties.PropertyMapper; + +/** + * Helper class used to map {@link GrpcServerProperties} to + * {@link InProcessServerBuilder}. + * + * @author Chris Bono + */ +class InProcessServerFactoryPropertyMapper extends DefaultServerFactoryPropertyMapper { + + InProcessServerFactoryPropertyMapper(GrpcServerProperties properties) { + super(properties); + } + + @Override + void customizeServerBuilder(InProcessServerBuilder serverBuilder) { + PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); + customizeInboundLimits(serverBuilder, mapper); + } + +} diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 71ca9f4..1c22801 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -47,6 +47,18 @@ "description": "Whether to enable client autoconfiguration.", "defaultValue": true }, + { + "name": "spring.grpc.client.inprocess.enabled", + "type": "java.lang.Boolean", + "description": "Whether to configure the in-process channel factory.", + "defaultValue": true + }, + { + "name": "spring.grpc.client.inprocess.exclusive", + "type": "java.lang.Boolean", + "description": "Whether the inprocess channel factory should be the only channel factory available. When the value is true, no other channel factory will be configured.", + "defaultValue": true + }, { "name": "spring.grpc.client.observations.enabled", "type": "java.lang.Boolean", diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 5862f10..4c86fdd 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-grpc-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,4 @@ +org.springframework.grpc.autoconfigure.client.CompositeChannelFactoryAutoConfiguration org.springframework.grpc.autoconfigure.client.GrpcClientAutoConfiguration org.springframework.grpc.autoconfigure.client.GrpcClientObservationAutoConfiguration org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration @@ -8,4 +9,4 @@ org.springframework.grpc.autoconfigure.server.GrpcServerReflectionAutoConfigurat org.springframework.grpc.autoconfigure.server.exception.GrpcExceptionHandlerAutoConfiguration org.springframework.grpc.autoconfigure.server.security.GrpcSecurityAutoConfiguration org.springframework.grpc.autoconfigure.server.security.OAuth2ClientAutoConfiguration -org.springframework.grpc.autoconfigure.server.security.OAuth2ResourceServerAutoConfiguration \ No newline at end of file +org.springframework.grpc.autoconfigure.server.security.OAuth2ResourceServerAutoConfiguration diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/CompositeChannelFactoryAutoConfigurationTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/CompositeChannelFactoryAutoConfigurationTests.java new file mode 100644 index 0000000..3c0de81 --- /dev/null +++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/CompositeChannelFactoryAutoConfigurationTests.java @@ -0,0 +1,144 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.grpc.autoconfigure.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +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 io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.netty.NettyChannelBuilder; + +/** + * Tests for {@link CompositeChannelFactoryAutoConfiguration}. + * + * @author Chris Bono + */ +@SuppressWarnings({ "unchecked", "rawtypes" }) +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_ZAA, + 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_ZAA = mock(); + + @Bean + @Order(3) + GrpcChannelFactory channelFactoryFoo() { + return CHANNEL_FACTORY_FOO; + } + + @Bean + @Order(1) + GrpcChannelFactory channelFactoryBar() { + return CHANNEL_FACTORY_BAR; + } + + @Bean + @Order(2) + GrpcChannelFactory channelFactoryZaa() { + return CHANNEL_FACTORY_ZAA; + } + + } + +} diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfigurationTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfigurationTests.java index 4660022..484d6cf 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfigurationTests.java +++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/client/GrpcClientAutoConfigurationTests.java @@ -43,13 +43,16 @@ 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 org.springframework.grpc.server.GrpcServerFactory; 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; @@ -67,6 +70,10 @@ class GrpcClientAutoConfigurationTests { .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class, SslAutoConfiguration.class)); } + private ApplicationContextRunner contextRunnerWithoutInProcessChannelFactory() { + return this.contextRunner().withPropertyValues("spring.grpc.client.inprocess.enabled=false"); + } + @Test void whenGrpcStubNotOnClasspathThenAutoConfigurationIsSkipped() { this.contextRunner() @@ -223,72 +230,169 @@ class GrpcClientAutoConfigurationTests { } @Test - void whenHasUserDefinedChannelFactoryDoesNotAutoConfigureBean() { - GrpcChannelFactory customChannelFactory = mock(GrpcChannelFactory.class); + 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 whenShadedAndNonShadedNettyOnClasspathShadedNettyFactoryIsAutoConfigured() { + 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 whenOnlyNonShadedNettyOnClasspathNonShadedNettyFactoryIsAutoConfigured() { + 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 shadedNettyChannelFactoryAutoConfiguredAsExpected() { - channelFactoryAutoConfiguredAsExpected(this.contextRunner(), ShadedNettyGrpcChannelFactory.class); - } - - @Test - void nettyChannelFactoryAutoConfiguredAsExpected() { - channelFactoryAutoConfiguredAsExpected(this.contextRunner() - .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)), - NettyGrpcChannelFactory.class); - } - - @Test - void noChannelFactoryAutoConfiguredAsExpected() { + 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)); } - private void channelFactoryAutoConfiguredAsExpected(ApplicationContextRunner contextRunner, - Class expectedChannelFactoryType) { - contextRunner.withPropertyValues("spring.grpc.server.port=0") + @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(expectedChannelFactoryType) + .isInstanceOf(InProcessGrpcChannelFactory.class)); + } + + @Test + void shadedNettyChannelFactoryAutoConfiguredAsExpected() { + this.contextRunnerWithoutInProcessChannelFactory() + .withPropertyValues("spring.grpc.server.port=0") + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(ShadedNettyGrpcChannelFactory.class) .hasFieldOrPropertyWithValue("credentials", context.getBean(NamedChannelCredentialsProvider.class)) .extracting("targets") .isInstanceOf(GrpcClientProperties.class)); } + @Test + void nettyChannelFactoryAutoConfiguredAsExpected() { + this.contextRunnerWithoutInProcessChannelFactory() + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder.class)) + .withPropertyValues("spring.grpc.server.port=0") + .run((context) -> assertThat(context).getBean(GrpcChannelFactory.class) + .isInstanceOf(NettyGrpcChannelFactory.class) + .hasFieldOrPropertyWithValue("credentials", context.getBean(NamedChannelCredentialsProvider.class)) + .extracting("targets") + .isInstanceOf(GrpcClientProperties.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.contextRunner(), builder, ShadedNettyGrpcChannelFactory.class); + channelFactoryAutoConfiguredWithCustomizers(this.contextRunnerWithoutInProcessChannelFactory(), builder, + ShadedNettyGrpcChannelFactory.class); } @Test void nettyChannelFactoryAutoConfiguredWithCustomizers() { NettyChannelBuilder builder = mock(); - channelFactoryAutoConfiguredWithCustomizers(this.contextRunner() + 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); + } + @SuppressWarnings("unchecked") private > void channelFactoryAutoConfiguredWithCustomizers( ApplicationContextRunner contextRunner, ManagedChannelBuilder mockChannelBuilder, diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfigurationTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfigurationTests.java index a61a597..cd2f683 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfigurationTests.java +++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfigurationTests.java @@ -45,6 +45,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.server.InProcessGrpcServerFactory; import org.springframework.grpc.server.NettyGrpcServerFactory; import org.springframework.grpc.server.ServerBuilderCustomizer; import org.springframework.grpc.server.ShadedNettyGrpcServerFactory; @@ -86,7 +87,9 @@ class GrpcServerAutoConfigurationTests { return runner .withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class, GrpcServerFactoryAutoConfiguration.class, SslAutoConfiguration.class)) - .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) .withBean(BindableService.class, () -> service); } @@ -130,21 +133,6 @@ class GrpcServerAutoConfigurationTests { .run((context) -> assertThat(context).hasSingleBean(GrpcServerAutoConfiguration.class)); } - @Test - void whenHasUserDefinedServerLifecycleDoesNotAutoConfigureBean() { - GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class); - this.contextRunnerWithLifecyle() - .withBean("customServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle) - .run((context) -> assertThat(context).getBean(GrpcServerLifecycle.class).isSameAs(customServerLifecycle)); - } - - @Test - void serverLifecycleAutoConfiguredAsExpected() { - this.contextRunnerWithLifecyle() - .run((context) -> assertThat(context).getBean(GrpcServerLifecycle.class) - .hasFieldOrPropertyWithValue("factory", context.getBean(GrpcServerFactory.class))); - } - @Test void whenHasUserDefinedGrpcServiceDiscovererDoesNotAutoConfigureBean() { GrpcServiceDiscoverer customGrpcServiceDiscoverer = mock(GrpcServiceDiscoverer.class); @@ -205,6 +193,16 @@ class GrpcServerAutoConfigurationTests { .run((context) -> assertThat(context).getBean(GrpcServerFactory.class).isSameAs(customServerFactory)); } + @Test + void userDefinedServerFactoryWithInProcessServerFactory() { + GrpcServerFactory customServerFactory = mock(GrpcServerFactory.class); + this.contextRunner() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withBean("customServerFactory", GrpcServerFactory.class, () -> customServerFactory) + .run((context) -> assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("customServerFactory", "inProcessGrpcServerFactory")); + } + @Test void whenShadedAndNonShadedNettyOnClasspathShadedNettyFactoryIsAutoConfigured() { this.contextRunner() @@ -212,6 +210,14 @@ class GrpcServerAutoConfigurationTests { .isInstanceOf(ShadedNettyGrpcServerFactory.class)); } + @Test + void shadedNettyFactoryWithInProcessServerFactory() { + this.contextRunner() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .run((context) -> assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("shadedNettyGrpcServerFactory", "inProcessGrpcServerFactory")); + } + @Test void whenOnlyNonShadedNettyOnClasspathNonShadedNettyFactoryIsAutoConfigured() { this.contextRunner() @@ -221,42 +227,120 @@ class GrpcServerAutoConfigurationTests { } @Test - void shadedNettyServerFactoryAutoConfiguredAsExpected() { - serverFactoryAutoConfiguredAsExpected(this.contextRunner(), ShadedNettyGrpcServerFactory.class); + void nonShadedNettyFactoryWithInProcessServerFactory() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .run((context) -> assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("nettyGrpcServerFactory", "inProcessGrpcServerFactory")); } @Test - void serverFactoryAutoConfiguredWhenServletDisabled() { - serverFactoryAutoConfiguredAsExpected(this.contextRunner(new WebApplicationContextRunner()) - .withPropertyValues("spring.grpc.server.servlet.enabled=false"), GrpcServerFactory.class); - } - - @Test - void nettyServerFactoryAutoConfiguredAsExpected() { - serverFactoryAutoConfiguredAsExpected(this.contextRunner() - .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)), - NettyGrpcServerFactory.class); - } - - @Test - void noServerFactoryAutoConfiguredAsExpected() { + void whenShadedNettyAndNettyNotOnClasspathNoServerFactoryIsAutoConfigured() { this.contextRunner() .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) .run((context) -> assertThat(context).doesNotHaveBean(GrpcServerFactory.class)); } - private void serverFactoryAutoConfiguredAsExpected(AbstractApplicationContextRunner contextRunner, - Class expectedServerFactoryType) { - contextRunner.withPropertyValues("spring.grpc.server.host=myhost", "spring.grpc.server.port=6160") + @Test + void noServerFactoryWithInProcessServerFactory() { + this.contextRunner() + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withPropertyValues("spring.grpc.server.inprocess.name=foo") .run((context) -> assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(InProcessGrpcServerFactory.class)); + } + + @Test + void shadedNettyServerFactoryAutoConfiguredWithCustomLifecycle() { + GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class); + this.contextRunnerWithLifecyle() + .withBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle) + .run((context) -> { + assertThat(context).getBean(GrpcServerFactory.class).isInstanceOf(ShadedNettyGrpcServerFactory.class); + assertThat(context).getBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class) + .isSameAs(customServerLifecycle); + }); + } + + @Test + void nettyServerFactoryAutoConfiguredWithCustomLifecycle() { + GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class); + this.contextRunnerWithLifecyle() + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle) + .run((context) -> { + assertThat(context).getBean(GrpcServerFactory.class).isInstanceOf(NettyGrpcServerFactory.class); + assertThat(context).getBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class) + .isSameAs(customServerLifecycle); + }); + } + + @Test + void inProcessServerFactoryAutoConfiguredWithCustomLifecycle() { + GrpcServerLifecycle customServerLifecycle = mock(GrpcServerLifecycle.class); + this.contextRunnerWithLifecyle() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)) + .withBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class, () -> customServerLifecycle) + .run((context) -> { + assertThat(context).getBean(GrpcServerFactory.class).isInstanceOf(InProcessGrpcServerFactory.class); + assertThat(context).getBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class) + .isSameAs(customServerLifecycle); + }); + } + + @Test + void shadedNettyServerFactoryAutoConfiguredAsExpected() { + serverFactoryAutoConfiguredAsExpected( + this.contextRunner() + .withPropertyValues("spring.grpc.server.host=myhost", "spring.grpc.server.port=6160"), + ShadedNettyGrpcServerFactory.class, "myhost:6160", "shadedNettyGrpcServerLifecycle"); + } + + @Test + void serverFactoryAutoConfiguredInWebAppWhenServletDisabled() { + serverFactoryAutoConfiguredAsExpected( + this.contextRunner(new WebApplicationContextRunner()) + .withPropertyValues("spring.grpc.server.host=myhost", "spring.grpc.server.port=6160") + .withPropertyValues("spring.grpc.server.servlet.enabled=false"), + GrpcServerFactory.class, "myhost:6160", "shadedNettyGrpcServerLifecycle"); + } + + @Test + void nettyServerFactoryAutoConfiguredAsExpected() { + serverFactoryAutoConfiguredAsExpected(this.contextRunner() + .withPropertyValues("spring.grpc.server.host=myhost", "spring.grpc.server.port=6160") + .withClassLoader(new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)), + NettyGrpcServerFactory.class, "myhost:6160", "nettyGrpcServerLifecycle"); + } + + @Test + void inProcessServerFactoryAutoConfiguredAsExpected() { + serverFactoryAutoConfiguredAsExpected( + this.contextRunner() + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)), + InProcessGrpcServerFactory.class, "foo", "inProcessGrpcServerLifecycle"); + } + + private void serverFactoryAutoConfiguredAsExpected(AbstractApplicationContextRunner contextRunner, + Class expectedServerFactoryType, String expectedAddress, String expectedLifecycleBeanName) { + contextRunner.run((context) -> { + assertThat(context).getBean(GrpcServerFactory.class) .isInstanceOf(expectedServerFactoryType) - .hasFieldOrPropertyWithValue("address", "myhost:6160") + .hasFieldOrPropertyWithValue("address", expectedAddress) .extracting("serviceList", InstanceOfAssertFactories.list(ServerServiceDefinition.class)) .singleElement() .extracting(ServerServiceDefinition::getServiceDescriptor) .extracting(ServiceDescriptor::getName) - .isEqualTo("my-service")); + .isEqualTo("my-service"); + assertThat(context).getBean(expectedLifecycleBeanName, GrpcServerLifecycle.class).isNotNull(); + }); } @Test @@ -314,10 +398,11 @@ class GrpcServerAutoConfigurationTests { .withPropertyValues("spring.grpc.server.ssl.bundle=ssltest", "spring.ssl.bundle.jks.ssltest.keystore.location=classpath:test.jks", "spring.ssl.bundle.jks.ssltest.keystore.password=secret", - "spring.ssl.bundle.jks.ssltest.key.password=password") + "spring.ssl.bundle.jks.ssltest.key.password=password", "spring.grpc.server.host=myhost", + "spring.grpc.server.port=6160") .withClassLoader( new FilteredClassLoader(io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)), - NettyGrpcServerFactory.class); + NettyGrpcServerFactory.class, "myhost:6160", "nettyGrpcServerLifecycle"); } @Configuration(proxyBeanMethods = false) diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcChannelFactory.java b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcChannelFactory.java deleted file mode 100644 index 01b0eab..0000000 --- a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcChannelFactory.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.grpc.test; - -import java.util.List; - -import org.springframework.grpc.client.ClientInterceptorsConfigurer; -import org.springframework.grpc.client.DefaultGrpcChannelFactory; - -import io.grpc.ChannelCredentials; -import io.grpc.ManagedChannelBuilder; -import io.grpc.inprocess.InProcessChannelBuilder; - -public class InProcessGrpcChannelFactory extends DefaultGrpcChannelFactory { - - public InProcessGrpcChannelFactory(ClientInterceptorsConfigurer interceptorsConfigurer) { - super(List.of(), interceptorsConfigurer); - } - - @Override - protected ManagedChannelBuilder newChannelBuilder(String path, ChannelCredentials creds) { - return InProcessChannelBuilder.forName(path); - } - -} diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactoryAutoConfiguration.java b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactoryAutoConfiguration.java deleted file mode 100644 index aac0402..0000000 --- a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactoryAutoConfiguration.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.grpc.test; - -import java.util.List; - -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.grpc.autoconfigure.client.ClientInterceptorsConfiguration; -import org.springframework.grpc.autoconfigure.client.GrpcClientAutoConfiguration; -import org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration; -import org.springframework.grpc.client.ClientInterceptorsConfigurer; -import org.springframework.grpc.server.ServerBuilderCustomizer; -import org.springframework.grpc.server.service.GrpcServiceDiscoverer; - -import io.grpc.BindableService; -import io.grpc.inprocess.InProcessServerBuilder; - -@AutoConfiguration(before = { GrpcServerFactoryAutoConfiguration.class, GrpcClientAutoConfiguration.class }) -@ConditionalOnProperty(prefix = "spring.grpc.inprocess", name = "enabled", havingValue = "true") -@ConditionalOnClass(BindableService.class) -@Import(ClientInterceptorsConfiguration.class) -public class InProcessGrpcServerFactoryAutoConfiguration { - - private final String address = InProcessServerBuilder.generateName(); - - @Bean - @ConditionalOnBean(BindableService.class) - InProcessGrpcServerFactory grpcServerFactory(GrpcServiceDiscoverer grpcServicesDiscoverer, - List> customizers) { - InProcessGrpcServerFactory factory = new InProcessGrpcServerFactory(address, customizers); - grpcServicesDiscoverer.findServices().forEach(factory::addService); - return factory; - } - - @Bean - InProcessGrpcChannelFactory grpcChannelFactory(ClientInterceptorsConfigurer interceptorsConfigurer) { - InProcessGrpcChannelFactory factory = new InProcessGrpcChannelFactory(interceptorsConfigurer); - factory.setVirtualTargets(path -> address); - return factory; - } - -} diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessTestAutoConfiguration.java b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessTestAutoConfiguration.java new file mode 100644 index 0000000..7ff3209 --- /dev/null +++ b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessTestAutoConfiguration.java @@ -0,0 +1,131 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.grpc.test; + +import java.time.Duration; +import java.util.List; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.grpc.autoconfigure.client.ClientInterceptorsConfiguration; +import org.springframework.grpc.autoconfigure.client.GrpcClientAutoConfiguration; +import org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration; +import org.springframework.grpc.client.ClientInterceptorsConfigurer; +import org.springframework.grpc.client.InProcessGrpcChannelFactory; +import org.springframework.grpc.server.InProcessGrpcServerFactory; +import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; +import org.springframework.grpc.server.service.GrpcServiceDiscoverer; + +import io.grpc.BindableService; +import io.grpc.ChannelCredentials; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; + +@AutoConfiguration(before = { GrpcServerFactoryAutoConfiguration.class, GrpcClientAutoConfiguration.class }) +@ConditionalOnProperty(prefix = "spring.grpc.test.inprocess", name = "enabled", havingValue = "true") +@ConditionalOnClass(BindableService.class) +@Import(ClientInterceptorsConfiguration.class) +public class InProcessTestAutoConfiguration { + + private final String address = InProcessServerBuilder.generateName(); + + @Bean + @ConditionalOnBean(BindableService.class) + @Order(Ordered.HIGHEST_PRECEDENCE) + TestInProcessGrpcServerFactory testInProcessGrpcServerFactory(GrpcServiceDiscoverer grpcServicesDiscoverer, + List> customizers) { + var factory = new TestInProcessGrpcServerFactory(address, customizers); + grpcServicesDiscoverer.findServices().forEach(factory::addService); + return factory; + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + TestInProcessGrpcChannelFactory testInProcessGrpcChannelFactory( + ClientInterceptorsConfigurer interceptorsConfigurer) { + return new TestInProcessGrpcChannelFactory(address, interceptorsConfigurer); + } + + @Bean(name = "inProcessGrpcServerLifecycle") + @ConditionalOnBean(InProcessGrpcServerFactory.class) + @Order(Ordered.HIGHEST_PRECEDENCE) + GrpcServerLifecycle inProcessGrpcServerLifecycle(InProcessGrpcServerFactory factory, + ApplicationEventPublisher eventPublisher) { + return new GrpcServerLifecycle(factory, Duration.ofSeconds(30), eventPublisher); + } + + /** + * Specialization of {@link InProcessGrpcServerFactory}. + */ + public static class TestInProcessGrpcServerFactory extends InProcessGrpcServerFactory { + + public TestInProcessGrpcServerFactory(String address, + List> serverBuilderCustomizers) { + super(address, serverBuilderCustomizers); + } + + } + + /** + * Specialization of {@link InProcessGrpcChannelFactory} that allows the channel + * factory to support all targets, not just those that start with 'in-process:'. + */ + public static class TestInProcessGrpcChannelFactory extends InProcessGrpcChannelFactory { + + TestInProcessGrpcChannelFactory(String address, ClientInterceptorsConfigurer interceptorsConfigurer) { + super(List.of(), interceptorsConfigurer); + setVirtualTargets((path) -> address); + } + + /** + * {@inheritDoc} + * @param target the target string as described in method javadocs + * @return {@code true} so that the test factory can handle all targets not just + * those prefixed with 'in-process:' + */ + @Override + public boolean supports(String target) { + return true; + } + + /** + * {@inheritDoc} + *

+ * Overrides the parent behavior so that the channel factory can handle all + * targets, not just those that prefixed with 'in-process:'. + * @param target the target of the channel + * @param creds the credentials for the channel which are ignored in this case + * @return a new inprocess channel builder instance + */ + @Override + protected InProcessChannelBuilder newChannelBuilder(String target, ChannelCredentials creds) { + if (target.startsWith("in-process:")) { + return super.newChannelBuilder(target, creds); + } + return InProcessChannelBuilder.forName(target); + } + + } + +} diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessTransportEnvironmentPostProcessor.java b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessTransportEnvironmentPostProcessor.java index 5822bcd..d420670 100644 --- a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessTransportEnvironmentPostProcessor.java +++ b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessTransportEnvironmentPostProcessor.java @@ -17,6 +17,7 @@ package org.springframework.grpc.test; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; import org.springframework.boot.SpringApplication; @@ -39,7 +40,8 @@ public class InProcessTransportEnvironmentPostProcessor implements EnvironmentPo return; } MapPropertySource inProcessTransportPropertySource = new MapPropertySource("inProcessTransportPropertySource", - Collections.singletonMap("spring.grpc.inprocess.enabled", "true")); + Map.of("spring.grpc.test.inprocess.enabled", "true", "spring.grpc.client.inprocess.exclusive", "true", + "spring.grpc.server.inprocess.exclusive", "true")); environment.getPropertySources().addFirst(inProcessTransportPropertySource); } diff --git a/spring-grpc-test/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-grpc-test/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 7c5e6cc..255fb3c 100644 --- a/spring-grpc-test/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-grpc-test/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -2,10 +2,10 @@ "groups": [], "properties": [ { - "name": "spring.grpc.inprocess.enabled", + "name": "spring.grpc.test.inprocess.enabled", "type": "java.lang.Boolean", "description": "Whether to enable the in-process server and client for testing.", - "defaultValue": true + "defaultValue": false } ] } diff --git a/spring-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 2098b62..e1d85da 100644 --- a/spring-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-grpc-test/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,2 +1 @@ -org.springframework.grpc.test.InProcessGrpcServerFactoryAutoConfiguration - +org.springframework.grpc.test.InProcessTestAutoConfiguration diff --git a/spring-grpc-test/src/test/java/org/springframework/grpc/test/InProcessTestAutoConfigurationTests.java b/spring-grpc-test/src/test/java/org/springframework/grpc/test/InProcessTestAutoConfigurationTests.java new file mode 100644 index 0000000..9ac8d32 --- /dev/null +++ b/spring-grpc-test/src/test/java/org/springframework/grpc/test/InProcessTestAutoConfigurationTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.grpc.test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +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.runner.ApplicationContextRunner; +import org.springframework.grpc.autoconfigure.client.GrpcClientAutoConfiguration; +import org.springframework.grpc.autoconfigure.server.GrpcServerAutoConfiguration; +import org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration; +import org.springframework.grpc.client.GrpcChannelFactory; +import org.springframework.grpc.server.GrpcServerFactory; + +import io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; + +/** + * Tests for {@link InProcessTestAutoConfiguration}. + * + * @author Chris Bono + */ +class InProcessTestAutoConfigurationTests { + + private final BindableService service = mock(); + + private final ServerServiceDefinition serviceDefinition = ServerServiceDefinition.builder("my-service").build(); + + @BeforeEach + void prepareForTest() { + when(service.bindService()).thenReturn(serviceDefinition); + } + + private ApplicationContextRunner contextRunner() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(InProcessTestAutoConfiguration.class, + GrpcServerAutoConfiguration.class, GrpcServerFactoryAutoConfiguration.class, + SslAutoConfiguration.class, GrpcClientAutoConfiguration.class)) + .withBean(BindableService.class, () -> service); + } + + @Test + void whenTestInProcessEnabledPropIsSetToTrueDoesAutoConfigureBeans() { + this.contextRunner() + .withPropertyValues("spring.grpc.test.inprocess.enabled=true", "spring.grpc.server.inprocess.name=foo") + .run((context) -> { + assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("testInProcessGrpcServerFactory", "nettyGrpcServerFactory"); + assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("testInProcessGrpcChannelFactory", "nettyGrpcChannelFactory"); + }); + } + + @Test + void whenTestInProcessEnabledPropIsNotSetDoesNotAutoConfigureBeans() { + this.contextRunner().withPropertyValues("spring.grpc.server.inprocess.name=foo").run((context) -> { + assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("inProcessGrpcServerFactory", "nettyGrpcServerFactory"); + assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("inProcessGrpcChannelFactory", "nettyGrpcChannelFactory"); + }); + } + + @Test + void whenTestInProcessEnabledPropIsSetToFalseDoesNotAutoConfigureBeans() { + this.contextRunner() + .withPropertyValues("spring.grpc.test.inprocess.enabled=false", "spring.grpc.server.inprocess.name=foo") + .run((context) -> { + assertThat(context).getBeans(GrpcServerFactory.class) + .containsOnlyKeys("inProcessGrpcServerFactory", "nettyGrpcServerFactory"); + assertThat(context).getBeans(GrpcChannelFactory.class) + .containsOnlyKeys("inProcessGrpcChannelFactory", "nettyGrpcChannelFactory"); + }); + } + +}