Merge branch '4.1.x'

Closes gh-51291
This commit is contained in:
Stéphane Nicoll
2026-08-07 11:18:17 +02:00
8 changed files with 64 additions and 94 deletions
@@ -21,11 +21,14 @@ import java.util.Map;
import io.grpc.Server;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.env.ConfigTreePropertySource.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
@@ -36,34 +39,41 @@ import org.springframework.grpc.server.InProcessGrpcServerFactory;
import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent;
/**
* {@link ApplicationContextInitializer} that sets {@link Environment} properties for the
* ports that {@link Server gRPC servers} are actually listening on. The property
* {@literal "local.grpc.server.port"} can be injected directly into tests using
* {@link Value @Value} or obtained through the {@link Environment}.
* {@link EnableAutoConfiguration Auto-configuration} for an {@link ApplicationListener}
* that sets {@link Environment} properties for the port that a {@link Server gRPC server}
* is actually listening on. The property {@value PROPERTY_NAME} can be injected directly
* into tests using {@link Value @Value} or obtained through the {@link Environment}.
* <p>
* Properties are automatically propagated up to any parent context.
*
* @author Dave Syer
* @author Chris Bono
* @author Phillip Webb
* @author Stephane Nicoll
* @since 4.1.1
*/
class GrpcPortInfoApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@AutoConfiguration(beforeName = "org.springframework.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration")
@ConditionalOnClass(GrpcServerStartedEvent.class)
public final class GrpcServerPortInfoAutoConfiguration {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.addApplicationListener(new Listener(applicationContext));
/**
* Property that contains the port that a {@link Server gRPC server} is actually
* listening on.
*/
public static final String PROPERTY_NAME = "local.grpc.server.port";
@Bean
GrpcPortInfoApplicationListener grpcPortInfoApplicationListener(ConfigurableApplicationContext applicationContext) {
return new GrpcPortInfoApplicationListener(applicationContext);
}
private static class Listener implements ApplicationListener<GrpcServerStartedEvent> {
private static final String PROPERTY_NAME = "local.grpc.server.port";
static class GrpcPortInfoApplicationListener implements ApplicationListener<GrpcServerStartedEvent> {
private static final String PROPERTY_SOURCE_NAME = "server.ports";
private final ConfigurableApplicationContext applicationContext;
Listener(ConfigurableApplicationContext applicationContext) {
GrpcPortInfoApplicationListener(ConfigurableApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@@ -1,3 +0,0 @@
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.grpc.test.autoconfigure.GrpcPortInfoApplicationContextInitializer
@@ -0,0 +1 @@
org.springframework.boot.grpc.test.autoconfigure.GrpcServerPortInfoAutoConfiguration
@@ -20,10 +20,10 @@ import io.grpc.Server;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.grpc.test.autoconfigure.GrpcServerPortInfoAutoConfiguration.GrpcPortInfoApplicationListener;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.grpc.server.GrpcServerFactory;
import org.springframework.grpc.server.InProcessGrpcServerFactory;
import org.springframework.grpc.server.NettyGrpcServerFactory;
@@ -35,22 +35,36 @@ import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GrpcPortInfoApplicationContextInitializer}.
* Tests for {@link GrpcServerPortInfoAutoConfiguration}.
*
* @author Phillip Webb
*/
class GrpcPortInfoApplicationContextInitializerTests {
class GrpcServerPortInfoAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GrpcServerPortInfoAutoConfiguration.class));
private static final String PORT_PROPERTY = "local.grpc.server.port";
@Test
void whenServerHasAddressInitializerSetsPortProperty() {
void createsGrpcPortInfoApplicationListenerBean() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(GrpcPortInfoApplicationListener.class));
}
@Test
void whenNoGrpcServerStartedEventClassDoesNotCreateBean() {
this.contextRunner.withClassLoader(new FilteredClassLoader(GrpcServerStartedEvent.class))
.run((context) -> assertThat(context).doesNotHaveBean(GrpcPortInfoApplicationListener.class));
}
@Test
void whenServerHasAddressListenerSetsPortProperty() {
NettyGrpcServerFactory factory = mock();
testListener(factory, 65535, "65535");
}
@Test
void whenServerHasNoAddressInitializerSetsNoPortProperty() {
void whenServerHasNoAddressListenerSetsNoPortProperty() {
NettyGrpcServerFactory factory = mock();
testListener(factory, -1, null);
}
@@ -68,25 +82,14 @@ class GrpcPortInfoApplicationContextInitializerTests {
}
private void testListener(GrpcServerFactory factory, int port, @Nullable String expected) {
try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class)) {
context.getBean(GrpcPortInfoApplicationContextInitializer.class).initialize(context);
this.contextRunner.run((context) -> {
GrpcServerLifecycle lifecycle = mock();
Server server = mock();
given(lifecycle.getFactory()).willReturn(factory);
GrpcServerStartedEvent event = new GrpcServerStartedEvent(lifecycle, server, "localhost", port);
context.publishEvent(event);
assertThat(context.getEnvironment().getProperty(PORT_PROPERTY)).isEqualTo(expected);
}
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
GrpcPortInfoApplicationContextInitializer grpcPortInfoApplicationContextInitializer() {
return new GrpcPortInfoApplicationContextInitializer();
}
});
}
}
@@ -28,7 +28,7 @@ dependencies {
}
implementation("io.grpc:grpc-netty-shaded")
dockerTestImplementation(project(":starter:spring-boot-starter-test"))
dockerTestImplementation(project(":starter:spring-boot-starter-grpc-server-test"))
dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter")
}
@@ -21,13 +21,9 @@ import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.startupcheck.IndefiniteWaitOneShotStartupCheckStrategy;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import smoketest.grpcservernettyshaded.SampleGrpcServerNettyShadedApplicationTests.GrpcServerStartedEventListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Import;
import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent;
import static org.assertj.core.api.Assertions.assertThat;
@@ -38,18 +34,16 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
@SpringBootTest(properties = "spring.grpc.server.port=0")
@Testcontainers(disabledWithoutDocker = true)
@Import(GrpcServerStartedEventListener.class)
class SampleGrpcServerNettyShadedApplicationTests {
@Autowired
private GrpcServerStartedEventListener startedEventListener;
@Value("${local.grpc.server.port}")
private int port;
@Test
@SuppressWarnings("resource")
void test() {
int port = this.startedEventListener.getPort();
String address = "host.testcontainers.internal:" + port;
org.testcontainers.Testcontainers.exposeHostPorts(port);
String address = "host.testcontainers.internal:" + this.port;
org.testcontainers.Testcontainers.exposeHostPorts(this.port);
try (GenericContainer<?> container = new GenericContainer<>(
DockerImageName.parse("fullstorydev/grpcurl:v1.9.3"))
.withCommand("-d", "{\"name\": \"spring\"}", "--plaintext", address, "HelloWorld/SayHello")
@@ -60,19 +54,4 @@ class SampleGrpcServerNettyShadedApplicationTests {
}
static class GrpcServerStartedEventListener implements ApplicationListener<GrpcServerStartedEvent> {
private int port;
@Override
public void onApplicationEvent(GrpcServerStartedEvent event) {
this.port = event.getPort();
}
int getPort() {
return this.port;
}
}
}
@@ -25,7 +25,7 @@ description = "Spring Boot gRPC server smoke test"
dependencies {
implementation(project(":starter:spring-boot-starter-grpc-server"))
dockerTestImplementation(project(":starter:spring-boot-starter-test"))
dockerTestImplementation(project(":starter:spring-boot-starter-grpc-server-test"))
dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter")
}
@@ -21,35 +21,30 @@ import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.startupcheck.IndefiniteWaitOneShotStartupCheckStrategy;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import smoketest.grpcserver.SampleGrpcServerApplicationTests.GrpcServerStartedEventListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Import;
import org.springframework.grpc.server.lifecycle.GrpcServerStartedEvent;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for the default Spring gRPC netty server.
* Integration tests for the default Spring gRPC Netty server.
*
* @author Phillip Webb
* @author Stephane Nicoll
*/
@SpringBootTest(properties = "spring.grpc.server.port=0")
@Testcontainers(disabledWithoutDocker = true)
@Import(GrpcServerStartedEventListener.class)
class SampleGrpcServerApplicationTests {
@Autowired
private GrpcServerStartedEventListener startedEventListener;
@Value("${local.grpc.server.port}")
private int port;
@Test
@SuppressWarnings("resource")
void test() {
int port = this.startedEventListener.getPort();
String address = "host.testcontainers.internal:" + port;
org.testcontainers.Testcontainers.exposeHostPorts(port);
String address = "host.testcontainers.internal:" + this.port;
org.testcontainers.Testcontainers.exposeHostPorts(this.port);
try (GenericContainer<?> container = new GenericContainer<>(
DockerImageName.parse("fullstorydev/grpcurl:v1.9.3"))
.withNetworkAliases("")
@@ -61,19 +56,4 @@ class SampleGrpcServerApplicationTests {
}
static class GrpcServerStartedEventListener implements ApplicationListener<GrpcServerStartedEvent> {
private int port;
@Override
public void onApplicationEvent(GrpcServerStartedEvent event) {
this.port = event.getPort();
}
int getPort() {
return this.port;
}
}
}