springboot的错误处理机制以及自定义错误处理

springboot的版本

2.0.2.RELEASE

springboot默认的错误处理机制

默认出错后的效果

  • 使用浏览器请求,出错后返回的信息
默认错误页面.png
  • 使用postman请求,出错后返回的信息
postman请求的出错提示.png

springboot错误处理的原理

  • ErrorMvcAutoConfiguration类中,注入了BasicErrorController来处理默认/error的请求
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class })
public class ErrorMvcAutoConfiguration {
    
    //错误属性的定义
    @Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes(
                this.serverProperties.getError().isIncludeException());
    }
    
    //处理错误信息的控制逻辑
    @Bean
    @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
    public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
        return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
                this.errorViewResolvers);
    }
    
    //默认的错误页面
    @Configuration
    @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
    @Conditional(ErrorTemplateMissingCondition.class)
    protected static class WhitelabelErrorViewConfiguration {

        private final SpelView defaultErrorView = new SpelView(
                "<html><body><h1>Whitelabel Error Page</h1>"
                        + "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
                        + "<div id='created'>${timestamp}</div>"
                        + "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
                        + "<div>${message}</div></body></html>");

        @Bean(name = "error")
        @ConditionalOnMissingBean(name = "error")
        public View defaultErrorView() {
            return this.defaultErrorView;
        }

        // If the user adds @EnableWebMvc then the bean name view resolver from
        // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
        @Bean
        @ConditionalOnMissingBean
        public BeanNameViewResolver beanNameViewResolver() {
            BeanNameViewResolver resolver = new BeanNameViewResolver();
            resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
            return resolver;
        }

    }
    
}
  • BasicErrorController里面处理错误请求的方法
@Controller
//查找配置文件中的server.error.path,如果没有,再从配置文件中查找error.path,如果没有就采用默认的/error
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
   
    //定义了处理错误信息的默认路径  /error
   private final ErrorProperties errorProperties;
  
    //产生html类型的数据;浏览器发送的请求来到这个方法处理
    @RequestMapping(produces = "text/html")
    public ModelAndView errorHtml(HttpServletRequest request,
            HttpServletResponse response) {
        //获取请求的状态信息
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
                request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        //去寻找具体的页面
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null ? modelAndView : new ModelAndView("error", model));
    }
    
    
    //产生json数据,其他客户端来到这个方法处理;
    @RequestMapping
    @ResponseBody
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request,
                isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(body, status);
    }
}
  • 寻找错误页面的处理流程
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request,
      HttpServletResponse response) {
   HttpStatus status = getStatus(request);
   Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
         request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
   response.setStatus(status.value());
   //调用解析方法
   ModelAndView modelAndView = resolveErrorView(request, response, status, model);
   //如果都没有找到,就会采用默认的页面,此页面在ErrorMvcAutoConfiguration也已经注册,WhitelabelErrorViewConfiguration
   return (modelAndView != null ? modelAndView : new ModelAndView("error", model));
}

//调用此方法
protected ModelAndView resolveErrorView(HttpServletRequest request,
            HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
    for (ErrorViewResolver resolver : this.errorViewResolvers) {
        //用视图解析去,是寻找相关页面,没有配置的话,
        //使用org.springframework.boot.autoconfigure.web.servlet.error.DefaultErrorViewResolver
        ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
        if (modelAndView != null) {
            return modelAndView;
        }
    }
    return null;
}



@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
                                     Map<String, Object> model) {
    //默认去 error/  下面去寻找对应http状态码的页面,例如:404.html  500.html
    ModelAndView modelAndView = resolve(String.valueOf(status), model);
    
    //去寻找 4xx   5xx开头的页面
    if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
        modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
    }
    return modelAndView;
}

//去error/  路径下寻找页面
private ModelAndView resolve(String viewName, Map<String, Object> model) {
    String errorViewName = "error/" + viewName;
    TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
        .getProvider(errorViewName, this.applicationContext);
    if (provider != null) {
        return new ModelAndView(errorViewName, model);
    }
    return resolveResource(errorViewName, model);
}

//再去classpath:/META-INF/resources/", "classpath:/resources/","classpath:/static/", "classpath:/public/"这些路径下,去寻找对应的状态码的html页面
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
    for (String location : this.resourceProperties.getStaticLocations()) {
        try {
            Resource resource = this.applicationContext.getResource(location);
            resource = resource.createRelative(viewName + ".html");
            if (resource.exists()) {
                return new ModelAndView(new HtmlResourceView(resource), model);
            }
        }
        catch (Exception ex) {
        }
    }
    return null;
}
  • 错误的json数据里面属性的来源

    这里面定义的属性有:

    timestamp:时间戳

    status:状态码

    error:错误提示

    exception:异常对象

    path:路径

    package org.springframework.boot.web.servlet.error;
    /**
     * Default implementation of {@link ErrorAttributes}. Provides the following attributes
     * when possible:
     * <ul>
     * <li>timestamp - The time that the errors were extracted</li>
     * <li>status - The status code</li>
     * <li>error - The error reason</li>
     * <li>exception - The class name of the root exception (if configured)</li>
     * <li>message - The exception message</li>
     * <li>errors - Any {@link ObjectError}s from a {@link BindingResult} exception
     * <li>trace - The exception stack trace</li>
     * <li>path - The URL path when the exception was raised</li>
     * </ul>
     *
     * @author Phillip Webb
     * @author Dave Syer
     * @author Stephane Nicoll
     * @author Vedran Pavic
     * @since 2.0.0
     * @see ErrorAttributes
     */
     
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public class DefaultErrorAttributes
          implements ErrorAttributes, HandlerExceptionResolver, Ordered {
    

自定义错误响应

  • 可以定义一个我们自己的错误类

    package com.nanc.exception;
    
    public class UserNotExistException extends RuntimeException{
    
       public UserNotExistException(){
          super("异常信息---用户不存在");
       }
    }
    
  • 自定义异常处理类

    package com.nanc.controller;
    
    import com.nanc.exception.UserNotExistException;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.HashMap;
    import java.util.Map;
    
    @ControllerAdvice
    public class MyExceptionHandler {
    
       //这个没有自适应效果(浏览器请求返回页面,其他的就返回json数据),可以转发到/error就可以自适应了
       //@ResponseBody
       //@ExceptionHandler(UserNotExistException.class)
       //public Map<String, Object> handleException(Exception e) {
       // Map<String, Object> map = new HashMap<>();
       //
       // map.put("code", "user not exist");
       // map.put("message", e.getMessage());
       //
       // return map;
       //}
    
       //@ExceptionHandler(UserNotExistException.class)
       //public String handleException(Exception e) {
       // Map<String, Object> map = new HashMap<>();
       //  //自己定义的数据在返回的数据中无法使用
       // map.put("code", "user not exist");
       // map.put("message", e.getMessage());
       //
       // //转发到/error
       // return "forward:/error";
       //}
    
       @ExceptionHandler(UserNotExistException.class)
       public String handleException(Exception e, HttpServletRequest request) {
          Map<String, Object> map = new HashMap<>();
          //传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程
          /*
           * Integer statusCode = (Integer) request
           .getAttribute("javax.servlet.error.status_code");
           */
          request.setAttribute("javax.servlet.error.status_code", 500);
    
          map.put("code", "user not exist");
          map.put("message", e.getMessage());
          request.setAttribute("ext", map);
    
          //转发到/error
          return "forward:/error";
       }
    
    
    }
    
  • 定制自己的一些错误属性

    package com.nanc.component;
    
    import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.WebRequest;
    
    import java.util.Map;
    
    //给容器中加入我们自己定义的ErrorAttributes
    @Component
    public class MyErrorAttributes extends DefaultErrorAttributes {
    
       @Override
       public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
          Map<String, Object> map =  super.getErrorAttributes(webRequest, includeStackTrace);
    
          map.put("company","xxxxx");
    
          //我们的异常处理器携带的数据,这些数据是从MyExceptionHandler中传递过来的
          Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
          map.put("ext",ext);
          return map;
       }
    }
    
  • 错误页面

    错误页面.png
<!-- 提供5xx页面中的部分信息 -->
<!-- 使用默认的thymeleaf模板 -->
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
   <h1>status:[[${status}]]</h1>
   <h2>timestamp:[[${timestamp}]]</h2>
   <h2>timestamp:[[${#dates.format(timestamp, 'yyyy-MM-dd HH:mm:ss')}]]</h2>
   <h2>exception:[[${exception}]]</h2>
   <h2>message:[[${message}]]</h2>
   <h2>path:[[${path}]]</h2>
   <h2>errors:[[${errors}]]</h2>
   <h2>ext:[[${ext.code}]]</h2>
   <h2>ext:[[${ext.message}]]</h2>
</main>

注意事项

如果请求返回的是json数据,时间可能会差8个小时,解决办法,在application.yml添加如下配置

spring:
  mvc:
    date-format: yyyy-MM-dd
  jackson: # 解决差8个小时的问题
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容