1、默认的错误处理机制

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

SpringBoot错误处理机制 - 图1
浏览器发送请求的请求头

SpringBoot错误处理机制 - 图2
1.2 如果是其他客户端(app),默认响应一个json数据(postman模拟)
SpringBoot错误处理机制 - 图3
客户端的请求头

SpringBoot错误处理机制 - 图4

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的,如下:

  1. @Bean
  2. public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
  3. return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
  4. }

点进去看ErrorPageCustomizer的内容是什么,如下:

  1. /**
  2. * {@link WebServerFactoryCustomizer} that configures the server's error pages.
  3. */
  4. private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
  5. private final ServerProperties properties;
  6. private final DispatcherServletPath dispatcherServletPath;
  7. protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
  8. this.properties = properties;
  9. this.dispatcherServletPath = dispatcherServletPath;
  10. }
  11. @Override
  12. public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
  13. ErrorPage errorPage = new ErrorPage(
  14. this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
  15. errorPageRegistry.addErrorPages(errorPage);
  16. }
  17. @Override
  18. public int getOrder() {
  19. return 0;
  20. }
  21. }

并且发现ErrorPageCustomizer类是ErrorMvcAutoConfiguration的静态内部类。
可以看到它有一个重载方法registerErrorPages(),如下:

  1. @Override
  2. public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
  3. ErrorPage errorPage = new ErrorPage(
  4. this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
  5. errorPageRegistry.addErrorPages(errorPage);
  6. }

发生错误以后,系统会去哪个路径呢?这个是由registerErrorPages()中的getPath()决定的,getPath()方法如下:

  1. public String getPath() {
  2. return this.path;
  3. }

再点击path,查看它是怎么定义的,如下:

  1. /**
  2. * Path of the error controller.
  3. */
  4. @Value("${error.path:/error}")
  5. 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组件,如下:

  1. @Bean
  2. @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
  3. public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
  4. ObjectProvider<ErrorViewResolver> errorViewResolvers) {
  5. return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
  6. errorViewResolvers.orderedStream().collect(Collectors.toList()));
  7. }

点进去看看BasicErrorController是什么内容,如下:

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

可以看到它是一个controller,而且处理的请求是配置文件中server.error.path属性值,缺省值是error.path属性值,error.path的缺省值是/error。从ErrorPageCustomizer类中,我们学习到系统一发生4xx/5xx就会默认去/error的controller中处理请求。也就是默认会去BasicErrorController请求处理。
再看到BasicErrorController有2个处理请求的核心方法,分别是 errorHtml()、error(),如下:

  1. @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
  2. public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
  3. HttpStatus status = getStatus(request);
  4. Map<String, Object> model = Collections
  5. .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
  6. response.setStatus(status.value());
  7. ModelAndView modelAndView = resolveErrorView(request, response, status, model);
  8. return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
  9. }
  10. @RequestMapping
  11. public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
  12. HttpStatus status = getStatus(request);
  13. if (status == HttpStatus.NO_CONTENT) {
  14. return new ResponseEntity<>(status);
  15. }
  16. Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
  17. return new ResponseEntity<>(body, status);
  18. }

第一个方法errorHtml()返回值为ModelAndView,而且该方法会产生html类型的数据。这个方法就是在浏览器中展示错误页面的处理方法。比如浏览器访问某个地址,发生4xx错误,那么controller方法响应给它的是一个浏览器能显示的视图。

第二个方法返回值为ResponseEntity>,它返回的是json数据。这个方法就是在某些客户端展示错误的数据 的 方法。比如Postman访问某个地址,发生了4xx错误,那么controller方法响应给它的是一组json数据

是根据什么来区分到底用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()的内容,如下:

  1. @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
  2. public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
  3. HttpStatus status = getStatus(request);
  4. Map<String, Object> model = Collections
  5. .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
  6. response.setStatus(status.value());
  7. ModelAndView modelAndView = resolveErrorView(request, response, status, model);
  8. return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
  9. }

