Zero‑Downtime SpringBoot Updates: Share the Same Port Seamlessly

This article explains how to achieve zero‑downtime updates for SpringBoot applications by allowing two processes to share the same port, detailing the underlying Tomcat and ServletContainerInitializer mechanisms, providing a step‑by‑step implementation with code examples, and demonstrating seamless hot‑swap without service interruption.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Zero‑Downtime SpringBoot Updates: Share the Same Port Seamlessly

When updating code on a personal or enterprise server, the usual approach is to stop the existing process because the new process would try to bind to the same port, causing a conflict. This article introduces a technique that allows two SpringBoot processes to truly share the same port, enabling seamless updates.

Design Idea

The solution involves several core concepts:

Understanding the principle of the embedded Servlet container in SpringBoot.

How DispatcherServlet is passed to the Servlet container.

First, the Tomcat container must support embedding. Tomcat provides the class org.apache.catalina.startup.Tomcat. Creating and starting a Tomcat instance is as simple as:

Tomcat tomcat = new Tomcat();
Tomcat.start();

Tomcat also offers methods to add servlets and configure connectors.

In SpringBoot, the appropriate ServletWebServerFactory is obtained based on the embedded container dependency. For Tomcat, the factory class is TomcatServletWebServerFactory. The factory can create a WebServer with start and stop methods.

The DispatcherServlet is not added directly via tomcat.addServlet. Instead, SpringBoot registers a ServletContainerInitializer implementation ( TomcatStarter) that internally holds a collection of ServletContextInitializer instances, one of which adds the DispatcherServlet to Tomcat.

During Tomcat startup, the initializer callback invokes all ServletContextInitializer beans to set up the servlet context. The method getWebServer receives this collection as a parameter.

To retrieve the collection of ServletContextInitializer beans, SpringBoot uses the helper class ServletContextInitializerBeans:

protected static Collection<ServletContextInitializer> getServletContextInitializerBeans(ConfigurableApplicationContext context) {
    return new ServletContextInitializerBeans(context.getBeanFactory());
}

With these pieces in place, the update process follows four steps:

Check if the target port is occupied.

If occupied, start the new application on an alternative port.

After the new instance is ready, stop the old process.

Recreate the container instance and bind the original DispatcherServlet to the original port.

This sequence ensures the service downtime is minimal, often less than one second.

Implementation Code

@SpringBootApplication
@EnableScheduling
public class WebMainApplication {
    public static void main(String[] args) {
        String[] newArgs = args.clone();
        int defaultPort = 8088;
        boolean needChangePort = false;
        if (isPortInUse(defaultPort)) {
            newArgs = new String[args.length + 1];
            System.arraycopy(args, 0, newArgs, 0, args.length);
            newArgs[newArgs.length - 1] = "--server.port=9090";
            needChangePort = true;
        }
        ConfigurableApplicationContext run = SpringApplication.run(WebMainApplication.class, newArgs);
        if (needChangePort) {
            String command = String.format("lsof -i :%d | grep LISTEN | awk '{print $2}' | xargs kill -9", defaultPort);
            try {
                Runtime.getRuntime().exec(new String[]{"sh", "-c", command}).waitFor();
                while (isPortInUse(defaultPort)) {}
                ServletWebServerFactory webServerFactory = getWebServerFactory(run);
                ((TomcatServletWebServerFactory) webServerFactory).setPort(defaultPort);
                WebServer webServer = webServerFactory.getWebServer(invokeSelfInitialize((ServletWebServerApplicationContext) run));
                webServer.start();
                ((ServletWebServerApplicationContext) run).getWebServer().stop();
            } catch (IOException | InterruptedException ignored) {}
        }
    }

    private static ServletContextInitializer invokeSelfInitialize(ServletWebServerApplicationContext context) {
        try {
            Method method = ServletWebServerApplicationContext.class.getDeclaredMethod("getSelfInitializer");
            method.setAccessible(true);
            return (ServletContextInitializer) method.invoke(context);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    private static boolean isPortInUse(int port) {
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            return false;
        } catch (IOException e) {
            return true;
        }
    }

    protected static Collection<ServletContextInitializer> getServletContextInitializerBeans(ConfigurableApplicationContext context) {
        return new ServletContextInitializerBeans(context.getBeanFactory());
    }

    private static ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext context) {
        String[] beanNames = context.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
        return (ServletWebServerFactory) context.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
    }
}

Testing

A simple demo controller is used to verify the behavior:

@RestController
@RequestMapping("port/test")
public class TestPortController {
    @GetMapping("test")
    public String test() {
        return "1";
    }
}

Package the application as a JAR, run version 1, then start version 2 without stopping version 1. Using a request testing tool (e.g., "Cool Request"), the endpoint remains available, and after a brief moment the new version takes over with virtually no downtime.

Thus, by leveraging Tomcat's embedded capabilities and SpringBoot's internal initializers, seamless zero‑downtime updates can be achieved.

SpringBootServlettomcatHotSwapZeroDowntime
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.