拦截器(拦截器中异步调用:A filter or servlet of … chain… asynchronous)

springboot在拦截器中异步调用接口报错:A filter or servlet of the current chain does not support asynchronous operations

    最近捣鼓拦截项目中所有的请求路径,并进行日志记录。记录下用户是否操作过某些功能。
    需求是暗中进行用户操作功能的记录,不管记录是否成功写入到数据库,还是新增日志记录报错,都不影响用户的正常操作。
    在下才疏学浅,找到了一个方案,同时也遇到了一些问题,这才记录下来,供给大家参考。

第一版方案:打算在过滤器中拦截请求的参数与响应的结果(没有采用)

提前说下遇到的问题:

  1. 一次完整的servlet请求,首先经过过滤器,再是拦截器,再是接口,响应数据返回经过拦截器,过滤器返回前端。请求参数从过滤器中被取出后,后续的拦截器和接口(处理器)不能再从request中接收请求体,除非继承HttpServletRequestWrapper,对原始请求进行包装后,再将二次封装后的自定义request传入过滤链,同时,响应数据的获取也是同样的道理,一经取出,流就为null,除非二次封装,继承HttpServletResponseWrapper
  2. 要在获取请求体和返回体后,异步调用保存日志的接口,不管这个接口是否发生异常,不影响主线程继续操作。在过滤器中注入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,保存日志的目的了。

(0)

相关推荐