0

    Spring Boot异常处理

    2023.06.07 | admin | 143次围观

    概要

    本教程将演示如何用Spring为REST API实现异常处理。我们还将了解一些历史概况,看看不同版本引入了哪些新选项。

    在Spring 3.2之前,在Spring MVC应用程序中处理异常的两种主要方法是HandlerExceptionResolver或@ExceptionHandler注释。两者都有一些明显的缺点。

    从3.2开始,我们有了@ControllerAdvice注释来解决前两种解决方案的局限性,并在整个应用程序中促进统一的异常处理。

    现在,Spring 5引入了ResponseStatusException类REST APIs中基本错误处理的一种快速方法。

    所有这些都有一个共同点:它们很好地处理了关注点的分离。该应用程序通常会抛出异常来指示某种类型的故障,这些故障将被单独处理。

    最后,我们将看到Spring Boot带来了什么,以及我们如何配置它来满足我们的需求。

    2. 案例1: 使用@ExceptionHandler

    第一种解决方案在@Controller级别工作。我们将定义一个方法来处理异常,并用@ExceptionHandler对其进行注释:

    public class FooController{
        
        //...
        @ExceptionHandler({ CustomException1.class, CustomException2.class })
        public void handleException() {
            //
        }
    }

    这种方法有一个主要缺点:@ExceptionHandler注释方法仅对特定的控制器有效,而不是对整个应用程序全局有效。当然,将这一点添加到每个控制器会使它不太适合一般的异常处理机制。

    我们可以通过让所有控制器扩展一个基本控制器类来解决这个限制。

    然而,这种解决方案对于应用程序来说可能是个问题,因为无论什么原因,这都是不可能的。例如,控制器可能已经从另一个基类扩展,该基类可能在另一个jar中或者不可直接修改,或者它们本身不可直接修改。

    接下来,我们将看看解决异常处理问题的另一种方法——这种方法是全局的,不包括对现有构件(如控制器)的任何更改。

    3. 案例2: HandlerExceptionResolver

    第二种解决方案是定义HandlerExceptionResolver。这将解决应用程序抛出的任何异常。它还允许我们在REST API中实现统一的异常处理机制。

    在使用自定义解析器之前,让我们先看一下现有的实现。

    3.1.ExceptionHandlerExceptionResolver

    这个解析器是在Spring 3.1中引入的,默认情况下在DispatcherServlet中启用。这实际上是前面介绍的@ExceptionHandler机制如何工作的核心组件。

    3.2.DefaultHandlerExceptionResolver

    这个解析器是在Spring 3.0中引入的,在DispatcherServlet中默认启用。

    它用于解决标准的Spring异常及其相应的HTTP状态代码,即客户机错误4xx和服务器错误5xx状态代码。下面是它处理的Spring异常的完整列表,以及它们如何映射到状态代码。

    虽然它确实正确地设置了响应的状态代码,但有一个限制是,它没有为响应的主体设置任何内容。对于REST API来说——状态代码真的不足以向客户端呈现信息——响应还必须有一个主体,以允许应用程序给出关于失败的附加信息。

    这可以通过ModelAndView配置视图分辨率和渲染错误内容来解决,但解决方案显然不是最优的。这就是为什么Spring 3.2引入了一个更好的选项,我们将在后面的部分讨论。

    3.3.ResponseStatusExceptionResolver

    Spring 3.0中也引入了这个解析器,在DispatcherServlet中默认启用。

    它的主要职责是使用自定义异常上可用的@ResponseStatus注释,并将这些异常映射到HTTP状态代码。

    这种自定义异常可能如下所示:

    @ResponseStatus(value = HttpStatus.NOT_FOUND)
    public class MyResourceNotFoundException extends RuntimeException {
        public MyResourceNotFoundException() {
            super();
        }
        public MyResourceNotFoundException(String message, Throwable cause) {
            super(message, cause);
        }
        public MyResourceNotFoundException(String message) {
            super(message);
        }
        public MyResourceNotFoundException(Throwable cause) {
            super(cause);
        }
    }

    与DefaultHandlerExceptionResolver相同,这个解析器在处理响应正文的方式上有所限制——它确实在响应上映射了状态代码,但是正文仍然为空。

    3.4.CustomHandlerExceptionResolver

    DefaultHandlerExceptionResolver和ResponseStatusExceptionResolver的结合为Spring RESTful服务提供了良好的错误处理机制。如前所述服务器内部错误的状态码是,缺点是无法控制响应的主体。

    理想情况下,我们希望能够输出JSON或XML,这取决于客户机要求的格式(通过Accept头)。

    仅此一点就证明了创建一个新的自定义异常解决程序的合理性:

    @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模型服务器内部错误的状态码是,因此仍有改进的空间。

    4. 案例3:@ControllerAdvice

    Spring Boot异常处理

    Spring 3.2通过@ControllerAdvice注释支持全局@ExceptionHandler。

    这使得一种机制脱离了旧的MVC模型,并利用了ResponseEntity以及@ExceptionHandler的类型安全性和灵活性:

    @ControllerAdvice
    public class RestResponseEntityExceptionHandler 
      extends ResponseEntityExceptionHandler {
        @ExceptionHandler(value 
          = { IllegalArgumentException.class, IllegalStateException.class })
        protected ResponseEntity handleConflict(
          RuntimeException ex, WebRequest request) {
            String bodyOfResponse = "This should be application specific";
            return handleExceptionInternal(ex, bodyOfResponse, 
              new HttpHeaders(), HttpStatus.CONFLICT, request);
        }
    }

    @ControllerAdvice注释允许我们将以前分散的多个@ ExceptionHandlers整合到一个单一的全局错误处理组件中。

    实际机制非常简单,但也非常灵活:

    这里需要记住的一点是,将使用@ExceptionHandler声明的异常与用作方法参数的异常进行匹配。

    如果这些不匹配,编译器不会抱怨——没有理由应该抱怨Spring也不会处理。

    但是,当异常在运行时实际抛出时,异常解决机制将失败,并显示:

    java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
    HandlerMethod details: ...

    5. 案例4:ResponseStatusException(Spring 5 and Above)

    Spring 5引入了ResponseStatusException类。

    我们可以创建它的一个实例,提供一个HttpStatus和一个可选的原因:

    @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);
        }
    }

    使用ResponseStatusException有什么好处?

    非常适合原型开发:我们可以很快实现一个基本的解决方案。

    缺点呢?

    我们还应该注意,在一个应用程序中结合不同的方法是可能的。

    例如,我们可以在全局实现@ControllerAdvice,但也可以在本地实现ResponseStatusExceptions。

    然而,我们需要小心:如果同一个异常可以用多种方式处理,我们可能会注意到一些令人惊讶的行为。一个可能的惯例是总是以一种方式处理一种特定的异常。

    6. 处理Spring安全中拒绝的访问

    当经过身份验证的用户试图访问他没有足够权限访问的资源时,就会发生访问被拒绝。

    6.1.REST和方法级安全性

    最后,让我们看看如何处理方法级安全注释抛出的拒绝访问异常——@PreAuthorize, @PostAuthorize, @Secure.

    当然,我们将使用前面讨论过的全局异常处理机制来处理AccessDeniedException:

    @ControllerAdvice
    public class RestResponseEntityExceptionHandler 
      extends ResponseEntityExceptionHandler {
        @ExceptionHandler({ AccessDeniedException.class })
        public ResponseEntity handleAccessDeniedException(
          Exception ex, WebRequest request) {
            return new ResponseEntity(
              "Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN);
        }
        
        ...
    }

    7. Spring Boot支持

    Spring Boot提供了一个ErrorController实现来以合理的方式处理错误。

    简而言之,它为浏览器提供了一个回退错误页面(也称为白色标签错误页面),并为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 getErrorAttributes(
          WebRequest webRequest, ErrorAttributeOptions options) {
            Map 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> xmlError(HttpServletRequest request) {
            
        // ...
        }
    }

    注意:这里我们仍然依赖于server.error.* Boot属性我们可能已经在我们的项目中定义了,它被绑定到ServerProperties bean。

    8. 总结

    本文讨论了在Spring中为REST API实现异常处理机制的几种方法,从旧的机制开始,继续Spring 3.2支持,直到4.x和5.x。

    对于Spring安全相关的代码,您可以检查spring-security-rest模块。

    版权声明

    本文仅代表作者观点。
    本文系作者授权发表,未经许可,不得转载。

    发表评论