🤖🤖-摘要:
本文介绍了SpringBoot Web中的错误处理机制,包括默认处理机制和SpringMVC的处理方式。并详细分析了SpringBoot错误原理以及它的配置类ErrorMvcAutoConfiguration,最后给出了最佳实践建议。

错误处理

默认机制

SpringBoot在web场景下,当应用程序发生错误或异常时,SpringBoot会自动应用ErrorMvcAutoConfiguration进行配置.

// Load before the main WebMvcAutoConfiguration so that the error View is available
// 在WebMvcAutoConfiguration自动装配之前
@AutoConfiguration(before = WebMvcAutoConfiguration.class)
// 条件:普通的servlet web类型
@ConditionalOnWebApplication(type = Type.SERVLET)
// 条件:有Servlet和DispatcherServlet类
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
// 绑定配置文件:server.*和spring.mvc.*
@EnableConfigurationProperties({ServerProperties.class, WebMvcProperties.class})
public class ErrorMvcAutoConfiguration {
//...
}

两大处理机制:
机制一: SpringBoot会自适应处理错误,响应页面或JSON数据(内容协商)

同一请求-浏览器返回白页 同一请求-客户端返回JSON 同一请求-客户端也可设置返回XML

机制二: SpringMVC的错误处理机制依然保留,MVC处理不了,才会交给boot进行处理

SpringBoot错误处理机制

SpringMVC处理错误

@Controller
public class ErrorController {

/**
* 测试MVC的错误处理机制
* 默认情况下--不处理错误:
* 浏览器返回白页,因为请求头中: Accept:text/html
* 移动端postman返回JSON.因为请求头中: (Accept:* 所有类型,优先JSON)
* 自己处理错误: handleException()
*/
@GetMapping("testError")
public String testError() {

// 错误出现
int i = 12 / 0;

return "testError";
}

/**
* 自定义处理所有错误
* @ExceptionHandler 可以标识一个方法, 默认只能处理这个类发生的指定错误
* @ControllerAdvice AOP思想, 可以统一处理所有方法, 如 GlobalExceptionHandler.java
*/
@ResponseBody
@ExceptionHandler(Exception.class)
public String handleException(Exception e) {

return "错误已发生,原因:" + e.getMessage();
}
}
mvc处理错误

统一错误处理:

@ControllerAdvice // 统一处理所有Controller
public class GlobalExceptionHandler {

/**
* 自定义处理所有错误
* @ExceptionHandler 可以标识一个方法, 默认只能处理这个类发生的指定错误
* @ControllerAdvice AOP思想, 可以统一处理所有方法
*/
@ResponseBody
@ExceptionHandler(Exception.class)
public String handleException(Exception e) {

return "统一处理,错误已发生,原因:" + e.getMessage();
}
}
mvc统一处理错误

SpringBoot错误原理浅析

自动配置类ErrorMvcAutoConfiguration, 主要包含以下功能:

注册组件: BasicErrorController

这是一个默认的错误处理控制器,用于处理一般的错误请求.

可以在配置文件中配置:server.error.path=/error(默认值)
当发生错误以后,将SpringMVC不能处理的错误请求转发给/error进行处理

@Controller
// 可以处理配置文件中:server.error.path 的映射
// 或者处理配置文件中:error.path 的映射
// 以上都没配置,就会将请求映射到: /error
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//...
}

它会根据请求的Accept头部信息返回对应的错误响应,比如JSON,XMLHTML格式.
内容协商机制

// "text/html"
// 返回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);
// 返回上面解析的视图,或者新建一个error视图(SpringBoot默认有一个error页面,状态码999)
return(modelAndView!=null)?modelAndView:new ModelAndView("error",model);
}

// 返回 ResponseEntity,即JSON数据
@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);
}

错误视图解析:

//1、解析错误的自定义视图地址
ModelAndView modelAndView=resolveErrorView(request,response,status,model);
//2、如果解析不到错误页面的地址,默认的错误页就是 error
return(modelAndView!=null)?modelAndView:new ModelAndView("error",model);

1.解析错误视图:

protected ModelAndView resolveErrorView(HttpServletRequest request,HttpServletResponse response,HttpStatus status,Map<String, Object> model){
// 遍历错误视图解析器:errorViewResolvers
for(ErrorViewResolver resolver:this.errorViewResolvers){
ModelAndView modelAndView=resolver.resolveErrorView(request,status,model);
if(modelAndView!=null){
return modelAndView;
}
}
return null;
}

在自动配置类,会将默认的错误视图解析器放在容器中

@Configuration(proxyBeanMethods = false)
// 绑定配置文件中 web.* 和 web.mvc.*
@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){
// 1. 获取状态码
// 2. 根据状态码解析错误视图(如: 404 500 等)
ModelAndView modelAndView=resolve(String.valueOf(status.value()),model);
// 3. 状态码没有精确匹配,则模糊匹配(如:4xx 5xx, 注意只有这俩)
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){
// 错误视图名: error/404 或 error/4xx
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){
// 遍历四个静态资源目录:classpath:/META-INF/resources/","classpath:/resources/",
// "classpath:/static/", "classpath:/public/
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页

如果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;
}

// If the user adds @EnableWebMvc then the bean name view resolver from
// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
@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());
}

小结一下

先尝试解析错误页, 解析失败则在静态资源目录下查找

  1. 解析一个错误页
  • 如果发生了500、404、503、403 这些错误
    • 如果有模板引擎,默认在classpath:/templates/error/精确码.html
    • 如果没有模板引擎,在静态资源文件夹下找精确码.html
  • 如果匹配不到精确码.html这些精确的错误页,就去找5xx.html, 4xx.html模糊匹配
    • 如果有模板引擎,默认在classpath:/templates/error/5xx.html
    • 如果没有模板引擎,在静态资源文件夹下找5xx.html
  1. 如果模板引擎路径templates下有error.html页面, 就直接渲染

自定义错误响应

  • 自定义json响应
    • 使用@ControllerAdvice + @ExceptionHandler 进行统一异常处理
  • 自定义页面响应
    • 根据boot的错误页面规则,自定义页面模板

最佳实践

  • 前后分离
    • 后台发生的所有错误, @ControllerAdvice + @ExceptionHandler进行统一异常处理
  • 服务端页面渲染
    • 不可预知的错误,HTTP码表示的服务器端或客户端错误
      • classpath:/templates/error/下面,放常用精确的错误码页面。500.html404.html
      • classpath:/templates/error/下面,放通用模糊匹配的错误码页面。 5xx.html4xx.html
    • 发生业务错误
      • 核心业务, 每一种错误, 都应该代码控制, 跳转到自己定制的错误页
      • 通用业务, classpath:/templates/error.html页面, 显示错误信息

无论是返回页面或者JSON数据, 可用的Model数据都一样, 如下:
model