Spring的REST API异常处理

Spring的REST错误处理-异常处理

1、概述

	本文将阐述如何使用REST API的spring实现异常处理。我们还将获得一下历史概述,并查看不同版本引入了那些新选项。
	在spring3.2之前,在spring MVC应用程序中处理异常主要有两种方法:HandlerExceptionResolver或@ExceptionHandler注解,两者都有明显的缺点。从3.2开始,spring有了@ControllerAdvice批注,已解决前两个方案的局限性,并在整个应用中促进统一异常处理。现在Spring5引入了ResponseStatusException类:一种在REST API中进行基本错误处理的快速方法。
	所有这些确实有一个共同点-它们很好的处理了关注点分离。该应用程序通常可以引发异常以指示某种类型的异常-异常将随后单独处理。
	最后,我们将看到springboot带来的好处,以及如何配置它以满足我们的需求。

2、解决方案1-控制器级别@ExceptionHandler

第一个解决方案在Controller级别起作用-我们将定义一个异常处理的方法,并使用@ExceptionHandler对其进行注解:

public class TestController {
	//...
	@ExceptionHandler({CustomException1.class, CustomException2.class})
	public void handlerException() {
	    //...
	}
}

这种方法有一个主要缺点-带@ExceptionHandler注释的方法仅对特定持有的Controller有效,而对整个应用程序无效,当然,将其添加到每个控制器使其不适用常规异常处理机制。
我们可以让所有的Controller扩展Base Controller类来解决此限制,但是,对于无论出于何种原因都无法实现的应用程序来说,这可能是个问题。例如,控制器可能已经从另一个基类扩展了,该基类可能在另一个jar中,或者不能直接修改,或者它们本身也不能直接修改。
接下来,我们将讨论另一种解决异常处理问题的方法-一种全局的方法,不包括对现有工件(如Controller)的任何更改。

3、解决方案2-HandlerExceptionResolver

第二种解决方案是定义HandlerExceptionResolver-这将解决应用程序引发的任何异常。它还将使我们能够在REST API中实现统一的异常处理机制。
在使用自定义解析器之前,让我们看一下现有的实现。

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

3.2 DefaultHandlerExceptionResolver
该解析器在spring3.0中引入,默认情况下在DispatcherServlet中启用。它用于将标准Spring异常解析为其对应的HTTP状态代码,即客户端错误-4xx和服务器错误-5xx代码。这是它处理Spring Exception的完整列表,以及它们如何映射带状态代码
在这里插入图片描述
尽管它正确设置了响应的状态码,但一个限制是它没有对响应的主体设置任何内容。对于REST API,状态码实际上是不足以向客户端提供信息-响应也必须具有主体,以允许应用程序提供有关故障的其他信息。
可以通过配置视图分辨率并通过ModelAndView呈现错误内容来解决此问题,但是解决方案不是最佳的。这就是为什么Spring 3.2引入了一个更好的选项的原因,我们将在后面的部分中进行讨论。

3.3 ResponseStatusExceptionResolver
此解析器也在Spring 3.0中引入,并且默认情况下在DispatchServlet中启用。它的主要职责是使用自定义异常上可用的@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相同,此解析器在处理响应主体方面受到限制-它确实将状态码映射到响应上,但主体仍为null。

3.4 SimpleMappingExceptionResolver和AnnotationMethodHandlerExceptionResolver
该SimpleMappingExceptionResolver已经有相当长的一段时间-它是上了年纪的Spring MVC模型出来,是不是一个REST服务非常相关的。我们基本上使用它来映射异常类名称以查看名称。
该AnnotationMethodHandlerExceptionResolver在Spring3.0中引入处理过的异常@ExceptionHandler注释但已被弃用ExceptionHandlerExceptionResolver如Spring3.2。

3.5 自定义HandlerExceptionResolver
DefaultHandlerExceptionResolver和ResponseStatusExceptionResolver的组合在为Spring RESTful服务提供良好错误处理机制方面还有很长的路要走。如前所述,不利方面是无法控制响应主体。
理想情况下,我们希望能够输出JOSN或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 Service的错误处理的一致且易于配置的机制。但是,它确实有局限性:它与低级HtttpServletResponse进行交互,并且适合使用ModelAndView的旧MVC模型-因此仍有改进的空间。

4、解决方案3-@ControllerAdvice

Spring 3.2通过@ControllerAdvice注释为全局@ExceptionHandler提供支持。这将启用一种机制,该机制有别于旧的MVC模型,并利用ResponseEntity以及@ExceptionHandler的类型安全性和灵活性:

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

该@ControllerAdvice注释使我们能够巩固的多,此@ExceptionHandler期从之前一个单一的,到全球性的错误处理组件。
实际的机制非常简单,但也非常灵活。它给我们:

  • 完全控制响应的主体以及状态码
  • 将多个异常映射到同一方法,以一起处理,并且 它充分利用了更新的RESTful ResposeEntity响应
    这里要记住的一件事是将@ExceptionHandler声明的异常与用作方法参数的异常进行匹配。如果这些不匹配,则编译器将不会报错-没有理由,并且Spring也不会抛出异常。
    但是,当实际在运行时引发异常时,异常解决机制将因以下原因而失败:
