天天看点

Spring Boot 1.X和2.X优雅重启实战

Spring Boot 1.X优雅地停止应用

项目在重新发布的过程中,如果有的请求时间比较长,还没执行完成,此时重启的话就会导致请求中断,影响业务功能,优雅重启可以保证在停止的时候,不接收外部的新的请求,等待未完成的请求执行完成,这样可以保证数据的完整性。

在pom.xml中引入actuator依赖

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

配置文件增加

endpoints.shutdown.enabled=true           

启动项目,通过发送关闭命令到endpoint停止运行

curl -X POST http://127.0.0.1:8080/shutdown           

此时会返回401状态,表示没有认证

需要关闭权限验证,在配置文件添加

endpoints.shutdown.sensitive=false           

表示shutdown不需要验证,或者直接关闭全部安全验证

management.security.enabled=false           

此时再去执行发现可以停止应用,但是这样的话谁都可以拿这个接口去停止应用,如果是在公网的话,感觉就像在裸奔一样,因此我们可以利用

Spring Security

来处理用户身份验证,去掉这个配置。

pom.xml添加依赖

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

配置文件添加

security.user.name=admin
security.user.password=123456
management.security.enabled=true
management.security.role=ADMIN           

启动项目,通过下面的方式停止应用的运行

curl -X POST --user admin:123456 http://127.0.0.1:8080/shutdown           

为了在应用退出前能尽可能的保证数据的完整性,在接收到shutdown指令之后在完成一些事情,可以在tomcat的自定义接口上做一些工作。

ShutdownConfig

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;

/**
 * Spring Boot1.X Tomcat容器优雅停机  
 */
@Configuration
public class ShutdownConfig {
 
    @Bean
    public GracefulShutdown gracefulShutdown() {
        return new GracefulShutdown();
    }
 
    @Bean
    public EmbeddedServletContainerCustomizer tomcatCustomizer() {
        return container -> {
            if (container instanceof TomcatEmbeddedServletContainerFactory) {
                ((TomcatEmbeddedServletContainerFactory) container).addConnectorCustomizers(gracefulShutdown());
            }
        };
    }
    private static class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
        private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);
        private volatile Connector connector;
        private final int waitTime = 120;
        @Override
        public void customize(Connector connector) {
            this.connector = connector;
        }
        @Override
        public void onApplicationEvent(ContextClosedEvent event) {
            this.connector.pause();
            Executor executor = this.connector.getProtocolHandler().getExecutor();
            if (executor instanceof ThreadPoolExecutor) {
                try {
                    ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                    log.info("shutdown start");
                    threadPoolExecutor.shutdown();
                    log.info("shutdown end");
                    if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                        log.info("Tomcat 进程在" + waitTime + "秒内无法结束,尝试强制结束");
                    }
                    log.info("shutdown success");
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}           

Spring Boot 2.X优雅地停止应用

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>           
# 暴露所有,也可以只暴露shutdown
#management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.include=shutdown
management.endpoint.shutdown.enabled=true           
curl -X POST http://127.0.0.1:8080/shutdown           

这样当然是不安全的,还是需要借助

Spring Security

来处理用户身份验证

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-security</artifactId>  
</dependency>           
spring.security.user.name=admin
spring.security.user.password=123456
spring.security.user.roles=ADMIN           
curl -i -X POST --user admin:123456 http://127.0.0.1:8080/actuator/shutdown           

这时并没有出现我们期待的响应状态是200的

Shutting down, bye...

消息出现,而是

HTTP/1.1 401 
Set-Cookie: JSESSIONID=6B5EF5B52BB5D9B57CD8CE5F405D264F; Path=/; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
WWW-Authenticate: Basic realm="Realm"
Content-Length: 0
Date: Tue, 09 Apr 2019 06:20:39 GMT           

意思已经很明显了,就是未授权,但是我们明明是传了用户名和密码的,为什么还是未授权的状态,这里补充一点Spring Security的知识。

Spring Boot 1.X和2.X优雅重启实战
Spring Boot 1.X和2.X优雅重启实战

首先当用户发送请求的时候,会进入到

UsernamePasswordAuthenticationFilter

中得到一个

UsernamePasswordAuthenticationToken

,它其实相当于一个令牌,不过还没有经过认证,然后调用

AuthenticationManager

的实现类

ProviderManager

中判断登录方式是否支持,如果支持,则会调用

AuthenticationProvider

接口的抽象实现类

AbstractUserDetailsAuthenticationProvider

进行用户身份验证,如果在认证时用户的缓存信息不存在,则需要先通过其子类

DaoAuthenticationProvider

获取

UserDetails

后进行用户身份验证。

当然,我们的用户名密码肯定是没有问题的,到底是没有接收到参数还是认证失败。有兴趣的同学可以试一下请求

curl -i -v --user admin:123456 http://127.0.0.1:8080/actuator           

是可以返回的,也就是说Spring Security对一些特殊的请求有特殊的处理。

查看

官方文档

)可以知道,默认情况下要求对应用程序中的每个URL进行身份验证,而且会启用CSRF保护,以防止CSRF攻击应用程序,Spring Security CSRF会针对除了"GET", "HEAD", "TRACE", "OPTIONS"之外的其他方法("PATCH", "POST", "PUT", "DELETE")进行防护。 所以在默认配置下,即便已经登录了,页面中发起这几种请求依然会被拒绝。

