Spring Boot 优雅的关闭

本文将介绍几种方式去优雅的关闭 Spring Boot。

本文将介绍几种方式来优雅的关闭 Spring Boot,分别如下:

准备项目

假如我们创建了一个 Spring Boot 项目,该项目的结构如下图:

Spring Boot 优雅的关闭

其中,TerminateBean 类的代码如下:

package com.huangx.springboot.springboot_shutdown_demo1.bean;

import javax.annotation.PreDestroy;

public class TerminateBean {

    @PreDestroy
    public void preDestroy() {
        System.out.println("TerminalBean is destroyed");
    }

}

上面类中,使用 @PreDestroy 标记 preDestroy() 方法,表示当 Spring Boot 关闭前将会调用该方法。

ShutDownConfig 类的代码如下:

package com.huangx.springboot.springboot_shutdown_demo1.config;

import com.huangx.springboot.springboot_shutdown_demo1.bean.TerminateBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShutDownConfig {

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

}

上面代码使用 @Configuration 和 @Bean 注解将 TerminateBean 配置到 Spring Boot,Spring Boot 关闭时才能调用它的 preDestroy() 方

法。

在下面介绍的几种关闭 Spring Boot 的方式中,仅仅只有 SpringbootShutdownDemo1Application 类代码实现不同。上面两个类的代码均是一致的,因此就不会赘述。

利用 Spring Boot 的 actuator

Spring Boot 自带监控功能 Actuator,可以帮助实现对程序内部运行情况监控,比如监控状况、Bean加载情况、环境变量、日志信息、线程信息等。我们将利用 Actuator 实现关闭 Spring Boot,如下:

(1)添加 actuator 的 maven 依赖,如下:

<!-- 添加该依赖,可以实现通过 URL 关闭 Spring Boot -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

(2)默认情况下,actuator 的 shutdown 是禁用的,我们需要打开它。修改 application.properties 或 application.yml 文件,如下:

# 配置远程关闭 SpringBoot
management.endpoint.shutdown.enabled=true
management.endpoints.web.exposure.include=shutdown

(3)SpringbootShutdownDemo1Application 类代码如下:

@SpringBootApplication
@RestController
public class SpringbootShutdownDemo1Application {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootShutdownDemo1Application.class, args);
    }

    @RequestMapping("/")
    public String index() {
        return "Spring Boot Shutdown Demo1";
    }

}

(4)启动 Spring Boot 项目,使用浏览器访问 http://localhost:8080/actuator  将会输出如下信息:

{
    "_links":{
        "self":{
            "href":"http://localhost:8080/actuator",
            "templated":false
        },
        "shutdown":{
            "href":"http://localhost:8080/actuator/shutdown",
            "templated":false
        }
    }
}

我们可以通过调用 http://localhost:8080/actuator/shutdown 去关闭 Spring Boot 应用(注意:该接口需要 POST 提交),如果调用成功,接口返回如下信息:

{
    "message": "Shutting down, bye..."
}

同时,Spring Boot 将会调用 TerminateBean 对象的 preDestroy() 方法,如下:

Spring Boot 优雅的关闭

调用 ConfigurableApplicationContext 的 close 方法

我们直接调用 ConfigurableApplicationContext 对象的 close() 方法。其中,SpringApplication.run() 方法将返回 ConfigurableApplicationContext 对象。我们可以提供一个 Restful 接口 /shutdown 实现通过接口方式关闭 Spring Boot 程序。代码如下:

@SpringBootApplication
@RestController
public class SpringbootShutdownDemo2Application {
    private static ConfigurableApplicationContext content;

    public static void main(String[] args) {
        content = SpringApplication.run(SpringbootShutdownDemo2Application.class, args);
    }

    @RequestMapping("/")
    public String index() {
        return "Spring Boot Shutdown Demo2";
    }

