使用 spring boot 開發通用程序

  • tag: spring 學習筆記
  • date: 2018-03

spring 是什麼?spring 核心是應用組件容器,管理組件生命週期,依賴關係,並提倡面向接口編程實現模塊間鬆耦合。
spring boot 是什麼?spring boot 是按特定(約定)方式使用 spring 及相關程序庫以簡化應用開發的一套框架和工具。
如下統稱 spring。
本文使用 spring boot 2.0.0.RELEASE 測試。java

ApplicationRunner

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
  • spring 容器生命週期即應用生命週期,spring boot 默認註冊了 spring 容器 shutdown hook,jvm 退出時會自動關閉 spring 容器。
    固然也能夠手動關閉 spring 容器,這時會自動移除註冊的 shutdown hook。

程序退出碼

程序退出時一般返回非 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

程序異常映射爲退出碼

主線程發生異常時還能夠自定義設置退出碼:編程

  • 配置 ExitCodeExceptionMapper 可將主線程產生的異常映射爲退出碼。
  • 此時還會調用 "getExitCodeFromExitCodeGeneratorException()" 檢查異常自己是否爲 ExitCodeGenerator。
    但 SpringApplication.exit() 沒有這個邏輯,爲保持一致性,不建議使用此類異常(?)。
  • 定義好異常和退出碼規範,可方便實現自動化檢查控制。

將異常映射爲退出碼,示例代碼以下:tomcat

public class AppExitCodeExceptionMapper implements ExitCodeExceptionMapper {

    @Override
    public int getExitCode(Throwable exception) {
        return 2;
    }

}
  • 上述簡單示例將全部 Throwable 映射爲退出碼 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 執行了這個邏輯。

多線程應用中工做線程發生異常,能否設置進程退出碼呢?

多線程應用結構

先來看看多線程應用結構:

  • 多線程應用默認最後一個非 deamon 線程結束後退出進程。
  • 能夠顯式控制應用生命週期,顯式執行退出,這樣就不用關心是否 daemon 線程,簡化開發。
  • 退出應用時顯式關閉 Spring 容器,線程池也由 spring 容器管理,此時便可退出全部線程。非 daemon 線程能夠更優雅的結束,由於 jvm 會等待其結束。
  • 應用退出前須要保持至少一個非 daemon 線程,主線程便可做爲這個線程,實現應用主控邏輯,主函數結束即退出應用。
  • 桌面應用、後臺服務(如 web 服務器)等須要顯式等待應用退出。顯式等待應放在主函數主控邏輯以後。即全部 ApplicationRunner 以後,避免阻塞其餘 ApplicationRunner。
  • 爲簡單一致性,將顯式退出(關閉容器)操做放在主函數等待結束後。容器在主線程中建立,在主線程中銷燬,邏輯更加清晰和一致。
  • Ctrl-C 或 kill 等顯式退出進程時,shutdown hook 會關閉容器,但不會等待非 deamon 線程(如主線程)。(會喚醒 sleep ?但不會影響 CountDownLatch.await() ?)

示例程序移動部分邏輯到工做線程,代碼以下:

相關文章
相關標籤/搜索