官方文档关于HttpSecurity的介绍中说明了默认配置

Spring Boot 1.X和2.X优雅重启实战

这个默认的配置在类

WebSecurityConfigurerAdapter

的方法

configure(HttpSecurity http)

可以看到。顺着这个思路,我们实现一个自定义的配置类

WebSecurityConfig

,关掉CSRF保护

import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable() //1
            .authorizeRequests()
            .anyRequest().authenticated() //2
//            .requestMatchers(EndpointRequest.toAnyEndpoint()).fullyAuthenticated()  //3
//            .requestMatchers(EndpointRequest.to("shutdown")).fullyAuthenticated()  //4
            .and()
            .formLogin()
            .and()
            .httpBasic();
    }
}
           

这个时候再次执行停止应用的指令就可以成功,可以在自定义的这个类中实现更加精细的控制。

同样的我们可以定义一个类

ShutdownConfig

在Tomcat退出之前做一些事情

import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextClosedEvent;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
/**
 * Spring Boot2.X Tomcat容器优雅停机
 *
 */
@Configuration
public class ShutdownConfig {
 
    @Bean
    public GracefulShutdown gracefulShutdown() {
        return new GracefulShutdown();
    }

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        tomcat.addConnectorCustomizers(gracefulShutdown());
        return tomcat;
    }

    private static class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
        private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);
        private volatile Connector connector;
        private final int waitTime = 120;

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

        @Override
        public void onApplicationEvent(ContextClosedEvent event) {
            this.connector.pause();
            Executor executor = this.connector.getProtocolHandler().getExecutor();
            if (executor instanceof ThreadPoolExecutor) {
                try {
                    ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                    log.info("shutdown start");
                    threadPoolExecutor.shutdown();
                    log.info("shutdown end");
                    if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                        log.info("Tomcat 进程在" + waitTime + "秒内无法结束,尝试强制结束");
                    }
                    log.info("shutdown success");
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}           

优雅重启

停止应用除了我们上面介绍的通过endpoint指令外别忘记kill,但是我们要优雅停止那就不能使用

kill -9 PID

,而是使用

kill PID

发送终止信号来结束进程,等效于

kill -15 PID

,不加的话默认的就是-15,下面是对比图

Spring Boot 1.X和2.X优雅重启实战
Spring Boot 1.X和2.X优雅重启实战

可见使用

kill -9 PID

有多暴力,下面是完整的重启脚本

restart.sh

#!/bin/sh
#set -x

APPS_DIR='appsDir'
APPLICATION_NAME='applicationName'
PID=''
STOP_TIME_OUT=30 #秒
USER_NAME='admin'
USER_PWD='123456'
STOP_URL='http://127.0.0.1:8081/actuator/shutdown'

function getPid { 
    PID=`ps -ef | grep "${APPLICATION_NAME}" | grep -v "grep" | awk '{print $2}'`
}

function startApplication {
    echo 'starting ...'
    #启动参数自己调整
    java -jar ${APPS_DIR}${APPLICATION_NAME}.jar
}

function stopApplication { 
    echo 'waiting ...'
    info=`curl -i --user ${USER_NAME}:${USER_PWD} -X POST ${STOP_URL}`
    code=`echo $info|grep "HTTP"|awk '{print $2}'`
    if [ "$code" != "200" ];then
        echo 'endpoint stop failed  ...'
        getPid;
        start=$(date +%s)
        while [[ $PID != "" ]]; do
            end=$(date +%s)
            time=$(( $end - $start ))
            echo "waiting kill pid ${PID} cost ${time} ..."
            if [[ time -gt $STOP_TIME_OUT ]]; then
                kill ${PID}
            fi
            sleep 1
            getPid;
        done
    fi
    
    echo 'stoped ...'
}

function restart {
    stopApplication;
    startApplication;
}

restart;
           

【转载请注明出处】:

https://developer.aliyun.com/article/754662
Spring Boot 1.X和2.X优雅重启实战