Spring Retry
Spring Retry为Spring应用程序提供了声明性重试支持。 它用于Spring批处理、Spring集成、Apache Hadoop(等等)的Spring。
使用场景
在分布式系统中,为了保证数据分布式事务的强一致性,大家在调用RPC接口或者发送MQ时,针对可能会出现网络抖动请求超时情况采取一下重试操作。 大家用的最多的重试方式就是MQ了,但是如果你的项目中没有引入MQ,那就不方便了。
还有一种方式,是开发者自己编写重试机制,但是大多不够优雅。
本文主要介绍一下如何使用Spring Retry实现重试操作
快速开始
maven 引入
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!--依赖于 aspectj-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
</dependencies>
代码示例
- SimpleDemo.java
简单例子
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
public class SimpleDemo {
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDemo.class);
public static void main(String[] args) throws Exception {
RetryTemplate template = new RetryTemplate();
// 策略
SimpleRetryPolicy policy = new SimpleRetryPolicy();
policy.setMaxAttempts(2);
template.setRetryPolicy(policy);
String result = template.execute(
new RetryCallback<String, Exception>() {
@Override
public String doWithRetry(RetryContext arg0) {
throw new NullPointerException();
}
}
,
new RecoveryCallback<String>() {
@Override
public String recover(RetryContext context) {
return "recovery callback";
}
}
);
LOGGER.info("result: {}", result);
}
}
- 运行结果
01:20:02.750 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=0
01:20:02.759 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=1
01:20:02.760 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=1
01:20:02.761 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=2
01:20:02.761 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry failed last attempt: count=2
01:20:02.761 [main] INFO com.github.houbb.retry.spring.commonway.SimpleDemo - result: recovery callback
类结构说明
概览
-
RetryCallback: 封装你需要重试的业务逻辑(上文中的doSth)
-
RecoverCallback:封装在多次重试都失败后你需要执行的业务逻辑(上文中的doSthWhenStillFail)
-
RetryContext: 重试语境下的上下文,可用于在多次Retry或者Retry 和Recover之间传递参数或状态(在多次doSth或者doSth与doSthWhenStillFail之间传递参数)
-
RetryOperations : 定义了“重试”的基本框架(模板),要求传入RetryCallback,可选传入RecoveryCallback;
-
RetryListener:典型的“监听者”,在重试的不同阶段通知“监听者”(例如doSth,wait等阶段时通知)
-
RetryPolicy : 重试的策略或条件,可以简单的进行多次重试,可以是指定超时时间进行重试(上文中的someCondition)
-
BackOffPolicy: 重试的回退策略,在业务逻辑执行发生异常时。如果需要重试,我们可能需要等一段时间(可能服务器过于繁忙,如果一直不间隔重试可能拖垮服务器), 当然这段时间可以是 0,也可以是固定的,可以是随机的(参见tcp的拥塞控制算法中的回退策略)。回退策略在上文中体现为wait();
-
RetryTemplate :RetryOperations的具体实现,组合了RetryListener[],BackOffPolicy,RetryPolicy。
核心功能
为了使处理更加健壮且不容易失败,有时它可以帮助自动重试失败的操作,以防在后续尝试中可能成功。
易受这种处理影响的错误本质上是暂时的。
例如,由于网络故障或数据库更新中的DeadLockLoserException而导致web服务或RMI服务失败的远程调用可能在短时间等待后自动解决。
为了自动化这些操作的重试,Spring批处理采用了重试操作策略。
RetryOperations
RetryOperations界面如下:
public interface RetryOperations {
<T> T execute(RetryCallback<T> retryCallback) throws Exception;
<T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback)
throws Exception;
<T> T execute(RetryCallback<T> retryCallback, RetryState retryState)
throws Exception, ExhaustedRetryException;
<T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback,
RetryState retryState) throws Exception;
}
RetryCallback
基本回调是一个简单的接口,允许您插入一些业务逻辑以重试:
public interface RetryCallback<T> {
T doWithRetry(RetryContext context) throws Throwable;
}
回调被执行,如果它失败(通过抛出异常),它将被重试,直到它成功,或者实现决定中止。
在RetryOperations接口中有许多重载的执行方法,当所有的重试尝试都结束时,它们处理各种用于恢复的用例,以及重试状态(允许客户机和实现在调用之间存储信息)。
重试策略
RetryPolicy
public interface RetryPolicy extends Serializable {
boolean canRetry(RetryContext context);
RetryContext open(RetryContext parent);
void close(RetryContext context);
void registerThrowable(RetryContext context, Throwable throwable);
}
canRetry 在每次重试的时候调用,是否可以继续重试的判断条件
open 重试开始前调用,会创建一个重试上下文到RetryContext,保存重试的堆栈等信息 registerThrowable 每次重试异常时调用(有异常会继续重试)
以 SimpleRetryPolicy 为例,当重试次数达到3(默认3次)停止重试,重试次数保存在重试上下文中。
常见策略
-
NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
-
AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
-
SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
-
TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
-
ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
-
CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
-
CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以, 悲观组合重试策略是指只要有一个策略不允许重试即可以,但不管哪种组合方式,组合中的每一个策略都会执行
重试回退策略
重试回退策略,指的是每次重试是立即重试还是等待一段时间后重试。
默认情况下是立即重试,如果需要配置等待一段时间后重试则需要指定回退策略BackoffRetryPolicy。
-
NoBackOffPolicy:无退避算法策略,每次重试时立即重试
-
FixedBackOffPolicy:固定时间的退避策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒
-
UniformRandomBackOffPolicy:随机时间退避策略,需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒
-
ExponentialBackOffPolicy:指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier
-
ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数可以实现随机乘数回退
有状态重试 OR 无状态重试
所谓无状态重试是指重试在一个线程上下文中完成的重试,反之不在一个线程上下文完成重试的就是有状态重试。 之前的SimpleRetryPolicy就属于无状态重试,因为重试是在一个循环中完成的。那么什么会后会出现或者说需要有状态重试呢?通常有两种情况:事务回滚和熔断。
数据库操作异常DataAccessException,不能执行重试,而如果抛出其他异常可以重试。
熔断的意思不在当前循环中处理重试,而是全局重试模式(不是线程上下文)。 熔断会跳出循环,那么必然会丢失线程上下文的堆栈信息。 那么肯定需要一种“全局模式”保存这种信息,目前的实现放在一个cache(map实现的)中,下次从缓存中获取就能继续重试了。
注解
项目结构
.
├── Application.java
├── controller
│ └── RetryController.java
└── service
└── RemoteService.java
- RemoteService.java
@Service
public class RemoteService {
private static final Logger LOGGER = LoggerFactory.getLogger(RemoteService.class);
/**
* 调用方法
*/
@Retryable(value = RuntimeException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 5000L, multiplier = 2))
public void call() {
LOGGER.info("Call something...");
throw new RuntimeException("RPC调用异常");
}
/**
* 补偿机制
* @param e 异常
*/
@Recover
public void recover(RuntimeException e) {
LOGGER.info("Start do recover things....");
LOGGER.warn("We meet ex: ", e);
}
}
- RetryController.java
@RestController
@RequestMapping("/retry")
public class RetryController {
private final RemoteService remoteService;
@Autowired
public RetryController(RemoteService remoteService) {
this.remoteService = remoteService;
}
/**
* 重试方法
* @return http result
*/
@RequestMapping("/")
public ResponseEntity retry() {
remoteService.call();
return ResponseEntity.ok("you get it");
}
}
- Application.java
用于启动项目:
@SpringBootApplication
@EnableRetry
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
System.out.println("Start at http://localhost:18080/retry/");
}
}
测试
启动 main() 方法之后,直接访问 http://localhost:18080/retry/
- 日志
打印日志如下:
2018-06-23 11:30:39.993 INFO 6861 --- [io-18080-exec-1] c.g.h.s.b.retry.service.RemoteService : Call something...
2018-06-23 11:30:44.997 INFO 6861 --- [io-18080-exec-1] c.g.h.s.b.retry.service.RemoteService : Call something...
2018-06-23 11:30:55.000 INFO 6861 --- [io-18080-exec-1] c.g.h.s.b.retry.service.RemoteService : Call something...
2018-06-23 11:30:55.000 INFO 6861 --- [io-18080-exec-1] c.g.h.s.b.retry.service.RemoteService : Start do recover things....
2018-06-23 11:30:55.008 WARN 6861 --- [io-18080-exec-1] c.g.h.s.b.retry.service.RemoteService : We meet ex:
java.lang.RuntimeException: RPC调用异常
...
- 说明
可见一共重试了 3 次调用,并且最后会调用我们定义的 recover()
方法
注解说明
@EnableRetry
表示是否开始重试。
序号 | 属性 | 类型 | 默认值 | 说明 |
1 | proxyTargetClass | boolean | false | 指示是否要创建基于子类的(CGLIB)代理,而不是创建标准的基于Java接口的代理。 |
@Retryable
标注此注解的方法在发生异常时会进行重试
序号 | 属性 | 类型 | 默认值 | 说明 |
1 | interceptor | String | ”” | 将 interceptor 的 bean 名称应用到 retryable() |
2 | value | Class[] | {} | 可重试的异常类型。 |
3 | label | String | ”” | 统计报告的唯一标签。如果没有提供,调用者可以选择忽略它,或者提供默认值。 |
4 | maxAttempts | int | 3 | 尝试的最大次数(包括第一次失败),默认为3次。 |
5 | backoff | @Backoff | @Backoff() | 指定用于重试此操作的backoff属性。默认为空 |
@Backoff
序号 | 属性 | 类型 | 默认值 | 说明 | |
1 | delay | long | 0 | 如果不设置则默认使用 1000 milliseconds | 重试等待 |
2 | maxDelay | long | 0 | 最大重试等待时间 | |
3 | multiplier | long | 0 | 用于计算下一个延迟延迟的乘数(大于0生效) | |
4 | random | boolean | false | 随机重试等待时间 |
@Recover
用于恢复处理程序的方法调用的注释。一个合适的复苏handler有一个类型为可投掷(或可投掷的子类型)的第一个参数
和返回与@Retryable
方法相同的类型的值。
可抛出的第一个参数是可选的(但是没有它的方法只会被调用)。
从失败方法的参数列表按顺序填充后续的参数。
修改参数的值
如果我们想在重试的时候修改其中的值,应该怎么做呢?(比如每次的重试次数这个属性都想进行递增设置)
java-configuration-for-retry-proxies
代码
- CbTime.java
意在模拟一个请求入参。
public class CbTime {
private int time;
private int msg;
public int getTime() {
return time;
}
public void setTime(int time) {
this.time = time;
}
public int getMsg() {
return msg;
}
public void setMsg(int msg) {
this.msg = msg;
}
@Override
public String toString() {
return "CbTime{" +
"time=" + time +
", msg=" + msg +
'}';
}
}
- TimeService.java
模拟重试次数的服务。
import com.github.houbb.spring.boot.retry.model.CbTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Service
public class TimeService {
private static final Logger LOGGER = LoggerFactory.getLogger(TimeService.class);
/**
* 调用方法
*/
@Retryable(value = RuntimeException.class,
maxAttempts = 5,
backoff = @Backoff(delay = 2000L, multiplier = 1.5))
public void call(CbTime cbTime) {
LOGGER.info("Call " + cbTime);
cbTime.setTime(cbTime.getTime() + 1);
throw new RuntimeException();
}
/**
* 补偿机制
*
* @param e 异常
*/
@Recover
public void recover(RuntimeException e, CbTime cbTime) {
LOGGER.info("开始处理回复工作: " + cbTime);
LOGGER.warn("We meet ex: ", e);
}
}
@RestController
@RequestMapping("/retry")
public class RetryController {
private final RemoteService remoteService;
private final TimeService timeService;
@Autowired
public RetryController(RemoteService remoteService, TimeService timeService) {
this.remoteService = remoteService;
this.timeService = timeService;
}
/**
* 重试方法
* @return http result
*/
@RequestMapping("/time")
public ResponseEntity retryTime() {
CbTime cbTime = new CbTime();
cbTime.setTime(1);
cbTime.setMsg(0);
timeService.call(cbTime);
return ResponseEntity.ok("you get it");
}
}
测试
页面请求 http://localhost:18080/retry/time
- 测试日志
2018-07-23 12:58:18.735 INFO 849 --- [io-18080-exec-1] c.g.h.s.boot.retry.service.TimeService : Call CbTime{time=1, msg=0}
2018-07-23 12:58:20.739 INFO 849 --- [io-18080-exec-1] c.g.h.s.boot.retry.service.TimeService : Call CbTime{time=2, msg=0}
2018-07-23 12:58:23.742 INFO 849 --- [io-18080-exec-1] c.g.h.s.boot.retry.service.TimeService : Call CbTime{time=3, msg=0}
2018-07-23 12:58:28.244 INFO 849 --- [io-18080-exec-1] c.g.h.s.boot.retry.service.TimeService : Call CbTime{time=4, msg=0}
2018-07-23 12:58:34.998 INFO 849 --- [io-18080-exec-1] c.g.h.s.boot.retry.service.TimeService : Call CbTime{time=5, msg=0}
2018-07-23 12:58:34.999 INFO 849 --- [io-18080-exec-1] c.g.h.s.boot.retry.service.TimeService : 开始处理回复工作: CbTime{time=6, msg=0}
2018-07-23 12:58:35.006 WARN 849 --- [io-18080-exec-1] c.g.h.s.boot.retry.service.TimeService : We meet ex:
java.lang.RuntimeException: null
at com.github.houbb.spring.boot.retry.service.TimeService.call(TimeService.java:41) ~[classes/:na]
at com.github.houbb.spring.boot.retry.service.TimeService$$FastClassBySpringCGLIB$$51a56830.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:738) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.retry.interceptor.RetryOperationsInterceptor$1.doWithRetry(RetryOperationsInterceptor.java:91) ~[spring-retry-1.2.1.RELEASE.jar:na]
at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:287) ~[spring-retry-1.2.1.RELEASE.jar:na]
at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:180) ~[spring-retry-1.2.1.RELEASE.jar:na]
at org.springframework.retry.interceptor.RetryOperationsInterceptor.invoke(RetryOperationsInterceptor.java:115) ~[spring-retry-1.2.1.RELEASE.jar:na]
at org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor.invoke(AnnotationAwareRetryOperationsInterceptor.java:152) ~[spring-retry-1.2.1.RELEASE.jar:na]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:673) [spring-aop-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at com.github.houbb.spring.boot.retry.service.TimeService$$EnhancerBySpringCGLIB$$7cacf383.call(<generated>) [classes/:na]
at com.github.houbb.spring.boot.retry.controller.RetryController.retryTime(RetryController.java:61) [classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_91]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_91]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_91]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_91]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) [spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133) [spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97) [spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827) [spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738) [spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) [spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967) [spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901) [spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) [spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861) [spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:635) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) [spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) [tomcat-embed-websocket-8.5.23.jar:8.5.23]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) [spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:108) [spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81) [spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197) [spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.3.13.RELEASE.jar:4.3.13.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:478) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:803) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459) [tomcat-embed-core-8.5.23.jar:8.5.23]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.23.jar:8.5.23]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_91]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_91]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.23.jar:8.5.23]
at java.lang.Thread.run(Thread.java:745) [na:1.8.0_91]
项目地址
参考资料
https://blog.csdn.net/u011116672/article/details/77823867
https://blog.csdn.net/u010081710/article/details/77881781
https://blog.csdn.net/paul_wei2008/article/details/53871442