springboot错误相应定制 介绍 在springboot中如果发生访问错误,如404的话,如果是浏览器访问它会给你一个默认的定制页面比如下面这样
如果是其他的,会返回json数据(来自idea插件RestfulToolkit):
那我们可以定制自己的错误页面吗,答案是肯定的,下面就讲解如何定制自己的错误页面已经信息
方法 一、定制错误页面
使用了模板引擎情况
使用了模板引擎thymeleaf的情况下,我们只需要在resources的templates下创建error文件夹,在里面创建以状态码开头的html文件就行,比如:404.html就会对应到404的页面。除此之外,我们可以使用4xx和5xx作为错误页面的文件名来匹配这种类型的所有错误,但是精确优先(优先寻找精确的状态 码.html)。
举例:
我们在error文件夹下创建404.html和4xx.html
404.html
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html > <html lang ="en" xmlns:th ="http://www.thymeleaf.org" > <head > <meta charset ="UTF-8" > <title > 404</title > </head > <body > <h1 > status:[[${status}]]</h1 > <h2 > timestamp:[[${timestamp}]]</h2 > </body > </html >
4xx.html
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > 4XX</title > </head > <body > <h2 > status:[[${status}]]</h2 > </body > </html >
结果:
404:
400错误:
没有使用模板引擎
没有使用模板引擎的话,直接在resources的static下创建error文件夹,然后在里面创建以状态码开头的html文件就行,4xx和5xx在这里面同样生效
二、定制错误json数据 为了比较好定制json错误,我们创建一个UserNotExistException
1 2 3 4 5 public class UserNotExistException extends RuntimeException { public UserNotExistException () { super ("用户不存在" ); } }
然后在controller里面加上一个映射
1 2 3 4 @GetMapping("/exception") public String toException () { throw new UserNotExistException(); }
这样当我们访问http://localhost:8080/exception时,就可以发生错误然后有错误页面和数据
定制错误的json数据我们步骤如下:
编写一个ExceptionHandler ,加上@ControllerAdvice注解,并且编写处理Exception的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @ControllerAdvice public class MyExceptionHandler { @ExceptionHandler(value = UserNotExistException.class) public String handleUserNotExistException (Exception e, HttpServletRequest request) { Map<String,Object> map = new HashMap<>(); System.out.println("user处理器被执行" ); request.setAttribute("javax.servlet.error.status_code" ,401 ); map.put("code" ,"user.notexist" ); map.put("message" ,"user部分发生错误" ); request.setAttribute("ext" , map); return "forward:/error" ; } @ExceptionHandler(value = Exception.class) public String handleException (Exception e, HttpServletRequest request) { Map<String,Object> map = new HashMap<>(); System.out.println("处理器被执行" ); request.setAttribute("javax.servlet.error.status_code" ,500 ); map.put("code" ,"find Exception" ); map.put("message" ,e.getMessage()); request.setAttribute("ext" , map); return "forward:/error" ; } }
可以看到我们将错误设置,然后将错误信息的map放在了request里面
编写ErrorAttributes
1 2 3 4 5 6 7 8 9 10 11 12 13 @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("name" ,"zhouning" ); Map<String,Object> ext = (Map<String, Object>) webRequest.getAttribute("ext" , 0 ); map.put("ext" , ext); return map; } }
然后在我们编写的MyErrorAttributes里面我们可以将上面的map取出来,然后放到父类得到的map中。
最终效果
可以看到我们的信息显示在了上面,并且只有handleUserNotExistException做出相应(响应更加精确的错误)。
缺点
定制json数据的方法其实有很多,比如自己编写一个controller同样可以实现,但是这种方法简洁一些所以推荐使用这种。这个方法的缺点就是无法处理404发生错误的请求,其他的可以,目前除了重新写一个controller我没有想到怎么编写可以处理404错误,知道的人可以指点一下。
原理 上面是举出实际例子,但是光靠例子很难理解,现在主要讲解一下里面的原理。我们需要找到ErrorMvcAutoConfiguration这个类
1.BasicErrorController 在ErrorMvcAutoConfiguration里面我们能够找到添加了BasicErrorController
1 2 3 4 5 6 7 8 @Bean @ConditionalOnMissingBean( value = {ErrorController.class}, search = SearchStrategy.CURRENT ) public BasicErrorController basicErrorController (ErrorAttributes errorAttributes, ObjectProvider<ErrorViewResolver> errorViewResolvers) { return new BasicErrorController(errorAttributes, this .serverProperties.getError(), (List)errorViewResolvers.orderedStream().collect(Collectors.toList())); }
然后点进去可以看到具体的实现,可以发现很多东西如下所示(加一些注释上面):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 @Controller @RequestMapping({"${server.error.path:${error.path:/error}}"}) public class BasicErrorController extends AbstractErrorController { private final ErrorProperties errorProperties; public BasicErrorController (ErrorAttributes errorAttributes, ErrorProperties errorProperties) { this (errorAttributes, errorProperties, Collections.emptyList()); } public BasicErrorController (ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) { super (errorAttributes, errorViewResolvers); Assert.notNull(errorProperties, "ErrorProperties must not be null" ); this .errorProperties = errorProperties; } public String getErrorPath () { return this .errorProperties.getPath(); } @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) { HttpStatus status = this .getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity(status); } else { Map<String, Object> body = this .getErrorAttributes(request, this .isIncludeStackTrace(request, MediaType.ALL)); return new ResponseEntity(body, status); } } @ExceptionHandler({HttpMediaTypeNotAcceptableException.class}) public ResponseEntity<String> mediaTypeNotAcceptable (HttpServletRequest request) { HttpStatus status = this .getStatus(request); return ResponseEntity.status(status).build(); } protected boolean isIncludeStackTrace (HttpServletRequest request, MediaType produces) { IncludeStacktrace include = this .getErrorProperties().getIncludeStacktrace(); if (include == IncludeStacktrace.ALWAYS) { return true ; } else { return include == IncludeStacktrace.ON_TRACE_PARAM ? this .getTraceParameter(request) : false ; } } protected ErrorProperties getErrorProperties () { return this .errorProperties; } }
从上面的解析我们可以看到转发“/error”的原因,以及返回html页面和返回json数据的相应处理。
需要注意的地方:
getErrorAttributes在处理html和json的数据里面都被掉用
先使用resolveErrorView解析视图,如果没有视图,再返回ModelAndView("error", model);这个视图
2.DefaultErrorViewResolver 我们往下翻发现这样的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Configuration( proxyBeanMethods = false ) static class DefaultErrorViewResolverConfiguration { private final ApplicationContext applicationContext; private final ResourceProperties resourceProperties; DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, ResourceProperties resourceProperties) { this .applicationContext = applicationContext; this .resourceProperties = resourceProperties; } @Bean @ConditionalOnBean({DispatcherServlet.class}) @ConditionalOnMissingBean({ErrorViewResolver.class}) DefaultErrorViewResolver conventionErrorViewResolver () { return new DefaultErrorViewResolver(this .applicationContext, this .resourceProperties); } }
我们进进入DefaultErrorViewResolver,可以在里面找到这样的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 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) { String errorViewName = "error/" + viewName; TemplateAvailabilityProvider provider = this .templateAvailabilityProviders.getProvider(errorViewName, this .applicationContext); return provider != null ? new ModelAndView(errorViewName, model) : this .resolveResource(errorViewName, model); } 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 ; }
这部分和上面相呼应,也就解释了为啥文件放在error/文件夹下。
需要注意的点:
视图解析时有模板引擎,先使用模板引擎解析视图,如果模板引擎解析出来为null,再从静态资源里面解析,如果静态资源里面都为null,那么就返回null。这也对应了BasicErrorController中最终可能ModelAndView("error", model)
3.WhitelabelErrorViewConfiguration 上面看到了ModelAndView("error", model)这个视图但是不知道是什么样,我在ErrorMvcAutoConfiguration里面找到了这个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Configuration( proxyBeanMethods = false ) @ConditionalOnProperty( prefix = "server.error.whitelabel", name = {"enabled"}, matchIfMissing = true ) @Conditional({ErrorMvcAutoConfiguration.ErrorTemplateMissingCondition.class}) protected static class WhitelabelErrorViewConfiguration { private final ErrorMvcAutoConfiguration.StaticView defaultErrorView = new ErrorMvcAutoConfiguration.StaticView(); protected WhitelabelErrorViewConfiguration () { } @Bean( name = {"error"} ) @ConditionalOnMissingBean( name = {"error"} ) public View defaultErrorView () { return this .defaultErrorView; } @Bean @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver () { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(2147483637 ); return resolver; } }
然后我们找到StaticView
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private static class StaticView implements View { public void render (Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if (response.isCommitted()) { String message = this .getMessage(model); logger.error(message); } else { response.setContentType(TEXT_HTML_UTF8.toString()); StringBuilder builder = new StringBuilder(); Date timestamp = (Date)model.get("timestamp" ); Object message = model.get("message" ); Object trace = model.get("trace" ); if (response.getContentType() == null ) { response.setContentType(this .getContentType()); } 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(this .htmlEscape(model.get("error" ))).append(", status=" ).append(this .htmlEscape(model.get("status" ))).append(").</div>" ); if (message != null ) { builder.append("<div>" ).append(this .htmlEscape(message)).append("</div>" ); } if (trace != null ) { builder.append("<div style='white-space:pre-wrap;'>" ).append(this .htmlEscape(trace)).append("</div>" ); } builder.append("</body></html>" ); response.getWriter().append(builder.toString()); } } }
我们找到了原本html的原型
4.DefaultErrorAttributes 我们在BasicErrorController中看到resolve和resolveResource方法里面都掉用了getErrorAttributes这个方法得到信息,我们点进去,发现父类AbstractErrorController里面是这样的
1 2 3 4 protected Map<String, Object> getErrorAttributes (HttpServletRequest request, boolean includeStackTrace) { WebRequest webRequest = new ServletWebRequest(request); return this .errorAttributes.getErrorAttributes(webRequest, includeStackTrace); }
我们找到这个errorAttributes发现是这样的private final ErrorAttributes errorAttributes
然后我们在ErrorMvcAutoConfiguration里面找到DefaultErrorAttributes
1 2 3 4 5 6 7 8 9 @Bean @ConditionalOnMissingBean( value = {ErrorAttributes.class}, search = SearchStrategy.CURRENT ) public DefaultErrorAttributes errorAttributes () { return new DefaultErrorAttributes(this .serverProperties.getError().isIncludeException()); }
点进去,发现它实现的就是ErrorAttributes
1 2 3 4 5 6 7 8 9 10 11 public class DefaultErrorAttributes implements ErrorAttributes , HandlerExceptionResolver , Ordered { 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; } }
也就是说在BasicErrorController掉用的方法getErrorAttributes默认是DefaultErrorAttributes,而当我们实现一个ErrorAttributes并切添加进容器时,默认就不会添加DefaultErrorAttributes,而使用我们实现的这个类,这也是为什么我们自定义数据继承了DefaultErrorAttributes。
总结 当我们springboot出现异常比如404、503等,BasicErrorController会对这些错误进行反应,返回对应的html或者json,`BasicErrorController`返回html时,是通过`DefaultErrorViewResolver`进行视图解析,当解析返回null时,掉用系统自带的`StaticView`,而`BasicErrorController`的数据信息来源则是由`DefaultErrorAttributes`进行提供。
以上都是一些我自己的理解,如果有错误的地方欢迎指出来,一起学习。