从倒数第二行看到,最终返回的是modelAndView或者new ModelAndView(x, model),而modelAndView是由resolveErrorView(x,x,x, model)。(注意,这里无论是由resolveErrorView()得到的modelAndView,还是new ModelAndView(),他们都使用了model这个数据。后面需要知道这个知识点)

先了解resolveErrorView(),它的作用是去哪个页面作为错误页面:包含页面地址以及页面内容。
resolveErrorView()方法如下:

  1. protected ModelAndView resolveErrorView(HttpServletRequest request,
  2. HttpServletResponse response,
  3. HttpStatus status, Map<String, Object> model) {
  4. for (ErrorViewResolver resolver : this.errorViewResolvers) {
  5. ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
  6. if (modelAndView != null) {
  7. return modelAndView;
  8. }
  9. }
  10. return null;
  11. }

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中有如下:

  1. @Bean
  2. @ConditionalOnBean(DispatcherServlet.class)
  3. @ConditionalOnMissingBean(ErrorViewResolver.class)
  4. DefaultErrorViewResolver conventionErrorViewResolver() {
  5. return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
  6. }

点击DefaultErrorViewResolver,看看内容是什么,如下:(代码中的注解就是解释,注意看

  1. public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
  2. private static final Map<Series, String> SERIES_VIEWS;
  3. static {
  4. Map<Series, String> views = new EnumMap<>(Series.class);
  5. views.put(Series.CLIENT_ERROR, "4xx");//所有错误页面都是用各自的状态码命名,太麻烦了
  6. views.put(Series.SERVER_ERROR, "5xx");//这里使用4xx,5xx命名
  7. SERIES_VIEWS = Collections.unmodifiableMap(views);
  8. }

可以看到它会处理4xx以及5xx的错误。
再来看看DefaultErrorViewResolver的 resolveErrorView() 方法,如下:

  1. @Override
  2. public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
  3. Map<String, Object> model) {
  4. ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
  5. if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
  6. modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
  7. }
  8. return modelAndView;
  9. }

从代码可知,最终返回的是modelAndView,它是由 resolve() 方法得来的。
再来看看rsolve()方法,如下:(代码中的注解就是解释,注意看

  1. private ModelAndView resolve(String viewName, Map<String, Object> model) {
  2. //SpringBoot默认可以找到一个error/404之类的页面。
  3. String errorViewName = "error/" + viewName;
  4. //模板引擎可以解析这个页面就用模板引擎解析
  5. TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
  6. this.applicationContext);
  7. if (provider != null) {
  8. //模板引擎可用的情况下,返回 error/viewName指定的视图地址
  9. return new ModelAndView(errorViewName, model);
  10. }
  11. //模板引擎不可用,就在静态资源文件夹下找error/ViewName对应的页面,比如error/404
  12. return resolveResource(errorViewName, model);
  13. }

第二行代码可以看出,SpringBoot默认可以去找到一个页面,路径是error/viewName,而viewName是由入参得来的,而resolveErrorView传递进的入参是一个枚举类型,里面有很多状态码(比如1xx,2xx,3xx,4xx,5xx)。也就是SpringBoot可以找到一个error/404之类的页面。

最后一行调用了resolveResource(),如下:(代码中的注解就是解释,注意看)

  1. private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
  2. //获取所有的存放静态资源的地址
  3. for (String location : this.resourceProperties.getStaticLocations()) {
  4. try {
  5. Resource resource = this.applicationContext.getResource(location);
  6. //创建错误处理的响应页面的名称,命名格式为xxx.html,比如为4xxx.html 或者 5xxx.html等等
  7. resource = resource.createRelative(viewName + ".html");
  8. if (resource.exists()) {
  9. //返回ModelAndView
  10. return new ModelAndView(new HtmlResourceView(resource), model);
  11. }
  12. }
  13. catch (Exception ex) {
  14. }
  15. }
  16. return null;
  17. }

