SpringBoot 统一异常处理
异常和响应码
因为用RESTful设计的接口, 应该用状态码反映请求的错误, 不应该统一返回200 的状态码, 然后再通过 msg 来描述错误. 所以统一异常处理比较关键.
异常一般分为 业务异常
和 非业务异常
状态码 | 使用场景 |
---|---|
400 bad request | 常用在参数校验 |
401 unauthorized | 未经验证的用户,常见于未登录。如果经过验证后依然没权限,应该 403(即 authentication 和 authorization 的区别)。 |
403 forbidden | 无权限 |
404 not found | 资源不存在 |
500 internal server error | 非业务类异常 |
503 service unavaliable | 由容器抛出,自己的代码不要抛这个异常 |
业务异常通常返回4xx的状态码
非业务异常只需要返回 500
, 提示 服务器错误,请稍候重试
默认异常处理
SpringBoot 提供了默认的处理异常方式,当出现异常时就会默认映射到 /error
。处理异常的程序在类BasicErrorController
中.
该类提供了两种异常处理的方法 :
- 方法
errorHtml
用于处理浏览器端请求时出现的异常. - 方法
error
用于处理机器客户端请求时出现的异常。
这两种请求的的区别在于请求头中 Accept
的值 :
- 值为
text/html
时,方法errorHtml
执行,返回HTML
页面。 - 值为
application/json
时,方法error
执行,返回json
数据。
errorHtml
和 error
两个方法的源代码
@RequestMapping(produces = "text/html")
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
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = getStatus(request);
return new ResponseEntity<>(body, status);
}
浏览器端错误页面
如果想自定义页面替换这个页面, 你只需在 /error 文件夹下添加一个表示错误页面的文件。该文件可以是一个静态的HTML,也可以使用模板。这个文件的名字应该是精确的状态码或者是表示一个系列的模糊名称。如下:
src/
+- main/
+- java/
| + <source code>
+- resources/
+- public/
+- error/
| +- 404.html
+- <other public assets>
src/
+- main/
+- java/
| + <source code>
+- resources/
+- templates/
+- error/
| +- 5xx.ftl
+- <other templates>
其背后的原理在于上面提到的 errorHtml
方法。当出现异常时,该方法会查询是否有在 error
文件夹下提供对应错误状态码的静态资源文件,如果有则返回该文件,没有则返回上小节讲到的白色错误标签页。如果想要知道更详细的细节请查看相关源码。
JSON格式错误
当请求头中的Accept的值为 application/json
时,就会返回 json
数据了。出现异常时,BasicErrorController
类中的 error
方法将被执行。会获取错误信息然后以 Json
格式返回。如下图:
自定义异常处理
下面有两种方式自定义异常处理
- 对于应用级别的业务异常处理,我们可以通过注解
@ControllerAdvice
或@RestControllerAdvice
来实现异常处理。但是上面的注解只能捕获处理应用级别的异常,例如Controller
中抛出自定义的异常。却不能处理容器级别的异常,例如Filter
中抛出的异常等。 - 要想处理容器级别的异常,需要继承
BasicErrorController
类,重写errorHtml
和error
方法。或者实现ErrorController
接口,起到和类BasicErrorController
相似的作用。
处理应用级别异常
下面是返回 JSON
格式的处理类
@RestControllerAdvice
public class ExpectedException {
private static final long serialVersionUID = 1L;
@ExceptionHandler(value = MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public R defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
return new R<>(null, "缺少参数");
}
}
-
@RestControllerAdvice
与@ControllerAdvice
+@ResponseBody
起到的作用是一样的 - 注解
@ExceptionHandler
里面的value
的类是我们捕获的处理异常, 此类异常会被defaultErrorHandler
方法处理. -
@ResponseStatus(HttpStatus.BAD_REQUEST)
注解, 指定了此异常返回的状态码, 因为是缺少参数, 所以返回400
的状态码 -
R
类为一个ResultJSON
类, 把内容封装起来, 一般有code
,meg
,data
三个属性. 返回一个JSON
字符串.
处理容器级别的异常
@ControllerAdvice
注解的异常处理类并不能处理容器级别的异常, 我们可以通过继承 BasicErrorController
类重写 errorHtml
或 error
方法,以达到我们想要的返回结果。
还可以通过实现
ErrorController
接口的方法来实现自定义异常处理,BasicErrorController
类也是实现了ErrorController
接口,因此这种方式和BasicErrorController
类有相似作用。实现起来相对麻烦,本文只讲解继承BasicErrorController
类的方式。
自定义错误处理类 CustomizeErrorController
:
@Controller
public class CustomizeErrorController extends BasicErrorController{
public CustomizeErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers){
super(errorAttributes, errorProperties, errorViewResolvers);
}
@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 ? new ModelAndView("error", model) : modelAndView;
}
@RequestMapping
@ResponseBody
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);
ApiResponseBody responseBody = new ApiResponseBody((int)body.get("status"), (String) body.get("error") + ": " + (String) body.get("message"));
return new ResponseEntity(responseBody, status);
}
}
该类将会覆盖 BasicErrorController
类起到处理异常的作用。但这里要注意,如果想要保留对SpringBoot默认的对浏览器请求的异常处理(也就是根据异常错误状态码返回 error
文件夹下对应的错误页面),还需新建一个配置文件 CustomErrorConfiguration
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
@AutoConfigureBefore({WebMvcAutoConfiguration.class})
@EnableConfigurationProperties({ResourceProperties.class})
public class CustomErrorConfiguration {
private final ServerProperties serverProperties;
private final List<ErrorViewResolver> errorViewResolvers;
public CustomErrorConfiguration(ServerProperties serverProperties, ObjectProvider<List<ErrorViewResolver>> errorViewResolversProvider) {
this.serverProperties = serverProperties;
this.errorViewResolvers = (List)errorViewResolversProvider.getIfAvailable();
}
@Bean
public CustomizeErrorController customizeErrorController(ErrorAttributes errorAttributes){
return new CustomizeErrorController(errorAttributes, this.serverProperties.getError(),errorViewResolvers);
}
}