SpringBoot2.0—错误处理原理&定制错误页面

  • SpringBoot默认的错误处理机制
  • 如何定制错误响应:

SpringBoot默认的错误处理机制

默认效果:
1).返回一个默认的错误页面
2).如果是其他客户端,默认返回json数据
原理:
可以参照ErrorMvcAutoConfiguration,该类添加了如下组件
1).DefaultErrorAttributes

@Bean
    @ConditionalOnMissingBean(
        value = {ErrorAttributes.class},
        search = SearchStrategy.CURRENT
    )
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
    }

2).BasicErrorController

@Bean
    @ConditionalOnMissingBean(
        value = {ErrorController.class},
        search = SearchStrategy.CURRENT
    )
    public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
        return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
    }

3).ErrorPageCustomizer

@Bean
    public ErrorMvcAutoConfiguration.ErrorPageCustomizer errorPageCustomizer() {
        return new ErrorMvcAutoConfiguration.ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
    }

4).DefaultErrorViewResolver

@Bean
        @ConditionalOnBean({DispatcherServlet.class})
        @ConditionalOnMissingBean
        public DefaultErrorViewResolver conventionErrorViewResolver() {
            return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
        }

步骤:
4.1).系统出现4xx或者5xx之类的错误,ErrorPageCustomizer就会生效,作用定制错误的响应规则

@Value("${error.path:/error}")
//系统出现错误来到error请求进行处理
    private String path = "/error";

4.2).来到error请求后,由BasicErrorController进行处理

@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {

而BasicErrorController中有以下两种请求:返回html页面和json数据

@RequestMapping(
        produces = {"text/html"}
    )
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = this.getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        //去哪个页面作为错误页面
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = this.getStatus(request);
        return new ResponseEntity(body, status);
    }

如何区分是发送html还是json的?

springboot 400页面_springBoot


根据浏览器发请求的请求头text/html,而其他客户端发送的请求头/*

怎么去错误页面?

protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
        Iterator var5 = this.errorViewResolvers.iterator();

        ModelAndView modelAndView;
        do {
            if (!var5.hasNext()) {
                return null;
            }

            ErrorViewResolver resolver = (ErrorViewResolver)var5.next();
            modelAndView = resolver.resolveErrorView(request, status, model);
        } while(modelAndView == null);

        return modelAndView;
    }

得到所有的ErrorViewResolver 异常视图解析器,得到modelAndView
4.3 ).去哪个页面,由DefaultErrorViewResolver解析得到的

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);
    }

 public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
        }

        return modelAndView;
    }

    private ModelAndView resolve(String viewName, Map<String, Object> model) {
        //默认springBoot可以去找到一个 error/404
        String errorViewName = "error/" + viewName;
        // 模板引擎可以解析这个页面地址就用模板引擎解析
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
        //模板引擎可用的返回errorViewName 指定的视图地址,不可以用调用resolveResource在静态文件夹下找errorViewName 对应的页面error/404.html
        return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
    }

resolveResource在(this.resourceProperties.getStaticLocations())静态文件夹下找
errorViewName 对应的页面error/404.html

private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
        String[] var3 = this.resourceProperties.getStaticLocations();
        int var4 = var3.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            String location = var3[var5];

            try {
                Resource resource = this.applicationContext.getResource(location);
                resource = resource.createRelative(viewName + ".html");
                if (resource.exists()) {
                    return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
                }
            } catch (Exception var8) {
            }
        }

        return null;
    }

如何定制错误响应:

1).如何定制错误的页面:
1.1).a.有模板引擎的情况下,将错误页面命名为错误状态码.html,放在模板引擎文件夹里面的error文件夹下 error/状态码.html
b.我们还可以定制 4xx.html 和5xx作为错误页面来匹配这种类型的所有错误,有精确的(404.html)优先精确的页面
页面能获取的信息

timestamp:时间戳
status:状态码
error:错误提示
exception:异常对象
message:异常消息
errors:JSR303数据校验的错误都在这里

通过DefaultErrorAttributes获取

帮我们在页面共享信息;
 public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap();
        errorAttributes.put("timestamp", new Date());
        errorAttributes.put("path", request.path());
        Throwable error = this.getError(request);
        HttpStatus errorStatus = this.determineHttpStatus(error);
        errorAttributes.put("status", errorStatus.value());
        errorAttributes.put("error", errorStatus.getReasonPhrase());
        errorAttributes.put("message", this.determineMessage(error));
        this.handleException(errorAttributes, this.determineException(error), includeStackTrace);
        return errorAttributes;
    }

1.2).没有模板引擎(模板引擎找不到这个错误页面,比如放在了static文件夹下),在静态资源文件夹下找
1.3).以上都没有错误页面,默认来到springBoot默认的错误提示页面

2).如何定制错误的json数据
2.1).该方式浏览器返回的也是json,没有自适应效果

@ResponseBody
        @ExceptionHandler(UserNotExistException.class)
        public Map<String,Object> handleException(Exception e){
            Map<String,Object> map = new HashMap<>();
            map.put("code","user.notexist");
            map.put("message",e.getMessage());
            return map;
        }

2.1).如何自适应(浏览器返回页面,其他返回json),需要设置错误状态码,否则解析不到错误状态码,导致200也是返回错误页面

@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.notexist");
        map.put("message","用户出错啦");

        request.setAttribute("ext",map);
        //转发到/error
        return "forward:/error";
    }

2.3).自适应外,我们还有讲定制的数据返回
出现错误后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)规定的方法);
a、完全来编写一个ErrorController的实现类【或者是编写AbstractErrorController的子类】,放在容器中;
b.、页面上能用的数据,或者是json返回能用的数据都是通过errorAttributes.getErrorAttributes得到;
容器中DefaultErrorAttributes.getErrorAttributes();默认进行数据处理的;

自定义ErrorAttributes

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 {
    //返回值的map就是页面和json能获取的所有字段
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        map.put("company","123");

        //我们的异常处理器携带的数据
        Map<String,Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
        map.put("ext",ext);
        return map;
    }
}

最终的效果:响应是自适应的,可以通过定制ErrorAttributes改变需要返回的内容.