本文主要对ErrorMvcAutoConfiguration进行说明

在web开发过程中总会遇到各种各样的错误,错误页面可以给予我们很好的帮助,本文介绍下springboot中的错误页面的自动配置与定制错误页面。

使用SpringBoot开发Web项目时,可以看到是有默认的错误处理
1)浏览器访问不存在的请求地址时,包含有时间、状态码等信息。
image.png
2)使用postman(非浏览器客户端)发送请求,返回的则是json数据
image.png

会出现这种现象是由于请求头的原因
浏览器发送请求会携带有text/html的请求头 如下
image.png
而非浏览器请求头中不包含这个值
首先需要将Developer Tools调试出来
image.png

image.png

ErrorMvcAutoConfiguration原理

​ 步骤:一但系统出现4xx或者5xx之类的错误;ErrorPageCustomizer就会生效(定制错误的响应规则);就会来到/error请求;就会被BasicErrorController处理;

在这个自动配置类中可以看到有以下组件

1.DefaultErrorAttributes

页面能获取的信息;

​ timestamp:时间戳

​ status:状态码

​ error:错误提示

​ exception:异常对象

​ message:异常消息

​ errors:JSR303数据校验的错误都在这里

1
2
3
4
5
6
7
8
9
@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.BasicErrorController

默认处理/error请求

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
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
//产生html类型的数据,浏览器发送的请求会被这个方法处理
//需要定制时只需要在模板引擎路径或者静态路径下放入error/状态码 对应文件即可

//匹配顺序为先精确匹配 后模糊匹配,先找模板引擎 再找静态文件夹
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);

//如果获取不到则使用默认错误页面 在ErrorMvcAutoConfiguration中有一个error的视图bean
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

//---------------------------------------------------------------------------------
AbstractErrorController.java

protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,Map<String, Object> model) {
//获取DefaultErrorViewResolver 并调用resolveErrorView方法 得到ModelAndView
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
//-------------------------------------------------------------------------------

@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);
}
}

3.ErrorPageCustomizer

1
2
3
4
5
6
7
8
9
10
11
12
//可以看到此方法 用于注册错误页面 地址默认为/error
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}

--------------------------------------------------
public class ErrorProperties {
@Value("${error.path:/error}")
private String path = "/error";

4.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
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
//先使用精确匹配 即error/404
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
//精确匹配不到时 使用模糊匹配即 error/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);
}
return resolveResource(errorViewName, model);
}

private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
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;
}

ErrorMvcAutoController

1
2
3
4
5
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}

定制错误页面

只需将状态码对应文件放入error文件夹下即可,会自动进行匹配,先精确匹配,后模糊匹配,先匹配模板引擎,再匹配静态文件

比如访问一个不存在的url

查找路径顺序 (注:stati指静态路径 包含**classpath:/META-INF/resources/“,”classpath:/resources/“, “classpath:/static/“, “classpath:/public/**)

/templates/error/404.html—–不存在时—->/static/error/404.html—–不存在时—->/templates/error/4xx.html——-不存在时—–>/static/error/4xx.html——不存在时—–>error视图

定制错误的json数据

1、自定义异常处理&返回定制的json数据

1
2
3
4
5
6
7
8
9
10
11
12
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(UserNotExistException.class)
public Map<String,Object> handleException(Exception e){
Map<String,Object> map = new HashMap<>();
map.put("code","user.notexist");
map.put("message",e.getMessage());
return map;
}
}
//没有自适应效果...

2、转发到/error进行自适应响应效果处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
//传入我们自己的错误状态码 4xx 5xx,否则就不会进入定制错误页面的解析流程
/**
* Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
*/
request.setAttribute("javax.servlet.error.status_code",500);
map.put("code","user.notexist");
map.put("message",e.getMessage());
//转发到/error
return "forward:/error";
}

3、将我们定制的数据携带出去

出现错误以后,会来到/error请求,会被BasicErrorController处理,响应出去可以获取的数据是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)规定的方法);

​ 1、完全来编写一个ErrorController的实现类【或者是编写AbstractErrorController的子类】,放在容器中;

​ 2、页面上能用的数据,或者是json返回能用的数据都是通过errorAttributes.getErrorAttributes得到;

​ 容器中DefaultErrorAttributes.getErrorAttributes();默认进行数据处理的;

自定义ErrorAttributes

1
2
3
4
5
6
7
8
9
10
11
12
13
//给容器中加入我们自己定义的ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {

@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
map.put("company","atguigu");
Map<String,Object> ext = (Map<String,Object>)requestAttributes.getAttribute("ext",0);
map.putAll(ext);
return map;
}
}

最终的效果:响应是自适应的,可以通过定制ErrorAttributes改变需要返回的内容