java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...

5、解决方案4-ResponseStatusException(Spring5以及更高)

Spring5引入了ResponseStatusException类。我们可以创建一个提供HttpStatus是实例,还可以选择一个reason and a cause:

@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有什么好处?

  • 出色的原型制作:我们可以很快实现基本解决方案
  • 一种类型,多种状态代码:一种异常类型可以导致多种不同的响应。与@ExceptionHandler相比,这减少了紧密耦合
  • 我们将不必创建那么多的自定义异常类
  • 由于可以通过编程方式创建异常,因此可以更好地控制异常处理
    那权衡又如何呢?
  • 没有统一的异常处理方式:实施一些应用程序范围的约定比@ControllerAdvice提供全局方法要困难得多。
  • 代码复制:我们可能会发现自己在多个控制器中复制代码
    我们还应该注意,可以在一个应用程序中组合不同的方法。
    例如,我们可以 全局实现@ControllerAdvice,还可以 局部实现 ResponseStatusException。但是,我们需要注意:如果可以通过多种方式处理相同的异常,我们可能会注意到一些令人惊讶的行为。一种可能的约定是始终以一种方式处理一种特定类型的异常。

6、处理Spring Security中拒绝的访问

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

6.1 MVC –自定义错误页面
首先,让我们看一下该解决方案的MVC风格,看看如何为Access Denied自定义错误页面:
XML配置:

<http>
    <intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/>   
    ... 
    <access-denied-handler error-page="/my-error-page" />
</http>

和Java配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedPage("/my-error-page");
}

当用户尝试在没有足够权限的情况下访问资源时,他们将被重定向到“ / my-error-page ”。

6.2 自定义AccessDeniedHandler
接下来,让我们看看如何编写我们的自定义AccessDeniedHandler:

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
 
    @Override
    public void handle
      (HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) 
      throws IOException, ServletException {
        response.sendRedirect("/my-error-page");
    }
}

现在,我们使用XML Configuration对其进行配置:

<http>
    <intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/> 
    ...
    <access-denied-handler ref="customAccessDeniedHandler" />
</http>

或使用Java配置:

@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;
 
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
        ...
        .and()
        .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
}

注意如何–在我们的CustomAccessDeniedHandler中,我们可以通过重定向或显示自定义错误消息来根据需要自定义响应。

6.3 REST和方法级安全性
最后,让我们看看如何处理方法级别的安全@PreAuthorize,@PostAuthorize和@Secure Access Denied。

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

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

7、Spring Boot支持

Spring Boo提供了一个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允许使用属性配置以下功能:

  • Server.error.whitelabel.enable: 可用于禁用Whitelable错误页面并依靠servlet容器提供HTML错误消息
  • Server.error.include-stacktrace: 具有始终值,它在HTML和JSON默认响应中都包含stacktrace
    除了这些属性,我们还可以为错误提供我们自己的视图解析器映射,以覆盖whitelabel页面。
    我们还可以通过在上下文中包含ErrorAttributes bean来定制要在响应中显示的属性。我们可以扩展Spring Boot提供的DefaultErrorAttributes类,使事情变得更容易:
 @Component
public class MyCustomErrorAttributes extends DefaultErrorAttributes {
 
    @Override
    public Map<String, Object> getErrorAttributes(
      WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = 
          super.getErrorAttributes(webRequest, includeStackTrace);
        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) {
        super(errorAttributes, new ErrorProperties());
    }
 
    @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
    public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) {
        
    // ...
 
    }
}

8、结论

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
DataX 的 REST API 是基于 Spring Boot 实现的,代码位于 `datax-restapi` 模块中。以下是该模块的源码结构: ``` ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── alibaba │ │ │ └── datax │ │ │ └── restapi │ │ │ ├── controller // REST API 控制器 │ │ │ ├── exception // 异常处理类 │ │ │ ├── service // 服务类 │ │ │ ├── util // 工具类 │ │ │ ├── Application.java // Spring Boot 应用启动类 │ │ │ ├── Constant.java // 常量定义类 │ │ │ ├── DataxJob.java // DataX 任务实体类 │ │ │ ├── DataxJobConfig.java // DataX 任务配置实体类 │ │ │ └── Result.java // 接口返回结果类 │ │ └── resources │ │ ├── application.yml // 应用配置文件 │ │ ├── banner.txt // 启动时的 ASCII Art │ │ └── logback.xml // 日志配置文件 │ └── test │ └── java │ └── com │ └── alibaba │ └── datax │ └── restapi │ ├── controller │ ├── service │ └── util └── pom.xml // Maven 项目配置文件 ``` 其中,`controller` 包下是 REST API 的控制器,`service` 包下是服务类,`exception` 包下是异常处理类,`util` 包下是工具类。实体类 `DataxJob` 和 `DataxJobConfig` 分别对应 DataX 任务和任务配置。`Constant` 类定义了接口返回结果的常量值。 具体的源码实现可以参考 DataX 在 GitHub 上的源码:https://github.com/alibaba/DataX/tree/master/datax-restapi

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值