Restore RANDOM_PORT handing in tests with a separate management port

This commit fixes a regression where RANDOM_PORT was no longer honored
if a defined management port is set. Due to the modularization efforts,
the code has moved from an EnvironmentPostProcessor to an
ApplicationListener. Unfortunately, the listener is registered too
late to handle the event it is listening to. While the event type could
have been changed, the listener was added on the ApplicationContext
which are not honored before the ApplicationContext is in a state to be
used.

The contract of ContextCustomizerFactory is already giving us everything
we need. While the environment is post-processed later than we would
like, it is still post-processed before the refresh state so that the
additional property is honored.

This commit also adds an integration test to cover this scenario.

Closes gh-48653
This commit is contained in:
Stéphane Nicoll
2025-12-31 17:10:39 +01:00
parent 47c24fcb93
commit 1f4a8dff98
4 changed files with 101 additions and 59 deletions
@@ -20,10 +20,8 @@ import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.env.ConfigurableEnvironment;
@@ -33,6 +31,8 @@ import org.springframework.core.env.PropertyResolver;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.PropertySources;
import org.springframework.lang.Contract;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.support.TestPropertySourceUtils;
import org.springframework.util.ClassUtils;
@@ -45,25 +45,13 @@ import org.springframework.util.ClassUtils;
* @author Andy Wilkinson
* @author Phillip Webb
*/
class SpringBootTestRandomPortApplicationListener implements SmartApplicationListener, Ordered {
class SpringBootTestRandomPortContextCustomizer implements ContextCustomizer {
private static final String TEST_SOURCE_NAME = TestPropertySourceUtils.INLINED_PROPERTIES_PROPERTY_SOURCE_NAME;
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType);
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent environmentPreparedEvent) {
postProcessEnvironment(environmentPreparedEvent.getEnvironment());
}
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
postProcessEnvironment(context.getEnvironment());
}
void postProcessEnvironment(ConfigurableEnvironment environment) {
@@ -92,6 +80,16 @@ class SpringBootTestRandomPortApplicationListener implements SmartApplicationLis
return (!managementPort.equals(serverPort)) ? "0" : "";
}
@Override
public boolean equals(Object obj) {
return (obj != null) && (obj.getClass() == getClass());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
private enum Port {
SERVER("server.port"), MANAGEMENT("management.server.port");
@@ -20,15 +20,13 @@ import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.MergedContextConfiguration;
/**
* {@link ContextCustomizerFactory} apply
* {@link SpringBootTestRandomPortApplicationListener} to tests.
* {@link ContextCustomizerFactory} implementation to apply
* {@link SpringBootTestRandomPortContextCustomizer} to tests.
*
* @author Phillip Webb
*/
@@ -37,26 +35,7 @@ class SpringBootTestRandomPortContextCustomizerFactory implements ContextCustomi
@Override
public @Nullable ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
return new Customizer();
}
static class Customizer implements ContextCustomizer {
@Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
context.addApplicationListener(new SpringBootTestRandomPortApplicationListener());
}
@Override
public boolean equals(Object obj) {
return (obj != null) && (obj.getClass() == getClass());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
return new SpringBootTestRandomPortContextCustomizer();
}
}
@@ -34,14 +34,14 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link SpringBootTestRandomPortApplicationListener}.
* Tests for {@link SpringBootTestRandomPortContextCustomizer}.
*
* @author Madhura Bhave
* @author Andy Wilkinson
*/
class SpringBootTestRandomPortApplicationListenerTests {
class SpringBootTestRandomPortContextCustomizerTests {
private final SpringBootTestRandomPortApplicationListener listener = new SpringBootTestRandomPortApplicationListener();
private final SpringBootTestRandomPortContextCustomizer customizer = new SpringBootTestRandomPortContextCustomizer();
private MockEnvironment environment;
@@ -56,7 +56,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
@Test
void postProcessWhenServerAndManagementPortIsZeroInTestPropertySource() {
addTestPropertySource("0", "0");
this.listener.postProcessEnvironment(this.environment);
this.customizer.postProcessEnvironment(this.environment);
assertThat(this.environment.getProperty("server.port")).isEqualTo("0");
assertThat(this.environment.getProperty("management.server.port")).isEqualTo("0");
}
@@ -67,7 +67,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
Map<String, Object> source = new HashMap<>();
source.put("management.server.port", "0");
this.propertySources.addLast(new MapPropertySource("other", source));
this.listener.postProcessEnvironment(this.environment);
this.customizer.postProcessEnvironment(this.environment);
assertThat(this.environment.getProperty("server.port")).isEqualTo("0");
assertThat(this.environment.getProperty("management.server.port")).isEqualTo("0");
}
@@ -77,7 +77,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
addTestPropertySource("8080", "8081");
this.environment.setProperty("server.port", "8080");
this.environment.setProperty("management.server.port", "8081");
this.listener.postProcessEnvironment(this.environment);
this.customizer.postProcessEnvironment(this.environment);
assertThat(this.environment.getProperty("server.port")).isEqualTo("8080");
assertThat(this.environment.getProperty("management.server.port")).isEqualTo("8081");
}
@@ -85,7 +85,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
@Test
void postProcessWhenTestServerPortIsZeroAndTestManagementPortIsNotNull() {
addTestPropertySource("0", "8080");
this.listener.postProcessEnvironment(this.environment);
this.customizer.postProcessEnvironment(this.environment);
assertThat(this.environment.getProperty("server.port")).isEqualTo("0");
assertThat(this.environment.getProperty("management.server.port")).isEqualTo("8080");
}
@@ -93,7 +93,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
@Test
void postProcessWhenTestServerPortIsZeroAndManagementPortIsNull() {
addTestPropertySource("0", null);
this.listener.postProcessEnvironment(this.environment);
this.customizer.postProcessEnvironment(this.environment);
assertThat(this.environment.getProperty("server.port")).isEqualTo("0");
assertThat(this.environment.getProperty("management.server.port")).isNull();
}
@@ -106,7 +106,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
other.put("management.server.port", "8081");
MapPropertySource otherSource = new MapPropertySource("other", other);
this.propertySources.addLast(otherSource);
this.listener.postProcessEnvironment(this.environment);
this.customizer.postProcessEnvironment(this.environment);
assertThat(this.environment.getProperty("server.port")).isEqualTo("0");
assertThat(this.environment.getProperty("management.server.port")).isEmpty();
}
@@ -118,7 +118,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
addTestPropertySource("0", null);
this.propertySources
.addLast(new MapPropertySource("other", Collections.singletonMap("management.server.port", "8080")));
this.listener.postProcessEnvironment(this.environment);
this.customizer.postProcessEnvironment(this.environment);
assertThat(this.environment.getProperty("server.port")).isEqualTo("0");
assertThat(this.environment.getProperty("management.server.port")).isEmpty();
}
@@ -128,7 +128,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
addTestPropertySource("0", null);
this.propertySources
.addLast(new MapPropertySource("other", Collections.singletonMap("management.server.port", "8081")));
this.listener.postProcessEnvironment(this.environment);
this.customizer.postProcessEnvironment(this.environment);
assertThat(this.environment.getProperty("server.port")).isEqualTo("0");
assertThat(this.environment.getProperty("management.server.port")).isEqualTo("0");
}
@@ -138,7 +138,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
addTestPropertySource("0", null);
this.propertySources
.addLast(new MapPropertySource("other", Collections.singletonMap("management.server.port", "-1")));
this.listener.postProcessEnvironment(this.environment);
this.customizer.postProcessEnvironment(this.environment);
assertThat(this.environment.getProperty("server.port")).isEqualTo("0");
assertThat(this.environment.getProperty("management.server.port")).isEqualTo("-1");
}
@@ -148,7 +148,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
addTestPropertySource("0", null);
this.propertySources
.addLast(new MapPropertySource("other", Collections.singletonMap("management.server.port", 8081)));
this.listener.postProcessEnvironment(this.environment);
this.customizer.postProcessEnvironment(this.environment);
assertThat(this.environment.getProperty("server.port")).isEqualTo("0");
assertThat(this.environment.getProperty("management.server.port")).isEqualTo("0");
}
@@ -162,7 +162,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
testPropertySource.getSource().put("port", "9090");
this.propertySources
.addLast(new MapPropertySource("other", Collections.singletonMap("management.server.port", "${port}")));
this.listener.postProcessEnvironment(this.environment);
this.customizer.postProcessEnvironment(this.environment);
assertThat(this.environment.getProperty("server.port")).isEqualTo("0");
assertThat(this.environment.getProperty("management.server.port")).isEqualTo("0");
}
@@ -173,7 +173,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
this.propertySources
.addLast(new MapPropertySource("other", Collections.singletonMap("management.server.port", "${port}")));
assertThatExceptionOfType(PlaceholderResolutionException.class)
.isThrownBy(() -> this.listener.postProcessEnvironment(this.environment))
.isThrownBy(() -> this.customizer.postProcessEnvironment(this.environment))
.withMessage("Could not resolve placeholder 'port' in value \"${port}\"");
}
@@ -188,7 +188,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
source.put("server.port", "${port}");
source.put("management.server.port", "9090");
this.propertySources.addLast(new MapPropertySource("other", source));
this.listener.postProcessEnvironment(this.environment);
this.customizer.postProcessEnvironment(this.environment);
assertThat(this.environment.getProperty("server.port")).isEqualTo("0");
assertThat(this.environment.getProperty("management.server.port")).isEqualTo("0");
}
@@ -201,7 +201,7 @@ class SpringBootTestRandomPortApplicationListenerTests {
source.put("management.server.port", "9090");
this.propertySources.addLast(new MapPropertySource("other", source));
assertThatExceptionOfType(PlaceholderResolutionException.class)
.isThrownBy(() -> this.listener.postProcessEnvironment(this.environment))
.isThrownBy(() -> this.customizer.postProcessEnvironment(this.environment))
.withMessage("Could not resolve placeholder 'port' in value \"${port}\"");
}
@@ -0,0 +1,65 @@
/*
* Copyright 2012-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package smoketest.actuator;
import org.junit.jupiter.api.Test;
import org.springframework.boot.resttestclient.TestRestTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalManagementPort;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for separate management and main service ports when a management port
* is set.
*
* @author Stephane Nicoll
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, args = "--management.server.port=1234")
class ManagementRandomPortWithConfiguredPortSampleActuatorApplicationTests {
@LocalServerPort
private int serverPort;
@LocalManagementPort
private int managementPort;
@Test
void randomPortsShouldBeDifferent() {
assertThat(this.serverPort).isPositive();
assertThat(this.managementPort).isPositive();
assertThat(this.serverPort).isNotEqualTo(this.managementPort);
}
@Test
void randomManagementPortShouldBeApplied() {
assertThat(this.managementPort).isNotEqualTo(1234);
}
@Test
void testHealth() {
ResponseEntity<String> entity = new TestRestTemplate().withBasicAuth("user", "password")
.getForEntity("http://localhost:" + this.managementPort + "/actuator/health", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).contains("\"status\":\"UP\"");
}
}