总结:
从上面分析errorHtml()处理请求是怎么样的,我们可以自己定制错误页面。

  • 有模模板引擎的情况下:使用error/状态码。将错误页面命名为错误状态码.html,放在模板引擎文件夹里面的error文件夹下,发生此状态的错误就会来到对应的页面。由于错误状态码有太多太多,因此我们可以使用4xx/5xx匹配这种类型的所有错误,精确优先(优先寻找 精确的状态码.html)
  • 没有模板引擎的情况下:会去静态资源文件夹下的error文件夹寻找

    1.2.5 DefaultErrorAttributes

    了解完SpringBoot是怎么寻找错误页面后,我们继续了解错误页面上的数据内容是从哪里获取的,以及可以获取什么数据。

错误页面上的数据肯定在modelAndView中,所以我们从controller中找起,前面我们了解过BasicErrorController,因此到BasicErrorController的errorHtml()找, 如下:(注意代码中的注释)

  1. @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
  2. public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
  3. HttpStatus status = getStatus(request);
  4. //2.model是由getErrorAttributes()获取的
  5. Map<String, Object> model = Collections
  6. .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
  7. response.setStatus(status.value());
  8. //1. 这里model被传入modelAndView
  9. ModelAndView modelAndView = resolveErrorView(request, response, status, model);
  10. return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
  11. }

从上面看到model被传入modelAndView,model是由getErrorAttributes()获取的。点进去看看getErrorAttributes(),如下:

  1. protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
  2. WebRequest webRequest = new ServletWebRequest(request);
  3. return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
  4. }

点进去看看errorAttributes,如下:

  1. public abstract class AbstractErrorController implements ErrorController {
  2. private final ErrorAttributes errorAttributes;

点进去看看ErrorAttributes,如下:

  1. public interface ErrorAttributes {
  2. Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace);
  3. Throwable getError(WebRequest webRequest);
  4. }

ErrorAttributes是一个接口,我们将光标放在ErrorAttributes,按ctrl+alt+b,就会打开到如下:

  1. public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {

最终找到DefaultErrorAttributes,这个类在ErrorMvcAutoConfiguration中是有注册的。

DefaultErrorAttribute中最关键的一个属性是includeException,它是一个布尔值而且缺省值是false(所以它会导致某些异常信息不能被存储到某个Map中),通过它来决定是否添加某些异常信息到errorAttributes(这是一个Map,存储异常信息)中。

  1. private final boolean includeException;
  2. public DefaultErrorAttributes() {
  3. this(false);
  4. }

在DefaultErrorAttributes中找到getErrorAttributes(),如下:

  1. public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
  2. Map<String, Object> errorAttributes = new LinkedHashMap();
  3. errorAttributes.put("timestamp", new Date());
  4. this.addStatus(errorAttributes, webRequest);
  5. this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
  6. this.addPath(errorAttributes, webRequest);
  7. return errorAttributes;
  8. }

使用getErrorAttributes()方法,错误页面最终能获取到信息有如下:

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

总结:错误页面最终能获取到信息有timestamp:时间戳, status:状态码,error:错误提示, exception:异常对象,message:异常信息, eerrors:JSR303数据校验都在这里

1.2.6 SpringBoot默认的错误提示

