🤖🤖-摘要: 本文介绍了SpringBoot Web中的错误处理机制,包括默认处理机制和SpringMVC的处理方式。并详细分析了SpringBoot错误原理以及它的配置类ErrorMvcAutoConfiguration,最后给出了最佳实践建议。
错误处理 默认机制 SpringBoot在web场景 下,当应用程序发生错误或异常时,SpringBoot会自动应用ErrorMvcAutoConfiguration进行配置.
@AutoConfiguration(before = WebMvcAutoConfiguration.class) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({Servlet.class, DispatcherServlet.class}) @EnableConfigurationProperties({ServerProperties.class, WebMvcProperties.class}) public class ErrorMvcAutoConfiguration { }
两大处理机制: 机制一: SpringBoot 会自适应处理错误,响应页面或JSON 数据(内容协商)
机制二: SpringMVC 的错误处理机制依然保留,MVC处理不了,才会交给boot进行处理
SpringMVC处理错误 @Controller public class ErrorController { @GetMapping("testError") public String testError () { int i = 12 / 0 ; return "testError" ; } @ResponseBody @ExceptionHandler(Exception.class) public String handleException (Exception e) { return "错误已发生,原因:" + e.getMessage(); } }
统一错误处理:
@ControllerAdvice public class GlobalExceptionHandler { @ResponseBody @ExceptionHandler(Exception.class) public String handleException (Exception e) { return "统一处理,错误已发生,原因:" + e.getMessage(); } }
SpringBoot错误原理浅析 自动配置类ErrorMvcAutoConfiguration, 主要包含以下功能:
注册组件: BasicErrorController 这是一个默认的错误处理控制器,用于处理一般的错误请求.
可以在配置文件中配置:server.error.path=/error(默认值) 当发生错误以后,将SpringMVC不能处理的错误请求转发给/error进行处理
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { }
它会根据请求的Accept头部信息返回对应的错误响应,比如JSON ,XML 或HTML 格式.内容协商机制
@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,getErrorAttributeOptions(request,MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView=resolveErrorView(request,response,status,model); return (modelAndView!=null )?modelAndView:new ModelAndView ("error" ,model); } @RequestMapping public 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,getErrorAttributeOptions(request,MediaType.ALL)); return new ResponseEntity <>(body,status); }
错误视图解析:
ModelAndView modelAndView=resolveErrorView(request,response,status,model); return (modelAndView!=null )?modelAndView:new ModelAndView ("error" ,model);
1.解析错误视图:
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 ; }
在自动配置类,会将默认的错误视图解析器 放在容器中
@Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({WebProperties.class, WebMvcProperties.class}) static class DefaultErrorViewResolverConfiguration { private final ApplicationContext applicationContext; private final Resources resources; DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, WebProperties webProperties) { this .applicationContext = applicationContext; this .resources = webProperties.getResources(); } @Bean @ConditionalOnBean(DispatcherServlet.class) @ConditionalOnMissingBean(ErrorViewResolver.class) DefaultErrorViewResolver conventionErrorViewResolver () { return new DefaultErrorViewResolver (this .applicationContext, this .resources); } }
默认的错误视图解析过程:
@Override public 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; } 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); } private ModelAndView resolveResource (String viewName,Map<String, Object> model) { for (String location:this .resources.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 ; }
2.解析不到错误视图: 精确状态码以及模糊状态码都没有匹配时,则映射到error视图
在template目录下创建error.html就会返回(注意:将上面统一错误处理注释)
<!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > 模板: error 页 </body > </html >
效果:
如果error视图页没有:
自动配置类ErrorMvcAutoConfiguration,在容器中放入了error组件,提供了默认白页功能:
@Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true) @Conditional(ErrorTemplateMissingCondition.class) protected static class WhitelabelErrorViewConfiguration { private final StaticView defaultErrorView = new StaticView (); @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView () { return this .defaultErrorView; } @Bean @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver () { BeanNameViewResolver resolver = new BeanNameViewResolver (); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10 ); return resolver; } }
创建白页:
private static class StaticView implements View { private static final MediaType TEXT_HTML_UTF8 = new MediaType ("text" , "html" , StandardCharsets.UTF_8); private static final Log logger = LogFactory.getLog(StaticView.class); @Override public void render (Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if (response.isCommitted()) { String message = getMessage(model); logger.error(message); return ; } response.setContentType(TEXT_HTML_UTF8.toString()); StringBuilder builder = new StringBuilder (); Object timestamp = model.get("timestamp" ); Object message = model.get("message" ); Object trace = model.get("trace" ); if (response.getContentType() == null ) { response.setContentType(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(htmlEscape(model.get("error" ))) .append(", status=" ) .append(htmlEscape(model.get("status" ))) .append(").</div>" ); if (message != null ) { builder.append("<div>" ).append(htmlEscape(message)).append("</div>" ); } if (trace != null ) { builder.append("<div style='white-space:pre-wrap;'>" ).append(htmlEscape(trace)).append("</div>" ); } builder.append("</body></html>" ); response.getWriter().append(builder.toString()); }
小结一下 先尝试解析错误页, 解析失败则在静态资源目录下查找
解析 一个错误页如果发生了500、404、503、403 这些错误如果有模板引擎,默认在classpath:/templates/error/精确码.html 如果没有模板引擎,在静态资源文件夹下找精确码.html 如果匹配不到精确码.html这些精确的错误页,就去找5xx.html, 4xx.html模糊匹配 如果有模板引擎,默认在classpath:/templates/error/5xx.html 如果没有模板引擎,在静态资源文件夹下找5xx.html 如果模板引擎路径templates下有error.html页面, 就直接渲染 自定义错误响应 自定义json响应使用@ControllerAdvice + @ExceptionHandler 进行统一异常处理 自定义页面响应 最佳实践 前后分离后台发生的所有错误, @ControllerAdvice + @ExceptionHandler进行统一异常处理 服务端页面渲染不可预知的错误,HTTP码表示的服务器端或客户端错误给classpath:/templates/error/下面,放常用精确的错误码页面。500.html,404.html 给classpath:/templates/error/下面,放通用模糊匹配的错误码页面。 5xx.html,4xx.html 发生业务错误核心业务 , 每一种错误, 都应该代码控制, 跳转到自己定制的错误页 通用业务 , classpath:/templates/error.html页面, 显示错误信息 无论是返回页面或者JSON数据, 可用的Model 数据都一样, 如下: