springboot在拦截器中异步调用接口报错:A filter or servlet of the current chain does not support asynchronous operations
最近捣鼓拦截项目中所有的请求路径,并进行日志记录。记录下用户是否操作过某些功能。
需求是暗中进行用户操作功能的记录,不管记录是否成功写入到数据库,还是新增日志记录报错,都不影响用户的正常操作。
在下才疏学浅,找到了一个方案,同时也遇到了一些问题,这才记录下来,供给大家参考。
第一版方案:打算在过滤器中拦截请求的参数与响应的结果(没有采用)
提前说下遇到的问题:
- 一次完整的servlet请求,首先经过过滤器,再是拦截器,再是接口,响应数据返回经过拦截器,过滤器返回前端。请求参数从过滤器中被取出后,后续的拦截器和接口(处理器)不能再从request中接收请求体,除非继承HttpServletRequestWrapper,对原始请求进行包装后,再将二次封装后的自定义request传入过滤链,同时,响应数据的获取也是同样的道理,一经取出,流就为null,除非二次封装,继承HttpServletResponseWrapper
- 要在获取请求体和返回体后,异步调用保存日志的接口,不管这个接口是否发生异常,不影响主线程继续操作。在过滤器中注入service,需要将过滤器交给spring管理才行,还要配置另一个保存日志接口的ip与端口属性,主功能项目与保存日志的项目是拆分的,通过resttemplate远程调用。这似乎是可以解决的,但是不如拦截器中处理方便。
过滤器中的代码:
@Component
@WebFilter(filterName = "logFilter", urlPatterns = "/*", asyncSupported = true)
public class CustomFilter implements Filter {
...
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 请求url,这是打印日志所需,可以不获取
String url = request.getRequestURI();
// // 获取请求报文,注意这里就已经将请求体内容读取了,保存在requestBody 这个变量中
// String requestBody = this.getRequestBody(request);
// // 注意这里新建自定义的RequestWrapper对象并将获取的requestBody传入,就是将流重新写入,具体代码在RequestWrapper构造方法中
// RequestWrapper requestWrapper = new RequestWrapper(request, requestBody);
// // 获取header,这里都是通过自定义的方法获取日志信息,可以不获取
// Map<String, String> headerMap = requestWrapper.getHeaderMap();
// // 获取paramMap,这里都是通过自定义的方法获取日志信息,可以不获取
// Map<String, String> parameterMap = requestWrapper.getParameterMaps();
//响应处理 包装响应对象并缓存响应数据,这里相当于直接将原生的servletResponse复制了一份
ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse) servletResponse);
// 执行后续业务代码,注意是将我们自定义的两个请求和响应对象传进去,请求体的内容已经被重新写入
filterChain.doFilter(request, servletResponse);
// 注意这里是从自定义的响应对象中取响应体
String responseBody = new String(responseWrapper.getResponseData(), StandardCharsets.UTF_8);
// System.out.println(responseBody);
// 注意由于响应体内容已经被我们获取,页面这时候是拿不到响应数据的,要手动写回去
servletResponse.setContentLength(-1);
// 需要注意的是,这里是从原生的方法参数中定义的ServletResponse 对象中获取输出流,从自定义的响应对象responseWrapper获取的输出流是写不出东西的!!!
ServletOutputStream output = servletResponse.getOutputStream();
// 写出响应体
output.write(responseBody.getBytes());
output.flush();
}
...
}
增强request的自定义类(源自网络):
public class RequestWrapper extends HttpServletRequestWrapper {
/**
* 请求body
*/
private String body;
HttpServletRequest req = null;
/**
* 请求header
*/
private Map<String, String> headerMap = new HashMap<>();
/**
* 请求paramMap
*/
private Map<String, String> parameterMap = new HashMap<>();
/**
* Constructs a request object wrapping the given request.
* @param request The request to wrap
* @throws IllegalArgumentException if the request is null
*/
public RequestWrapper(HttpServletRequest request)
throws IOException {
super(request);
}
public RequestWrapper(HttpServletRequest request, String requestBody) {
super(request);
this.body = requestBody;
this.req = request;
Enumeration<String> headers = request.getHeaderNames();
while (headers.hasMoreElements()) {
String headerKey = headers.nextElement();
headerMap.put(headerKey, request.getHeader(headerKey));
}
Enumeration<String> parameterNames = request.getParameterNames();
while (parameterNames.hasMoreElements()) {
String name = parameterNames.nextElement();
parameterMap.put(name, request.getParameter(name));
}
}
@Override
public ServletInputStream getInputStream()
throws IOException {
return new ServletInputStream() {
private InputStream in = new ByteArrayInputStream(body.getBytes(req.getCharacterEncoding()));
@Override
public int read()
throws IOException {
return in.read();
}
@Override
public boolean isFinished() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isReady() {
// TODO Auto-generated method stub
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
// TODO Auto-generated method stub
}
};
}
@Override
public BufferedReader getReader()
throws IOException {
return new BufferedReader(new StringReader(body));
}
public String getBody() {
// 请求中的数据
return this.body;
}
public Map<String, String> getHeaderMap() {
return this.headerMap;
}
public Map<String, String> getParameterMaps() {
return this.parameterMap;
}
}
增强response的自定义类(源自网络):
public class ResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer = null;
private ServletOutputStream out = null;
private PrintWriter writer = null;
public ResponseWrapper(HttpServletResponse resp) throws IOException {
super(resp);
/**
* 替换默认的输出端,作为response输出数据的存储空间(即真正存储数据的流)
*/
buffer = new ByteArrayOutputStream();
/**
* response输出数据时是调用getOutputStream()和getWriter()方法获取输出流,再将数据输出到输出流对应的输出端的。
* 此处指定getOutputStream()和getWriter()返回的输出流的输出端为buffer,即将数据保存到buffer中。
*/
out = new WapperedOutputStream(buffer);
writer = new PrintWriter(new OutputStreamWriter(buffer, this.getCharacterEncoding()));
}
//重载父类获取outputstream的方法
@Override
public ServletOutputStream getOutputStream()
throws IOException {
return out;
}
//重载父类获取writer的方法
@Override
public PrintWriter getWriter()
throws UnsupportedEncodingException {
return writer;
}
/**
* 这是将数据输出的最后步骤
* @throws IOException
*/
@Override
public void flushBuffer()
throws IOException {
if (out != null) {
out.flush();
}
if (writer != null) {
writer.flush();
}
}
@Override
public void reset() {
buffer.reset();
}
public byte[] getResponseData()
throws IOException {
flushBuffer();//将out、writer中的数据强制输出到WapperedResponse的buffer里面,否则取不到数据
return buffer.toByteArray();
}
//内部类,对ServletOutputStream进行包装,指定输出流的输出端
private class WapperedOutputStream extends ServletOutputStream {
private ByteArrayOutputStream bos = null;
public WapperedOutputStream(ByteArrayOutputStream stream)
throws IOException {
bos = stream;
}
//将指定字节写入输出流bos
@Override
public void write(int b)
throws IOException {
bos.write(b);
}
@Override
public boolean isReady(){
return false;
}
@Override
public void setWriteListener(WriteListener writeListener){
}
}
}
第二个方案:拦截器中获取路径、请求体、响应体、可能存在的主业务异常结果
这个方案,解决了第一个方案的问题,但也在实际应用中遇到了“A filter or servlet of the current chain does not support asynchronous operations”的异常报错。
大意是:在一次请求中的servlet与Filter中,有使用async-异步方法的话,所有的途径的过滤器与处理器都要支持异步调用。
@WebFilter(filterName = “logFilter”, urlPatterns = “/*”, asyncSupported = true)
在拦截器中的异步调用部分,是从网上找寻的代码,仅供参考
拦截器代码:
public class CustomInterceptor implements HandlerInterceptor {
@Autowired(required = false)
private RestTemplate restTemplate;
/**
* 全局的日志项目的url地址
*/
@Value("${log.addr}")
String logAddr;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return HandlerInterceptor.super.preHandle(request, response, handler);
}
/**
* 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后),如果异常发生,则该方法不会被调用
*
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HttpSession session = request.getSession();
Integer clazzId = session.getAttribute("clazzId") == null ? 0 : Integer.valueOf(session.getAttribute("clazzId").toString());
if (StringUtils.isEmpty(clazzId)){
clazzId = request.getParameter("examinationId") == null ? null : Integer.parseInt(request.getParameter("examinationId"));
}
//若干的参数
......
//请求的接口路径
String urlStr = request.getServletPath();
// 接口的别的参数
Map<String,Object> otherParamMap = new HashMap<>();
// 获取别的参数
Enumeration<String> enum2 = request.getParameterNames();
while (enum2.hasMoreElements()) {
String paramName = enum2.nextElement();
//代码有删减。。。
}
//获取项目的根路径 课程代码
String courseCode = request.getContextPath().substring(1);
ResponseWrapper responseWrapper = new ResponseWrapper(response);
// 拦截器 获取接口的相应结果,注意这里是从自定义的响应对象中取响应体
String responseBody = new String(responseWrapper.getResponseData(), StandardCharsets.UTF_8);
ResultData resultData = new ResultData();
// 如果能被转成map
if (responseBody.startsWith("{")) {
Map<String, Object> resultMap = mapper.readValue(responseBody, HashMap.class);
if (resultMap != null) {
resultData.setData(resultMap.get("data"));
resultData.setMessage(resultMap.get("message") == null ? null : resultMap.get("message").toString());
if (!Boolean.valueOf(resultMap.get("data").toString())) {
resultData.setSuccess(resultMap.get("success") == null || Boolean.valueOf(resultMap.get("success").toString()));
}
}
}
// 如果是普通字符串
else {
resultData.setMessage(responseBody.substring(0, responseBody.length() <= 255 ? responseBody.length() : 255));
}
Map<String, Object> paramMap = new HashMap<>();
//若干参数,有删减
if (!otherParamMap.isEmpty()) {
String str = mapper.writeValueAsString(otherParamMap);
if (str.length() > 1000){
str = str.substring(0,950) + "...";
}
paramMap.put("otherParams", str);
}
/** 关联的某些外部数据的id */
/** 请求的接口路径 */
if (!StringUtils.isEmpty(urlStr)) {
paramMap.put("urlStr", urlStr);
}
/** 请求结果 0-默认,1-true,2-false */
Boolean resultFlag = resultData.getSuccess();
paramMap.put("requestReslut", 1);
if (!resultFlag) {
paramMap.put("requestReslut", 0);
}
/** 接口返回的信息 */
paramMap.put("responseMessage", resultData.getMessage());
/** 接口返回的异常信息 存在data 里 */
paramMap.put("exceptStr", resultData.getData());
if (logAddr == null) {
throw new RuntimeException("缺少日志项目的地址,请在配置文件中补充");
}
if (logAddr.endsWith("/")) {
logAddr = logAddr.substring(0, logAddr.length() - 1);
}
AsyncContext asyncContext = request.startAsync();
//设置超时时间
asyncContext.setTimeout(2000);
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
// 设置请求头为form形式
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 设置参数, 和MsgVO中变量名对应
MultiValueMap<String, Object> map = new LinkedMultiValueMap<String, Object>();
for (String key : paramMap.keySet()) {
map.add(key, paramMap.get(key));
}
// 封装请求参数
HttpEntity requestb = new HttpEntity(map, headers);
ResponseEntity responseEntity = restTemplate.postForEntity(url, requestb, Object.class);
} catch (Exception e) {
System.out.println("异步处理发生异常:" + e.getMessage());
}
// 异步请求完成通知,整个请求完成
asyncContext.complete();
}
});
}
}
这样写的优点大概是,获取请求体和响应数据比较方便。在发生接口异常时,也能捕捉到异常信息(截取有效信息),存入日志中。
使用注解,易于被依赖进其他项目
新增注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CustomLogInterceptorConfig.class)
public @interface EnableLog {
}
注解生效时扫描的配置类:
友情提示:项目中有类已经集成了 WebMvcConfigurationSupport 或 WebMvcConfigurerAdapter 或 WebMvcConfigurer 你再定集成这些类也不会生效;
也就是当项目中有多个
implements WebMvcConfigurer
extends WebMvcConfigurationSupport
extends WebMvcConfigurerAdapter
生效的指不定是哪一个呢!!!
注解扫描jar中的指定拦截器配置类,然后拦截生效!
@Configuration
@ComponentScan("xxx.xxx.xxx.**")
public class CustomLogInterceptorConfig implements WebMvcConfigurer {
@Bean(name = "customLogInterceptor")
public CustomLogInterceptor getCustomLogInterceptor(){
return new CustomLogInterceptor();//有参构造方法进行属性赋值
}
/**
* 添加自定义的拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getCustomLogInterceptor()).addPathPatterns("/**");
}
}
在注入resttemplate时,如果主项目已经有了该bean,name依赖项目可以如下配置:
@Bean
@ConditionalOnBean(RestTemplate.class)
public RestTemplate restTemplate() {
return new RestTemplate();
}
这样,大致就达成了,自动拦截url,保存日志的目的了。