08.SpringBoot之异常处理

SpringBoot之异常处理

定制错误页面

默认异常处理机制

错误页面,首先我们来看一下没有配置错误页面的时候,出现错误时是什么样。

1
2
3
4
5
6
7
@Controller
public class HelloController {
@GetMapping("/hello")
public void hello(){
int i = 1 / 0;
}
}

访问:

其实,这个页面可以看出一些错误信息,但是对于用户来说就显得不太友好了,用户并不关心甚至不知道这是什么错误,同时也不需要知道,没有意义。那么我们为了提升用户体验,需要对错误进行定制一番。

而对于其他客户端,默认返回json数据时,是这样的:

1
2
3
4
5
6
7
{
"timestamp": "2020-03-02T08:55:14.118+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/book"
}

我们来看一下原理:

  1. DefaultErrorAttributes

异常处理在 ErrorMvcAutoConfiguration 错误处理的自动装配。

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

我们进入这个类里面看一下 getErrorAttributes()方法。帮我们在页面共享信息。

1
2
3
4
5
6
7
8
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
this.addStatus(errorAttributes, webRequest);
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
this.addPath(errorAttributes, webRequest);
return errorAttributes;
}
  1. BasicErrorController
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
@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);
}
}
  1. ErrorPageCustomizer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

private final ServerProperties properties;

private final DispatcherServletPath dispatcherServletPath;

protected ErrorPageCustomizer(ServerProperties properties,
DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}

@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}

@Override
public int getOrder() {
return 0;
}
}

this.properties.getError().getPath()) 这句取到的数据就是:

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

此时我们回来看BacisErrorController:

1
2
3
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {}

可以发现这里指定了默认值,当然,我们也可以通过server.error.path在application.properties文件中指定。

  1. 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
@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/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
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;
}

一旦系统出现4xx或者5xx之类的错误。ErrorPageCustomizer就会生效,(定错误的响应规则)就会来到/error请求,就会被BasicErrorController处理。

自定义Controller实现BasicErrorController的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
@RequestMapping("/hello")
public class HelloController {
@GetMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView helloHtml(){
ModelAndView modelAndView = new ModelAndView();
System.out.println("处理浏览器页面请求");
modelAndView.setViewName("error/404.html");
return modelAndView;
}

@GetMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String,Object> body = new HashMap<>();
body.put("message","处理其他客户端请求");
return new ResponseEntity<>(body, HttpStatus.OK);
}
}

静态页面

下面我们就先利用静态页面来显示定制错误页面:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>404</h1>
</body>
</html>

将上述页面命名为404. 并且将其中的404改为 500,4xx,5xx , 并且改响应的文件名。页面存在 static/error目录中。

我们再次访问:

同时访问一个不存在的地址,比如localhost:8080/hello2. 会出现404. 如果我们响应的把404.html 和 500.html页面删除,显示的就是5xx.html 和 4xx.html。

动态页面

这里我们使用thymeleaf 来做动态错误页面定制,这样,我们可以定制通用的页面。

引入依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

定制动态页面:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>templates-4xx</h1>
</body>
</html>

同样,定制400.html, 4xx.html, 500.html, 5xx.html.放在 resources/templates/error 目录下。运行可以达到同样的效果,并且可以使用thymeleaf进行定制。

如果静态页面和动态页面都存在呢?或者是动态页面有404.html,静态没有404.html,或是静态页面有404.html,动态页面没有404.html呢?以及4xx.html?

经测试:优先使用精确的(404.html) ,没有则使用通用的(4xx.html)。优先使用动态的(templates/error),没有再使用静态的(static/error);

即完整的错误页面查找方式应该是这样:发生500错误–>查找动态 500.html –>查找静态 500.html –> 查找动态 5xx.html–>查找静态 5xx.html

定制异常数据

我们可以通过注解@ControllerAdvice@ExceptionHandler注解来处理异常。如下:

自定义异常处理&返回定制Json数据

1
2
3
4
5
6
7
8
9
10
11
12
@ControllerAdvice
public class MyExceptionHandler {
//浏览器,客户端返回的都是json数据
@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;
}
}

转发/error进行自适应响应结果处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e){
Map<String,Object> map = new HashMap<>();
map.put("code","user.notexist");
map.put("message",e.getMessage());
return "forward:/error";
}

@ExceptionHandler(UserNotExistException.class)
public String handleException(Exception e, HttpServletRequest request){
Map<String,Object> map = new HashMap<>();
//传入我们自己的错误状态码。
request.setAttribute("javax.servlet.error.status_code",500);//要想进行自适应需要设置状态码
map.put("code","user.notexist");
map.put("message",e.getMessage());
return "forward:/error";
}

前面我们已经可以看到一些错误基本数据:

1
2
3
4
5
6
7
{
"timestamp": "2020-03-02T08:55:14.118+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/book"
}

那么我们如何修改默认的数据呢?

将定制数据携带出去

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

​ 1)完全类编写一个errorcontroller的实现类,或继承AbstractErrorController,放在容器中

​ 2)页面上能用的数据,或者是json返回能用的数据,都是errorAttributes.getErrorAttributes获得的。

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

1
2
3
4
5
6
7
8
9
10
11
12
@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("author","ooyhao");
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyy-MM-dd HH:mm:ss");
map.put("timestamp",simpleDateFormat.format(new Date()));
return map;
}
}

测试及结果:

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×