翻译:https://www.baeldung.com/exception-handling-for-rest-with-spring
本教程将展示如何为 REST API 实现 Spring 异常处理。我们将了解到有多种实现方式。它们都有一个共同点:都能很好地处理关注点分离。应用可以正常抛出异常以表明某种失败,异常将被单独处理。
我们可以使用 @ExceptionHandler 来注解方法,当发生指定异常时,Spring 将自动调用这些方法。我们可以通过注解指定异常,也可以将其声明为方法参数,这使我们能够从异常对象中读取详细信息以正确处理异常。该方法本身作为控制器方法被处理,所以:
一个返回 400 状态码的最简单的异常处理器可能是这样的:
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(CustomException1.class)
public void handleException1() { }
我们也可以将处理的异常声明为方法参数,例如,读取异常详细信息并创建一个符合 RFC-9457 标准的问题详细信息对象:
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
public ProblemDetail handleException2(CustomException2 ex) {
// ...
}
自 Spring 6.2 起,我们可以针对不同的内容类型编写不同的异常处理器:
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler( produces = MediaType.APPLICATION_JSON_VALUE )
public CustomExceptionObject handleException3Json(CustomException3 ex) {
// ...
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler( produces = MediaType.TEXT_PLAIN_VALUE )
public String handleException3Text(CustomException3 ex) {
// ...
}
而且,我们还可以针对不同类型的异常编写异常处理器。如果处理方法需要详细信息,我们使用所有异常类型的共享超类作为方法参数:
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({
CustomException4.class,
CustomException5.class
})
public ResponseEntity<CustomExceptionObject> handleException45(Exception ex) {
// ...
}
我们可以将此类处理器方法放置在控制器类中:
@RestController
public class FooController {
//...
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(CustomException1.class)
public void handleException() {
// ...
}
}
我们可以在需要控制器特定异常处理时使用这种方法。但它的缺点是除非我们将它放在基类中并使用继承,否则无法在多个控制器中使用它。但还有另一种更适合组合而非继承的方法。
@ControllerAdvice 包含多个控制器共享的代码。它是一种特殊的 Spring 组件。对于 REST API 而言,每个方法的返回值都应被渲染到响应体中,因此有一个 @RestControllerAdvice。
因此,为了处理应用中所有控制器的特定异常,我们可以编写一个简单的类:
@RestControllerAdvice
public class MyGlobalExceptionHandler {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(CustomException1.class)
public void handleException() {
// ...
}
}
我们应该知道,还有一个基类(ResponseEntityExceptionHandler),我们可以通过继承它来使用常见的预定义功能,如 ProblemDetails 生成。我们还可以继承用于处理典型 MVC 异常的方法:
@ControllerAdvice
public class MyCustomResponseEntityExceptionHandler
extends ResponseEntityExceptionHandler {
@ExceptionHandler({
IllegalArgumentException.class,
IllegalStateException.class
})
ResponseEntity<Object> handleConflict(RuntimeException ex, WebRequest request) {
String bodyOfResponse = "This should be application specific";
return super.handleExceptionInternal(ex, bodyOfResponse,
new HttpHeaders(), HttpStatus.CONFLICT, request);
}
@Override
protected ResponseEntity<Object> handleHttpMediaTypeNotAcceptable(
HttpMediaTypeNotAcceptableException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request {
// ... (customization, maybe invoking the overridden method)
}
}
在上述示例中,请注意,由于所有方法都返回一个 ResponseEntity ,所以我们使用了普通的 @ControllerAdvice 注解,而没有在类上添加 @RestControllerAdvice 注解。
另一种简单的方法是直接用 @ResponseStatus 注解我们的自定义异常:
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class MyResourceNotFoundException extends RuntimeException {
// ...
}
与 DefaultHandlerExceptionResolver 一样,此解析器在处理响应体方面存在限制 —— 它会将状态码映射到响应上,但响应体仍然是 null 。我们只能用它来处理我们的自定义异常,因为我们无法注解已经编译好的现有类。而且,在分层架构中,我们只应将此方法用于边界特定的异常。
顺便说一下,我们应该注意到,在这种情况下,异常通常是从 RuntimeException 派生而来的,因为我们这里不需要编译器检查。否则,这将导致我们代码中出现不必要的 throws 声明。
控制器还可以抛出一个 ResponseStatusException 。我们可以通过提供一个 HttpStatus 来创建它的实例,并可选地提供一个原因和一个原因实例:
@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") Long id) {
try {
// ...
}
catch (MyResourceNotFoundException ex) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "Foo Not Found", ex);
}
}
使用 ResponseStatusException 有什么好处呢?
那权衡又是什么呢?
另一种解决方案是定义一个自定义的 HandlerExceptionResolver 。它将解析应用抛出的任何异常。它还将允许我们在 REST API 中实现一个统一的异常处理机制。
在 DispatcherServlet 中默认启用了几种现有实现:
DefaultHandlerExceptionResolver 和 ResponseStatusExceptionResolver 的组合为我们提供了一个良好的 Spring RESTful 服务的错误处理机制。缺点是,如之前所述,我们对响应体没有控制权。
理想情况下,我们希望能够根据客户端请求的格式(通过 Accept 头)输出 JSON 或 XML。
这本身就证明了创建一个新的自定义异常解析器是合理的:
@Component
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, handler);
}
// ...
} catch (Exception handlerException) {
logger.warn("Handling of [" + ex.getClass().getName() + "]
resulted in Exception", handlerException);
}
return null;
}
private ModelAndView handleIllegalArgument(
IllegalArgumentException ex, HttpServletResponse response) throws IOException {
response.sendError(HttpServletResponse.SC_CONFLICT);
String accept = request.getHeader(HttpHeaders.ACCEPT);
// ...
return new ModelAndView();
}
}
这里需要注意的一个细节是,我们能够访问请求本身,因此我们可以考虑客户端发送的 Accept 头的值。
例如,如果客户端请求 application/json,那么在发生错误时,我们希望确保返回一个用 application/json 编码的响应体。
另一个重要的实现细节是,我们返回一个 ModelAndView —— 这是响应的主体,它允许我们设置任何必要的内容。
这种方法是 Spring REST 服务的错误处理机制,它是一致且易于配置的。
然而,它也有局限性:它与低级的 HtttpServletResponse 交互,并且符合使用 ModelAndView 的旧的 MVC 模型。
在典型的 REST 实现中,我们经常要处理以下几种异常:
当然,我们将使用前面讨论的全局异常处理机制来处理 AccessDeniedException:
@RestControllerAdvice
public class MyGlobalExceptionHandler {
@ResponseStatus(value = HttpStatus.FORBIDDEN)
@ExceptionHandler( AccessDeniedException.class )
public void handleAccessDeniedException() {
// ...
}
}
Spring Boot 提供了一个 ErrorController 实现,以一种合理的方式处理错误。
简而言之,它为浏览器提供了一个回退错误页面(即 Whitelabel 错误页面),并为 RESTful、非 HTML 请求提供了一个 JSON 响应:
{
"timestamp": "2019-01-17T16:12:45.977+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Error processing the request!",
"path": "/my-endpoint-with-exceptions"
}
与往常一样,Spring Boot 允许通过属性配置这些功能:
除了这些属性,我们还可以提供自己的视图解析器映射,用于 /error,以覆盖 Whitelabel 页面。
我们还可以通过在上下文中包含一个 ErrorAttributes bean 来定制我们希望在响应中显示的属性。我们可以扩展 Spring Boot 提供的 DefaultErrorAttributes 类来简化操作:
@Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(
WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
errorAttributes.put("locale", webRequest.getLocale().toString());
errorAttributes.remove("error");
//...
return errorAttributes;
}
}
如果我们想进一步定制应用对特定内容类型错误的处理方式,我们可以注册一个 ErrorController bean。
同样,我们可以利用 Spring Boot 提供的默认 BasicErrorController 来帮助我们。
例如,假设我们想定制应用对 XML 端点触发的错误的处理方式。我们只需定义一个使用 @RequestMapping 的公共方法,并声明它生成 application/xml 媒体类型:
@Component
public class MyErrorController extends BasicErrorController {
public MyErrorController(
ErrorAttributes errorAttributes, ServerProperties serverProperties) {
super(errorAttributes, serverProperties.getError());
}
@RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) {
// ...
}
}
注意:这里我们仍然依赖我们在项目中可能已经定义的绑定到 ServerProperties bean 的 server.error.* Spring Boot 属性。
在本文中,我们讨论了几种为 Spring REST API 实现异常处理机制的方法。我们根据其使用场景对它们进行了比较。
我们应该注意到,在一个应用中可以结合使用不同的方法。例如,我们可以实现一个@ControllerAdvice 进行全局处理,同时在本地使用 ResponseStatusException 进行处理。