Tomcat's Double Port Mystery: Unraveling the @PostConstruct Port Puzzle in Spring Boot Tests
Scenario: You're writing a Spring Boot application that interacts with a Tomcat server. To ensure proper integration, you need to know the port Tomcat is listening on during testing. You might use @PostConstruct
to retrieve the port after Tomcat is initialized. However, you observe that Tomcat is actually listening on two ports, leaving you confused and wondering why.
The Code:
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyConfig {
private int tomcatPort;
@PostConstruct
public void retrieveTomcatPort(ServletWebServerFactory serverFactory) {
WebServer server = serverFactory.getWebServer();
tomcatPort = server.getPort();
System.out.println("Tomcat port retrieved: " + tomcatPort);
}
@Bean
public ServletWebServerFactory servletContainer() {
return new TomcatServletWebServerFactory();
}
}
The Mystery:
When you run your Spring Boot test, you'll see Tomcat listening on both the default port (usually 8080) and a dynamically assigned port (e.g., 50000). This can be confusing, especially when your application only expects to use a single port for communication.
The Explanation:
The root of this issue lies in the way Spring Boot handles embedded servers during testing.
- Embedded Tomcat: Spring Boot embeds Tomcat (or other servers) to create a self-contained application.
- Default Port: By default, Tomcat tries to bind to port 8080. This is generally the desired behavior for production deployments.
- Dynamic Port Assignment: During tests, Spring Boot uses a
RandomPortServerFactory
to ensure a clean test environment. This factory assigns a random port for each test to prevent conflicts and ensure consistent behavior.
The Confusion:
The @PostConstruct
method runs after Tomcat is initialized. This means that by the time the retrieveTomcatPort
method is called, Tomcat has already started on both ports: 8080 (the default) and the dynamically assigned port. The serverFactory.getWebServer()
call returns the actual Tomcat server instance, which is using the dynamically assigned port.
The Solution:
To address this, you have two main options:
- Ignore the Default Port: Since the default port is not used during testing, you can simply ignore it. Your application should only rely on the dynamically assigned port retrieved via
serverFactory.getWebServer().getPort()
. - Disable Default Port Binding: You can override the default port by setting
server.port
in your application'sapplication.properties
orapplication.yml
. This will prevent Tomcat from attempting to bind to port 8080.
Example with Explicit Port Configuration:
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyConfig {
private int tomcatPort;
@PostConstruct
public void retrieveTomcatPort(ServletWebServerFactory serverFactory) {
WebServer server = serverFactory.getWebServer();
tomcatPort = server.getPort();
System.out.println("Tomcat port retrieved: " + tomcatPort);
}
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.setPort(0); // Disable default port binding
return factory;
}
}
Conclusion:
Understanding the way Spring Boot handles embedded servers during testing is crucial for avoiding port-related confusion. By either ignoring the default port or explicitly disabling its binding, you can ensure a predictable and consistent testing environment for your Spring Boot application.
Resources: