Zero‑Downtime Code Updates in Spring Boot by Sharing a Single Port
The article explains how to update a Spring Boot application without stopping the service by letting two processes share the same port, detailing the underlying Tomcat and ServletContainerInitializer mechanisms, the step‑by‑step implementation, and a demo that shows sub‑second downtime during hot swaps.
Design Idea
When updating code on a server, the usual approach is to stop the old process because the new one needs the same port, causing a period of unavailability. The article proposes a technique that allows two Spring Boot processes to share the same port, eliminating downtime.
Understand the principle of Spring Boot's embedded servlet container.
Understand how DispatcherServlet is passed to the servlet container.
Spring Boot relies on the servlet container (e.g., Tomcat). Tomcat provides the class org.apache.catalina.startup.Tomcat; creating a new instance and calling start() launches the server and allows adding servlets and connectors.
public class Main {
public static void main(String[] args) {
try {
Tomcat tomcat = new Tomcat();
tomcat.getConnector();
tomcat.getHost();
Context context = tomcat.addContext("/", null);
tomcat.addServlet("/", "index", new HttpServlet(){
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().append("hello");
}
});
context.addServletMappingDecoded("/", "index");
tomcat.init();
tomcat.start();
} catch (Exception e) {}
}
}Spring Boot obtains a ServletWebServerFactory (e.g., TomcatServletWebServerFactory) from the context and calls getWebServer() to obtain a WebServer with start() and stop() methods.
The DispatcherServlet is not added directly via tomcat.addServlet. Instead, Spring Boot registers an implementation of ServletContainerInitializer (named TomcatStarter) that receives a collection of ServletContextInitializer instances. During container startup, Tomcat invokes these initializers, which include the one that registers DispatcherServlet. The method getWebServer() receives this collection as a parameter.
To retrieve the collection, Spring Boot uses ServletContextInitializerBeans, which implements Collection<ServletContextInitializer>.
Overall process:
Check whether the target port is already in use.
If it is, start the new instance on an alternative port.
After the new instance is ready, terminate the old process.
Re‑create the server instance on the original port and bind the existing DispatcherServlet to it.
This sequence makes the transition almost seamless.
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;
}
}
private 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 context.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
}Test
A simple demo controller is created:
@RestController
@RequestMapping("port/test")
public class TestPortController {
@GetMapping("test")
public String test() {
return "1";
}
}The controller is packaged into a JAR (v1). After confirming it works, the return value is changed to "2" and repackaged as v2. Both JARs are kept.
Run v1 and test the endpoint with a tool (e.g., Cool Request). Then start v2 without stopping v1. The new process binds to the alternative port, the test continues to succeed, and after a brief moment (< 1 s) the old process is killed and the new one takes over the original port, demonstrating near‑zero downtime.
The demonstration confirms that the technique achieves seamless code updates with only a sub‑second interruption.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Architect Handbook
Focused on Java interview questions and practical article sharing, covering algorithms, databases, Spring Boot, microservices, high concurrency, JVM, Docker containers, and ELK-related knowledge. Looking forward to progressing together with you.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