在ErrorMvcAutoConfiguration中,有WhitelabelErrorViewConfiguration静态内部类,如下:

  1. protected static class WhitelabelErrorViewConfiguration {
  2. private final StaticView defaultErrorView = new StaticView();//这里存储了默认的错误页面内容
  3. @Bean(name = "error")
  4. @ConditionalOnMissingBean(name = "error")
  5. public View defaultErrorView() {
  6. return this.defaultErrorView;//返回默认错误视图
  7. }

点击StaticView,可以看到如下:

  1. private static class StaticView implements View {

它也是一个静态内部类,它有一个render()方法。
render()方法有部分代码如下:

  1. builder.append("<html><body><h1>Whitelabel Error Page</h1>").append(
  2. "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
  3. .append("<div id='created'>").append(timestamp).append("</div>")
  4. .append("<div>There was an unexpected error (type=").append(htmlEscape(model.get("error")))
  5. .append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");

上面的内容就是SpringBoot的默认错误页面

1.2.7 四个核心组件之间的关系

四个核心组件,指的是前面介绍的ErrorPageCustomizer、BasicErrorController、DefaultErrorAttributes、DefaultErrorViewResolver。
关系如下:
SpringBoot错误处理机制 - 图5

1.2.8 总结

SpringBoot错误处理机制 - 图6

2. 定制错误页面

https://blog.csdn.net/qq_40634846/article/details/107710404
整理SpringBoot寻找错误页面的路径,如下:

SpringBoot错误处理机制 - 图7
所以 定制页面的方法 如下:

  1. 可以在templates/error/下创建状态码.html文件
  2. 可以在静态资源文件夹/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,如下:

  1. @Configuration
  2. public class MyErrorPageConfig {
  3. @Bean
  4. public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){
  5. return new WebServerFactoryCustomizer<ConfigurableWebServerFactory>() {
  6. @Override
  7. public void customize(ConfigurableWebServerFactory factory) {
  8. ErrorPage errorPage1 = new ErrorPage(HttpStatus.NOT_FOUND, "/page/error/4xx.html");
  9. ErrorPage errorPage2 = new ErrorPage(HttpStatus.NOT_FOUND, "/page/error/5xx.html");
  10. factory.addErrorPages(errorPage1, errorPage2);
  11. }
  12. };
  13. }
  14. }

2.3 实现自适应

自适应就是根据发送的/error请求是浏览器还是客户端,使用不同的controller方法进行处理,并返回不同类型的数据
**
创建一个实现了ErrorController接口的MyBasicErrorController类,如下:

  1. /**
  2. * 定制ErrorController,目的是能使SpringBoot找到自己定制的错误页面
  3. * 大部分的代码BasicController一致,关键点是修改错误页面的路径
  4. */
  5. @Controller
  6. @RequestMapping(value = "/error")
  7. public class MyBasicErrorController implements ErrorController {
  8. @RequestMapping(produces = {"text/html"})//返回给浏览器
  9. public String handlerError(HttpServletRequest request, Model model){
  10. WebRequest webRequest = new ServletWebRequest(request);//对request进行包装,目的是能操作更多的方法
  11. HttpStatus status = this.getStatus(request);//获取status
  12. String path = (String) webRequest.getAttribute("javax.servlet.error.request_uri", 0);
  13. String message = (String) webRequest.getAttribute("javax.servlet.error.message", 0);
  14. if(message.equals("")){
  15. message = "No Available Message";
  16. }
  17. //携带错误数据信息
  18. model.addAttribute("timestamp", new Date());
  19. model.addAttribute("statusCode", status.value());
  20. model.addAttribute("error", status.getReasonPhrase());
  21. model.addAttribute("message", message);
  22. model.addAttribute("path", path);
  23. int i = status.value() / 100;//判断是4xx还是5xx错误
  24. if(i == 4){
  25. return "layuimini/page/error/4xx";//使用自己定制的错误页面
  26. }else if(i == 5){
  27. return "layuimini/page/error/5xx";//使用自己定制的错误页面
  28. }
  29. return null;
  30. }
  31. @RequestMapping//返回给客户端
  32. public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
  33. WebRequest webRequest = new ServletWebRequest(request);//对request进行包装,目的是能操作更多的方法
  34. HttpStatus status = this.getStatus(request);//获取status
  35. Map<String, Object> map = new HashMap<>();
  36. if (status == HttpStatus.NO_CONTENT) {
  37. return new ResponseEntity(status);
  38. } else {
  39. String path = (String) webRequest.getAttribute("javax.servlet.error.request_uri", 0);
  40. String message = (String) webRequest.getAttribute("javax.servlet.error.message", 0);
  41. map.put("timestamp", new Date());
  42. map.put("statusCode", status.value());
  43. map.put("error", status.getReasonPhrase());
  44. map.put("message", message);
  45. map.put("path", path);
  46. return new ResponseEntity(map, status);
  47. }
  48. }
  49. protected HttpStatus getStatus(HttpServletRequest request) {
  50. Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code");
  51. if (statusCode == null) {
  52. return HttpStatus.INTERNAL_SERVER_ERROR;
  53. } else {
  54. try {
  55. return HttpStatus.valueOf(statusCode);
  56. } catch (Exception var4) {
  57. return HttpStatus.INTERNAL_SERVER_ERROR;
  58. }
  59. }
  60. }
  61. @Override
  62. public String getErrorPath() {
  63. return "null";
  64. }
  65. }

SpringBoot的错误页面机制的自适应,是由BasicErrorController实现的,而这个BasicErrorController只有在容器中没有ErrorController的情况下,才会被注册进容器,因此我们创建一个实现了ErrorController接口的类,这个BasicErrorController就失效,然后我们仿照BasicErrorController里面的方法来实现自己的controller就可以了。如下:

  1. @Bean
  2. @ConditionalOnMissingBean(
  3. value = {ErrorController.class},//没有ErrorController才会去注册BasicErrorController
  4. search = SearchStrategy.CURRENT
  5. )
  6. public BasicErrorController basicErrorController(ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) {
  7. return new BasicErrorController(errorAttributes, this.serverProperties.getError(), (List)errorViewResolvers.orderedStream().collect(Collectors.toList()));
  8. }

3. 定制错误数据

https://blog.csdn.net/qq_40634846/article/details/107488716

3.1 自定义错误异常UserNotExistException

定义异常类并继承RuntimeExcception,如下:

  1. public class UserNotExistException extends RuntimeException{
  2. public UserNotExistException() {
  3. super("用户不存在");
  4. }
  5. }

定义controller,如下:

  1. @ResponseBody
  2. @RequestMapping("/hello")
  3. public String hello(@RequestParam("user")String user){
  4. if(user.equals("aaa")){
  5. throw new UserNotExistException();
  6. }
  7. return "hello world!";
  8. }

并且在错误页面获取某些错误信息,如下:

  1. <h1>status:[[ ${status} ]]</h1>
  2. <h2>timestamp:[[ ${timestamp} ]]</h2>
  3. <h2>exception:[[ ${exception} ]]</h2>
  4. <h2>message:[[ ${message} ]]</h2>

浏览器地址输入localhost:8080/crud/hello?user=aaa,就会来到controller并抛出UserNotExistException异常。效果如下:

SpringBoot错误处理机制 - 图8
如果错误页面上的exception为空白,只需在application.properties上配置server.error.include-exception=true。原理可以参考SpringBoot获取不到${exception}Day41——错误处理原理&定制错误页面

3.2 初步定制错误数据

前面已经定义好错误异常UserNotExistException,现在定义处理这个UserNotExistException的controller。如下:
MyExceptionHandler.java

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

这里定制错误数据code以及message。方法返回值的类型是json。因此,这样的定制错误数据,浏览器或客户端返回的都是json类数据。

3.3 改进定制错误数据——自适应

根据前面学习过的错误原理机制。我们只需将controller方法的返回值改成返回到/error,相当于发送了error请求,这样BasicErrorController就会根据Request Headers请求头的Accept属性决定发送哪种类型的错误视图,是发json数据,还是发text/html数据。这样就能完成自适应了。

如下:

  1. //2. 方式二:自适应返回数据
  2. @ExceptionHandler(UserNotExistException.class)
  3. public String handleException(Exception e, HttpServletRequest request){
  4. Map<String, Object> map = new HashMap<>();
  5. map.put("code", "user.notexist");
  6. map.put("message", "用户出错了");
  7. //实现自适应性,使得能通过发送器/error请求去到错误页面的处理方法
  8. return "forward:/error";
  9. }

效果:
浏览器的错误页面

SpringBoot错误处理机制 - 图9
其他客户端的数据

SpringBoot错误处理机制 - 图10
现在虽然能自适应了,但是状态码却变成了200,而且错误页面也不是我们自己定制的错误页面。

3.4 实现自适应并前往自定制错误页面

3.4.1 分析

从前面的效果可以看到状态码变成了200,而不是SpringBoot默认的4xx/5xx。因此我们应该将状态码设置成4xx/5xx再将视图返回出去。
在BasicErrorController中的errorHtml()中有一句是设置状态码,如下:(注意注释)

  1. @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
  2. public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
  3. HttpStatus status = getStatus(request);//2.调用getStatus()获取status
  4. Map<String, Object> model = Collections
  5. .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
  6. response.setStatus(status.value());//1.设置响应状态码status
  7. ModelAndView modelAndView = resolveErrorView(request, response, status, model);
  8. return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
  9. }

