公司的充电桩项目交给我们两个实习生负责
从头开发 Bug不断 最后勉强上线
还是需要隔几天修个Bug提交一下
用户和充电桩通信启动时间比较久可能需要三十几秒如果在这中间停止项目用户收不到反馈会卡死
所以需要项目关闭时 1.拒绝新请求 2.处理已经接受的请求 3.请求超时强制关闭

定制 Tomcat Connector 行为

要平滑关闭 Spring Boot 应用的前提就是首先要关闭其内置的 Web 容器,不再处理外部新进入的请求。为了能让应用接受关闭事件通知的时候,保证当前 Tomcat 处理所有已经进入的请求,我们需要实现 TomcatConnectorCustomizer 接口,这个接口的源码十分简单,从注释可以看出这是实现自定义 Tomcat Connector 行为的回调接口:

Connector 属于 Tomcat 抽象组件,功能就是用来接受外部请求,以及内部传递,并返回响应内容,是Tomcat 中请求处理和响应的重要组件,具体实现有 HTTP Connector 和 AJP Connector。

通过定制 Connector 的行为,我们就可以允许在请求处理完毕后进行 Tomcat 线程池的关闭,具体实现代码如下(设置最长等待响应时间TIMEOUT,超时强制退出):

package com.yaojack.portal.controller;

import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Connector;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * (类功能描述)
 * 项目停止
 * @author YuJJ
 * @module
 * @date: 2020/5/11 16:38
 */
@Slf4j
public class CustomShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
        //设置用户请求最长等待时间
        private static final int TIMEOUT=10;
        private volatile Connector connector;
        @Override
        public void onApplicationEvent(ContextClosedEvent event) {
        //暂停接受外部的所有新请求
        this.connector.pause();
        //获取Connector 对应的线程池
        Executor executor = this.connector.getProtocolHandler().getExecutor();
        if (executor instanceof ThreadPoolExecutor) {
            try {
                log.warn("WEB应用准备关闭");
                ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                //初始化一个关闭任务位于当前待处理完毕的任务之后,并拒绝新的任务提交。
                threadPoolExecutor.shutdown();
                if (!threadPoolExecutor.awaitTermination(TIMEOUT, TimeUnit.SECONDS)){
                    //等待前一个已提交的任务执行完成后关闭,并且仅在最大等待时间内进行
                    log.warn("WEB应用等待关闭超过最大时长" + TIMEOUT + "秒,将进行强制关闭!");
                    threadPoolExecutor.shutdownNow();//尝试强制关闭线程池
                    if (!threadPoolExecutor.awaitTermination(TIMEOUT, TimeUnit.SECONDS)) {
                        log.error("WEB应用关闭失败!");
                    }
                }
                log.info("WEB应用正常关闭");
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();

            }
        }
    }
        @Override
        public void customize (Connector connector){
            this.connector = connector;
        }



}

上述代码定义的 TIMEOUT 变量为 Tomcat 线程池延时关闭的最大等待时间,一旦超过这个时间就会强制关闭线程池,也就无法处理所有请求了,我们通过控制 Tomcat 线程池的关闭时机,来实现优雅关闭 Web 应用的功能。另外需要注意的是我们的类 CustomShutdown 实现了 ApplicationListener 接口,意味着监听着 Spring 容器关闭的事件,即当前的 ApplicationContext 执行 close 方法。

内嵌 Tomcat 添加 Connector 回调

有了定制的 Connector 回调,我们需要在启动过程中添加到内嵌的 Tomcat 容器中,然后等待执行。那这一步又是如何实现的呢,可以参考下面代码(可以将代码放到启动类中):

package com.yaojack;
import lombok.extern.slf4j.Slf4j;
import com.yaojack.portal.controller.CustomShutdown;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.ServletComponentScan;
import com.yaojack.portal.websocket.NettyServer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

/**
 * springboot启动类
 * @author YuJJ
 * @return
 * @date 2020/5/11 11:13
 */
@SpringBootApplication
@ServletComponentScan(basePackages = "com.yaojack")
public class YjcMonitorApplication implements CommandLineRunner {
    public static ConfigurableApplicationContext ctx;
    public static List<NettyServer> nettyServersList;

    public static void main(String[] args) {
        ctx = SpringApplication.run(YjcMonitorApplication.class, args);
        //测试是否添加回调函数(测试)
        Object webServerFactory = ctx.getBean("webServerFactory");
        System.out.println(webServerFactory+"");
    }



