xChar

相关文章:

{% post_link 拦截和转发 %}

分发

请求的分发是通过doFilter实现的,可以在同一个拦截器内同时实现转发和分发

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    HttpServletResponse response = (HttpServletResponse) servletResponse;
    try {
        // 获取请求路径
        String uri = request.getRequestURI();
        if ("/core/web/ycApi/documentVerifySign".equals(uri)){
            // 前端发起的验证请求也需要转发, 去掉web标记
            uri = "/core/ycApi/documentVerifySign";
            log.info("/core/web/ycApi/documentVerifySign ==> /core/ycApi/documentVerifySign");
        }
        if (uri.startsWith("/core/web/ycApi")){
            // 页面请求,不做转发
            filterChain.doFilter(request, response);
            return;
        }
        // 转发
        forwardFilter(request, response, uri);
    } catch (Exception e){
        log.error("转发服务异常", e);
        // 交给Spring的异常解析器去处理
//            exceptionResolver.resolveException(request, response, null, e);
        // 必须得传 HandlerMethod 否则会走GlobalExceptionHandler,而不是指定的YanCaoApiAdvice
        // 没有通过servlet进行分发,因此没有获取到方法对象
        // 如有必要再添加对方法的匹配,这里默认去找Controller的第一个方法
        HandlerMethod handlerMethod = new HandlerMethod(ycController, ycController.getClass().getDeclaredMethods()[0]);
        exceptionResolver.resolveException(request, response, handlerMethod, e);
    }
}

分发的异常处理

  • 一般而言,异常会由HandlerExceptionResolverComposite处理,它包含了Advice注解的异常处理方案。

  • 但是有些情况下,请求没有执行到DispatcherServlet的分析,获得到handler不能指向我们需要的Advice,比如文件上传接口,超出了tomcat的最大限制导致的checkMultipart解析失败。

  • 因为DispatcherServlet#checkMultipart解析是在DispatcherServlet#getHandler之前,导致异常处理程序因为没有接收到handler而没法交给特定的Advice处理,从而转给全局Advice处理。

目前有两种解决方案

  1. 异常处理前拦截
  2. 异常处理后救场

方案一:拦截

异常处理前是指DispatcherServlet#processHandlerException方法内遍历handlerExceptionResolvers时,会先执行DefaultErrorAttributes#resolveException方法,在请求头中设置异常状态

request.setAttribute(ERROR_ATTRIBUTE, ex);

但是在这个过程中如果返回了视图对象ModelAndView,则不会往下执行异常的处理

因此,我们可以继承DefaultErrorAttributes对象,并注入我们自己的规则,拦截某些请求的异常流程,将请求分发中的异常交由指定的Advice处理。

@Slf4j
@Component
public class MyDefaultErrorAttributes extends DefaultErrorAttributes {
    @Autowired
    private YanCaoApiController ycController;
    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver exceptionResolver;

    /**
     * 文件上传分发出错导致链路没有解析出mappedHandler
     * @see org.springframework.web.servlet.DispatcherServlet#doDispatch(HttpServletRequest, HttpServletResponse)
     * @see org.springframework.web.servlet.DispatcherServlet#checkMultipart(HttpServletRequest)
     * 单独针对指定路径的文件上传请求做异常处理的指定
     * 因为没法指定handler,异常处理会走向全局处理
     * @see com.gomain.exception.GlobalExceptionHandler
     * 但是可以在异常处理(HandlerExceptionResolver)前,先通过DefaultErrorAttributes返回ModelAndView来提前将拦截掉异常处理
     * @see org.springframework.web.servlet.DispatcherServlet#processHandlerException(HttpServletRequest, HttpServletResponse, Object, Exception)
     * processHandlerException方法中的handlerExceptionResolvers是优先处理DefaultErrorAttributes,不返回null就可以实现对异常的拦截
     * 再次将异常处理交给了ycController对应的Advice,这里随意指定了ycController中的一个方法
     * 可以进一步细化到对应的类方法,即实现DispatcherServlet的部分功能
     * @param request current HTTP request
     * @param response current HTTP response
     * @param handler the executed handler, or {@code null} if none chosen at the
     * time of the exception (for example, if multipart resolution failed)
     * @param ex the exception that got thrown during handler execution
     * @return ModelAndView
     */
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        String uri = request.getRequestURI();
        if ("/core/web/ycApi/fileUploadStream".equals(uri)){
            log.error("文件上传分发失败, uri=/core/web/ycApi/fileUploadStream, error:{}", ex.getMessage());
            Method method;
            try {
                method = ycController.getClass().getMethod("fileUploadStream", MultipartFile.class, HttpServletRequest.class, HttpServletResponse.class);
            } catch (Exception e){
                log.error("没有找到方法:fileUploadStream");
                method = ycController.getClass().getDeclaredMethods()[0];
            }
            // 指定Advice处理异常
            HandlerMethod handlerMethod = new HandlerMethod(ycController, method);
            return exceptionResolver.resolveException(request, response, handlerMethod, ex);
        }
        return super.resolveException(request, response, handler, ex);
    }
}

方案二:救场

异常处理后救场是指默认filterChain.doFilter中发生异常,并由我们不认可的异常处理程序处理

分发链路处理中,并不会抛出异常,我们可以在分发链路执行完成后,检查请求头中是否有异常标记

通过修改响应体来达成目的

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    HttpServletResponse response = (HttpServletResponse) servletResponse; 
    try{
    
	...

        // 页面请求,不做转发
        filterChain.doFilter(request, response);
        // multipartResolver 检查请求时出错,会导致dispatcher解析不出handler,从而被全局异常捕获
            Object dispatcherException = request.getAttribute(DispatcherServlet.EXCEPTION_ATTRIBUTE);
            if (Objects.nonNull(dispatcherException)){
                log.error("分发过程中发生异常");
                //TODO 已提交的响应不能重置的解决方案
                // 分发发生异常时交由指定的Advice处理
                throw (Exception) dispatcherException;
            }
        // 分发过程中没有发生异常,将进入Controller处理
        return;
        
    ...
    
    } catch (Exception e){
        log.error("分发异常", e);
        HandlerMethod handlerMethod = new HandlerMethod(ycController, ycController.getClass().getDeclaredMethods()[0]);
        exceptionResolver.resolveException(request, response, handlerMethod, e);
    }
}

但是这里有个问题,通过doFilter处理过异常后,response的缓冲区是已提交状态,只能追加数据,不能重置数据

可以自定义ResponseWrap来达成对缓冲区的修改

参考:https://www.jianshu.com/p/5746b5876a46

解决方案有点偏,不尝试了。

Loading comments...