如上面代码中所述,我们查看getStatus()是什么内容,如下:(注意注释)

  1. protected HttpStatus getStatus(HttpServletRequest request) {
  2. //1.原来它是通过request.getAttribute("javax.servlet.error.status_code")获取状态码
  3. Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
  4. if (statusCode == null) {
  5. return HttpStatus.INTERNAL_SERVER_ERROR;
  6. }
  7. try {
  8. return HttpStatus.valueOf(statusCode);
  9. }
  10. catch (Exception ex) {
  11. return HttpStatus.INTERNAL_SERVER_ERROR;
  12. }
  13. }

如上代码所述,由此 我们可以在异常处理的controller方法中设置状态码,然后这个状态码会被上面的getStatus()方法拿出来,从而实现前往自己的错误页面。

3.4.2 实现

  1. //2. 方式二:自适应返回数据
  2. @ExceptionHandler(UserNotExistException.class)
  3. public String handleException(Exception e, HttpServletRequest request){
  4. Map<String, Object> map = new HashMap<>();
  5. /**
  6. * 传入自己的错误状态码4xx,5xx,使得springboot去到自己定制的错误页面。
  7. * 否则不会进入定制错误页面的解析流程
  8. */
  9. /**
  10. * SpringBoot是这样取得状态码的:
  11. * Integer statusCode =
  12. * (Integer) request.getAttribute("javax.servlet.error.status_code");
  13. */
  14. //因此我们识用request.setAttribute即可
  15. request.setAttribute("javax.servlet.error.status_code", 500);
  16. map.put("code", "user.notexist");
  17. map.put("message", "用户出错了");
  18. //实现自适应性,使得能通过发送器/error请求去到错误页面的处理方法
  19. return "forward:/error";
  20. }

