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/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/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/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..9b5f2109 --- /dev/null +++ b/spring-grpc-test/src/main/resources/additional-spring-configuration-metadata.json @@ -0,0 +1,9 @@ +{ + "groups": [], + "properties": [ + { + "name": "spring.grpc.inprocess.enabled", + "defaultValue": "true" + } + ] +}