    @RequestMapping("/shutdown")
    public void shutdown() {
        try {
            if(null == content) {
                throw new NullPointerException("not ConfigurableApplicationContext object");
            }
            content.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

启动程序后,调用 http://localhost:8080/shutdown  该接口支持 GET 和 POST。执行效果如下:

Spring Boot 优雅的关闭

注意:TerminateBean 对象的 preDestroy() 方法依然被调用了。

根据 pid 杀死进程

在 Spring Boot 启动的时候将进程号写入一个 pid 文件(如:my.pid),生成的路径你可以任意指定。然后通过 DOS命令或者Shell命令根据 PID Kill 进程即可。这个时候 TerminateBean 对象的 preDestroy() 方法也会调用的。这种方法大家使用的比较普遍。写一个 start.sh/start.bat 脚本用于启动 Spring Boot 程序,然后写一个 shutdown.sh/shutdown.bat 停止脚本将服务停止。实现如下:

package com.huangx.springboot.springboot_shutdown_demo3;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.ApplicationPidFileWriter;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.io.FileInputStream;

@SpringBootApplication
@RestController
public class SpringbootShutdownDemo3Application {

    public static void main(String[] args) {
        SpringApplication application =
                new SpringApplication(SpringbootShutdownDemo3Application.class);
        String tmpDir = System.getProperty("java.io.tmpdir");
        application.addListeners(new ApplicationPidFileWriter(tmpDir + "/shutdown.pid"));
        application.run();
    }

    @RequestMapping("/")
    public String index() {
        return "Spring Boot Shutdown Demo3";
    }


    @RequestMapping("/shutdown")
    public void shutdown() {
        try {
            String pidFilePath = System.getProperty("java.io.tmpdir") + "/shutdown.pid";
            File file = new File(pidFilePath);
            if(!file.exists() || !file.isFile()) {
                throw new IllegalArgumentException("pid 文件不存在或不是一个文件");
            }

            // 获取 PID
            FileInputStream inputStream = new FileInputStream(file);
            byte[] bytes = new byte[inputStream.available()];
            inputStream.read(bytes);
            inputStream.close();
            String pid = new String(bytes);
            System.out.println("PID=" + pid);

            // 根据 pid 结束进程
            boolean flag = cmd("taskkill /pid " + pid + " /F");
            if(flag) {
                file.delete(); // 删除 PID 文件
                System.out.println("kill process success");
            } else {
                System.err.println("kill process fail");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static boolean cmd(String command){
        boolean flag = false;
        try{
            Runtime.getRuntime().exec("cmd.exe /C "+command);
            flag = true;
        }catch(Exception e){
            e.printStackTrace();
        }
        return flag;
    }

}

上面代码,将 pid 保存到临时目录的 shutdown.pid 文件,然后通过调用 /shutdown 接口去读取 pid,使用 Windows 的“taskkill /pid /F”命令强制结束进程。结果如下图:

Spring Boot 优雅的关闭

根据结果得知,该种方法并没有触发TerminateBean 对象的 preDestroy() 方法。

SpringApplication.exit() 方法

直接通过调用 SpringApplication.exit()方法也可以退出程序,同时将生成一个退出码,这个退出码可以传递给所有的 context。这就是一个JVM 的钩子,通过调用这个方法的话会把 TerminateBean 对象的 preDestroy() 方法执行并停止,并且传递给具体的退出码给所有 Context。通过调用 System.exit(exitCode) 可以将这个错误码也传给 JVM。程序执行完后最后会输出:Process finished with exit code 0,给 JVM 一个信号。代码如下:

package com.huangx.springboot.springboot_shutdown_demo4;

import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class SpringbootShutdownDemo4Application {
    private static ConfigurableApplicationContext context;

    public static void main(String[] args) {
        context = SpringApplication.run(SpringbootShutdownDemo4Application.class, args);
    }

    @RequestMapping("/")
    public String index() {
        return "Spring Boot Shutdown Demo4";
    }

    @RequestMapping("/shutdown")
    public void shutdown() {
        try {
            int exitCode = SpringApplication.exit(context, new ExitCodeGenerator(){
                @Override
                public int getExitCode() {
                    return 0;
                }
            });
            System.exit(exitCode);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

启动 Spring Boot 程序,调用 /shutdown 接口,输出结果如下图:

Spring Boot 优雅的关闭

注意:TerminateBean 对象的 preDestroy() 方法依然被调用了

不是每一次努力都有收获,但是,每一次收获都必须努力。
0 不喜欢
说说我的看法 -
全部评论(
没有评论
关于
本网站属于个人的非赢利性网站,转载的文章遵循原作者的版权声明,如果原文没有版权声明,请来信告知:hxstrive@outlook.com
公众号