【转载请注明出处】:https://www.jianshu.com/p/53e6ff96368d
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优雅地停止应用
在pom.xml中引入actuator依赖
<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
启动项目,通过发送关闭命令到endpoint停止运行
curl -X POST http://127.0.0.1:8080/shutdown
这样当然是不安全的,还是需要借助Spring Security
来处理用户身份验证
pom.xml添加依赖
<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的知识。


首先当用户发送请求的时候,会进入到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的介绍中说明了默认配置

这个默认的配置在类
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,下面是对比图


可见使用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://www.jianshu.com/p/53e6ff96368d
网友评论