SpringBoot的错误处理机制

在web开发中的异常错误处理

SpringBoot默认有一套对web开发错误处理的机制,在autoConfiguration包下面找到了ErrorMvcAutoConfiguration

1
2
3
4
5
6
7
8
@Configuration
// 只有基于servlet的web程序
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// 需要先加载WebMvcAutoConfiguration
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {

处理机制

在这个自动配置类中,由三个最基本的bean组件组成,下面挨个看这些注入到容器中的bean的含义

errorPageCustomizer 定义错误页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
public ErrorPageCustomizer errorPageCustomizer() {
return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
}

// 在ErrorPageCustomizer有一个registerErrorPages 注册页面的方法
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
// 用于获取error页面的地址
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
// 地址在 this.properties.getError().getPath()变量中具体的值:
@Value("${error.path:/error}")
// 配置文件中error.path下的/error或者根目录下的/error文件夹
private String path = "/error";

所以errorPageCustomizer的主要功能就是找到地址,拼装成ErrorPage;

WhitelabelErrorViewConfiguration 空白页的配置

1
2
3
4
5
6
7
8
9
protected static class WhitelabelErrorViewConfiguration {
// 在StaticView中定义了默认的Whitelabel页面格式,
private final StaticView defaultErrorView = new StaticView();

@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}

basicErrorController 控制器

basicErrorController就是简单的控制器

1
2
3
4
5
@Controller //controller实现 注册到容器中
// 错误映射地址 error.path 或
// server.error.path 下 /error
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController

在这个Controller中映射到了/error地址上,有具有两个RequestMapping进行映射。

请求的媒体类型为text/html

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
// 媒体类型为 text/html 时候使用这个对/error的接收
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
// 获得request中的状态码
// Integer statusCode =
// (Integer) request.getAttribute("javax.servlet.error.status_code");
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);
}

其中使用夫类中的getErrorAttributes方法来获取基本属性数据,通过return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace)直接调用已经注入到容器中的ErrorAttributes类及其子类。如默认的DefaultErrorAttributes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
// 返回到页面的当前系统时间
errorAttributes.put("timestamp", new Date());
// 下面的 javax.xxxx 都是从requeset中得到的
// 返回状态码 javax.servlet.error.status_code
addStatus(errorAttributes, webRequest);
//错误信息 avax.servlet.error.message
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
// 请求路径 javax.servlet.error.request_uri
addPath(errorAttributes, webRequest);
return errorAttributes;
}

在获取完需要返回的数据之后,返回一个modelAndView对象,也就是一个带有显示的界面。

默认使用DefaultErrorViewResolver来进行对ErrorView的解析。

1
2
3
4
5
6
7
8
9
10
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
// 获取modelandview对象
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
// 如果没有modelandview
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}

如果没有通过resolve方法找到一个modelAndView。则会有类似通过 5xx.html 页面来展示5开头的那些错误的页面。可选值有下面两种:

1
2
3
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用resolve方法来获取ModelAndView
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//这里的viewName为状态编码,视图为 error/「状态码值」

String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
// 如403错误,那么就会返回 error/403.html 前提是有模版引擎
return new ModelAndView(errorViewName, model);
}
// 没有就去 { "classpath:/META-INF/resources/",
// "classpath:/resources/", "classpath:/static/", "classpath:/public/" };
// 这几个路径上找 路径下的error/xxx.html,这些路径就是默认的资源文件路径
return resolveResource(errorViewName, model);
}

如果到上一步仍不存在view则说明,模版引擎/基本资源文件夹下均不存在error文件夹下的xxx.html文件,则会返回一个默认的view 。(modelAndView != null) ? modelAndView : new ModelAndView("error", model);就是在标题【WhitelabelErrorViewConfiguration】中提及到的StaticView

请求为其他媒体类型

1
2
3
4
5
6
7
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
// 直接返回map
return new ResponseEntity<>(body, status);
}

errorAttributes 错误页面属性信息

1
2
3
4
5
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
}

这个bean主要是使用getErrorAttributes提供ErrorController返回值的信息,如果状态码等。。如果想更改错误信息的返回值内容,可以继承DefaultErrorAttributes,然后在getErrorAttributes方法里添加想要添加的内容即可。

添加错误页面和修改返回信息

