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() ?)示例程序移動部分邏輯到工做線程,代碼以下: