From 296653e4ac79047ab15bd4fb0ba3cdb5e4d3160c Mon Sep 17 00:00:00 2001 From: andreilisa Date: Fri, 1 Nov 2024 17:16:32 +0200 Subject: [PATCH 1/4] #7: Support for in-process transport for testing Signed-off-by: andreilisa --- ...essApplicationContextInitializerTests.java | 101 ++++++++++++++++++ spring-grpc-test/pom.xml | 4 + ...nProcessApplicationContextInitializer.java | 77 +++++++++++++ .../main/resources/META-INF/spring.factories | 3 +- 4 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java create mode 100644 spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java diff --git a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java new file mode 100644 index 00000000..d87797b9 --- /dev/null +++ b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java @@ -0,0 +1,101 @@ +/* + * 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.sample; + +import io.grpc.ManagedChannel; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.grpc.test.InProcessApplicationContextInitializer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +/* + * @author Andrei Lisa + */ + +@SpringBootTest +class InProcessApplicationContextInitializerTests { + + private AnnotationConfigApplicationContext context; + + @BeforeEach + public void setUp() { + System.clearProperty("spring.grpc.inprocess"); + context = new AnnotationConfigApplicationContext(); + } + + @AfterEach + public void cleanUp() { + InProcessApplicationContextInitializer.shutdown(); + context.close(); + } + + @Nested + class WhenDefaultEnabled { + + @Test + void shouldInitializeInProcessServer() { + new InProcessApplicationContextInitializer().initialize(context); + context.refresh(); + + ManagedChannel channel = context.getBean("grpcInProcessChannel", ManagedChannel.class); + assertThat(channel).isNotNull().isInstanceOf(ManagedChannel.class); + } + + } + + @Nested + class WhenDisabledByProperty { + + @Test + void shouldNotInitializeInProcessServer() { + System.setProperty("spring.grpc.inprocess", "false"); + new InProcessApplicationContextInitializer().initialize(context); + context.refresh(); + assertThatThrownBy(() -> { + context.getBean("grpcInProcessChannel", ManagedChannel.class); + }).isInstanceOf(NoSuchBeanDefinitionException.class); + } + + } + + @Nested + class WhenShutdownIsCalled { + + @Test + void shouldShutdownInProcessServer() { + new InProcessApplicationContextInitializer().initialize(context); + context.refresh(); + + ManagedChannel channel = context.getBean("grpcInProcessChannel", ManagedChannel.class); + assertThat(channel).isNotNull(); + + InProcessApplicationContextInitializer.shutdown(); + + assertThat(channel).isNotNull(); + assertThat(channel.isShutdown()).isTrue(); + } + + } + +} diff --git a/spring-grpc-test/pom.xml b/spring-grpc-test/pom.xml index 481f4217..c165fdc3 100644 --- a/spring-grpc-test/pom.xml +++ b/spring-grpc-test/pom.xml @@ -35,5 +35,9 @@ io.grpc grpc-testing + + io.grpc + grpc-inprocess + diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java new file mode 100644 index 00000000..b1f30c00 --- /dev/null +++ b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java @@ -0,0 +1,77 @@ +/* + * 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 io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; + +/* + * @author Andrei Lisa + */ +public class InProcessApplicationContextInitializer + implements ApplicationContextInitializer { + + private static final String PROPERTY_SOURCE_NAME = "spring.grpc.inprocess"; + + private static final String CHANNEL_NAME = "grpcInProcessChannel"; + + private static Server grpcServer; + + private static ManagedChannel grpcChannel; + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + String inProcessEnabled = System.getProperty(PROPERTY_SOURCE_NAME, "true"); + + if ("true".equalsIgnoreCase(inProcessEnabled) && isJarOnClasspath()) { + try { + String serverName = InProcessServerBuilder.generateName(); + + grpcServer = InProcessServerBuilder.forName(serverName).directExecutor().build().start(); + + grpcChannel = InProcessChannelBuilder.forName(serverName).directExecutor().build(); + applicationContext.getBeanFactory().registerSingleton(CHANNEL_NAME, grpcChannel); + + } + catch (Exception e) { + throw new RuntimeException("Failed to initialize in-process gRPC server", e); + } + } + } + + private boolean isJarOnClasspath() { + try { + Class.forName("io.grpc.inprocess.InProcessChannelBuilder"); + return true; + } + catch (ClassNotFoundException e) { + return false; + } + } + + public static void shutdown() { + if (grpcChannel != null) + grpcChannel.shutdownNow(); + if (grpcServer != null) + grpcServer.shutdownNow(); + } + +} diff --git a/spring-grpc-test/src/main/resources/META-INF/spring.factories b/spring-grpc-test/src/main/resources/META-INF/spring.factories index 93d33e3a..6039a0a5 100644 --- a/spring-grpc-test/src/main/resources/META-INF/spring.factories +++ b/spring-grpc-test/src/main/resources/META-INF/spring.factories @@ -1,3 +1,4 @@ # Application Context Initializers org.springframework.context.ApplicationContextInitializer=\ -org.springframework.grpc.test.ServerPortInfoApplicationContextInitializer +org.springframework.grpc.test.ServerPortInfoApplicationContextInitializer,\ +org.springframework.grpc.test.InProcessApplicationContextInitializer From f645b981c9bf3ecafbf5698da9e0e7ac133ee62d Mon Sep 17 00:00:00 2001 From: andreilisa Date: Sat, 9 Nov 2024 12:22:45 +0200 Subject: [PATCH 2/4] #7: fix comments Signed-off-by: andreilisa --- ...essApplicationContextInitializerTests.java | 22 ++++---- ...nProcessApplicationContextInitializer.java | 32 +++++------ .../test/InProcessServerShutdownListener.java | 55 +++++++++++++++++++ ...itional-spring-configuration-metadata.json | 9 +++ 4 files changed, 89 insertions(+), 29 deletions(-) create mode 100644 spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessServerShutdownListener.java create mode 100644 spring-grpc-test/src/main/resources/additional-spring-configuration-metadata.json diff --git a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java index d87797b9..21751345 100644 --- a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java +++ b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java @@ -24,19 +24,16 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.grpc.test.InProcessApplicationContextInitializer; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -/* - * @author Andrei Lisa - */ - @SpringBootTest class InProcessApplicationContextInitializerTests { - private AnnotationConfigApplicationContext context; + private GenericApplicationContext context; @BeforeEach public void setUp() { @@ -46,7 +43,6 @@ public void setUp() { @AfterEach public void cleanUp() { - InProcessApplicationContextInitializer.shutdown(); context.close(); } @@ -55,6 +51,7 @@ class WhenDefaultEnabled { @Test void shouldInitializeInProcessServer() { + System.setProperty("spring.grpc.inprocess", "true"); new InProcessApplicationContextInitializer().initialize(context); context.refresh(); @@ -72,9 +69,9 @@ void shouldNotInitializeInProcessServer() { System.setProperty("spring.grpc.inprocess", "false"); new InProcessApplicationContextInitializer().initialize(context); context.refresh(); - assertThatThrownBy(() -> { - context.getBean("grpcInProcessChannel", ManagedChannel.class); - }).isInstanceOf(NoSuchBeanDefinitionException.class); + + assertThatThrownBy(() -> context.getBean("grpcInProcessChannel", ManagedChannel.class)) + .isInstanceOf(NoSuchBeanDefinitionException.class); } } @@ -84,18 +81,19 @@ class WhenShutdownIsCalled { @Test void shouldShutdownInProcessServer() { + System.setProperty("spring.grpc.inprocess", "true"); new InProcessApplicationContextInitializer().initialize(context); + context.registerShutdownHook(); context.refresh(); ManagedChannel channel = context.getBean("grpcInProcessChannel", ManagedChannel.class); assertThat(channel).isNotNull(); - InProcessApplicationContextInitializer.shutdown(); + context.close(); - assertThat(channel).isNotNull(); assertThat(channel.isShutdown()).isTrue(); } } -} +} \ No newline at end of file diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java index b1f30c00..b0d61718 100644 --- a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java +++ b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java @@ -16,14 +16,21 @@ package org.springframework.grpc.test; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; + import io.grpc.ManagedChannel; import io.grpc.Server; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; -/* +/** + * An {@link ApplicationContextInitializer} that configures and registers an in-process + * gRPC {@link Server} and {@link ManagedChannel} within a Spring + * {@link ConfigurableApplicationContext}. This initializer is intended for testing + * environments, allowing gRPC communication within the same process without network + * overhead. + * * @author Andrei Lisa */ public class InProcessApplicationContextInitializer @@ -33,23 +40,21 @@ public class InProcessApplicationContextInitializer private static final String CHANNEL_NAME = "grpcInProcessChannel"; - private static Server grpcServer; - - private static ManagedChannel grpcChannel; - @Override public void initialize(ConfigurableApplicationContext applicationContext) { - String inProcessEnabled = System.getProperty(PROPERTY_SOURCE_NAME, "true"); + String inProcessEnabled = applicationContext.getEnvironment().getProperty(PROPERTY_SOURCE_NAME); if ("true".equalsIgnoreCase(inProcessEnabled) && isJarOnClasspath()) { try { String serverName = InProcessServerBuilder.generateName(); - grpcServer = InProcessServerBuilder.forName(serverName).directExecutor().build().start(); + Server grpcServer = InProcessServerBuilder.forName(serverName).directExecutor().build().start(); - grpcChannel = InProcessChannelBuilder.forName(serverName).directExecutor().build(); + ManagedChannel grpcChannel = InProcessChannelBuilder.forName(serverName).directExecutor().build(); applicationContext.getBeanFactory().registerSingleton(CHANNEL_NAME, grpcChannel); + applicationContext.addApplicationListener(new InProcessServerShutdownListener(grpcServer, grpcChannel)); + } catch (Exception e) { throw new RuntimeException("Failed to initialize in-process gRPC server", e); @@ -67,11 +72,4 @@ private boolean isJarOnClasspath() { } } - public static void shutdown() { - if (grpcChannel != null) - grpcChannel.shutdownNow(); - if (grpcServer != null) - grpcServer.shutdownNow(); - } - } diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessServerShutdownListener.java b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessServerShutdownListener.java new file mode 100644 index 00000000..3f3a0c32 --- /dev/null +++ b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessServerShutdownListener.java @@ -0,0 +1,55 @@ +/* + * 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 org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; + +import io.grpc.ManagedChannel; +import io.grpc.Server; + +/** + * Listener that automatically shuts down the in-process gRPC {@link Server} and + * {@link ManagedChannel} when the Spring + * {@link org.springframework.context.ApplicationContext} is closed. This helps to ensure + * proper resource cleanup after the application context is no longer active, avoiding the + * need for manual shutdown calls in tests or other classes. + * + * @author Andrei Lisa + */ +final class InProcessServerShutdownListener implements ApplicationListener { + + private final Server grpcServer; + + private final ManagedChannel grpcChannel; + + InProcessServerShutdownListener(Server grpcServer, ManagedChannel grpcChannel) { + this.grpcServer = grpcServer; + this.grpcChannel = grpcChannel; + } + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + if (grpcChannel != null) { + grpcChannel.shutdownNow(); + } + if (grpcServer != null) { + grpcServer.shutdownNow(); + } + } + +} diff --git a/spring-grpc-test/src/main/resources/additional-spring-configuration-metadata.json b/spring-grpc-test/src/main/resources/additional-spring-configuration-metadata.json new file mode 100644 index 00000000..61ea0696 --- /dev/null +++ b/spring-grpc-test/src/main/resources/additional-spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{ + "groups": [], + "properties": [ + { + "name": "spring.grpc.inprocess", + "defaultValue": "true" + } + ] +} From 4493635f901ce43c11eaf3d2e87386a7e9401501 Mon Sep 17 00:00:00 2001 From: andreilisa Date: Mon, 11 Nov 2024 17:58:33 +0200 Subject: [PATCH 3/4] #7: resolve comments Signed-off-by: andreilisa --- ...essApplicationContextInitializerTests.java | 76 +++++++------------ ...nProcessApplicationContextInitializer.java | 4 +- ...itional-spring-configuration-metadata.json | 2 +- 3 files changed, 31 insertions(+), 51 deletions(-) diff --git a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java index 21751345..4882e0df 100644 --- a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java +++ b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java @@ -16,82 +16,62 @@ package org.springframework.grpc.sample; +import static org.assertj.core.api.Assertions.assertThat; + import io.grpc.ManagedChannel; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.grpc.test.InProcessApplicationContextInitializer; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import org.springframework.context.ConfigurableApplicationContext; @SpringBootTest class InProcessApplicationContextInitializerTests { - private GenericApplicationContext context; - - @BeforeEach - public void setUp() { - System.clearProperty("spring.grpc.inprocess"); - context = new AnnotationConfigApplicationContext(); - } - - @AfterEach - public void cleanUp() { - context.close(); - } - @Nested - class WhenDefaultEnabled { + @SpringBootTest(properties = { "spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0", + "spring.grpc.inprocess.enabled=true" }) + class WithInProcessEnabled { - @Test - void shouldInitializeInProcessServer() { - System.setProperty("spring.grpc.inprocess", "true"); - new InProcessApplicationContextInitializer().initialize(context); - context.refresh(); + @Autowired + private ConfigurableApplicationContext context; + @Test + void testInitialize_WithEnabledProperty_ShouldRegisterServerAndChannel() { ManagedChannel channel = context.getBean("grpcInProcessChannel", ManagedChannel.class); - assertThat(channel).isNotNull().isInstanceOf(ManagedChannel.class); + assertThat(channel).isNotNull(); + assertThat(channel.isShutdown()).isFalse(); } } @Nested - class WhenDisabledByProperty { + @SpringBootTest(properties = { "spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0", + "spring.grpc.inprocess.enabled=false" }) + class WithInProcessDisabled { - @Test - void shouldNotInitializeInProcessServer() { - System.setProperty("spring.grpc.inprocess", "false"); - new InProcessApplicationContextInitializer().initialize(context); - context.refresh(); + @Autowired + private ConfigurableApplicationContext context; - assertThatThrownBy(() -> context.getBean("grpcInProcessChannel", ManagedChannel.class)) - .isInstanceOf(NoSuchBeanDefinitionException.class); + @Test + void testInitialize_WithDisabledProperty_ShouldNotRegisterServerAndChannel() { + assertThat(context.containsBean("grpcInProcessChannel")).isFalse(); } } @Nested - class WhenShutdownIsCalled { + @SpringBootTest(properties = { "spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0", + "spring.grpc.inprocess.enabled=true" }) + class WithDefaultInProcessEnabled { - @Test - void shouldShutdownInProcessServer() { - System.setProperty("spring.grpc.inprocess", "true"); - new InProcessApplicationContextInitializer().initialize(context); - context.registerShutdownHook(); - context.refresh(); + @Autowired + private ConfigurableApplicationContext context; + @Test + void testDefaultEnabledProperty_ShouldRegisterServerAndChannel() { ManagedChannel channel = context.getBean("grpcInProcessChannel", ManagedChannel.class); assertThat(channel).isNotNull(); - - context.close(); - - assertThat(channel.isShutdown()).isTrue(); } } diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java index b0d61718..2ddad34b 100644 --- a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java +++ b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java @@ -36,13 +36,13 @@ public class InProcessApplicationContextInitializer implements ApplicationContextInitializer { - private static final String PROPERTY_SOURCE_NAME = "spring.grpc.inprocess"; + private static final String PROPERTY_SOURCE_NAME = "spring.grpc.inprocess.enabled"; private static final String CHANNEL_NAME = "grpcInProcessChannel"; @Override public void initialize(ConfigurableApplicationContext applicationContext) { - String inProcessEnabled = applicationContext.getEnvironment().getProperty(PROPERTY_SOURCE_NAME); + String inProcessEnabled = applicationContext.getEnvironment().getProperty(PROPERTY_SOURCE_NAME, "true"); if ("true".equalsIgnoreCase(inProcessEnabled) && isJarOnClasspath()) { try { diff --git a/spring-grpc-test/src/main/resources/additional-spring-configuration-metadata.json b/spring-grpc-test/src/main/resources/additional-spring-configuration-metadata.json index 61ea0696..9b5f2109 100644 --- a/spring-grpc-test/src/main/resources/additional-spring-configuration-metadata.json +++ b/spring-grpc-test/src/main/resources/additional-spring-configuration-metadata.json @@ -2,7 +2,7 @@ "groups": [], "properties": [ { - "name": "spring.grpc.inprocess", + "name": "spring.grpc.inprocess.enabled", "defaultValue": "true" } ] From 957c3204e09771cc8fe150ab54953e0aa097db7d Mon Sep 17 00:00:00 2001 From: andreilisa Date: Wed, 13 Nov 2024 17:21:21 +0200 Subject: [PATCH 4/4] #7: use factory instead of ApplicationContextInitializer Signed-off-by: andreilisa --- .../sample/GrpcServerIntegrationTests.java | 54 ++++++++++- ...essApplicationContextInitializerTests.java | 79 ---------------- ...nProcessApplicationContextInitializer.java | 75 --------------- .../grpc/test/InProcessGrpcServerFactory.java | 92 +++++++++++++++++++ ...ProcessGrpcServerFactoryConfiguration.java | 65 +++++++++++++ .../main/resources/META-INF/spring.factories | 3 +- 6 files changed, 211 insertions(+), 157 deletions(-) delete mode 100644 samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java delete mode 100644 spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java create mode 100644 spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactory.java create mode 100644 spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactoryConfiguration.java diff --git a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerIntegrationTests.java b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerIntegrationTests.java index d4f5ac5c..78009bd2 100644 --- a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerIntegrationTests.java +++ b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/GrpcServerIntegrationTests.java @@ -24,12 +24,15 @@ import org.junit.jupiter.api.condition.OS; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Import; import org.springframework.grpc.autoconfigure.server.GrpcServerProperties; import org.springframework.grpc.client.GrpcChannelFactory; import org.springframework.grpc.sample.proto.HelloReply; import org.springframework.grpc.sample.proto.HelloRequest; import org.springframework.grpc.sample.proto.SimpleGrpc; import org.springframework.grpc.server.GrpcServerFactory; +import org.springframework.grpc.test.InProcessGrpcServerFactoryConfiguration; import org.springframework.grpc.test.LocalGrpcPort; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; @@ -143,4 +146,53 @@ private void assertThatResponseIsServedToChannel(ManagedChannel clientChannel) { assertThat(response.getMessage()).isEqualTo("Hello ==> Alien"); } -} + @Nested + @SpringBootTest(properties = { "spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0" }) + @Import(InProcessGrpcServerFactoryConfiguration.class) + class WithInProcessEnabled { + + @Autowired + private ConfigurableApplicationContext context; + + @Test + void testInitialize_WithEnabledProperty_ShouldRegisterServerAndChannel() { + ManagedChannel channel = context.getBean("grpcInProcessChannel", ManagedChannel.class); + assertThat(channel).isNotNull(); + assertThat(channel.isShutdown()).isFalse(); + } + + } + + @Nested + @SpringBootTest(properties = { "spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0", + "spring.grpc.inprocess.enabled=false" }) + @Import(InProcessGrpcServerFactoryConfiguration.class) + class WithInProcessDisabled { + + @Autowired + private ConfigurableApplicationContext context; + + @Test + void testInitialize_WithDisabledProperty_ShouldNotRegisterServerAndChannel() { + assertThat(context.containsBean("grpcInProcessChannel")).isFalse(); + } + + } + + @Nested + @SpringBootTest(properties = { "spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0" }) + @Import(InProcessGrpcServerFactoryConfiguration.class) + class WithInProcessChannel { + + @Autowired + private ManagedChannel grpcInProcessChannel; + + @Test + void sayHelloReturnsExpectedMessage() { + + assertThatResponseIsServedToChannel(grpcInProcessChannel); + } + + } + +} \ No newline at end of file diff --git a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java b/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java deleted file mode 100644 index 4882e0df..00000000 --- a/samples/grpc-server/src/test/java/org/springframework/grpc/sample/InProcessApplicationContextInitializerTests.java +++ /dev/null @@ -1,79 +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.sample; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.grpc.ManagedChannel; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.ConfigurableApplicationContext; - -@SpringBootTest -class InProcessApplicationContextInitializerTests { - - @Nested - @SpringBootTest(properties = { "spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0", - "spring.grpc.inprocess.enabled=true" }) - class WithInProcessEnabled { - - @Autowired - private ConfigurableApplicationContext context; - - @Test - void testInitialize_WithEnabledProperty_ShouldRegisterServerAndChannel() { - ManagedChannel channel = context.getBean("grpcInProcessChannel", ManagedChannel.class); - assertThat(channel).isNotNull(); - assertThat(channel.isShutdown()).isFalse(); - } - - } - - @Nested - @SpringBootTest(properties = { "spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0", - "spring.grpc.inprocess.enabled=false" }) - class WithInProcessDisabled { - - @Autowired - private ConfigurableApplicationContext context; - - @Test - void testInitialize_WithDisabledProperty_ShouldNotRegisterServerAndChannel() { - assertThat(context.containsBean("grpcInProcessChannel")).isFalse(); - } - - } - - @Nested - @SpringBootTest(properties = { "spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0", - "spring.grpc.inprocess.enabled=true" }) - class WithDefaultInProcessEnabled { - - @Autowired - private ConfigurableApplicationContext context; - - @Test - void testDefaultEnabledProperty_ShouldRegisterServerAndChannel() { - ManagedChannel channel = context.getBean("grpcInProcessChannel", ManagedChannel.class); - assertThat(channel).isNotNull(); - } - - } - -} \ No newline at end of file diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java deleted file mode 100644 index 2ddad34b..00000000 --- a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessApplicationContextInitializer.java +++ /dev/null @@ -1,75 +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 org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; - -import io.grpc.ManagedChannel; -import io.grpc.Server; -import io.grpc.inprocess.InProcessChannelBuilder; -import io.grpc.inprocess.InProcessServerBuilder; - -/** - * An {@link ApplicationContextInitializer} that configures and registers an in-process - * gRPC {@link Server} and {@link ManagedChannel} within a Spring - * {@link ConfigurableApplicationContext}. This initializer is intended for testing - * environments, allowing gRPC communication within the same process without network - * overhead. - * - * @author Andrei Lisa - */ -public class InProcessApplicationContextInitializer - implements ApplicationContextInitializer { - - private static final String PROPERTY_SOURCE_NAME = "spring.grpc.inprocess.enabled"; - - private static final String CHANNEL_NAME = "grpcInProcessChannel"; - - @Override - public void initialize(ConfigurableApplicationContext applicationContext) { - String inProcessEnabled = applicationContext.getEnvironment().getProperty(PROPERTY_SOURCE_NAME, "true"); - - if ("true".equalsIgnoreCase(inProcessEnabled) && isJarOnClasspath()) { - try { - String serverName = InProcessServerBuilder.generateName(); - - Server grpcServer = InProcessServerBuilder.forName(serverName).directExecutor().build().start(); - - ManagedChannel grpcChannel = InProcessChannelBuilder.forName(serverName).directExecutor().build(); - applicationContext.getBeanFactory().registerSingleton(CHANNEL_NAME, grpcChannel); - - applicationContext.addApplicationListener(new InProcessServerShutdownListener(grpcServer, grpcChannel)); - - } - catch (Exception e) { - throw new RuntimeException("Failed to initialize in-process gRPC server", e); - } - } - } - - private boolean isJarOnClasspath() { - try { - Class.forName("io.grpc.inprocess.InProcessChannelBuilder"); - return true; - } - catch (ClassNotFoundException e) { - return false; - } - } - -} diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactory.java b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactory.java new file mode 100644 index 00000000..4fe089cf --- /dev/null +++ b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactory.java @@ -0,0 +1,92 @@ +/* + * 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.Map; + +import org.springframework.context.ConfigurableApplicationContext; + +import io.grpc.BindableService; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; + +/** + * A factory for managing an in-process gRPC server and channel, with automatic lifecycle + * management. + *

+ * This class is responsible for initializing, starting, and stopping an in-process gRPC + * server. It automatically registers all available {@link BindableService} beans in the + * application context and provides a corresponding {@link ManagedChannel} for + * communication. The server and channel are managed as singletons, and their lifecycle is + * tied to the Spring application's lifecycle. + *

+ * + * @author Andrei Lisa + */ + +class InProcessGrpcServerFactory { + + private static final String CHANNEL_NAME = "grpcInProcessChannel"; + + private final String serverName; + + private final boolean inProcessEnabled; + + private Server grpcServer; + + private final ConfigurableApplicationContext applicationContext; + + InProcessGrpcServerFactory(ConfigurableApplicationContext applicationContext, boolean inProcessEnabled) { + this.applicationContext = applicationContext; + this.serverName = InProcessServerBuilder.generateName(); + this.inProcessEnabled = inProcessEnabled; + } + + void afterPropertiesSet() { + if (!inProcessEnabled) { + return; + } + + if (grpcServer != null) { + throw new IllegalStateException("Server already started."); + } + + InProcessServerBuilder serverBuilder = InProcessServerBuilder.forName(serverName).directExecutor(); + + Map bindableServices = applicationContext.getBeansOfType(BindableService.class); + bindableServices.values().forEach(serverBuilder::addService); + + grpcServer = serverBuilder.build(); + try { + grpcServer.start(); + } + catch (Exception e) { + throw new RuntimeException("Failed to start in-process gRPC server", e); + } + + registerChannel(); + } + + private void registerChannel() { + ManagedChannel grpcChannel = InProcessChannelBuilder.forName(serverName).directExecutor().build(); + applicationContext.getBeanFactory().registerSingleton(CHANNEL_NAME, grpcChannel); + applicationContext.addApplicationListener(new InProcessServerShutdownListener(grpcServer, grpcChannel)); + } + +} diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactoryConfiguration.java b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactoryConfiguration.java new file mode 100644 index 00000000..cdd3a849 --- /dev/null +++ b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessGrpcServerFactoryConfiguration.java @@ -0,0 +1,65 @@ +/* + * 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 org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for the {@link InProcessGrpcServerFactory} bean. This configuration + * ensures that the InProcessGrpcServerFactory is only created if it's not already + * available as a bean and if the necessary conditions are met. + * + * @author Andrei Lisa + */ +@Configuration(proxyBeanMethods = false) +public class InProcessGrpcServerFactoryConfiguration { + + @Value("${spring.grpc.inprocess.enabled:true}") + private String inProcessEnabled; + + @Bean + InProcessGrpcServerFactory grpcInProcessServerFactory(ConfigurableApplicationContext applicationContext) { + boolean enabled = Boolean.parseBoolean(inProcessEnabled) && isJarOnClasspath(); + if (enabled) { + InProcessGrpcServerFactory factory = new InProcessGrpcServerFactory(applicationContext, enabled); + try { + factory.afterPropertiesSet(); + } + catch (Exception e) { + throw new RuntimeException("Failed to initialize the InProcessGrpcServerFactory", e); + } + return factory; + } + else { + return null; + } + } + + private static boolean isJarOnClasspath() { + try { + Class.forName("io.grpc.inprocess.InProcessServerBuilder"); + return true; + } + catch (ClassNotFoundException e) { + return false; + } + } + +} diff --git a/spring-grpc-test/src/main/resources/META-INF/spring.factories b/spring-grpc-test/src/main/resources/META-INF/spring.factories index 6039a0a5..93d33e3a 100644 --- a/spring-grpc-test/src/main/resources/META-INF/spring.factories +++ b/spring-grpc-test/src/main/resources/META-INF/spring.factories @@ -1,4 +1,3 @@ # Application Context Initializers org.springframework.context.ApplicationContextInitializer=\ -org.springframework.grpc.test.ServerPortInfoApplicationContextInitializer,\ -org.springframework.grpc.test.InProcessApplicationContextInitializer +org.springframework.grpc.test.ServerPortInfoApplicationContextInitializer