下面具体来根据SpringBoot特性来添加错误页面,如404,500等。

​ 因为在处理机制这章里说明了SpringBoot如何处理错误,他会默认的访问/error/地址,并且如果是text/html的媒体类型,也就是网页访问的话,如果有模版引擎,他会去找在/error文件中对应编码的xxx.html页面进而去渲染这个页面。

​ 比如访问404,他会去找classpath:/error/404.html页面去渲染,如果没有,他会去找classpath:/error/4xx.html,如果还没有,他会返回一个页面。

添加error页面

​ 为了演示,创建404.html4xx.html来达到这样的目的:如果发生404错误,则访问404.html页面,如果发生402或者以4开头的错误,则访问4xx.html页面

目录结构和示例内容参考如下:

定义异常和对应的异常处理

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
47
48
49
50
51
52
53
54
public class NoAuthException extends RuntimeException {
public NoAuthException() {
super("无权限异常");
}
}

public class NoUserExcetion extends RuntimeException {
public NoUserExcetion() {
super("无用户异常");
}
}

// 异常处理类
@ControllerAdvice
public class MyExecptionHandler {

// 处理NoAuthException 异常
@ExceptionHandler(value = NoAuthException.class)
public String handlerNoAuthException(HttpServletRequest request){
// 这是必须的;定义返回的code值,返回指定的错误页面
request.setAttribute("javax.servlet.error.status_code",402);
Map<String,Object> map = new HashMap<>();
map.put("code",402);
map.put("message","this is my no auth message");
// 将自定义信息放入 request中
request.setAttribute("errData",map);
return "forward:/error";
}
//处理NoUserExcetion 异常
@ExceptionHandler(value = NoUserExcetion.class)
public String handlerNoUserException(HttpServletRequest request){
request.setAttribute("javax.servlet.error.status_code",400);
Map<String,Object> map = new HashMap<>();
map.put("code",404);
map.put("message","this is my no auth message");
request.setAttribute("errData",map);
return "forward:/error";
}
}

// controller类
@RequestMapping("/hello")
@ResponseBody
public String hello(String name){
if("abc".equals(name)){
// 如果是 /hello?name=abc的时候抛出NoAuthException异常 跳转到4xx页面
throw new NoAuthException();
}
if("123".equals(name)){
// 如果是 /hello?name=123的时候抛出NoUserExcetion的异常 跳转到404页面
throw new NoUserExcetion();
}
return "hello world";
}

定义自定义属性类

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
// 放到容器中,默认会替换到springboot的默认的DefaultErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
// 获取DefaultErrorAttributes的基本属性
Map<String,Object> map = super.getErrorAttributes(webRequest,includeStackTrace);
Map<String,Object> errData = (Map<String, Object>) webRequest.getAttribute("errData", 0);
// 从request中获取自定义的data,添加到返回的信息中。
map.put("errData",errData);
return map;
}
}

通过以上的设置,可以得到如下结果:

  • 如果是 /hello?name=abc的时候抛出NoAuthException异常 跳转到4xx页面
  • 如果是 /hello?name=123的时候抛出NoUserExcetion的异常 跳转到404页面
  • 如果没有访问地址也会到404页面

测试结果

  • 访问/hello?name=abc 跳转到4xx页面

4xx

  • 访问/hello?name=123 跳转到404页面

400

结论

在考虑对web错误页面处理的角度,无非就是两个方面:

一、 页面样式

​ 在有模版引擎的情况下,通过DefaultErrorController/error上的访问处理,来自动渲染在template/error/目录下的对应的错误code.html的的展示,如404.html。并且支持模糊匹配,如创建4xx.html页面,那么如果没有发现特定的错误代码页面,则自动的使用4xx页面。

二、 页面内容

页面内容可以通过继承DefaultErrorAttributes类来进行简单的实现,如果想全部替换掉SpringBoot的默认全部的返回内容,则需要实现ErrorAttributes进行实现,需要注意的是必须放到容器中才能生效。

-------------本文结束感谢您的阅读-------------

本文标题:SpringBoot的错误处理机制

文章作者:NanYin

发布时间:2019年07月17日 - 12:07

最后更新:2019年08月12日 - 13:08

原始链接:https://nanyiniu.github.io/2019/07/17/2019-07-17-SpringBoot%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86%E6%9C%BA%E5%88%B6/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。