Zero‑Downtime SpringBoot Port Sharing: Design and Implementation
This article explains how to achieve seamless code updates for SpringBoot applications by allowing two processes to share the same port, detailing the underlying servlet container mechanics, the role of DispatcherServlet, and providing a complete Java implementation with step‑by‑step instructions and code samples.
When updating a SpringBoot application on a server, the usual approach of stopping the old process before starting a new one causes port conflicts and temporary downtime. The article introduces a technique that enables two SpringBoot processes to share the same port, allowing near‑zero‑downtime deployments.
The design relies on understanding the embedded servlet container in SpringBoot. It first examines how SpringBoot creates the embedded Tomcat instance (via TomcatServletWebServerFactory ) and how the DispatcherServlet is registered through the ServletContainerInitializer mechanism, specifically using a TomcatStarter that aggregates ServletContextInitializer beans.
Key steps of the solution are:
Detect whether the default port (e.g., 8088) is already in use.
If occupied, start the new SpringBoot instance on an alternative port (e.g., 9090).
After the new instance is up, kill the old process that holds the original port.
Re‑create a Tomcat server bound to the original port, associate the existing DispatcherServlet , and start it, then stop the temporary server.
Below is a minimal Tomcat‑based example that demonstrates creating a Tomcat instance programmatically:
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) {}
}
}The full SpringBoot implementation that performs the zero‑downtime port switch is provided as follows:
@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 is also shown to verify that the service remains available during the switch. Screenshots demonstrate that the old version (v1) continues to serve requests while the new version (v2) is started, and the downtime is reduced to less than one second.
Overall, the article provides a practical, code‑driven method for achieving zero‑downtime deployments in SpringBoot monolithic applications by leveraging Tomcat’s embedded capabilities and Spring’s initialization hooks.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.