在错误页面显示出code、message,如下:

  1. <h1>status:[[ ${status} ]]</h1>
  2. <h2>timestamp:[[ ${timestamp} ]]</h2>
  3. <h2>exception:[[ ${exception} ]]</h2>
  4. <h2>message:[[ ${message} ]]</h2>
  5. <h2>company:[[ ${company} ]]</h2>
  6. <h2>code:[[ ${ext.code} ]]</h2>
  7. <h2>message:[[ ${ext.message} ]]</h2>

效果:

SpringBoot错误处理机制 - 图11
客户端也没有自己定制的错误数据,如下:

SpringBoot错误处理机制 - 图12
总结:能来到自己定制的错误页面,但是与自己定制的错误数据不同

3.5 将自己定制的数据携带出去

3.5.1 分析

将数据封装到视图中是发生在BasicErrorController中的errorHtml()以及error()中的,如下:(注意注释)

  1. @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
  2. public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
  3. HttpStatus status = getStatus(request);
  4. //2. model是通过getErrorAtributes()获取到的
  5. Map<String, Object> model = Collections
  6. .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
  7. response.setStatus(status.value());
  8. //1. model被封装ModelAndView对象中
  9. ModelAndView modelAndView = resolveErrorView(request, response, status, model);
  10. return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
  11. }
  12. @RequestMapping
  13. public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
  14. HttpStatus status = getStatus(request);
  15. if (status == HttpStatus.NO_CONTENT) {
  16. return new ResponseEntity<>(status);
  17. }
  18. //1.也是通过调用getErrorAttributes()获取到数据
  19. Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
  20. return new ResponseEntity<>(body, status);
  21. }

getErrorAtributes()是AbstractErrorController的方法,而且AbstractErrorController实现了ErrorController接口(这里很关键),如下:

  1. public abstract class AbstractErrorController implements ErrorController {
  2. ...
  3. protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
  4. boolean includeStackTrace) {
  5. WebRequest webRequest = new ServletWebRequest(request);
  6. //1.当前方法又调用了errorAttributes.getErrorAttributes()获取数据
  7. return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
  8. }

