Zero‑Downtime SpringBoot Updates: Swapping JARs Without Stopping the Service
The article explains how to achieve zero‑downtime code updates for SpringBoot applications by detecting port usage, launching a new instance on an alternate port, swapping the servlet container back to the original port, and terminating the old process, with a complete code example and a live demo showing sub‑second interruption.
When a SpringBoot application needs to be updated on a personal or enterprise server, the usual approach is to stop the existing process because the new process would try to bind the same port, causing a temporary outage.
A common workaround is to start the new version on a different port, update the Nginx proxy, and then stop the old process. This method works but requires manual port changes and scripting, which can be cumbersome.
The desired solution is to let the new SpringBoot process start automatically, detect the port conflict, and handle the swap without any manual intervention.
Design Idea
The implementation relies on several SpringBoot internals:
Understanding how the embedded Servlet container works inside SpringBoot.
How DispatcherServlet is passed to the servlet container.
First, the underlying servlet container (Tomcat) must support embedding. Tomcat provides the class org.apache.catalina.startup.Tomcat which can be instantiated and started directly.
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) {}
}
}SpringBoot creates the appropriate ServletWebServerFactory based on the servlet‑container dependency. For Tomcat the factory class is TomcatServletWebServerFactory. The factory can be obtained with the following helper method:
private static ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext context) {
String[] beanNames = context.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
return context.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}The getWebServer method of the factory returns a WebServer instance that offers start() and stop() operations.
How does DispatcherServlet reach the servlet container? SpringBoot does not call tomcat.addServlet directly. Instead it registers a ServletContainerInitializer implementation (named TomcatStarter) that adds a collection of ServletContextInitializer objects, one of which registers the DispatcherServlet. When Tomcat starts, it invokes the initializer, and SpringBoot subsequently runs all ServletContextInitializer instances.
The collection of initializers can be retrieved via the utility class ServletContextInitializerBeans:
protected static Collection<ServletContextInitializer> getServletContextInitializerBeans(ConfigurableApplicationContext context) {
return new ServletContextInitializerBeans(context.getBeanFactory());
}With these pieces in place, the overall algorithm becomes:
Check whether the default port (e.g., 8088) is already in use.
If it is, start a new SpringBoot instance on an alternative port (e.g., 9090).
Wait until the new instance has started.
Kill the old process that was listening on the default port.
Reconfigure the Tomcat factory to bind the default port again.
Start a new WebServer with the original DispatcherServlet and stop the old web server.
The complete implementation is shown 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<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 application is packaged as v1 and started. Using the IDE plugin "Cool Request" the endpoint returns 1. Without stopping v1, the v2 jar (with the controller returning 2) is launched. While both versions run, repeated requests show that the response switches to 2 after a very short pause (less than one second), confirming that the service experiences near‑zero downtime.
This approach demonstrates how SpringBoot’s embedded servlet infrastructure can be leveraged to perform seamless code updates without taking the service offline.
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 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.
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.
