From a Spring Boot test I am starting the embedded Tomcat on a random port:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TestApplicationTest {
@Test
void contextLoads() {
}
}
Inside the test scope, I have a configuration file in which I need to get the random port:
@Configuration
public class ConfigurationTest {
@LocalServerPort
int serverPort;
}
When I run the test I get an IllegalStateException:
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'local.server.port' in value "${local.server.port}"
at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:180)
at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:126)
at org.springframework.core.env.AbstractPropertyResolver.doResolvePlaceholders(AbstractPropertyResolver.java:239)
at org.springframework.core.env.AbstractPropertyResolver.resolveRequiredPlaceholders(AbstractPropertyResolver.java:210)
at org.springframework.context.support.PropertySourcesPlaceholderConfigurer.lambda$processProperties$0(PropertySourcesPlaceholderConfigurer.java:175)
at org.springframework.beans.factory.support.AbstractBeanFactory.resolveEmbeddedValue(AbstractBeanFactory.java:936)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1330)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1309)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:656)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:639)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399)
If I use ServletWebServerApplicationContext, or @Value("${server.port}") to get the port, everything is working fine.
Spring Boot version: 2.6.3 With Apache CXF version 3.5.0
Comment From: bclozel
Configuration properties are resolved and bound from the environment before the application starts. To get a random port from the OS, you need to start the application and the server, using the configuration properties you've provided.
This is by design and I'm closing this issue as a result.
Comment From: philwebb
It would be nice if we could get @LocalServerPort to work outside of tests.
@mihaelaDev You can use an event listener to grab the port once the web server has started. Depending on what you're trying to do this might work, but be aware that they even fires after beans have been created.
@EventListener
void onWebInit(WebServerInitializedEvent event) {
int port = event.getWebServer().getPort();
}
Comment From: snicoll
LocalServerPort uses field injection so I don't like the idea of expanding the scope beyond the test class.
Comment From: philwebb
It's an @Value so you can also use it on a constructor.
Comment From: philwebb
We're going to relocate the Local*Port annotations to the spring-boot-test jar to make it clearer that they are only intended for tests.
Comment From: wilkinsona
We have three annotations to move:
org.springframework.boot.actuate.autoconfigure.web.server.LocalManagementPortorg.springframework.boot.rsocket.context.LocalRSocketServerPortorg.springframework.boot.web.server.LocalServerPort
I'm not keen on an actuate package in spring-boot-test. I'm also not sure why LocalRSocketServerPort is in a context package. There's a org.springframework.boot.rsocket.server package and that feels better aligned with the current location of LocalServerPort. With that in mind, how about these new packages in spring-boot-test:
org.springframework.boot.test.web.server.LocalServerPortorg.springframework.boot.test.web.server.LocalManagementPortorg.springframework.boot.test.rsocket.LocalRSocketServerPort
Comment From: mihaelaDev
Maybe it was not clear that my configuration class is not productive code. I need the @LocalServerPort inside a configuration class that is in test scope:
- Inside the productive code: I have an SOAP CXF service implemented.
- Inside the
Test Sources Rootfolder: I have the@SpringBootTestclass and the@Configurationfile.
I want to start a @SpringBootTest with webEnvironment = RANDOM_PORT in which I want to integartion test the SOAP CXF service. In order to be able to do this, along the test, I have a @Configuration file to define the SOAP service client. In order to define the client I need the server port:
@Configuration
public class ConfigurationTest {
@LocalServerPort
private int randomServerPort;
@Bean
CXFSoapServiceMyService testBean() {
final String address = String.format("http://localhost:%d/MyService", randomServerPort);
JaxWsProxyFactoryBean factoryBean = new JaxWsProxyFactoryBean();
factoryBean.setAddress(address);
return factoryBean.create(CXFSoapServiceMyService.class);
}
}
Please find attached a project example with my use case.
Comment From: wilkinsona
@mihaelaDev The annotation can only be used for injection into something that's created after the web server has been started which happens right at the end of application context creation. Typically, this means injection directly into the test class itself. Your ConfigurationTest is created during application context refresh and, crucially, before the web server has been started and its port number is available.
One way to get this to work as you'd like is to defer the creation of your CXFSoapServiceMyService bean by marking it as @Lazy and inject the local server port into the bean method:
@Bean
@Lazy
CXFSoapServiceMyService testBean(@LocalServerPort int randomServerPort) {
final String address = String.format("http://localhost:%d/MyService", randomServerPort);
JaxWsProxyFactoryBean factoryBean = new JaxWsProxyFactoryBean();
factoryBean.setAddress(address);
return factoryBean.create(CXFSoapServiceMyService.class);
}
In your sample at least, this defers the need for local.server.port to have been set until after the web server has been started.
Comment From: mihaelaDev
@mihaelaDev The annotation can only be used for injection into something that's created after the web server has been started which happens right at the end of application context creation. Typically, this means injection directly into the test class itself. Your
ConfigurationTestis created during application context refresh and, crucially, before the web server has been started and its port number is available.One way to get this to work as you'd like is to defer the creation of your
CXFSoapServiceMyServicebean by marking it as@Lazyand inject the local server port into the bean method:
java @Bean @Lazy CXFSoapServiceMyService testBean(@LocalServerPort int randomServerPort) { final String address = String.format("http://localhost:%d/MyService", randomServerPort); JaxWsProxyFactoryBean factoryBean = new JaxWsProxyFactoryBean(); factoryBean.setAddress(address); return factoryBean.create(CXFSoapServiceMyService.class); }In your sample at least, this defers the need for
local.server.portto have been set until after the web server has been started.
Now it's clear. Thank you @wilkinsona ! I'll use your suggestion. Thank you all for the support!
Comment From: mohamedmiranfazil29
Excellent work