一、SpringBoot 默认的错误处理机制
SpringBoot 根据浏览器的请求头返回相应的信息:
- 浏览器,返回一个默认的错误页面。
- 如果是其他客户端,默认响应一个 JSON 数据。
SpringBoot 判断是浏览器还是客户端的方法:
二、SpringBoot 错误处理机制原理
ErrorMvcAutoConfiguration
类是 SpringBoot 自动配置的错误处理方式,它给容器添加了四大组件:
- DefaultErrorAttributes
- BasicErrorController
- ErrorPageCustomizer
- DefaultErrorViewResolver
当系统出现 4xx 或者 5xx 之类的错误,ErrorPageCustomizer
就会生效(定制错误的响应规则)。
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
响应的接口为 /error 。
/**
* Path of the error controller.
*/
@Value("${error.path:/error}")
private String path = "/error";
接着就会请求 /error ,会被 BasicErrorController
处理。
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
// 产生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, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
// 将哪个页面作为错位页面,包含页面的地址和页面内容
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
// 产生 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, isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
响应页面:去哪个页面由 ErrorViewResolver
接口的实现类 DefaultErrorViewResolver
决定的。
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
// 从所有的 ErrorViewResolver 得到 modelAndView
for (ErrorViewResolver resolver : this.errorViewResolvers) {
// 执行 DefaultErrorViewResolver 类的 resolveErrorView 方法
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
首先查看模板引擎是否能解析到指定状态码的页面,如果有则用模板引擎解析的页面,如果没有则去静态页面下查找。
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");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
// 首先对状态码进行精准匹配,例如404
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
// 如果精准匹配的状态码页面不存在,则进行模糊匹配。例如 4xx。
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
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) {
// 模板引擎可用的情况下返回 errorViewName 指定的视图地址
return new ModelAndView(errorViewName, model);
}
// 模板引擎不可用,就在静态资源文件夹下找 errorViewName 对应的页面,例如 error/404.html
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
// 去静态资源下查找 404.html
for (String location : this.resourceProperties.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;
}
DefaultErrorAttributes
为页面提供错误信息:
- timestamp:时间戳
- status:状态码
- error:错误提示
- exception:异常对象
- message:异常消息
- errors:JSR303 数据校验的错误都在这里
在错误页面中可以通过 [[${timestamp}]]
获取指定信息的值(仅在模板引擎下有效)。
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}
三、如何定制错误响应
1、如何定制错误页面
根据上面的原理分析,我们可知:
- 有模板引擎的情况下,将错误页面命名为
错误状态码.html
放在模板引擎文件夹里面的 error 文件夹下,发生此状态码的错误就会来到 对应的页面。 - 没有模板引擎(模板引擎找不到这个错误页面),静态资源文件夹下找。
- 以上都没有错误页面,就是默认来到 SpringBoot 默认的错误提示页面。
我们可以使用 4xx 或 5xx 作为错误页面的文件名也匹配这种类型的所有错误,精确优先(优先寻找精准的状态码.html)。
2、如何定制错误的 JSON 数据
方法一:
在 @ExceptionHandler
注解中捕获指定异常,返回特定的 JSON 数据,但是没有自适应的效果,无论是客户端还是浏览器返回的都是 JSON 数据,很显然这并不是我们想要的结果。
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(Exception.class)
public Map<String ,Object> handlerException(Exception e){
Map<String,Object> map=new HashMap<>();
map.put("code",500);
map.put("message",e.getMessage());
return map;
}
}
方法二:
将请求转发到 /error
可以实现自适应响应效果处理,但是我们的数据并没有携带出去。
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(Exception.class)
public String handlerException(Exception e, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
// 源码中获取装状态码的代码
// Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code")
// 设置状态码,否则状态码默认为200,不会跳转到错误页面
request.setAttribute("javax.servlet.error.status_code",500);
map.put("code", 500);
map.put("message", e.getMessage());
return "forward:/error";
}
}
数据没有携带出去的原因
当系统出现错误后,会来到 /error 请求,会被 BasicErrorController 处理。响应出去可以获取的数据是由 getErrorAttributes
得到的(该方法来自 ErrorAttributes
接口)。
现在我们来看 ErrorMvcAutoConfiguration
类中的 errorAttributes
方法。当容器中没有 ErrorAttributes
时,会向容器中添加一个 DefaultErrorAttributes
,所以 ErrorAttributes
接口的默认实现是 DefaultErrorAttributes
。
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
}
DefaultErrorAttributes
中的 getErrorAttributes
只添加了基本的错误信息,并没有添加我们自定义的错误信息。
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}
将我们的定制数据携带出去
经过上面的分析我们有两种解决方案:
- 完全来编写一个
ErrorController
的实现类或者编写AbstractErrorController
的子类放在容器中,重新定义/error
接口的映射规则(过于麻烦)。 - 页面上能用的数据或者是 JSON 返回的数据都是通过
getErrorAttributes
得到的,容器中DefaultErrorAttributes.getErrorAttributes()
是默认数据处理的方式。可以继承DefaultErrorAttributes
类重写getErrorAttributes
方法,我们自己定义数据的处理方式。
将数据封装在 HttpServletRequest
中。
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(Exception.class)
public String handlerException(Exception e, HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
// 源码中获取装状态码的代码
// Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code")
// 设置状态码,否则状态码默认为200,不会跳转到错误页面
request.setAttribute("javax.servlet.error.status_code",500);
map.put("code", 500);
map.put("message", e.getMessage());
request.setAttribute("ext",map);
return "forward:/error";
}
}
重写 getErrorAttributes
方法,使用 webRequest
获取异常处理器封装的数据,将其添加到 map 集合中即可。
@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","Yi-Xing");
// 我们的异常处理器携带的数据
Map<String, Object> ext=(Map<String, Object>)webRequest.getAttribute("ext", RequestAttributes.SCOPE_REQUEST);
map.put("ext",ext);
return map;
}
}
标题:SpringBoot 异常处理机制
作者:Yi-Xing
地址:http://zyxwmj.top/articles/2020/04/03/1585907905090.html
博客中若有不恰当的地方,请您一定要告诉我。前路崎岖,望我们可以互相帮助,并肩前行!