如何配置嵌入式Tomcat/Spring在启动失败时快速终止?
我使用嵌入式Tomcat Server,当配置错误的server.address(比如未分配给任何网卡的IP或123456这类明显无效值)时,希望嵌入式服务器及应用能快速失败,但目前服务器最终失败前会有长达7秒的不可接受延迟:
myprod 3.18.2 2024-01-12T15:27:30.889000Z INFO main:StartupInfoLogger:55 Starting CoreApplication v3.18.2 using Java 17.0.9 on e03182f0fab3 with PID 14 (/usr/local/lib/myprod/myprod-3.18.2 started by myprod in /) myprod 3.18.2 2024-01-12T15:27:30.894000Z DEBUG main:StartupInfoLogger:56 Running with Spring Boot v2.7.1, Spring v5.3.21 ... myprod 3.18.2 2024-01-12T15:27:32.896000Z INFO main:TomcatWebServer:108 Tomcat initialized with port(s): 9000 (http) myprod 3.18.2 2024-01-12T15:27:32.993000Z DEBUG main:ServletWebServerApplicationContext:286 Published root WebApplicationContext as ServletContext attribute with name [org.springframework.web.context.WebApplicationContext.ROOT] myprod 3.18.2 2024-01-12T15:27:32.993000Z INFO main:ServletWebServerApplicationContext:292 Root WebApplicationContext: initialization completed in 2047 ms myprod 3.18.2 2024-01-12T15:27:33.175000Z INFO main:LogHelper:31 HHH000204: Processing PersistenceUnitInfo [name: default] ... myprod 3.18.2 2024-01-12T15:27:37.828000Z WARN main:AbstractApplicationContext:591 Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat server
即便开启了spring.main.lazy-initialization=true,不开启的话失败延迟还会多2-3秒。请问是否有办法配置Spring/Tomcat实现更快失败?
更新
我通过设置懒加载JPA启动模式spring.data.jpa.repositories.bootstrap-mode=lazy,缩短了应用启动到嵌入式服务器尝试启动的时间:
myprod 3.18.2 2024-01-12T22:59:11.820000Z INFO main:StartupInfoLogger:55 Starting CoreApplication v3.18.2 using Java 17.0.9 on 0554100ff86a with PID 13 (/usr/local/lib/myprod/myprod-3.18.2 started by myprod in /) myprod 3.18.2 2024-01-12T22:59:11.825000Z DEBUG main:StartupInfoLogger:56 Running with Spring Boot v2.7.1, Spring v5.3.21 ... myprod 3.18.2 2024-01-12T22:59:13.835000Z INFO main:TomcatWebServer:108 Tomcat initialized with port(s): 9000 (http) ... myprod 3.18.2 2024-01-12T22:59:16.084000Z WARN main:AbstractApplicationContext:591 Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat server ... myprod 3.18.2 2024-01-12T22:59:17.574000Z WARN task-1:JdbcEnvironmentInitiator:133 HHH000342: Could not obtain connection to query metadata org.postgresql.util.PSQLException: Connection to localhost:5432 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections. ... myprod 3.18.2 2024-01-12T22:59:18.437000Z ERROR main:SpringApplication:824 Application run failed org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop'; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat server
现在嵌入式服务器抛出WebServerException的时间仅略超4秒,该优化效果可重复验证,同时持久层的数据库连接失败日志也确认数据库连接尝试现在是在服务器启动监听之后进行的。但问题是,即便抛出了WebServerException,应用仍会继续运行约2.5秒才最终停止,请问有没有办法让应用更快退出?
解决方案
1. 前置校验server.address有效性
在Spring Boot上下文初始化前,直接校验配置的IP是否合法且绑定在本地网卡上,无效则立即终止应用:
实现ApplicationListener<ApplicationEnvironmentPreparedEvent>做前置校验:
import org.springframework.context.ApplicationListener; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import java.net.InetAddress; import java.net.NetworkInterface; import java.util.Enumeration; public class ServerAddressValidator implements ApplicationListener<ApplicationEnvironmentPreparedEvent> { @Override public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { String serverAddress = event.getEnvironment().getProperty("server.address"); if (serverAddress == null) { return; } try { InetAddress targetAddr = InetAddress.getByName(serverAddress); boolean isLocalAddr = false; Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces(); while (networkInterfaces.hasMoreElements()) { NetworkInterface ni = networkInterfaces.nextElement(); Enumeration<InetAddress> localAddrs = ni.getInetAddresses(); while (localAddrs.hasMoreElements()) { if (localAddrs.nextElement().equals(targetAddr)) { isLocalAddr = true; break; } } if (isLocalAddr) break; } if (!isLocalAddr) { System.err.println("错误:配置的server.address[" + serverAddress + "]未绑定到本地网卡"); System.exit(1); } } catch (Exception e) { System.err.println("错误:server.address格式无效,值为[" + serverAddress + "]"); System.exit(1); } } }
在src/main/resources/META-INF/spring.factories中注册监听器:
org.springframework.context.ApplicationListener=com.yourpackage.ServerAddressValidator
2. 自定义启动失败时的快速退出逻辑
通过SpringApplicationRunListener监听启动失败事件,当检测到Tomcat启动失败时直接强制退出:
import org.springframework.boot.SpringApplicationRunListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.boot.SpringApplication; public class FastExitRunListener implements SpringApplicationRunListener { public FastExitRunListener(SpringApplication application, String[] args) {} @Override public void failed(ConfigurableApplicationContext context, Throwable exception) { // 判断是否为Tomcat启动失败的异常链 Throwable cause = exception; while (cause != null) { if (cause instanceof org.springframework.boot.web.server.WebServerException) { System.err.println("Tomcat启动失败,立即退出应用"); System.exit(1); } cause = cause.getCause(); } } // 其他接口方法留空实现即可 @Override public void starting() {} @Override public void environmentPrepared(org.springframework.core.env.ConfigurableEnvironment environment) {} @Override public void contextPrepared(ConfigurableApplicationContext context) {} @Override public void contextLoaded(ConfigurableApplicationContext context) {} @Override public void started(ConfigurableApplicationContext context) {} @Override public void running(ConfigurableApplicationContext context) {} }
同样在spring.factories中注册:
org.springframework.boot.SpringApplicationRunListener=com.yourpackage.FastExitRunListener
3. 缩短Tomcat绑定超时时间
自定义Tomcat配置,减少IP绑定阶段的等待时长:
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.stereotype.Component; @Component public class TomcatTimeoutCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> { @Override public void customize(TomcatServletWebServerFactory factory) { factory.addConnectorCustomizers(connector -> { // 设置连接超时为1秒 connector.setConnectionTimeout(1000); // 针对NIO协议设置SO超时 if (connector.getProtocolHandler() instanceof org.apache.coyote.http11.Http11NioProtocol) { ((org.apache.coyote.http11.Http11NioProtocol) connector.getProtocolHandler()).setSoTimeout(1000); } }); } }
内容的提问来源于stack exchange,提问作者IvanR

