这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战
一、默认处理机制
默认效果:
1、浏览器访问:返回一个默认的错误页面
浏览器请求头:
2、其他客户端,默认相应json数据
请求头:
1、原理
ErrorMvcAutoConfiguration,错误处理的自动配置
给容器添加了一下组件:
(1). ErrorPageCustomizer(错误页面定制器)
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
====getPath()方法
public String getPath() {
return this.path;
}
@Value("${error.path:/error}")
private String path = "/error"; // 系统出错以后来到error请求进行处理;(web.xml注册的错误页面规则)
(2). BasicErrorController:处理默认 /error 请求,能自适应返回页面或json数据
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
// 根据客户端的请求头,选择相应的数据:html或json
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // 产生html数据,浏览器
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);
}
@RequestMapping // 产生json数据,其他客户端
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);
}
}
(3). DefaultErrorViewResolver(加载错误页面默认解析器)
@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) {
// SpringBoot默认可以找到一个页面,error/status: error/404,error/500
String errorViewName = "error/" + viewName;
// 模板引擎可以解析这个页面地址就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
// 模板引擎可用的情况下返回到errorViewName指定的视图地址
return new ModelAndView(errorViewName, model);
}
// 模板引擎不可用,就在静态资源路径下找errorViewName对应的页面
return resolveResource(errorViewName, model);
}
(4). DefaultErrorAttribute,定制错误响应信息,
// 帮我们在页面中共享信息
@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;
}
2、默认实现步骤
一旦系统出现4xx或5xx的错误;ErrorPageCustomizer就会生效(定制错误的相应规则);就会来到/error请求;就会被BasicErrorController处理:
(1). 响应页面,errorHtml(...)方法
// BasicErrorController.java -> errorHtml(...) 方法中
...
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
...
=============
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
// 所有的ErrorViewResolver得到ModelAndView,都是由DefaultErrorViewResolver解析的
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
(2). 响应json数据,error(...)方法
二、定制错误响应
1、如何定制响应错误的页面
-
有模板引擎的情况下;error/状态码【将错误页面命名为 错误状态码.html 放在模板引擎文件夹下的 error文件夹下】,发生此状态码的错误就会来到 对应的页面;
- 我们可以使用 4xx 或 5xx 作为文件名来匹配这种类型的所有错误,精确的状态码.html 优先。
-
页面能获取到的信息(由DefaultErrorAttribute获取)
-
timestamp:时间戳
-
status:状态码
- error:错误提示
- exception:异常对象
- message:异常消息
- errors:JSR303数据校验的错误所在
-
-
没有模板引擎的情况下(模板引擎下找不到/error文件夹);在静态资源路径下找,但是获取不到动态信息(timestamp,status....)
-
以上都没有,使用SpringBoot默认警告页面
2、如何定制响应错误的json数据
SpringBoot 2.x 中需配置
# 页面中 [[${exception}]] 可以获取到数据
server.error.include-exception=true
-
(1). 自定义异常处理&返回定制json数据:(缺陷:浏览器访问也将返回json数据)
@ControllerAdvice public class MyExceptionHandler { @ResponseBody @ExceptionHandler public Map<String,Object> handlerException(Exception e){ Map<String,Object> map = new HashMap<>(); map.put("code","用户名不存在"); map.put("message",e.getMessage()); return map; } } // 无法自适应相应。浏览器和其他客户端都返回json数据
-
(2). 转发到/error进行自适应响应效果处理(缺陷:页面和json无法显示自定义数据)
@ExceptionHandler(UserNotExistException.class) public String handlerException(Exception e, HttpServletRequest request){ Map<String,Object> map = new HashMap<>(); map.put("code","用户名不存在"); map.put("message",e.getMessage()); // 转发前,先传入自身错误状态码,根据自定义错误类的需求 request.setAttribute("javax.servlet.error.status_code",500); // 转发到 /error能自适响应数据 return "forward:/error"; }
-
(3). 实现浏览器或其他客户端访问,相应对应页面,并显示自定义信息
BasicErrorController类
===
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
...
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
...
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
===
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
...
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
由 getErrorAttributes() 方法获取到错误信息。
SpringBoot自动配置中默认使用实现了ErrorController接口 AbstractErrorController 抽象类书写该方法。
所以我们有三种方法写自定义错误信息:
-
写AbstractErrorController 的子类,放到容器中
-
写实现了ErrorController接口的实现类,放到容器中(选)
-
写 DefaultErrorAttributes 的子类,它还可以获取到默认的错误信息(选)
ErrorMvcAutoConfiguration类 @Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException()); } // 如果没有 ErrorAttributes 类使用默认,我们写 ErrorAttributes 的实现类
// 给容器添加自定义的 ErrorAttribute @Component public class MyErrorAttributes extends DefaultErrorAttributes { public MyErrorAttributes() { super(true); } @Override public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map<String,Object> map = super.getErrorAttributes(webRequest,includeStackTrace); map.put("code23","user not exist 23"); // errorMap 这个request域中的key,来自自定义异常类中传入的值 Map<String,Object> extMap = (Map<String, Object>) webRequest.getAttribute("errorMap",0); map.put("ex",extMap); return map; } }
ps: 浏览器访问返回的错误页面中添加的错误信息对象,必须是map中定义的key,否则为空。
总结: 为在其他客户端访问时,返回自定义的json数据,切保证返回页面也可以拿到自定义数据。
写自定义异常类 -> 异常类中写map1存入错误信息键值对,并将map1数据存入request域中 -> 转发到 /error,可以自适应返回json或页面 -> 写 DefaultErrorAttributes 的子类,这样也可以拿到默认返回的数据 - > 拿到默认数据的map2,将request域中的map1取出存入到map2中(key自定义)- > 错误页面可取默认错误数据(默认对象)、map2数据(map2的key)、map1 数据(map2的key . map1的key)(有点绕...)