总结:不管是errorHtml()方法还是error()方法,封装的错误数据是从AbstractErrorController的getErrorAttributes获取到的(而且AbstractErrorController实现了ErrorController接口)。getErrorAttributes()里面又调用了errorAttributes.getErrorAttributes()。

从上面代码看到,最终是errorAttributes调用getErrorAttributes()获取数据。

看看这个getErrorAttributes()内容,如下:

  1. public interface ErrorAttributes {
  2. Map<String, Object> getErrorAttributes(WebRequest webRequest,
  3. boolean includeStackTrace);
  4. Throwable getError(WebRequest webRequest);
  5. }

如上所示,它是一个接口,将光标放在ErrorAttributes,按ctrl+alt+b查看它的实现类,如下:

SpringBoot错误处理机制 - 图13
原来DefaultErrorAttributes是它的实现类。
DefaultErrorAttributes里面的getErrorAttributes()方法如下:

  1. public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
  2. Map<String, Object> errorAttributes = new LinkedHashMap();
  3. errorAttributes.put("timestamp", new Date());
  4. this.addStatus(errorAttributes, webRequest);
  5. this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
  6. this.addPath(errorAttributes, webRequest);
  7. return errorAttributes;
  8. }

如上所示,最终找到了封装的错误数据是从DefaultErrorAttributes.getErrorAttributes获得的。
总结:最终最终,封装的错误数据是从DefaultErrorAttributes.getErrorAttributes获得的。

3.5.2 将数据携带出去的过程

SringBoot自动配置是这样做的:
出现错误,发送/error请求,来到BasicErrorController,根据自适应,去errorHtml()或者error()方法及逆行处理。处理的过程中,响应的数据由 getErrorAttributes()(它是AbstractErrorController(实现了ErrorController接口)的方法) 得到,最终会调用errorAttributes.getErrorAttributes()得到数据。最终最终会调用DefaultErrorAttributes的getErrorAttributes()得到数据。

总结一:因此,我们可以编写AbstractController的子类,放在容器中,从而将自己定制的错误数据携带出去。但是有更简单的解决方案,后面讲
自动配置是以上的做法,我们查看ErrorMvcAutoConfiguration,如下:(注意注释)

  1. @Bean
  2. //1.当没有ErrorController,才会注册BasicErrorControlelr。才会有以上的阐述的做法。
  3. @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
  4. public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
  5. ObjectProvider<ErrorViewResolver> errorViewResolvers) {
  6. return new BasicErrorController(errorAttributes,
  7. this.serverProperties.getError(),
  8. errorViewResolvers.orderedStream().collect(Collectors.toList()));
  9. }

总结二:也就是我们完全可以编写一个ErrorController的实现类,放在容器中,从而将自己定制的错误数据携带出去。但是有更简单的解决方案,后面讲
因为最终会调用DefaultErrorAttributes的getErrorAttributes()得到数据。所以来看看自动配置类ErrorMvcAutoConfiguration中是怎么注册DefaultErrorAttributes的,如下:

  1. @Bean
  2. //1.当没有ErrorAttributes时,才会注册DefaultErrorAttributes
  3. @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
  4. public DefaultErrorAttributes errorAttributes() {
  5. return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
  6. }

综上所述,可以编写一个DefaultErrorAttributes的子类来携带自己定制的错误数据。
总结三编写一个DefaultErrorAttributes的子类来携带自己定制的错误数据。

3.5.3 将数据携带出去的方案

方案一:编写AbstractController的子类或者编写ErrorController的实现类,放到容器中。但是编写controlelr过于沉重繁琐,不推荐此方案。

方案二:编写DefaultErrorAttributes的子类或者ErrorAttributes的实现类,放到容器中。推荐此方案

3.5.4 实现

