16.springboot的错误处理机制

1.如果是浏览器,返回一个默认的错误页面:包含了错误信息

2.如果是客户端访问(如用supui来调用,或其他工具来调用时)
返回:是一个json字符串

{
    "timestamp": "2020-09-24T03:02:39.488+00:00",
    "status": 404,
    "error": "Not Found",
    "message": "",
    "path": "/aa"
}
原理:

ErrorMvcAutoConfiguration类:错误处理的自动配置
源码给容器中添加了以下组件:
    1.DefaultErrorAttributes
        帮我们在页面共享信息
            1.时间戳:timesamp
            2.状态码:status
            3.错误提示:error
            4.异常对象:exception
            5.异常消息:message
           6.JSR303做校验:errors
       例如在页面上直接获取:
            <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
               <h1>status:[[${status}]]</h1>
               <h2>timestamp:[[${#dates.format(timestamp,'yyyy-MM-dd')}]]</h2>
            </main>
    2.BasicErrorController:处理默认的/error请求
                部分源码如下:如果没有配置server.error.path,就使用${error.path:/error}}
                @Controller
                @RequestMapping("${server.error.path:${error.path:/error}}")
                public class BasicErrorController extends AbstractErrorController {
                    ...
                    //返回html的格式,浏览器发送的请求来到这个方法处理
                    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
                    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
                       HttpStatus status = getStatus(request);
                       Map<String, Object> model = Collections
                             .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
                       response.setStatus(status.value());
                       //决定错误去哪个页面:modelAndView 包含了页面地址和页面数据
                       ModelAndView modelAndView = resolveErrorView(request, response, status, model);
                       return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
                    }
                    
                    //返回json的格式:其他客户端发送的请求来到这个方法处理
                    @RequestMapping
                    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
                       HttpStatus status = getStatus(request);
                       if (status == HttpStatus.NO_CONTENT) {
                          return new ResponseEntity<>(status);
                       }
                       Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
                       return new ResponseEntity<>(body, status);
                    }
                    ...
                }
            结论:发现都是处理/error的请求,但是有两种处理方式,分别返回html格式和json格式,理由如上述源码!
           这里分为了两种
               1.响应页面
                   源码如下:
                       protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
                              Map<String, Object> model) {
                          //通过所有的异常视图解析器(ErrorViewResolver-->也就是4.DefaultErrorViewResolver )得到ModelAndView
                           for (ErrorViewResolver resolver : this.errorViewResolvers) {
                              ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
                              if (modelAndView != null) {
                                 return modelAndView;
                              }
                           }
                           return null;
                        }
               2.响应json数据 
    3.ErrorPageCustomizer:部分原码如下
                @Value("${error.path:/error}")
                private String path = "/error";
                系统出现错误以后,来到error请求进行处理;(类似于web.xml注册的祖屋页面规则)
        
    4.DefaultErrorViewResolver:决定错误去哪个页面
        源码如下:
            @Override
            public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
               ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
               if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
                  modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
               }
               return modelAndView;
            }
            
            private ModelAndView resolve(String viewName, Map<String, Object> model) {
                //默认springboot可以找到一个页面:路径为error/404或error/其他错误码
               String errorViewName = "error/" + viewName;
               
               //使用模板引擎进行解析,如果可以解析就用模板引擎
               TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
                     this.applicationContext);
               if (provider != null) {
                   //模板引擎可用的情况下返回errorViewName指定的市入地址
                  return new ModelAndView(errorViewName, model);
               }
               //模板引擎不可用的话,就在静态资源文件夹("/META-INF/resources/","/resources/", "/static/", "/public/")下找errorViewName对应的页面  error/404
               return resolveResource(errorViewName, model);
            }


步骤:
    1.一但系统出现4xx或者5xx之类的错误,ErrorPageCustomizer就会生效,(定制错误的响应规则);就会来到/error请求
    2.接着会被BasicErrorController处理,决定返回的时页面还是json数据
    3.若2决定返回的是页面:通过DefaultErrorViewResolver决定返回的是哪个错误页面
那springboot如何区分是到底是浏览器访问还是其他客户端访问呢:
1.浏览器访问时:请求头里会有accept:text/html
2.如果是其它客户端访问时,例如postman:发现其请求头中的accept是:accept:"*/*"

这就是浏览器和客户端发送请求时的不同,也是springboot用于区分的标准


问题:如何定制错误响应
1.如何定制错误页面
1.1有模板引擎的情况下,其底层会去templates文件夹下/error/状态码.html
【将错误页面命名为 错误状态码.html 放在模板引擎文件夹下(templates)里面的error文件夹下的错误码.html,即路径是templates/error/错误码.html】,发生此状态码的错误就会来到对应的页面
1.2我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,精确优先

源码:
static {
   Map<Series, String> views = new EnumMap<>(Series.class);
   views.put(Series.CLIENT_ERROR, "4xx");
   views.put(Series.SERVER_ERROR, "5xx");
   SERIES_VIEWS = Collections.unmodifiableMap(views);
}

