spring 是什么?spring 核心是应用组件容器,管理组件生命周期,依赖关系,并提倡面向接口编程实现模块间松耦合。
spring boot 是什么?spring boot 是按特定(约定)方式使用 spring 及相关程序库以简化应用开发的一套框架和工具。
如下统称 spring。
本文使用 spring boot 2.0.0.RELEASE 测试。java
spring 普遍应用于 web 应用开发,使用 spring 开发命令行工具、后台服务等通用程序也很是方便。
开发 web 应用时,web 服务器(如 tomcat)启动后即开始监听请求。
开发命令行工具时,只须要实现一个 ApplicationRunner
,spring 容器启动后即自动执行之。
如开发一个查看文件大小的示例程序 atest.filesize.App
,代码以下:web
public class App implements ApplicationRunner { public static void main(String[] args) { SpringApplication app = new SpringApplication(App.class); app.setBannerMode(Banner.Mode.OFF); app.run(args); } @Override public void run(ApplicationArguments args) throws Exception { List<String> fileList = args.getNonOptionArgs(); Validate.isTrue(!fileList.isEmpty(), "missing file"); Validate.isTrue(fileList.size() == 1, "require only one file, got: %s", fileList); String path = fileList.get(0); File file = new File(path); if (!file.exists()) { throw new FileNotFoundException(path); } long size = file.length(); System.out.println(size); } }
ApplicationArguments
是 spring boot 解析后的命令行参数。args.getSourceArgs()
,或使用 CommandLineRunner
。程序退出时一般返回非 0 退出码表示错误(或非正常结束),方便 shell 脚本等自动化检查控制。
命令行下运行应用并查看退出码:spring
mvn compile dependency:build-classpath -Dmdep.outputFile=target/cp.txt java -cp "target/classes/:$(cat target/cp.txt)" atest.filesize.App ; echo "exit code: ${?}"
可看到主线程抛出异常时,java 进程默认返回非 0 退出码(默认为 1)。
ApplicationRunner 在主线程中执行,异常堆栈以下:shell
java.lang.IllegalStateException: Failed to execute ApplicationRunner at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:784) at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:771) at org.springframework.boot.SpringApplication.run(SpringApplication.java:335) at atest.filesize.App.main(App.java:18) Caused by: java.lang.IllegalArgumentException: missing file at org.apache.commons.lang3.Validate.isTrue(Validate.java:155) at atest.filesize.App.run(App.java:24) at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:781) ... 3 common frames omitted
spring boot 主线程中处理异常时,SpringBootExceptionHandler 默认将本身设置为线程 UncaughtExceptionHandler,
其检查发现已经打印了异常日志,所以再也不打印异常到 stderr。apache
主线程发生异常时还能够自定义设置退出码:编程
SpringApplication.exit()
没有这个逻辑,为保持一致性,不建议使用此类异常(?)。将异常映射为退出码,示例代码以下:tomcat
public class AppExitCodeExceptionMapper implements ExitCodeExceptionMapper { @Override public int getExitCode(Throwable exception) { return 2; } }
分析 spring 相关代码。服务器
查看主线程 handleRunFailure 调用栈:多线程
Thread [main](Suspended) SpringApplication.getExitCodeFromMappedException(ConfigurableApplicationContext, Throwable) line: 881 SpringApplication.getExitCodeFromException(ConfigurableApplicationContext, Throwable) line: 866 SpringApplication.handleExitCode(ConfigurableApplicationContext, Throwable) line: 852 SpringApplication.handleRunFailure(ConfigurableApplicationContext, SpringApplicationRunListeners, Collection<SpringBootExceptionReporter>, Throwable) line: 803 SpringApplication.run(String...) line: 338 App.main(String[]) line: 29
SpringApplication 方法:app
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(); }
ExitCodeGenerators 方法:
public void add(Throwable exception, ExitCodeExceptionMapper mapper) { Assert.notNull(exception, "Exception must not be null"); Assert.notNull(mapper, "Mapper must not be null"); add(new MappedExitCodeGenerator(exception, mapper)); }
spring boot 获取退出码并注册到 SpringBootExceptionHandler,
其将本身设置为线程 UncaughtExceptionHandler,退出码非 0 时调用 System.exit()
退出进程。
代码 SpringBootExceptionHandler.LoggedExceptionHandlerThreadLocal
:
@Override protected SpringBootExceptionHandler initialValue() { SpringBootExceptionHandler handler = new SpringBootExceptionHandler( Thread.currentThread().getUncaughtExceptionHandler()); Thread.currentThread().setUncaughtExceptionHandler(handler); return handler; }
代码 SpringBootExceptionHandler
:
@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); } } }
这里直接调用 System.exit()
过于粗暴,所以 只有主线程 handleRunFailure 执行了这个逻辑。
多线程应用中工做线程发生异常,能否设置进程退出码呢?
先来看看多线程应用结构:
Ctrl-C
或 kill 等显式退出进程时,shutdown hook 会关闭容器,但不会等待非 deamon 线程(如主线程)。(会唤醒 sleep ?但不会影响 CountDownLatch.await() ?)示例程序移动部分逻辑到工做线程,代码以下: