Spring Boot应用退出

每个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应用的行为呢?

1、Spring Boot应用正常退出

当SpringApplication#exit(ApplicationContext, ExitCodeGenerator…)方法调用,ExitCodeGenerator Bean的getExitCode()方法会执行,但这个退出码用在何处呢?

1.1、ExitCodeGenerator Bean生成退出码

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时,它有用在哪里呢?

1.2、ExitCodeGenerator Bean退出码使用场景

在真实的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程序而言并不友好。

ExitCodeGenerator Bean退出码用于ExitCodeEvent事件监听

从编程模型上,Spring Boot框架允许应用在Spring ConfigurableApplicationContext中增加ExitCodeEvent的监听器(ApplicationListener),前提是ExitCodeGenerator Bean返回非0的退出码。

2、Spring Boot应用异常退出

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()方法返回。

2.1、ExitCodeGenerator异常使用场景

在异常处理方法的调用链路中,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框架才会采纳该退出码。

2.2、ExitCodeExceptionMapper映射异常与退出码

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,异常下的退出码用在何处呢?

2.3、退出码用于SpringApplication异常结束

在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);
			}
		}
	}
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值