1.2在没有模板引擎的情况下如何定制呢?(templates文件夹下没有找到对应的错误码.html)
默认也是在静态资源文件夹下查找!"/META-INF/resources/","/resources/", "/static/", "/public/"
 
1.3模板引擎和静态文件夹下均没有,就是来到springboot的默认的错误提示页面
 
2.如何定制错误响应json

1.定制自己的异常类:
    public class MyException extends RuntimeException{
        public MyException() {
            super("自定义异常!");
        }
    }
2.创建自己的异常处理器
    @ControllerAdvice
    public class MyExceptionHandler {
        @ResponseBody
        @ExceptionHandler(MyException.class)--->处理的是自定义的异常类,也可以是所有异常:Exception.class
        public Map<String,Object> handleException(Exception e){
            Map<String, Object> map=new HashMap<>();
            map.put("code","521");
            map.put("message",e.getMessage());
            return map;
        }
    }
3.抛出异常的地方:当我们输入路径:localhost:8080/hello就会抛出异常
    @RequestMapping("hello")
    public void hellp(){
        throw new MyException();
    }
    
1.浏览器访问:localhost:8080/hello页面出现:

2.postman客户端访问时输出:

结论:但以上的处理没有达到自适应(浏览器访问时返回的时html页面,客户端访问时返回的时json数据)

那如何达到自适应效果呢
1.如何达到自适应效果呢(浏览器返回的是页面/客户端返回的是json数据)
    只需要将自定义异常处理器的代码改为如下:
        @ControllerAdvice
        public class MyExceptionHandler {
            @ExceptionHandler(MyException.class)
            public String handleException(Exception e, HttpServletRequest request){
                //源码中获取异常状态码:
                //Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
                //RequestDispatcher.ERROR_STATUS_CODE=javax.servlet.error.status_code
                //设置自定义的状态码:必须有这行(要不无法异常无法定位到自定义的错误页面)
                request.setAttribute("javax.servlet.error.status_code","521");
                Map<String, Object> map=new HashMap<>();
                map.put("code",400);
                map.put("message",e.getMessage());
                return "forward:/error";---->转发到/error路径:ErrorPageCustomizer会进行处理,和上面的结合上了
            }
        }
我们必须要设置自己的错误状态码,要不虽然可以实现自适应(浏览器返回的是页面/客户端返回的是json数据),但页面上返回的是下面图片,并不是咱们
自定义的页面,并且发现其状态码是200,

客户端返回的数据是:发现其数据是规定的数据,那如何携带自定义数据呢
{
    "timestamp": "2020-09-24T09:27:09.559+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "message": "自定义异常!",
    "path": "/hello"
}

如何给页面或客户端json传入自定义数据
加入以下自定义代码
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
    //返回值的map就是页面和json能获取的所有字段
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, options);
        map.put("wmd","吴孟达");
        return map;
    }
}

结果:不管输入什么错误路径,都会有自定义属性:wmd
{
    "timestamp": "2020-09-24T09:40:11.163+00:00",
    "status": 404,
    "error": "Not Found",
    "message": "",
    "path": "/aa",
    "wmd": "吴孟达"
}

 

最终的解决办法:
1.在原先的基础上:
    自定义异常处理器的代码:
        @ControllerAdvice
        public class MyExceptionHandler {
            @ExceptionHandler(MyException.class)
            public String handleException(Exception e, HttpServletRequest request){
                //设置自定义的状态码
                request.setAttribute("javax.servlet.error.status_code",500);
                Map<String, Object> map=new HashMap<>();
                map.put("code","500");
                map.put("message",e.getMessage());
                map.put("name","吴孟达");------->放入自定义对象
                request.setAttribute("ext",map);----->并将这个map放到request域中
                return "forward:/error";
            }
        }
        
2.自定义错误属性代码:
    @Component
    public class MyErrorAttributes extends DefaultErrorAttributes {
        @Override
        public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
            Map<String, Object> map = super.getErrorAttributes(webRequest, options);
            map.put("ld","刘丹");---->放入自定义属性
            Object ext=webRequest.getAttribute("ext",0);-->并从request域中获取上面放入的自定义对象值:0代表是从request域中获取
            map.put("ext",ext);--->将request域中的自定义属性值放入map中,这样页面和json都可取到
            return map;
        }
    }
    
3.输入错误路径输出:
    {
        "timestamp": "2020-09-24T10:05:48.082+00:00",
        "status": 500,
        "error": "Internal Server Error",
        "message": "",
        "path": "/hello",
        "ld": "刘丹",
        "ext": {
            "code": "500",
            "name": "吴孟达",
            "message": "自定义异常!"
        }
    }

 

 

posted @ 2022-05-10 22:07  努力的达子  阅读(108)  评论(0编辑  收藏  举报