Spring Boot 优雅地结束程序

作者 | 2020年4月1日

1、前言

对于生产环境的系统而言,管理Spring Boot应用程序的生命周期非常重要。Spring容器借助ApplicationContext处理所有Bean的创建、初始化和销毁。

本文的重点放在整个生命周期的销毁阶段,更具体地说,我们将研究关闭Spring Boot应用程序的不同方法。

2、Shutdown接口

默认情况下除了/shutdown之外的所有接口在Spring Boot应用程序中都会启用。/shutdown是Actuator提供的接口。

下面是用于启用该接口的Maven依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

另外,如果我们需要启用Spring Security,我们还需要依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

最后,我们需要在application.properties文件中启用/shutdown接口:

management.endpoints.web.exposure.include=*
management.endpoint.shutdown.enabled=true
endpoints.shutdown.enabled=true

注意,我们还要导出我们希望使用的接口,在上面的代码中我们使用通配符*导出了Actuator提供的全部接口,其中也包括/shutdown接口。

现在,要关闭Spring Boot应用程序,我们只需调用一个POST方法,如下所示:

curl -X POST http://localhost:port/actuator/shutdown

3、关闭ApplicationContext

我们还可以直接调用ApplicationContext(应用程序上下文)上面的close()方法。

让我们从一个创建应用程序上下文然后关闭它的例子开始:

ConfigurableApplicationContext ctx = new
  SpringApplicationBuilder(Application.class).web(WebApplicationType.NONE).run();
System.out.println("Spring Boot application started");
ctx.getBean(TerminateBean.class);
ctx.close();

这将销毁所有的bean、释放锁、然后关闭bean工厂。为了确定应用程序是否关闭,我们使用Spring标准生命周期回调注解@PreDestroy来验证:

public class TerminateBean {

    @PreDestroy
    public void onDestroy() throws Exception {
        System.out.println("Spring Container is destroyed!");
    }
}

我们还必须添加该类型的bean:

@Configuration

public class ShutdownConfig {

    @Bean
    public TerminateBean getTerminateBean() {
        return new TerminateBean();
    }
}

这是运行该例子后的输出:

Spring Boot application started
Closing AnnotationConfigApplicationContext@39b43d60
DefaultLifecycleProcessor - Stopping beans in phase 0
Unregistering JMX-exposed beans on shutdown
Spring Container is destroyed!

在这里需要记住一点:在关闭应用程序上下文时,它的父上下文由于具有单独的生命周期,因此不会受到影响。

3.1、关闭当前ApplicationContext

在上面的例子中,我们创建了一个子应用程序上下文,然后使用close()方法销毁了它。

如果我们希望关闭当前的应用程序上下文,一种简单的解决方案是调用Actuator提供的/shutdown接口。

但是,我们也可以创建一个自定义的接口:

@RestController
public class ShutdownController implements ApplicationContextAware {

    private ApplicationContext context;

    @PostMapping("/shutdownContext")
    public void shutdownContext() {
        ((ConfigurableApplicationContext) context).close();
    }

    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        this.context = ctx;

    }
}

在这里,我们添加了一个控制器,该控制器实现了ApplicationContextAware接口并覆写setter方法以获得当前的应用程序上下文。然后在处理请求时会调用close()方法。

然后,我们可以调用新的接口来关闭当前的应用程序上下文:

curl -X POST http://localhost:port/shutdownContext

注意,如果你在真实的程序中添加这样的接口,你需要给它加上一些保护措施。

4、退出SpringApplication

SpringApplication向JVM注册一个Shutdown Hook,以确保应用程序正确退出。

Bean可以通过实现ExitCodeGenerator接口来返回特定的错误代码:

ConfigurableApplicationContext ctx = new SpringApplicationBuilder(Application.class)
  .web(WebApplicationType.NONE).run();

int exitCode = SpringApplication.exit(ctx, new ExitCodeGenerator() {
@Override
public int getExitCode() {
        // return the error code
        return 0;
    }
});

System.exit(exitCode);

在Java8中可以使用lambda表达式:

SpringApplication.exit(ctx, () -> 0);

调用System.exit(exitCode)后,程序的退出代码是0:

Process finished with exit code 0

5、通过PID结束进程

最后,我们还可以使用bash脚本从程序外面关闭Spring Boot应用程序。此方法的第一步是让Spring Boot应用将其PID写入文件中:

SpringApplicationBuilder app = new SpringApplicationBuilder(Application.class)
  .web(WebApplicationType.NONE);
app.build().addListeners(new ApplicationPidFileWriter("./bin/shutdown.pid"));
app.run();

接下来,创建一个shutdown.sh文件:

kill $(cat ./bin/shutdown.pid)

shutdown.sh将从shutdown.pid文件中提取进程ID,然后调用kill命令杀掉应用。

6、结论

在本文中,我们介绍了几种可用于关闭正在运行的Spring Boot应用程序的方法。

应该由开发人员从中选择一个合适的方法,这些方法应该根据使用场景与目的挑选出合适的。

例如,当我们需要把程序退出代码传递到另一个环境(例如JVM)以进行进一步的操作时,首选.exit()。使用PID则更具灵活性,因为我们可以使用bash脚本来启动或重启应用程序。

最后,如果希望通过HTTP从外部关闭应用程序可以使用/shutdown接口。对于其它的情况可以使用.close()

发表评论

电子邮件地址不会被公开。 必填项已用*标注