每个SpringApplication都向JVM注册一个关闭钩子,以确保ApplicationContext在退出时优雅地关闭。所有标准的Spring生命周期回调(比如DisposableBean接口或@PreDestroy注释)都可以使用。
此外,如果bean希望在调用SpringApplication.exit()时返回特定的退出代码,则可以实现org.springframework.boot.ExitCodeGenerator接口。然后可以将此退出代码传递给System.exit(),将其作为状态代码返回,如下例所示:
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class MyApplication {
@Bean
public ExitCodeGenerator exitCodeGenerator() {
return () -> 42;
}
public static void main(String[] args) {
System.exit(SpringApplication.exit(SpringApplication.run(MyApplication.class, args)));
}
}
简言之,该特性是SpringApplication借助ConfigurableApplicationContext#registerShutdownHook API实现的。
当Spring Boot程序执行结束时,ExitCodeGenerator Bean将返回getExitCode()方法实现的退出码,不过这也暗示着一个前提条件,即Spring应用上下文必须是活的(ConfigurableApplicationContext#isActive),说明此时SpringApplication属于正常结束,相反当SpringApplication运行异常时,退出码又是如何影响Spring Boot应用的行为呢?
当SpringApplication#exit(ApplicationContext, ExitCodeGenerator…)方法调用,ExitCodeGenerator Bean的getExitCode()方法会执行,但这个退出码用在何处呢?
Spring Boot应用需要显示地将该退出码传递到System#exit(int)方法中,换言之,Spring Boot框架并没有为开发人员隐式地实现。
public static int exit(ApplicationContext context, ExitCodeGenerator... exitCodeGenerators) {
Assert.notNull(context, "Context must not be null");
int exitCode = 0;
try {
try {
ExitCodeGenerators generators = new ExitCodeGenerators();
Collection<ExitCodeGenerator> beans = context.getBeansOfType(ExitCodeGenerator.class).values();
generators.addAll(exitCodeGenerators);
generators.addAll(beans);
exitCode = generators.getExitCode();
if (exitCode != 0) {
context.publishEvent(new ExitCodeEvent(context, exitCode));
}
}
finally {
close(context);
}
}
catch (Exception ex) {
ex.printStackTrace();
exitCode = (exitCode != 0) ? exitCode : 1;
}
return exitCode;
}
System.exit(int)退出码是一种约定,为非0值表示异常退出。同时ExitCodeGenerator Bean的正常工作依赖于Spring应用上下文必须活动的前提(ConfigurableApplicationContext#isActive()方法返回true),属于Spring Boot正常结束流程。既然如此,通常情况下,其JVM进程退出码就是0。如果ExitCodeGenerator.getExitCode()方法也返回0,那么这样的实现毫无价值。然而返回值为非0时,它有用在哪里呢?
在真实的Spring Boot应用场景中,SpringApplication#exit(ApplicationContext, ExitCodeGenerator…)方法几乎没有被调用的理由,因为该方法最终会显示地关闭当前Spring 应用上下文:
private static void close(ApplicationContext context) {
if (context instanceof ConfigurableApplicationContext) {
ConfigurableApplicationContext closable = (ConfigurableApplicationContext) context;
closable.close();
}
}
一旦ConfigurableApplicationContext#close()方法被调用,即使是Web类型的Spring Boot应用程序也不会阻塞主线程,导致应用直接关闭。如此一来Spring Boot应用程序却成了一闪而过的执行程序,同时退出码的捕获对Java程序而言并不友好。
从编程模型上,Spring Boot框架允许应用在Spring ConfigurableApplicationContext中增加ExitCodeEvent的监听器(ApplicationListener),前提是ExitCodeGenerator Bean返回非0的退出码。
ExitCodeGenerator接口可以通过异常实现。当遇到这样的异常时,Spring Boot将返回由实现的getExitCode()方法提供的退出代码。结合SpringApplication源码分析,该部分内容在异常处理方法handleRunFailure的调用链路中出现:
private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception,
Collection<SpringBootExceptionReporter> exceptionReporters, SpringApplicationRunListeners listeners) {
...
handleExitCode(context, exception);
...
}
private void handleExitCode(ConfigurableApplicationContext context, Throwable exception) {
int exitCode = getExitCodeFromException(context, exception);
if (exitCode != 0) {
if (context != null) {
context.publishEvent(new ExitCodeEvent(context, exitCode));
}
SpringBootExceptionHandler handler = getSpringBootExceptionHandler();
if (handler != null) {
handler.registerExitCode(exitCode);
}
}
}
private int getExitCodeFromException(ConfigurableApplicationContext context, Throwable exception) {
int exitCode = getExitCodeFromMappedException(context, exception);
if (exitCode == 0) {
exitCode = getExitCodeFromExitCodeGeneratorException(exception);
}
return exitCode;
}
private int getExitCodeFromMappedException(ConfigurableApplicationContext context, Throwable exception) {
if (context == null || !context.isActive()) {
return 0;
}
ExitCodeGenerators generators = new ExitCodeGenerators();
Collection<ExitCodeExceptionMapper> beans = context.getBeansOfType(ExitCodeExceptionMapper.class).values();
generators.addAll(exception, beans);
return generators.getExitCode();
}
private int getExitCodeFromExitCodeGeneratorException(Throwable exception) {
if (exception == null) {
return 0;
}
if (exception instanceof ExitCodeGenerator) {
return ((ExitCodeGenerator) exception).getExitCode();
}
return getExitCodeFromExitCodeGeneratorException(exception.getCause());
}
当异常实现ExitCodeGenerator接口时,退出码直接采用getExitCode()方法返回。
在异常处理方法的调用链路中,ExitCodeGenerator异常获取退出码的逻辑在handleRunFailure方法中触发,而handleRunFailure方法仅在SpringApplication#run方法下的异常catch流程中:
public ConfigurableApplicationContext run(String... args) {
...
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
...
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
不难发现SpringApplicationRunListeners.starting()方法并不在try catch执行块之内。当SpringApplication管理的任一SpringApplicationRunListener实例在执行starting()方法异常时,ExitCodeGenerator异常并不会被捕获。SpringApplication内建SpringApplicationRunListener实现EventPublishingRunListener在执行starting()方法时,会广播Spring Boot事件ApplicationStartingEvent,该阶段并不在try catch逻辑中,因此即使异常实现ExitCodeGenerator也毫无效果。因此假设Spring Boot应用需要利用ExitCodeGenerator异常获取退出码,应该避免监听ApplicationStartingEvent事件。
SpringApplication获取退出码可能在更早的阶段实现,即getExitCodeFromMappedException(ConfigurableApplicationContext, Throwable)方法返回,此时Spring应用上下文活跃,并且包含ExitCodeFromMapper Bean的定义时,该方法将集合ExitCodeExceptionMapperBean的getExitCode()方法返回值,同样地,当退出码为非0时,Spring Boot框架才会采纳该退出码。
ExitCodeExceptionMapper接口定义了维护异常与退出码的映射关系:
@FunctionalInterface
public interface ExitCodeExceptionMapper {
int getExitCode(Throwable exception);
}
ExitCodeExceptionMapper的特性同样需要依赖ConfigurableApplicationContext依然活跃的前提,当ConfigurableApplicationContext.refresh()过程执行失败时,ExitCodeExceptionMapper Bean也不复存在。
特别提醒的是由于ExitCodeExceptionMapper Bean和ExitCodeGenerator异常同属于SpringApplication#handleRunFailure方法生命周期,故方法SpringApplicationRunListeners.starting()的执行异常均无法捕获,并且还需要保证Spring ConfigurableApplicationContext正常运行。
无论实现ExitCodeGenerator接口的Throwable实例还是ExitCodeExceptionMapper Bean,异常下的退出码用在何处呢?
在SpringApplication异常结束时,Spring Boot提供两种退出码与异常类型关联的方式,一是让Throwable对象实现ExitCodeGenerator接口,二是ExitCodeExceptionMapper 实现退出码与Throwable的映射。前者不依赖于当前Spring ConfigurableApplicationContext是否活跃,后者则依赖。两者分别在getExitCodeFromExitCodeGeneratorException方法和getExitCodeFromMappedException方法中执行,都在handleExitCode(ConfigurableApplicationContext,Exception)方法内部执行:
private void handleExitCode(ConfigurableApplicationContext context, Throwable exception) {
int exitCode = getExitCodeFromException(context, exception);
if (exitCode != 0) {
if (context != null) {
context.publishEvent(new ExitCodeEvent(context, exitCode));
}
SpringBootExceptionHandler handler = getSpringBootExceptionHandler();
if (handler != null) {
handler.registerExitCode(exitCode);
}
}
}
private int getExitCodeFromException(ConfigurableApplicationContext context, Throwable exception) {
int exitCode = getExitCodeFromMappedException(context, exception);
if (exitCode == 0) {
exitCode = getExitCodeFromExitCodeGeneratorException(exception);
}
return exitCode;
}
结合前面的讨论,当getExitCodeFromException返回非0时,同样依赖ConfigurableApplicationContext 发送ExitCodeEvent事件,与SpringApplication#exit方法不同的是,退出码将存储到SpringBootExceptionHandler对象中,而该对象来源于getSpringBootExceptionHandler()方法:
SpringBootExceptionHandler getSpringBootExceptionHandler() {
if (isMainThread(Thread.currentThread())) {
return SpringBootExceptionHandler.forCurrentThread();
}
return null;
}
private boolean isMainThread(Thread currentThread) {
return ("main".equals(currentThread.getName()) || "restartedMain".equals(currentThread.getName()))
&& "main".equals(currentThread.getThreadGroup().getName());
}
当isMainThread方法认为当前线程为主线程时,调用SpringBootExceptionHandler#forCurrentThread()方法获取SpringBootExceptionHandler实例。值得注意的是,其中存在判断当前线程名称是否为“restartedMain”的逻辑分支,这是因为应用依赖org.springframework.boot:spring-boot-devtools后,当spring-boot-devtools认为应用需要重启时,将启动RestartLauncher线程,该线程的名称为“restartedMain”:
class RestartLauncher extends Thread {
private final String mainClassName;
private final String[] args;
private Throwable error;
RestartLauncher(ClassLoader classLoader, String mainClassName, String[] args,
UncaughtExceptionHandler exceptionHandler) {
this.mainClassName = mainClassName;
this.args = args;
setName("restartedMain");
setUncaughtExceptionHandler(exceptionHandler);
setDaemon(false);
setContextClassLoader(classLoader);
}
}
综上所述,通常情况下isMainThread将返回true,因此getSpringBootExceptionHandler()方法返回SpringBootExceptionHandler.forCurrentThread()执行结果:
static SpringBootExceptionHandler forCurrentThread() {
return handler.get();
}
private static class LoggedExceptionHandlerThreadLocal extends ThreadLocal<SpringBootExceptionHandler> {
@Override
protected SpringBootExceptionHandler initialValue() {
SpringBootExceptionHandler handler = new SpringBootExceptionHandler(
Thread.currentThread().getUncaughtExceptionHandler());
Thread.currentThread().setUncaughtExceptionHandler(handler);
return handler;
}
}
按照ThreadLocal初始化的原理,当应用第一次执行SpringBootExceptionHandler.forCurrentThread()方法时,LoggedExceptionHandlerThreadLocal.initialValue()方法将被调用,返回SpringBootExceptionHandler对象,而SpringBootExceptionHandler又是Thread.UncaughtExceptionHandler的扩展类,当执行线程遇到未捕获的异常时,Thread.UncaughtExceptionHandler.uncaughtException(Thread, Throwable)方法将处理该异常。因此当主线程执行异常时,将被SpringBootExceptionHandler.uncaughtException(Thread,Throwable)方法处理:
@Override
public void uncaughtException(Thread thread, Throwable ex) {
try {
if (isPassedToParent(ex) && this.parent != null) {
this.parent.uncaughtException(thread, ex);
}
}
finally {
this.loggedExceptions.clear();
if (this.exitCode != 0) {
System.exit(this.exitCode);
}
}
}