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" }
|
我们来看一下原理:
- 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; }
|
- 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 { @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); } @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); } }
|
- 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文件中指定。
- 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) { String errorViewName = "error/" + viewName; TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,this.applicationContext); if (provider != null) { 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; }
|
一旦系统出现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 { @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; } }
|
测试及结果: