Why is Tomcat listening on two ports during a Spring Boot test with an @PostConstruct to retrieve the Tomcat port?

3 min read 05-10-2024
Why is Tomcat listening on two ports during a Spring Boot test with an @PostConstruct to retrieve the Tomcat port?


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:

  1. 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().
  2. Disable Default Port Binding: You can override the default port by setting server.port in your application's application.properties or application.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: