Spring Boot异常处理详解
- “错误”(Error)。
- “异常”(Exception)。
有别于人为失误造成的“故障”(Bug),异常在程序中代表的是出现了当前代码无法处理的状况。
例如:在一个对象不存在(值为 Null)的情况下,调用该对象的某个方法引发了空指针;用户输入了一段 URL,但并没有找到对应的资源;一段计算过程中,0被当作除数……完善的错误处理,使程序不会意外崩溃甚至能友好地提示用户进行正确操作,这是让程序变得愈发健壮的重要处理步骤。
图 1 所示为 Spring Boot 的 Whitelabel Error Page。
图1 Whitelabel Error Page
在 Java 开发中,异常特别是检查型异常(Checked Exception),通常需要进行
try/catch
处理。而在基于 Spring Boot 的开发过程中,异常处理有了更多处理方式。使用@ExceptionHandler处理异常
首先要介绍的解决方案是使用注解 @ExceptionHandler。该注解主要用于在 Controller 层面进行相同类型的异常处理。在对应 Controller 类中定义异常处理方法,并为其使用 @ExcptionHandler 注解。Spring 将检测到该注解,把该方法注册为对应异常类及其子类的异常处理程序。异常处理的示例代码如下:@ExceptionHandler({MyException.class}) public void handleException(MyException e){ //这里可以任意编写异常处理逻辑 log.info("got an exception"+e.toString()); }使用该注解的方法可以拥有非常灵活的签名,包括以下类型:
1)异常类型(Throwable)
可以选择一个大概的异常类型。例如,示例里的签名可以改为“Throwable e”或者“Exception e”,或者一个具体的异常类型。2)请求与响应对象(Request/Response)
比如 ServletRequest/HttpServletRequest。3)InputStream/Reader
用于访问请求的内容。4)OutputStream/Writer
用户访问响应的内容。5)Model
作为从该方法返回 Model 的替代方案。在返回类型方面也有灵活的选择:
- ModelAndView 对象。
- Model 对象,其对应视图由 RequestToViewNameTranslator 隐式确定。
- Map 对象,其对应视图同样由 RequestToViewNameTranslator 隐式确定。
- 值为某视图名的 String 对象,用于指定视图。
- 另外,在使用 @ResponseBody 注解标识的情况下,将返回值使用转换器转换为最终结果。
- 使用 HttpEntity<?> 或 ResponseEntity<?> 对象包装的结果。
- 使用 void 作为返回类型,然后用签名中的 Response、OutputStream 或者 Writer 编写响应内容。
使用HandlerExceptionResolver处理异常
@ExceptionHandler 功能足够强大,但在不进行特殊处理的前提之下只能处理单个 Controller 的异常。面对多个 Controller 抛出的异常,还需要依赖HandlerExceptionResolver 这一手段进行处理。使用 HandlerExceptionResolver 可以解决应用程序内的任何异常,并且依赖它可以实现 RESTful 服务的统一异常处理机制。HandlerExceptionResolver 是一个公共接口。常见的使用方式是实现一个自定义的处理类。在自定义处理类之前,可以了解一下现有的部分实现:
- ExceptionHandlerExceptionResolver:该处理类就是帮助 @ExceptionHandler 生效的组件。
- DefaultHandlerExceptionResolver:用于将标注的 Spring 异常解析为对应的 HTTP 状态码。
- ResponseStatusExceptionResolver:与注解 @ResponseStatus 一起使用,将自定义异常与 HTTP 状态码进行对应。
示例代码如下:
@ResponseStatus(value = HttpStatus.NOT_FOUND) public class MyException extends Exception { public MyException() { } public MyException(String message) { super(message); } }之所以需要自定义处理类,原因在于以上实现无法控制响应体的内容。而大多数情况下,REST 服务的响应都需要有 JSON 或者 XML 格式的响应内容。
自定义处理类的示例代码如下:
@Component @Slf4j public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver { @Override protected ModelAndView doResolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { if (ex instanceof IllegalArgumentException) { return handleIllegalArgument((IllegalArgumentException) ex, response, request); } //异常处理逻辑 } catch (Exception handlerException) { log.warn("Handling of [" + ex.getClass().getName() + "]resulted in Exception", handlerException); } return null; } private ModelAndView handleIllegalArgument(IllegalArgumentException ex, HttpServletResponse response, HttpServletRequest request) throws IOException { response.sendError(HttpServletResponse.SC_CONFLICT); String accept = request.getHeader(HttpHeaders.ACCEPT); //处理响应内容 return new ModelAndView(); } }
使用@ControllerAdvice处理异常
在 Spring 3.2 版本引入了 @ControllerAdvice 这一注解,为全局的 @ExceptionHandler 提供了支持。将这个注解批注在一个处理类上,即可让该类下由@ExceptionHandler 批注的方法在全局层面对异常进行处理。请求中包含不符合要求的内容将抛出异常 MethodArgumentNotValidException。默认的响应内容如下:
{
"timestamp":"2020-08-27T14:24:38.927+00:00",
"status":400,
"error":"Bad Request",
"trace":
"org.springframework.web.bind.MethodArgumentNotValidException: ......",
"message":"Validation failed for object='submitArticleQuery'. Error count: 1",
"errors":[
{
"codes":[
"NotNull.submitArticleQuery.content",
"NotNull.content",
"NotNull.java.lang.String",
"NotNull"
],
"arguments":[
{
"codes":[
"submitArticleQuery.content",
"content”
],
"arguments":null,
"defaultMessage":"content",
"code":"content"
}
],
"defaultMessage":"Content must not be null.",
"objectName":"submitArticleQuery",
"field":"content",
"rejectedValue":null,
"bindingFailure":false,
"code":"NotNull"
}
],
"path":"/article"
}
其中 trace 属性将会输出大段落的堆栈信息,在此处做了省略处理。对于调用方而言,返回的信息或许存在冗余。可以根据需求使用该方案对其进行调整。
在路径 src/main/java/com/example/myblog/controller 下创建 MyBlogControllerAdvice.java:
@ControllerAdvice @Slf4j public class MyBlogControllerAdvice { @ResponseBody @ExceptionHandler(value =MethodArgumentNotValidException.class) public Result<String> errorHandler(MethodArgumentNotValidException e) { String errorMsg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage(); log.error("未处理异常" + errorMsg); return new Result<String>().setMessage("参数错误:"+ errorMsg); } }响应结果如下:
{
"code": 0,
"data": null,
"message": "参数错误:Content must not be null."
}
抛出ResponseStatusException异常
上面介绍的方法大多适用于解决一个切面上的问题。如果仅需要针对少数接口进行异常处理,控制其返回给客户端的 HTTP 状态码、错误指引以及报错原因,那么 ResponseStatusException 将会是一个不错的选择。示例代码如下:@GetMapping(value = "/{id}") public Foo findById(@PathVariable("id")Long id, HttpServletResponse response) { try{ Foo resourceById=RestPreconditions.checkFound(service.findOne(id)); eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response)); return resourceById; } catch(MyResourceNotFoundException exc) { throw new ResponseStatusException(HttpStatus.NOT_FOUND,"Foo Not Found", exc); } }
声明:《Java系列教程》为本站“54笨鸟”官方原创,由国家机构和地方版权局所签发的权威证书所保护。