1、默认的错误处理机制
https://blog.csdn.net/qq_40634846/article/details/107463007
1.1 SpringBoot默认的错误效果
有时候点击添加按钮,跳转页面失败(显示报错找不到路径)。或者输入日期信息,点击提交按钮,发生4xx状态报错。然后会跳转到下图这个错误页面(这是SpringBoot默认的错误处理页面),这个就是错误处理。
1.1 浏览器,返回一个默认的错误页面

浏览器发送请求的请求头

1.2 如果是其他客户端(app),默认响应一个json数据(postman模拟)
客户端的请求头
1.2 错误处理原理
SpringBoot默认配置的错误处理都是由ErrorMvcAutoConfiguration类配置的。
1.2.1 错误处理的核心组件
核心组件都是在ErrorMvcAutoConfiguration配置的。有如下组件:
- DefaultErrorAttributes
- BasicErrorController
- ErrorPageCustomizer
- DefaultErrorViewResolver
SpringBoot自动配置错误处理是由ErrorMvcAutoConfiguration决定的。其中有4个核心组件起着重要作用,分别是:DefaultErrorAttributes、DefaultErrorViewResolver、BasicErrorController、ErrorPageCustomizer
1.2.2 ErrorPageCustomizer
一旦系统出现4xx或者5xx之类的错误,ErrorPageCustomizer就会生效(ErrorPageCustomizer是定制错误的响应规则)
ErrorMvcAutoConfiguration里面是这样注册ErrorPageCustomizer的,如下:
@Beanpublic ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);}
点进去看ErrorPageCustomizer的内容是什么,如下:
/*** {@link WebServerFactoryCustomizer} that configures the server's error pages.*/private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {private final ServerProperties properties;private final DispatcherServletPath dispatcherServletPath;protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {this.properties = properties;this.dispatcherServletPath = dispatcherServletPath;}@Overridepublic void registerErrorPages(ErrorPageRegistry errorPageRegistry) {ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));errorPageRegistry.addErrorPages(errorPage);}@Overridepublic int getOrder() {return 0;}}
并且发现ErrorPageCustomizer类是ErrorMvcAutoConfiguration的静态内部类。
可以看到它有一个重载方法registerErrorPages(),如下:
@Overridepublic void registerErrorPages(ErrorPageRegistry errorPageRegistry) {ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));errorPageRegistry.addErrorPages(errorPage);}
发生错误以后,系统会去哪个路径呢?这个是由registerErrorPages()中的getPath()决定的,getPath()方法如下:
public String getPath() {return this.path;}
再点击path,查看它是怎么定义的,如下:
/*** Path of the error controller.*/@Value("${error.path:/error}")private String path = "/error";
path是从配置文件中的error.path属性获取值,缺省值为/error
从上面path变量的定义可以得出,系统发生4xx/5xx错误后,会来到/error请求(也就是去到@RequestMapping(value=”/error”)的controller中进行处理),请求处理此错误。
1.2.3 BasicErrorController
上面谈到,系统一发生4xx/5xx就会默认去/error的controller中处理请求。
SpringBoot自动配置类ErrorMvcAutoConfiguration中的BasicErrorController组件,如下:
@Bean@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,ObjectProvider<ErrorViewResolver> errorViewResolvers) {return new BasicErrorController(errorAttributes, this.serverProperties.getError(),errorViewResolvers.orderedStream().collect(Collectors.toList()));}
点进去看看BasicErrorController是什么内容,如下:
@Controller@RequestMapping("${server.error.path:${error.path:/error}}")public class BasicErrorController extends AbstractErrorController {
可以看到它是一个controller,而且处理的请求是配置文件中server.error.path属性值,缺省值是error.path属性值,error.path的缺省值是/error。从ErrorPageCustomizer类中,我们学习到系统一发生4xx/5xx就会默认去/error的controller中处理请求。也就是默认会去BasicErrorController请求处理。
再看到BasicErrorController有2个处理请求的核心方法,分别是 errorHtml()、error(),如下:
@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, isIncludeStackTrace(request, MediaType.TEXT_HTML)));response.setStatus(status.value());ModelAndView modelAndView = resolveErrorView(request, response, status, model);return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);}@RequestMappingpublic 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, isIncludeStackTrace(request, MediaType.ALL));return new ResponseEntity<>(body, status);}
第一个方法errorHtml()返回值为ModelAndView,而且该方法会产生html类型的数据。这个方法就是在浏览器中展示错误页面的处理方法。比如浏览器访问某个地址,发生4xx错误,那么controller方法响应给它的是一个浏览器能显示的视图。
第二个方法返回值为ResponseEntity
是根据什么来区分到底用errorHtml()来处理/error还是用error()处理呢?
答:是根据请求头Request Headers的Accept属性来区分的。
解释:
在浏览器出现错误后,页面跳转到SpringBoot默认的错误页面,按F12,选择network可以看到如下:
因此浏览器发送的/error请求,BasicErrorController会使用errorHtml()来处理,因为errorHtml()是会产生html类型的数据。
在其他客户端出现错误后,发送的/error请求中,会有如下:
所以除了浏览器以外发送的请求,都会由BasicErrorController的error()方法处理。
总结1:浏览器发送的请求,会来到BasicErrorController的errorHtml()方法处理;其他客户端则来到BasicErrorController的error()方法处理。
总结2:一旦系统发生4xx/5xx之类的错误,ErrorPageCustomizer就会生效,就会发送/error请求,来到BasicErrorController的errorHtml()或error()进行处理。
现在分析是浏览器发送请求,所以详细看errorHtml()的内容,如下:
@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, isIncludeStackTrace(request, MediaType.TEXT_HTML)));response.setStatus(status.value());ModelAndView modelAndView = resolveErrorView(request, response, status, model);return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);}
从倒数第二行看到,最终返回的是modelAndView或者new ModelAndView(x, model),而modelAndView是由resolveErrorView(x,x,x, model)。(注意,这里无论是由resolveErrorView()得到的modelAndView,还是new ModelAndView(),他们都使用了model这个数据。后面需要知道这个知识点)
先了解resolveErrorView(),它的作用是去哪个页面作为错误页面:包含页面地址以及页面内容。
resolveErrorView()方法如下:
protected ModelAndView resolveErrorView(HttpServletRequest request,HttpServletResponse response,HttpStatus status, Map<String, Object> model) {for (ErrorViewResolver resolver : this.errorViewResolvers) {ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);if (modelAndView != null) {return modelAndView;}}return null;}
resolveErrorView()方法是拿到所有的errorViewResolvers,其中一个就是DefaultErrorViewResolver(后面会详细讲此组件)。这里的遍历使用了设计模式中的策略模式。拿到xxxErrorViewResolver后,就会执行resolverErrorView(),并将值赋给modelAndView。如果不为空,则把这个modelAndView返回出去,否则继续遍历。遍历完后,modelAndView仍为空,则返回null。所以结合上面errorHtml()方法返回值,当modelAndView为空的时候,它会new一个ModelAndView。
总结:errorHtml()方法里面是会调用到resolveErrorView()方法,而resolveErrorView()方法里面又会拿到所有的xxxErrorViewResolver,其中一个ErrorViewResolver就是DefaultErrorViewResolver。拿到这个DefaultErrorViewResolver的实例后,又会调用它的resolveErrorView()方法。 因此后面讲解DefaultErrorViewResolver
1.2.4 DefaultErrorViewResolver
上面提到errorHtml()中最终会调用到DefaultErrorViewResolver的resolveErrorView()方法。
首先我们从根源找DefaultErrorViewResolver,在ErrorMvcAutoConfiguration中有如下:
@Bean@ConditionalOnBean(DispatcherServlet.class)@ConditionalOnMissingBean(ErrorViewResolver.class)DefaultErrorViewResolver conventionErrorViewResolver() {return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);}
点击DefaultErrorViewResolver,看看内容是什么,如下:(代码中的注解就是解释,注意看)
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {private static final Map<Series, String> SERIES_VIEWS;static {Map<Series, String> views = new EnumMap<>(Series.class);views.put(Series.CLIENT_ERROR, "4xx");//所有错误页面都是用各自的状态码命名,太麻烦了views.put(Series.SERVER_ERROR, "5xx");//这里使用4xx,5xx命名SERIES_VIEWS = Collections.unmodifiableMap(views);}
可以看到它会处理4xx以及5xx的错误。
再来看看DefaultErrorViewResolver的 resolveErrorView() 方法,如下:
@Overridepublic 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;}
从代码可知,最终返回的是modelAndView,它是由 resolve() 方法得来的。
再来看看rsolve()方法,如下:(代码中的注解就是解释,注意看)
private ModelAndView resolve(String viewName, Map<String, Object> model) {//SpringBoot默认可以找到一个error/404之类的页面。String errorViewName = "error/" + viewName;//模板引擎可以解析这个页面就用模板引擎解析TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,this.applicationContext);if (provider != null) {//模板引擎可用的情况下,返回 error/viewName指定的视图地址return new ModelAndView(errorViewName, model);}//模板引擎不可用,就在静态资源文件夹下找error/ViewName对应的页面,比如error/404return resolveResource(errorViewName, model);}
第二行代码可以看出,SpringBoot默认可以去找到一个页面,路径是error/viewName,而viewName是由入参得来的,而resolveErrorView传递进的入参是一个枚举类型,里面有很多状态码(比如1xx,2xx,3xx,4xx,5xx)。也就是SpringBoot可以找到一个error/404之类的页面。
最后一行调用了resolveResource(),如下:(代码中的注解就是解释,注意看)
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {//获取所有的存放静态资源的地址for (String location : this.resourceProperties.getStaticLocations()) {try {Resource resource = this.applicationContext.getResource(location);//创建错误处理的响应页面的名称,命名格式为xxx.html,比如为4xxx.html 或者 5xxx.html等等resource = resource.createRelative(viewName + ".html");if (resource.exists()) {//返回ModelAndViewreturn new ModelAndView(new HtmlResourceView(resource), model);}}catch (Exception ex) {}}return null;}
总结:
从上面分析errorHtml()处理请求是怎么样的,我们可以自己定制错误页面。
- 有模模板引擎的情况下:使用error/状态码。将错误页面命名为错误状态码.html,放在模板引擎文件夹里面的error文件夹下,发生此状态的错误就会来到对应的页面。由于错误状态码有太多太多,因此我们可以使用4xx/5xx匹配这种类型的所有错误,精确优先(优先寻找 精确的状态码.html)
- 没有模板引擎的情况下:会去静态资源文件夹下的error文件夹寻找
1.2.5 DefaultErrorAttributes
了解完SpringBoot是怎么寻找错误页面后,我们继续了解错误页面上的数据内容是从哪里获取的,以及可以获取什么数据。
错误页面上的数据肯定在modelAndView中,所以我们从controller中找起,前面我们了解过BasicErrorController,因此到BasicErrorController的errorHtml()找, 如下:(注意代码中的注释)
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {HttpStatus status = getStatus(request);//2.model是由getErrorAttributes()获取的Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));response.setStatus(status.value());//1. 这里model被传入modelAndViewModelAndView modelAndView = resolveErrorView(request, response, status, model);return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);}
从上面看到model被传入modelAndView,model是由getErrorAttributes()获取的。点进去看看getErrorAttributes(),如下:
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {WebRequest webRequest = new ServletWebRequest(request);return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);}
点进去看看errorAttributes,如下:
public abstract class AbstractErrorController implements ErrorController {private final ErrorAttributes errorAttributes;
点进去看看ErrorAttributes,如下:
public interface ErrorAttributes {Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace);Throwable getError(WebRequest webRequest);}
ErrorAttributes是一个接口,我们将光标放在ErrorAttributes,按ctrl+alt+b,就会打开到如下:
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
最终找到DefaultErrorAttributes,这个类在ErrorMvcAutoConfiguration中是有注册的。
DefaultErrorAttribute中最关键的一个属性是includeException,它是一个布尔值而且缺省值是false(所以它会导致某些异常信息不能被存储到某个Map中),通过它来决定是否添加某些异常信息到errorAttributes(这是一个Map,存储异常信息)中。
private final boolean includeException;public DefaultErrorAttributes() {this(false);}
在DefaultErrorAttributes中找到getErrorAttributes(),如下:
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {Map<String, Object> errorAttributes = new LinkedHashMap();errorAttributes.put("timestamp", new Date());this.addStatus(errorAttributes, webRequest);this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);this.addPath(errorAttributes, webRequest);return errorAttributes;}
使用getErrorAttributes()方法,错误页面最终能获取到信息有如下:
- timestamp:时间戳
- status:状态码
- error:错误提示
- exception:异常对象
- message:异常信息
- eerrors:JSR303数据校验都在这里
总结:错误页面最终能获取到信息有timestamp:时间戳, status:状态码,error:错误提示, exception:异常对象,message:异常信息, eerrors:JSR303数据校验都在这里
1.2.6 SpringBoot默认的错误提示
在ErrorMvcAutoConfiguration中,有WhitelabelErrorViewConfiguration静态内部类,如下:
protected static class WhitelabelErrorViewConfiguration {private final StaticView defaultErrorView = new StaticView();//这里存储了默认的错误页面内容@Bean(name = "error")@ConditionalOnMissingBean(name = "error")public View defaultErrorView() {return this.defaultErrorView;//返回默认错误视图}
点击StaticView,可以看到如下:
private static class StaticView implements View {
它也是一个静态内部类,它有一个render()方法。
render()方法有部分代码如下:
builder.append("<html><body><h1>Whitelabel Error Page</h1>").append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>").append("<div id='created'>").append(timestamp).append("</div>").append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error"))).append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
1.2.7 四个核心组件之间的关系
四个核心组件,指的是前面介绍的ErrorPageCustomizer、BasicErrorController、DefaultErrorAttributes、DefaultErrorViewResolver。
关系如下:
1.2.8 总结
2. 定制错误页面
https://blog.csdn.net/qq_40634846/article/details/107710404
整理SpringBoot寻找错误页面的路径,如下:

所以 定制页面的方法 如下:
- 可以在templates/error/下创建状态码.html文件
- 可以在静态资源文件夹/error/下创建状态码.html文件
2.1 静态资源文件夹下面创建一个error文件夹
在静态资源文件夹下面创建一个error文件夹,在里面放置自己的错误页面。
在BasicErrorController中,封装视图的时候,当前项目如果有模板引擎,会先用模板引擎解析,找不到再去静态资源文件夹寻找视图(视图名是error/状态码.html,这是指error文件夹下的状态码.html文件,这是由私有方法实现的,所以外部无法修改)。因此上面的步骤就是SpringBoot先去templates文件夹下找,找不到,再去静态资源文件夹找(前提是你设置了静态资源文件夹;否则默认按SpringBoot的默认静态资源文件夹找,比如resources、public、static、resource)
2.2 WebServerFactoryCustomizer
创建一个MyErrorPageConfig类,最最最关键的是new ErrorPage()中第二个入参,如果是加了.html,那么就会直接找页面,如果没有加,那么将它当作请求去找controller,如下:
@Configurationpublic class MyErrorPageConfig {@Beanpublic WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {@Overridepublic void customize(ConfigurableWebServerFactory factory) {ErrorPage errorPage1 = new ErrorPage(HttpStatus.NOT_FOUND, "/page/error/4xx.html");ErrorPage errorPage2 = new ErrorPage(HttpStatus.NOT_FOUND, "/page/error/5xx.html");factory.addErrorPages(errorPage1, errorPage2);}};}}
2.3 实现自适应
自适应就是根据发送的/error请求是浏览器还是客户端,使用不同的controller方法进行处理,并返回不同类型的数据
**
创建一个实现了ErrorController接口的MyBasicErrorController类,如下:
/*** 定制ErrorController,目的是能使SpringBoot找到自己定制的错误页面* 大部分的代码BasicController一致,关键点是修改错误页面的路径*/@Controller@RequestMapping(value = "/error")public class MyBasicErrorController implements ErrorController {@RequestMapping(produces = {"text/html"})//返回给浏览器public String handlerError(HttpServletRequest request, Model model){WebRequest webRequest = new ServletWebRequest(request);//对request进行包装,目的是能操作更多的方法HttpStatus status = this.getStatus(request);//获取statusString path = (String) webRequest.getAttribute("javax.servlet.error.request_uri", 0);String message = (String) webRequest.getAttribute("javax.servlet.error.message", 0);if(message.equals("")){message = "No Available Message";}//携带错误数据信息model.addAttribute("timestamp", new Date());model.addAttribute("statusCode", status.value());model.addAttribute("error", status.getReasonPhrase());model.addAttribute("message", message);model.addAttribute("path", path);int i = status.value() / 100;//判断是4xx还是5xx错误if(i == 4){return "layuimini/page/error/4xx";//使用自己定制的错误页面}else if(i == 5){return "layuimini/page/error/5xx";//使用自己定制的错误页面}return null;}@RequestMapping//返回给客户端public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {WebRequest webRequest = new ServletWebRequest(request);//对request进行包装,目的是能操作更多的方法HttpStatus status = this.getStatus(request);//获取statusMap<String, Object> map = new HashMap<>();if (status == HttpStatus.NO_CONTENT) {return new ResponseEntity(status);} else {String path = (String) webRequest.getAttribute("javax.servlet.error.request_uri", 0);String message = (String) webRequest.getAttribute("javax.servlet.error.message", 0);map.put("timestamp", new Date());map.put("statusCode", status.value());map.put("error", status.getReasonPhrase());map.put("message", message);map.put("path", path);return new ResponseEntity(map, status);}}protected HttpStatus getStatus(HttpServletRequest request) {Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code");if (statusCode == null) {return HttpStatus.INTERNAL_SERVER_ERROR;} else {try {return HttpStatus.valueOf(statusCode);} catch (Exception var4) {return HttpStatus.INTERNAL_SERVER_ERROR;}}}@Overridepublic String getErrorPath() {return "null";}}
SpringBoot的错误页面机制的自适应,是由BasicErrorController实现的,而这个BasicErrorController只有在容器中没有ErrorController的情况下,才会被注册进容器,因此我们创建一个实现了ErrorController接口的类,这个BasicErrorController就失效,然后我们仿照BasicErrorController里面的方法来实现自己的controller就可以了。如下:
@Bean@ConditionalOnMissingBean(value = {ErrorController.class},//没有ErrorController才会去注册BasicErrorControllersearch = SearchStrategy.CURRENT)public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) {return new BasicErrorController(errorAttributes, this.serverProperties.getError(), (List)errorViewResolvers.orderedStream().collect(Collectors.toList()));}
3. 定制错误数据
https://blog.csdn.net/qq_40634846/article/details/107488716
3.1 自定义错误异常UserNotExistException
定义异常类并继承RuntimeExcception,如下:
public class UserNotExistException extends RuntimeException{public UserNotExistException() {super("用户不存在");}}
定义controller,如下:
@ResponseBody@RequestMapping("/hello")public String hello(@RequestParam("user")String user){if(user.equals("aaa")){throw new UserNotExistException();}return "hello world!";}
并且在错误页面获取某些错误信息,如下:
<h1>status:[[ ${status} ]]</h1><h2>timestamp:[[ ${timestamp} ]]</h2><h2>exception:[[ ${exception} ]]</h2><h2>message:[[ ${message} ]]</h2>
浏览器地址输入localhost:8080/crud/hello?user=aaa,就会来到controller并抛出UserNotExistException异常。效果如下:

如果错误页面上的exception为空白,只需在application.properties上配置server.error.include-exception=true。原理可以参考SpringBoot获取不到${exception}或Day41——错误处理原理&定制错误页面。
3.2 初步定制错误数据
前面已经定义好错误异常UserNotExistException,现在定义处理这个UserNotExistException的controller。如下:
MyExceptionHandler.java
@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;}
这里定制错误数据code以及message。方法返回值的类型是json。因此,这样的定制错误数据,浏览器或客户端返回的都是json类数据。
3.3 改进定制错误数据——自适应
根据前面学习过的错误原理机制。我们只需将controller方法的返回值改成返回到/error,相当于发送了error请求,这样BasicErrorController就会根据Request Headers请求头的Accept属性决定发送哪种类型的错误视图,是发json数据,还是发text/html数据。这样就能完成自适应了。
如下:
//2. 方式二:自适应返回数据@ExceptionHandler(UserNotExistException.class)public String handleException(Exception e, HttpServletRequest request){Map<String, Object> map = new HashMap<>();map.put("code", "user.notexist");map.put("message", "用户出错了");//实现自适应性,使得能通过发送器/error请求去到错误页面的处理方法return "forward:/error";}
效果:
浏览器的错误页面

其他客户端的数据

现在虽然能自适应了,但是状态码却变成了200,而且错误页面也不是我们自己定制的错误页面。
3.4 实现自适应并前往自定制错误页面
3.4.1 分析
从前面的效果可以看到状态码变成了200,而不是SpringBoot默认的4xx/5xx。因此我们应该将状态码设置成4xx/5xx再将视图返回出去。
在BasicErrorController中的errorHtml()中有一句是设置状态码,如下:(注意注释)
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {HttpStatus status = getStatus(request);//2.调用getStatus()获取statusMap<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));response.setStatus(status.value());//1.设置响应状态码statusModelAndView modelAndView = resolveErrorView(request, response, status, model);return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);}
如上面代码中所述,我们查看getStatus()是什么内容,如下:(注意注释)
protected HttpStatus getStatus(HttpServletRequest request) {//1.原来它是通过request.getAttribute("javax.servlet.error.status_code")获取状态码Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");if (statusCode == null) {return HttpStatus.INTERNAL_SERVER_ERROR;}try {return HttpStatus.valueOf(statusCode);}catch (Exception ex) {return HttpStatus.INTERNAL_SERVER_ERROR;}}
如上代码所述,由此 我们可以在异常处理的controller方法中设置状态码,然后这个状态码会被上面的getStatus()方法拿出来,从而实现前往自己的错误页面。
3.4.2 实现
//2. 方式二:自适应返回数据@ExceptionHandler(UserNotExistException.class)public String handleException(Exception e, HttpServletRequest request){Map<String, Object> map = new HashMap<>();/*** 传入自己的错误状态码4xx,5xx,使得springboot去到自己定制的错误页面。* 否则不会进入定制错误页面的解析流程*//*** SpringBoot是这样取得状态码的:* Integer statusCode =* (Integer) request.getAttribute("javax.servlet.error.status_code");*///因此我们识用request.setAttribute即可request.setAttribute("javax.servlet.error.status_code", 500);map.put("code", "user.notexist");map.put("message", "用户出错了");//实现自适应性,使得能通过发送器/error请求去到错误页面的处理方法return "forward:/error";}
在错误页面显示出code、message,如下:
<h1>status:[[ ${status} ]]</h1><h2>timestamp:[[ ${timestamp} ]]</h2><h2>exception:[[ ${exception} ]]</h2><h2>message:[[ ${message} ]]</h2><h2>company:[[ ${company} ]]</h2><h2>code:[[ ${ext.code} ]]</h2><h2>message:[[ ${ext.message} ]]</h2>
效果:

客户端也没有自己定制的错误数据,如下:

总结:能来到自己定制的错误页面,但是与自己定制的错误数据不同
3.5 将自己定制的数据携带出去
3.5.1 分析
将数据封装到视图中是发生在BasicErrorController中的errorHtml()以及error()中的,如下:(注意注释)
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {HttpStatus status = getStatus(request);//2. model是通过getErrorAtributes()获取到的Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));response.setStatus(status.value());//1. model被封装ModelAndView对象中ModelAndView modelAndView = resolveErrorView(request, response, status, model);return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);}@RequestMappingpublic ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {HttpStatus status = getStatus(request);if (status == HttpStatus.NO_CONTENT) {return new ResponseEntity<>(status);}//1.也是通过调用getErrorAttributes()获取到数据Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));return new ResponseEntity<>(body, status);}
getErrorAtributes()是AbstractErrorController的方法,而且AbstractErrorController实现了ErrorController接口(这里很关键),如下:
public abstract class AbstractErrorController implements ErrorController {...protected Map<String, Object> getErrorAttributes(HttpServletRequest request,boolean includeStackTrace) {WebRequest webRequest = new ServletWebRequest(request);//1.当前方法又调用了errorAttributes.getErrorAttributes()获取数据return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);}
总结:不管是errorHtml()方法还是error()方法,封装的错误数据是从AbstractErrorController的getErrorAttributes获取到的(而且AbstractErrorController实现了ErrorController接口)。getErrorAttributes()里面又调用了errorAttributes.getErrorAttributes()。
从上面代码看到,最终是errorAttributes调用getErrorAttributes()获取数据。
看看这个getErrorAttributes()内容,如下:
public interface ErrorAttributes {Map<String, Object> getErrorAttributes(WebRequest webRequest,boolean includeStackTrace);Throwable getError(WebRequest webRequest);}
如上所示,它是一个接口,将光标放在ErrorAttributes,按ctrl+alt+b查看它的实现类,如下:

原来DefaultErrorAttributes是它的实现类。
DefaultErrorAttributes里面的getErrorAttributes()方法如下:
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {Map<String, Object> errorAttributes = new LinkedHashMap();errorAttributes.put("timestamp", new Date());this.addStatus(errorAttributes, webRequest);this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);this.addPath(errorAttributes, webRequest);return errorAttributes;}
如上所示,最终找到了封装的错误数据是从DefaultErrorAttributes.getErrorAttributes获得的。
总结:最终最终,封装的错误数据是从DefaultErrorAttributes.getErrorAttributes获得的。
3.5.2 将数据携带出去的过程
SringBoot自动配置是这样做的:
出现错误,发送/error请求,来到BasicErrorController,根据自适应,去errorHtml()或者error()方法及逆行处理。处理的过程中,响应的数据由 getErrorAttributes()(它是AbstractErrorController(实现了ErrorController接口)的方法) 得到,最终会调用errorAttributes.getErrorAttributes()得到数据。最终最终会调用DefaultErrorAttributes的getErrorAttributes()得到数据。
总结一:因此,我们可以编写AbstractController的子类,放在容器中,从而将自己定制的错误数据携带出去。但是有更简单的解决方案,后面讲
自动配置是以上的做法,我们查看ErrorMvcAutoConfiguration,如下:(注意注释)
@Bean//1.当没有ErrorController,才会注册BasicErrorControlelr。才会有以上的阐述的做法。@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,ObjectProvider<ErrorViewResolver> errorViewResolvers) {return new BasicErrorController(errorAttributes,this.serverProperties.getError(),errorViewResolvers.orderedStream().collect(Collectors.toList()));}
总结二:也就是我们完全可以编写一个ErrorController的实现类,放在容器中,从而将自己定制的错误数据携带出去。但是有更简单的解决方案,后面讲
因为最终会调用DefaultErrorAttributes的getErrorAttributes()得到数据。所以来看看自动配置类ErrorMvcAutoConfiguration中是怎么注册DefaultErrorAttributes的,如下:
@Bean//1.当没有ErrorAttributes时,才会注册DefaultErrorAttributes@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)public DefaultErrorAttributes errorAttributes() {return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());}
综上所述,可以编写一个DefaultErrorAttributes的子类来携带自己定制的错误数据。
总结三:编写一个DefaultErrorAttributes的子类来携带自己定制的错误数据。
3.5.3 将数据携带出去的方案
方案一:编写AbstractController的子类或者编写ErrorController的实现类,放到容器中。但是编写controlelr过于沉重繁琐,不推荐此方案。
方案二:编写DefaultErrorAttributes的子类或者ErrorAttributes的实现类,放到容器中。推荐此方案
3.5.4 实现
首先在处理异常的方法(发送/error之前)中将扩展的数据放在ext中,如下:
/*** 定制错误数据*/@ControllerAdvicepublic class MyExceptionHandler {//2. 方式二:自适应返回数据@ExceptionHandler(UserNotExistException.class)public String handleException(Exception e, HttpServletRequest request){Map<String, Object> map = new HashMap<>();/*** 传入自己的错误状态码4xx,5xx,使得springboot去到自己定制的错误页面。* 否则不会进入定制错误页面的解析流程*//*** SpringBoot是这样取得状态码的:* Integer statusCode =* (Integer) request.getAttribute("javax.servlet.error.status_code");*///因此我们使用request.setAttribute即可request.setAttribute("javax.servlet.error.status_code", 500);map.put("code", "user.notexist");map.put("message", "用户出错了");request.setAttribute("ext", map);//将扩展的数据放在ext中//实现自适应性,使得能通过发送器/error请求去到错误页面的处理方法return "forward:/error";}
编写DefaultErrorAttributes子类,获取SpringBoot的默认错误数据以及在异常处理的方法中自己定制的数据,如下:
package com.atguigu.springboot.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*/@Componentpublic class MyErrorAttributes extends DefaultErrorAttributes {/*** 返回值的map就是页面和json能获得的所有字段,不仅能自适应返回html数据还是json数据,* 还能返回xxxExceptionhandler定制的数据*/@Overridepublic Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);map.put("company", "atguigu");//这里也可以自定义数据//webRequest是将requestAttributes包装起来的对象,而requestAttributes是对request包装起来的对象// webRequest.getAttribute()获取在MyExceptionHandler中封装好的数据//入参0表示从request域中获取,1表示从session域中获取//ext是我们异常处理器MyExceptionHandler携带的数据Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);map.put("ext", ext);return map;}}
效果:
3.6 解决exception显示不出来
3.6.1 情况
在添加了自己编写的MyErrorAttributes的时候,exception对象的信息又显示不出来了。
3.6.2 分析
原因肯定是因为调用super.getErrorAttributes(webRequest, includeStackTrace)时,super调用了入参includeException为false的DefaultErrorAttributes构造器。
因此我们需要传入值为true的includeException到DefaultErrorAttributes构造器中。
3.6.3 解决
在自定义的ErrorAttribute中,通过构造器,将includeException=true传给DefaultErrorAttributes
解决方案如下:
//MyErrorAttributes是我定义的DefaultErrorAttributes的子类public MyErrorAttributes() {//将includeException=true传给DefaultErrorAttributes,使得能获取出exception对象super(true);}
完整代码如下:
package com.atguigu.springboot.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*/@Componentpublic class MyErrorAttributes extends DefaultErrorAttributes {public MyErrorAttributes() {//将includeException=true传给DefaultErrorAttributes,使得能获取出exception对象super(true);}/*** 返回值的map就是页面和json能获得的所有字段,不仅能自适应返回html数据还是json数据,* 还能返回xxxExceptionhandler定制的数据*/@Overridepublic Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);map.put("company", "atguigu");//这里也可以自定义数据//webRequest是将requestAttributes包装起来的对象,而requestAttributes是对request包装起来的对象// webRequest.getAttribute()获取在MyExceptionHandler中封装好的数据//入参0表示从request域中获取,1表示从session域中获取//ext是我们异常处理器MyExceptionHandler携带的数据Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);map.put("ext", ext);return map;}}
3.7 总结
定制错误数据总结,如下:

