前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring Boot 1.X和2.X优雅重启实战

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

原创
作者头像
后端老鸟
修改2020-04-16 10:25:07
1.1K0
修改2020-04-16 10:25:07
举报
文章被收录于专栏:服务端技术

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

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

在pom.xml中引入actuator依赖

代码语言:txt
复制
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

配置文件增加

代码语言:txt
复制
endpoints.shutdown.enabled=true

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

代码语言:txt
复制
curl -X POST http://127.0.0.1:8080/shutdown

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

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

代码语言:txt
复制
endpoints.shutdown.sensitive=false

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

代码语言:txt
复制
management.security.enabled=false

此时再去执行发现可以停止应用,但是这样的话谁都可以拿这个接口去停止应用,如果是在公网的话,感觉就像在裸奔一样,因此我们可以利用Spring Security来处理用户身份验证,去掉这个配置。

pom.xml添加依赖

代码语言:txt
复制
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-security</artifactId>  
</dependency>

配置文件添加

代码语言:txt
复制
security.user.name=admin
security.user.password=123456
management.security.enabled=true
management.security.role=ADMIN

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

代码语言:txt
复制
curl -X POST --user admin:123456 http://127.0.0.1:8080/shutdown

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

ShutdownConfig

代码语言:txt
复制
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优雅地停止应用

在pom.xml中引入actuator依赖

代码语言:txt
复制
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

配置文件添加

代码语言:txt
复制
# 暴露所有,也可以只暴露shutdown
#management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.include=shutdown
management.endpoint.shutdown.enabled=true

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

代码语言:txt
复制
curl -X POST http://127.0.0.1:8080/shutdown

这样当然是不安全的,还是需要借助Spring Security来处理用户身份验证

pom.xml添加依赖

代码语言:txt
复制
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-security</artifactId>  
</dependency>

配置文件添加

代码语言:txt
复制
spring.security.user.name=admin
spring.security.user.password=123456
spring.security.user.roles=ADMIN

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

代码语言:txt
复制
curl -i -X POST --user admin:123456 http://127.0.0.1:8080/actuator/shutdown

这时并没有出现我们期待的响应状态是200的Shutting down, bye...消息出现,而是

代码语言:txt
复制
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的知识。

认证流程图
认证流程图
过滤器链
过滤器链

首先当用户发送请求的时候,会进入到UsernamePasswordAuthenticationFilter中得到一个UsernamePasswordAuthenticationToken,它其实相当于一个令牌,不过还没有经过认证,然后调用AuthenticationManager的实现类ProviderManager中判断登录方式是否支持,如果支持,则会调用AuthenticationProvider接口的抽象实现类AbstractUserDetailsAuthenticationProvider进行用户身份验证,如果在认证时用户的缓存信息不存在,则需要先通过其子类 DaoAuthenticationProvider获取UserDetails后进行用户身份验证。

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

代码语言:txt
复制
curl -i -v --user admin:123456 http://127.0.0.1:8080/actuato

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

查看官方文档(https://docs.spring.io/spring-security/site/docs/3.2.0.RC2/reference/htmlsingle/#jc

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

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

image.png
image.png

这个默认的配置在类WebSecurityConfigurerAdapter的方法 configure(HttpSecurity http)可以看到。顺着这个思路,我们实现一个自定义的配置类WebSecurityConfig,关掉CSRF保护

代码语言:txt
复制
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退出之前做一些事情

代码语言:txt
复制
 

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,下面是对比图

kill -9
kill -9
kill
kill

可见使用kill -9 PID有多暴力,下面是完整的重启脚本restart.sh

代码语言:txt
复制
#!/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}.ja

}



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://cloud.tencent.com/developer/article/1612982

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Spring Boot 1.X优雅地停止应用
  • Spring Boot 2.X优雅地停止应用
  • 优雅重启
相关产品与服务
多因子身份认证
多因子身份认证(Multi-factor Authentication Service,MFAS)的目的是建立一个多层次的防御体系,通过结合两种或三种认证因子(基于记忆的/基于持有物的/基于生物特征的认证因子)验证访问者的身份,使系统或资源更加安全。攻击者即使破解单一因子(如口令、人脸),应用的安全依然可以得到保障。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档