    //SpringBoot内置Tomcat 添加 Connector 回调(启动时添加)
    @Bean
    public CustomShutdown customShutdown() {
        return new CustomShutdown();
    }

    @Bean
    public ConfigurableServletWebServerFactory webServerFactory(final CustomShutdown customShutdown) {
        TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
        tomcatServletWebServerFactory.addConnectorCustomizers(customShutdown);
        return tomcatServletWebServerFactory;
    }

}

这里的 TomcatServletWebServerFactory 是 Spring Boot 实现内嵌 Tomcat 的工厂类,类似的其他 Web 容器,也有对应的工厂类如 JettyServletWebServerFactory,UndertowServletWebServerFactory。他们共同的特点就是继承同个抽象类 AbstractServletWebServerFactory,提供了 Web 容器默认的公共实现,如应用上下文设置,会话管理等。
如果我们需要定义Spring Boot 内嵌的 Tomcat 容器时,就可以使用 TomcatServletWebServerFactory 来进行个性化定义,例如下方为官方文档提供自定示例:

@Bean 
public ConfigurableServletwebServerFactory webServerFactory(){
		TomcatServletwebServerFactory factory =new TomcatServletwebServerFactory(); 
		factory.setPort(9000); 
		factory.setSessionTimeout(10,TimeUnit.MINUTES); 
		return factory;
}

好了说回正题,我们这里使用 addConnectorCustomizers 方法将自定义的 Connector 行为添加到内嵌的Tomcat 之上,为了查看加载效果,我们可以在 Spring Boot 程序启动后从容器中获取下webServerFactory 对象,然后观察,在它的 tomcatConnectorCustomizers 属性中可以看到已经有了 CustomeShutdown 对象。

spring boot如何关闭 springboot关闭tomcat_java

开启 Shutdown Endpoint

到目前让内嵌 Tomcat 容器平稳关闭的操作已经完成,接下来要做的就是如何关闭主动关闭 Spring 容器了,除了常规Linux 命令 Kill,我们可以利用 Spring Boot Actuator 来实现Spring 容器的远程关闭,怎么实现继续看

Spring Boot Actuator 是 Spring Boot 的一大特性,它提供了丰富的功能来帮助我们监控和管理生产环境中运行的 Spring Boot 应用。我们可以通过 HTTP 或者 JMX 方式来对我们应用进行管理,除此之外,它为我们的应用提供了审计,健康状态和度量信息收集的功能,能帮助我们更全面地了解运行中的应用。

在 Spring Boot Actuator 中也提供控制应用关闭的功能,所以我们要为应用引入 Spring Boot Actuator,具体方式就是要将对应的 starter 依赖添加到当前项目中,以 Maven 项目为例:

<!--        系统停止-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

Spring Boot Actuator 采用向外部暴露 Endpoint (端点)的方式来让我们与应用进行监控和管理,引入 spring-boot-starter-actuator 之后,我们就需要启用我们需要的 Shutdown Endpoint,在配置文件 application.properties(application-dev.yml) 中,设置如下

management:
  endpoint:
    shutdown:
      enabled: true
  endpoints:
    web:
      exposure:
        include: "*"

第一行表示启用 Shutdown Endpoint ,第二行表示向外部以 HTTP 方式暴露所有 Endpoint,默认情况下除了 Shutdown Endpoint 之外,其他 Endpoint 都是启用的。
到这里我们的前期配置工作就算完成了。当启动应用后,就可以通过POST 方式请求对应路径的 http://host:port/actuator/shutdown 来实现Spring Boot 应用远程关闭,是不是很简单呢。

模拟测试

这里为了模拟测试,我们首先模拟实现长达10s 时间处理业务的请求控制器 BusinessController,具体实现如下:

package com.yaojack.portal.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * (类功能描述)
 * 测试项目停止处理用户请求
 * @author Yujiajun
 * @module
 * @date: 2020/5/11 17:47
 */
@Slf4j
@RestController
@Controller
@RequestMapping("/test2")
public class BusinessController{
    @RequestMapping("/working")
    public String working()throws InterruptedException{
        log.warn("开始处理业务");
        Thread.sleep(10000);
        log.warn("结束处理业务");
        return "业务完成";
    }
}

用 Thread.sleep 来阻塞当前请求线程,模拟业务处理,在此同时用 HTTP 方式访问 Shutdown Endpoint 试图关闭应用,可以通过观察控制台日志看是否应用是否会完成请求的处理后才真正进行关闭。

首先用 curl 命令模拟发送业务请求:

curl http://127.0.0.1:8084/yjc-monitor/test2/working

然后在业务处理中,直接发送请求 actuator/shutdown,尝试关闭应用,同样采用 curl 方式:

curl -X POST http://127.0.0.1:8084/yjc-monitor/actuator/shutdown

actuator/shutdown 请求发送后会立即返回响应结果,但应用并不会停止:

需要再1的位置输入命令启用POST请求 actuator/shutdown不支持GET请求

配置完成后正常请求显示【2】提示 但不会立刻结束进程

如果application中Endpoint (端点)没有暴露会提示【3】的错误

spring boot如何关闭 springboot关闭tomcat_spring_02


控制台输出

spring boot如何关闭 springboot关闭tomcat_spring_03


可以看出在发送业务请求之后立刻发送关闭应用的请求,并不会立即将应用停止,而是在请求处理完毕之后,就是阻塞的 10s 后应用开始退出,这样可以保证已经接收到的请求能返回正常响应, 而关闭请求之后再进入的请求都不会被处理,到这里我们优雅关闭 Spring Boot 程序的操作就此实现了。

实现自动化重启

我需要停止项目 然后打包重新发布 所以未做脚本重启 先放链接 以后需要可以再研究

由于 Spring Boot 提供内嵌 Web 容器的便利性,我们经常将程序打包成 jar 然后发布。通常应用的启动和关闭操作流程是固定且重复的,本着 Don’t Repeat Yourself 原则,我们有必要将这个操作过程自动化,将关闭和启用的 SpringBoot应用的操作写成 shell 脚本,以避免出现人为的差错,并且方便使用,提高操作效率。下面是原答主针对示例程序所写的程序启动脚本:(具体脚本可在示例项目查看)

spring boot如何关闭 springboot关闭tomcat_tomcat_04


有了脚本,我们可以直接通过命令行方式平滑地更新部署 Spring Boot 程序,效果如下:

spring boot如何关闭 springboot关闭tomcat_java_05

项目关闭前自动调用方法存储数据

启动类:

package com.yaojack;
import lombok.extern.slf4j.Slf4j;
import com.yaojack.portal.controller.CustomShutdown;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.ServletComponentScan;
import com.yaojack.portal.websocket.NettyServer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

/**
 * springboot启动类
 * @author ChengYF
 * @return
 * @date 2020/5/11 11:13
 */
@SpringBootApplication
@ServletComponentScan(basePackages = "com.yaojack")
public class YjcMonitorApplication implements CommandLineRunner {
    public static ConfigurableApplicationContext ctx;
    public static List<NettyServer> nettyServersList;

    public static void main(String[] args) {
        ctx = SpringApplication.run(YjcMonitorApplication.class, args);
        //测试是否添加回调函数(测试)
        Object webServerFactory = ctx.getBean("webServerFactory");
        System.out.println(webServerFactory+"");
    }

    @Override
    public void run(String... strings) {
        nettyServersList = new ArrayList<>();
        NettyServer nettyServer = new NettyServer(2301);//聚能设备通信端口
        nettyServer.start(1);
        nettyServersList.add(nettyServer);
        NettyServer nettyServer2 = new NettyServer(2406);//聚能设备通信端口
        nettyServer2.start(1);
        nettyServersList.add(nettyServer2);
    }

    //SpringBoot内置Tomcat 添加 Connector 回调(启动时添加)
    @Bean
    public CustomShutdown customShutdown() {
        return new CustomShutdown();
    }

    @Bean
    public ConfigurableServletWebServerFactory webServerFactory(final CustomShutdown customShutdown) {
        TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
        tomcatServletWebServerFactory.addConnectorCustomizers(customShutdown);
        return tomcatServletWebServerFactory;
    }

}

代码中添加@PreDestroy注解,SpringBoot项目关闭前会自动调用@PreDestroy注解实现停机数据存储

@PreDestroy
    public void updateOnlineHistory(){
        System.out.println("关闭服务器前执行updateOnlineHistory");
    }

    @PreDestroy
    public void closeNetty(){
        System.out.println("关闭netty服务");
        for(NettyServer nettyServer : YjcMonitorApplication.nettyServersList) {
            nettyServer.closeNetty();
        }
    }

总结

本文主要探究了如何对基于Spring Boot 内嵌 Tomcat 的 Web 应用进行平滑关闭的实现,如果采用其他 Web 容器也类似方式,希望这篇文章有所帮助