首先在处理异常的方法(发送/error之前)中将扩展的数据放在ext中,如下:

  1. /**
  2. * 定制错误数据
  3. */
  4. @ControllerAdvice
  5. public class MyExceptionHandler {
  6. //2. 方式二:自适应返回数据
  7. @ExceptionHandler(UserNotExistException.class)
  8. public String handleException(Exception e, HttpServletRequest request){
  9. Map<String, Object> map = new HashMap<>();
  10. /**
  11. * 传入自己的错误状态码4xx,5xx,使得springboot去到自己定制的错误页面。
  12. * 否则不会进入定制错误页面的解析流程
  13. */
  14. /**
  15. * SpringBoot是这样取得状态码的:
  16. * Integer statusCode =
  17. * (Integer) request.getAttribute("javax.servlet.error.status_code");
  18. */
  19. //因此我们使用request.setAttribute即可
  20. request.setAttribute("javax.servlet.error.status_code", 500);
  21. map.put("code", "user.notexist");
  22. map.put("message", "用户出错了");
  23. request.setAttribute("ext", map);//将扩展的数据放在ext中
  24. //实现自适应性,使得能通过发送器/error请求去到错误页面的处理方法
  25. return "forward:/error";
  26. }

编写DefaultErrorAttributes子类,获取SpringBoot的默认错误数据以及在异常处理的方法中自己定制的数据,如下:

  1. package com.atguigu.springboot.component;
  2. import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
  3. import org.springframework.stereotype.Component;
  4. import org.springframework.web.context.request.WebRequest;
  5. import java.util.Map;
  6. /**
  7. * 给容器中加入我们自己定义的ErrorAttributes
  8. */
  9. @Component
  10. public class MyErrorAttributes extends DefaultErrorAttributes {
  11. /**
  12. * 返回值的map就是页面和json能获得的所有字段,不仅能自适应返回html数据还是json数据,
  13. * 还能返回xxxExceptionhandler定制的数据
  14. */
  15. @Override
  16. public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
  17. Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
  18. map.put("company", "atguigu");//这里也可以自定义数据
  19. //webRequest是将requestAttributes包装起来的对象,而requestAttributes是对request包装起来的对象
  20. // webRequest.getAttribute()获取在MyExceptionHandler中封装好的数据
  21. //入参0表示从request域中获取,1表示从session域中获取
  22. //ext是我们异常处理器MyExceptionHandler携带的数据
  23. Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
  24. map.put("ext", ext);
  25. return map;
  26. }
  27. }

效果:

SpringBoot错误处理机制 - 图14

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
解决方案如下:

  1. //MyErrorAttributes是我定义的DefaultErrorAttributes的子类
  2. public MyErrorAttributes() {
  3. //将includeException=true传给DefaultErrorAttributes,使得能获取出exception对象
  4. super(true);
  5. }

完整代码如下:

  1. package com.atguigu.springboot.component;
  2. import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
  3. import org.springframework.stereotype.Component;
  4. import org.springframework.web.context.request.WebRequest;
  5. import java.util.Map;
  6. /**
  7. * 给容器中加入我们自己定义的ErrorAttributes
  8. */
  9. @Component
  10. public class MyErrorAttributes extends DefaultErrorAttributes {
  11. public MyErrorAttributes() {
  12. //将includeException=true传给DefaultErrorAttributes,使得能获取出exception对象
  13. super(true);
  14. }
  15. /**
  16. * 返回值的map就是页面和json能获得的所有字段,不仅能自适应返回html数据还是json数据,
  17. * 还能返回xxxExceptionhandler定制的数据
  18. */
  19. @Override
  20. public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
  21. Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
  22. map.put("company", "atguigu");//这里也可以自定义数据
  23. //webRequest是将requestAttributes包装起来的对象,而requestAttributes是对request包装起来的对象
  24. // webRequest.getAttribute()获取在MyExceptionHandler中封装好的数据
  25. //入参0表示从request域中获取,1表示从session域中获取
  26. //ext是我们异常处理器MyExceptionHandler携带的数据
  27. Map<String, Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
  28. map.put("ext", ext);
  29. return map;
  30. }
  31. }

3.7 总结

定制错误数据总结,如下:

SpringBoot错误处理机制 - 图15