Zero‑Downtime SpringBoot Port Sharing: Design, Implementation, and Testing
This article explains how to enable two SpringBoot processes to share the same port for seamless code updates, covering the underlying servlet container mechanics, the required initialization flow, and providing a complete Java implementation with a demo that achieves near‑zero downtime during deployment.
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. A technique exists that allows two SpringBoot processes to truly share a single port, enabling seamless updates.
Simply starting the new version on a different port and switching Nginx routing can avoid downtime, but constantly changing ports and scripts is cumbersome. The goal is to let the new process start automatically, handle the port conflict, and replace the old process without manual intervention.
The solution requires understanding two key points: the principle of SpringBoot's embedded servlet container (e.g., Tomcat) and how DispatcherServlet is handed over to the servlet container.
Tomcat provides a Tomcat class (full name org.apache.catalina.startup.Tomcat ) that can be instantiated and started directly. The following snippet shows a minimal embedded Tomcat setup:
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) {}
}
}In SpringBoot, the appropriate ServletWebServerFactory (e.g., TomcatServletWebServerFactory ) is obtained from the application context, and its getWebServer() method returns a WebServer with start() and stop() operations.
The DispatcherServlet is not added to Tomcat via a simple tomcat.addServlet call. Instead, SpringBoot registers a ServletContainerInitializer implementation ( TomcatStarter ) that receives a collection of ServletContextInitializer beans. During Tomcat startup, this initializer invokes each ServletContextInitializer , which includes the registration of the DispatcherServlet .
Retrieving the collection of ServletContextInitializer beans is straightforward: the ServletContextInitializerBeans class implements Collection and can be obtained from the application context.
The overall workflow is:
Check whether the target port is already in use.
If occupied, start the new SpringBoot instance on an alternative port.
After the new instance is ready, terminate the old process.
Recreate the server instance on the original port and bind the original DispatcherServlet to it.
This sequence makes the transition almost instantaneous, achieving zero‑downtime updates.
The complete implementation is provided below:
@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
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);
}
}A simple test controller demonstrates the approach:
@RestController()
@RequestMapping("port/test")
public class TestPortController {
@GetMapping("test")
public String test() {
return "1";
}
}Package the application as a JAR (v1), run it, then build a new version (v2) with a different return value, and start the v2 JAR without stopping v1. Using a tool like Cool Request to hit the endpoint shows that the service remains available, and after a brief moment the response switches from "1" to "2", confirming near‑zero downtime.
Finally, the article invites readers to join a backend‑focused technical community for further discussion and knowledge sharing.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.