Spring Boot处理异常的时候有两种办法:
在编写REST风格的API的时候,如果通过浏览器访问不存在的地址,会得到一个HTML格式的错误。如果用非浏览器比如POSTMAN朝不存在的地址发一个响应,得到的是一个:
{ "timestamp": "2019-06-01T11:09:59.938+0000", "status": 404, "error": "Not Found", "message": "No message available", "path": "/userf" }
包含错误信息的JSON对象。这是怎么做到的呢。
同一个路径根据不同响应头返回不同内容
Spring 的错误处理的一个基础类叫做BasicErrorController
,简要代码如下:
package org.springframework.boot.autoconfigure.web.servlet.error; @Controller @RequestMapping({"${server.error.path:${error.path:/error}}"}) public class BasicErrorController extends AbstractErrorController { @RequestMapping( produces = {"text/html"} ) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = this.getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = this.resolveErrorView(request, response, status, model); return modelAndView != null ? modelAndView : new ModelAndView("error", model); } @RequestMapping public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = this.getStatus(request); return new ResponseEntity(body, status); } }
这个类实际上也是一个控制器,可以看到类的标注,响应/error这个路径。
这里有两个RequestMapping,一个说明了请求头里如果有Accept:text/html,就会返回后边的errorHtml对象。如果没有,就返回一个Map对象转换成的JSON对象。
这里实际上是要学习,如何使用同一个路径针对不同的请求头作出不同的响应。在Spring Security相关的开发中使用的比较广泛。
Spring Boot框架的默认错误处理
在POST请求中使用了BindingResult来捕捉错误。如果没有捕捉,直接POST一个密码为空的JSON过去,(关闭认证和CSRF),能看到错误的JSON字符串:
{ "timestamp": "2019-06-01T12:07:53.277+0000", "status": 400, "error": "Bad Request", "errors":[ { "codes":["NotBlank.user.password", "NotBlank.password", "NotBlank.java.lang.String", "NotBlank"], "arguments":[{"codes":["user.password", "password" ], "arguments": null, "defaultMessage": "password",…], "defaultMessage": "密码不能为空白", "objectName": "user", "field": "password", "rejectedValue": "", "bindingFailure": false, "code": "NotBlank" } ], "message": "Validation failed for object='user'. Error count: 1", "path": "/user" }
这实际上是框架捕捉了响应的错误信息,然后组装成了JSON进行返回。实际上这个异常被框架拦截了,说明框架有一个异常拦截器。
如果我们自己的控制器里要捕捉异常怎么做呢。
在GET方法里自己抛一个运行时错误:
@GetMapping("/{id:\\d+}") @JsonView(User.UserDetailView.class) public User queryDetail(@PathVariable String id) { throw new RuntimeException("故意抛出的错误"); }
用网页访问还可以看到错误页面,用REST访问也能拿到JSON,可以看到,前端只要了解了Spring Boot默认的错误机制,也可以处理错误。
自定义错误页面
Spring Boot会自动到resources/resources/error/下边寻找与错误码名称相同的HTML页面,比如404.html,作为浏览器出错的时候对应的返回页面。
而用REST访问的时候,该返回错误对象依然是错误对象。
这是一个比较简单的通过默认HTML页面处理错误处理。
自定义错误类来控制REST返回的JSON内容
要自定义的话,先要自定义一个错误类型:
package com.imooc.security.exhandler; public class UserNotExistException extends RuntimeException { private static final long serialVersionUID = -21378921789984L; public UserNotExistException(String id) { super("用户不存在,自定义的错误类, id是 " + id); this.id = id; } //自定义的部分 private String id; public String getId() { return id; } public void setId(String id) { this.id = id; } }
我们想返回id的内容,如果此时只是抛出自定义错误,Spring Boot打包之后的错误信息里不会包含id的内容,必须自己编写一个处理错误的类:
package com.imooc.security.exhandler; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.Map; //这个注解表示该控制器用于处理其他控制器中出现的异常 @ControllerAdvice public class ControllerExceptionHandler { //这个注解用于处理对应的异常,这里就是针对UserNotExistException异常 @ExceptionHandler(UserNotExistException.class) //加上了注解之后,这个方法的参数就是对应的异常对象 // @ResponseBody 表示将Controller返回的结果写入响应体,而不是像普通视图一样进行视图解析 //一般用在这种特殊的控制类中,因为不能直接标记上RestController @ResponseBody //可以自行设置一个响应码,该错误还是得抛出错误 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Map<String, Object> handleUserNotExistException(UserNotExistException ex) { Map<String, Object> result = new HashMap<>(); result.put("id", ex.getId()); result.put("message", ex.getMessage()); return result; } }
使用了@ControllerAdvice及对应注解之后,遇到这种错误就会返回此种JSON,而不是Spring Boot默认的JSON。
这样在编写REST客户端的时候,就可以灵活的定义错误,可以给前端传递更多的信息,这样就方便获取具体的内容。
如果愿意的话,在后端验证出现错误的时候,也可以抛出一个自定义的异常,把所有异常的字段和错误信息都返回给前端用于处理。