基于ControllerAdvice+ErrorController+Filter,Springboot全局化处理异常信息(自定义error页面或json返回)

适用要求:

     1. 自定义error页面,并能对error信息进行封装

     2.根据不同的异常返回不同的信息

     3. 能根据请求地址(或其他信息)决策返回页面还是json

 解决思路:

     1. 自定义error页面

这个比较简单,继承ErrorController接口实现自己的Controller即可。

可参见:org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController

 代码参照[非最终代码,部分命名可能不规范,参考用]:

package com.miniprogram.pa.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Controller
public class MyControllerAdvice implements ErrorController {
    private static Logger logger = LoggerFactory.getLogger(MyControllerAdvice.class);
    private final static String[] apiPathStartSet = {"/api/"};

    /**
     * 接管 /error 统一处理系统error 一般发生在页面错误及资源不足的情况,这时候需要根据http status判断
     * @param e
     * @param response
     * @param request
     * @param session
     * @param handler
     * @return
     * @throws IOException
     */
    @RequestMapping("/error")
    public ModelAndView error(HttpServletResponse response, HttpServletRequest request, HttpSession session, ModelMap modelMap) throws IOException {
        // 此时的http status是正确的,但是request及reponse是转发到/error之后的 
        logger.error("发生错误在请求:{}",request.getServletPath());
        
        // 返回
        return null;
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }
}

   error的设计对于使用要求3需要获取请求的ServletPath,然而,此时获取到的servletPath= ”/error“;

???? 无论请求任何不存在的地址,这里的ServletPath均为/error,并不是实际请求的地址,那么问题出现在哪呢?

在 error() 方法中,我们在返回前(return null;)断点调试一下,观察一下request及response里的内容:

再观察一下response

request及response确实已不是我们当初请求的那一对了,那么问题出现在哪里呢?

【原因】:Spring DispatcherServlet在根据请求path查找handler时没有找到对应的handler,此时会交由ErrorController去处理(执行原理自行解读源码),这也是为什么我们重新定义/error的原因;系统内置的ErrorController是org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController,这里有个假设,如果我们不直接实现接口ErrorController,而是通过继承BasicErrorController,是否可以取到原始的请求路径ServletPath?,取消自定义MyControllerAdvice,在BasicErrorController中断点,并观察:

失望,也就是说即使系统默认Controller也不能快捷的得到原始的ServerPath,那么眼下有两种办法,

  •     从coyoteRequest中取
  •     在请求处理前或者说dispatcherservlet分发至/error前记录下这个ServerPath。

先来看第一种:

所需要的数据在coyoteRequest里,获取路径:org.apache.catalina.core.ApplicationHttpRequest-->org.apache.catalina.connector.requestFacade(通过getRequest()获得)-->org.apache.catalina.connector.Request().getCoyoteRequest(),

,实际请求地址在((RequestFacade) ((ApplicationHttpRequest) request).request).request.coyoteRequest里。

研究一下获取路径:

RequestFacade facade = ((RequestFacade)(((ApplicationHttpRequest) request).getRequest()));这里可以轻松获取RequestFacade,但是RequestFacade 中并未提供其成员变量request的访问方法,该成员变量访问控制类型为peotect

由于这里并未提供直接访问的方法,需要换种方法获取[比如通过反射、子类继承获取Request],先看一下Request的实现

requestFacade和Request均实现了HttpServletRequest接口,但是两者没有继承关系,好在Request提供了getCoyoteRequest()方法,解决RequestFacade到Request的转化即可。

确定这个方案可行,先暂停反思一下,ApplicationHttpRequest、RequestFacade、Request均为包含在tomcat-embed-core.jar中,也就是说这些跟中间件强相关,如果更换中间件,这里就存在风险。

再来分析另一种方式

在请求处理前或者说dispatcherservlet分发至/error前记录下这个ServerPath。

由于是分发完成之前,如果使用Aop的话需要在Controller之上,Controller之上有什么呢?HandlerInterceptor?但是拦截器是在dispatcherservlet分发之后,Aop?由于这块没有思路设置切点为止,就先搁置,除此之外,有个更好的方式蹦出来,可以通过Filter,Filter在Servlet之前执行,且对Servlet的执行没有干扰。实现思路如下:

Filter里记录请求入口时记录到ThreadLocal里,ErrorController里取出;Filter代码如下:

import com.miniprogram.pa.LogGuidAspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class PathRecordFilter implements Filter {
    private static Logger logger = LoggerFactory.getLogger(PathRecordFilter.class);
    public static final ThreadLocal<String> servetPath = new NamedThreadLocal<String>("servetPath");
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        LogGuidAspect.init();
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        logger.info("filter :"+request.getServletPath());
        servetPath.set(request.getServletPath());
        chain.doFilter(servletRequest,servletResponse);
    }
}

在自定义的ErrorController里取到这个值:PathRecordFilter.servetPath.get();

根据servletPath及Header里的Accept信息(该思路来源于BasicErrorController)判断是否返回json代码如下:

private final static String[] apiPathStartSet = {"/api/"};
public boolean isJsonRequest(String servletPath, String accept){
        // 根据访问路径判断返回类型
        if (!StringUtils.isEmpty(servletPath)){
            for (String path : apiPathStartSet) {
                if(servletPath.startsWith(path)) {
                    return true;
                }
            }
        }
        // 根据Header里的Accept决策返回类型 这里的决策类似org.springframework.boot.autoconfigure.web.servlet.error.BaseErrorController
        if (!StringUtils.isEmpty(accept)
                && !accept.toLowerCase().contains(MediaType.TEXT_HTML_VALUE)){
            return true;
        }
        return false;
    }

对于自定义信息就简单了,有了原始的访问路径及response,可以知道请求servetPath时发生了什么,也就可以随意自定义错误信息了,当然如果需要其他的可以结合PathRecordFilter取出更多信息。

对于返回结果的处理,比较简单,这里直接给出源码:

 /**
     * 根据请求类型及数据返回
     * @param response 响应
     * @param respVo 自定义的返回信息
     * @param returnJson 是否返回json true: 返回json,false:返回页面
     */
    public ModelAndView dealReturn(boolean returnJson,HttpServletResponse response,BaseRespVo respVo) throws IOException {
        // 处理返回
        if (returnJson) {
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            out.write(JSON.toJSONString(respVo));
            return null;
        }else {
            response.setContentType("text/html;charset=utf-8");
            return new ModelAndView("error").addObject("errorMsg", respVo);
        }
    }

至此/error的问题就解决了

再来看统一异常的处理

1. 类加注解:@ControllerAdvice

2. 方法加注解:@ExceptionHandler(Throwable.class)

   统一异常获取返回类型可以通过HandlerMethod上的注解判断,

思路:HandlerMethod上有@ResponseBody注解或其所在Class有@ResponseBody或@RestController注解。

参考如下:

/**
     * 按异常类型接管系统异常,对于404等异常,并不会接管,也不会进入该方法内(被系统处理至BasicErrorController 返回 getErrorPath()页面)
     * @param e 捕获的异常
     * @param response
     * @param request
     * @param session
     * @param handler 处理本次请求的handler
     * @return
     * @throws IOException
     */
    @ExceptionHandler(Throwable.class)
    public ModelAndView throwableHandler(Throwable e, HttpServletResponse response, HttpServletRequest request, HttpSession session, HandlerMethod handler) throws IOException {
        //输出异常信息
        logger.error("系统统一处理",e);
        // 获取servlet路径
        String servletPath = request.getServletPath();
        logger.error("发生错误在请求:{}",servletPath);
        // 返回json或页面 true:json false:页面,默认页面
        boolean returnJson = false;
        // 确定返回数据
        BaseRespVo respVo = getRespVoByThrowable(e);
        // 根据处理的controller及方法上的注解判断判断,注意:找不到Controller(请求或页面)无法通过该方法确认
        if (handler != null) {
            if (handler.getMethodAnnotation(ResponseBody.class) != null
                    || handler.getBeanType().getAnnotation(RestController.class) != null
                    || handler.getBeanType().getAnnotation(ResponseBody.class) != null) {
                returnJson = true;
            }
        }else {
            // 一般来说 handler不会为null 以下代码不会走进来
            returnJson = isJsonRequest(servletPath, request.getHeader("Accept"));
        }
        // 处理返回
        return dealReturn(returnJson,response,respVo);

    }

需要注意的是ExceptionHandler的源代码说明上并未指明可以使用HandlerMethod,但实际是可以。

回顾一下,其实/error跟全局异常的处理,可以放到一个方法里(需求该对handler != null  的判断为非当前Controller,或当前Servletpath不能为/error),Servletpath在/error请求里需要从pathRecordFilter里取。

error页面代码参考如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
error page
[[${errorMsg}]]
</body>
</html>

整体效果如下:

访问页面不存在且访问路径api开头(Header中Accept信息不对结果产生影响),返回json

访问页面不存在且访问路径非api开头(Header中Accept信息(text/html)对结果产生影响),返回页面

访问页面不存在且访问路径非api开头(Header中Accept信息(text/json)对结果产生影响),返回json

页面存在且报错

完整代码见附件(免费交流)

说明一下:

 BusinessException.java 为自定义异常,可自行实现(继承Exception即可)

ReturnStatusEnum.java 为状态码枚举,结合返回信息要求可自行实现和修改

以上只是一种解决问题的思路和思考过程,仅作为记录和参考一下。

源代码地址:https://download.csdn.net/download/master336/12176728

发布了15 篇原创文章 · 获赞 14 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/master336/article/details/104421223