理解Spring @ControllerAdvice 国外文章翻译

@ControllerAdvice 是在Spring 3.2引入的。
它能够让你统一在一个地方处理应用程序所发生的所有异常。
而不是是在当个的控制器中分别处理。
你可以把它看作是@RequestMapping方法的异常拦截器
这篇文章我们我们通过一个简单的案例来看一下Controller层使用
全局的异常处理后的好处和一些注意点。

一个新闻爱好者社区写了一个应用程序,可以让使用者通过平台
进行文章的分享。这个应用程序有三个API 如下:

  •   GET /users/:user_id: 基于用户ID获取用户明细
    
  •   POST /users/:user_id/posts: 发布一篇文章并且该用户户是文章的拥有者
    
  •   POST /users/:user_id/posts/:post_id/comments: 评论某篇文章
    

控制器中的异常处理

UserController get方法中username可能是非法的值,这个用户根本
不在我们的系统中。所以自定义了一个异常类并把它抛出去。

第一种处理异常的方式:
就是在Controller层中加入 @ExceptionHandler 如下所示:

@RestController
@RequestMapping("/users")
public class UserController {
    @GetMapping("/{username}")
    public ResponseEntity get(@PathVariable String username) throws UserNotFoundException {
        // More logic on User

        throw UserNotFoundException.createWith(username);
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity handleContentNotAllowedException(UserNotFoundException unfe) {
        List errors = Collections.singletonList(unfe.getMessage());

        return new ResponseEntity<>(new ApiError(errors), HttpStatus.NOT_FOUND);
    }
}

PostController 发布文章时如果文章的内容不符合我们的规定
则抛出异常

@RestController
@RequestMapping("/users/{username}/posts")
public class PostController {


    @PostMapping
    public ResponseEntity create(@PathVariable String username, @RequestBody Post post)
            throws ContentNotAllowedException {
        List contentNotAllowedErrors = ContentUtils.getContentErrorsFrom(post);

        if (!contentNotAllowedErrors.isEmpty()) {
            throw ContentNotAllowedException.createWith(contentNotAllowedErrors);
        }

        // More logic on Post

        return new ResponseEntity<>(HttpStatus.CREATED);
    }

    @ExceptionHandler(ContentNotAllowedException.class)
    public ResponseEntity handleContentNotAllowedException(ContentNotAllowedException cnae) {
        List errorMessages = cnae.getErrors()
                .stream()
                .map(contentError -> contentError.getObjectName() + " " + contentError.getDefaultMessage())
                .collect(Collectors.toList());

        return new ResponseEntity<>(new ApiError(errorMessages), HttpStatus.BAD_REQUEST);
    }
}

CommentController 评论文章时如果评论的内容不符合我们的规定
则抛出异常

@RestController
@RequestMapping("/users/{username}/posts/{post_id}/comments")
public class CommentController {
    @PostMapping
    public ResponseEntity create(@PathVariable String username,
                                          @PathVariable(name = "post_id") Long postId,
                                          @RequestBody Comment comment)
            throws ContentNotAllowedException{
        List contentNotAllowedErrors = ContentUtils.getContentErrorsFrom(comment);

        if (!contentNotAllowedErrors.isEmpty()) {
            throw ContentNotAllowedException.createWith(contentNotAllowedErrors);
        }

        // More logic on Comment

        return new ResponseEntity<>(HttpStatus.CREATED);
    }

    @ExceptionHandler(ContentNotAllowedException.class)
    public ResponseEntity handleContentNotAllowedException(ContentNotAllowedException cnae) {
        List errorMessages = cnae.getErrors()
                .stream()
                .map(contentError -> contentError.getObjectName() + " " + contentError.getDefaultMessage())
                .collect(Collectors.toList());

        return new ResponseEntity<>(new ApiError(errorMessages), HttpStatus.BAD_REQUEST);
    }
}

优化和改进

在写完了上面的代码之后,发现这种方式可以进行优化和改进,
因为异常在很多的控制器中重复的出现PostController#create and CommentController#create throw the same exception
出现重复的代码我们潜意识就会思考如果有一个中心化的点来统一处理这些异常那就好了,但注意无论何时controller层都要抛出异常,如果有显示捕获异常
那么要将哪些无法处理的异常抛出,之后就会用统一的异常处理器进行处理,而不是每个控制器都进行处理。

@ControllerAdvice 登场

如下图所示:


image.png

GlobalExceptionHandler类被@ControllerAdvice注释,那么它就会拦截应用程序中所有来自控制器的异常。
在应用程序中每个控制器都定义了一个与之对应的异常,因为每个异常都需要进行不同的处理。
这样子的话如果用户API接口发生异常,我们根据异常的类型进行判定然后返回具体的错误信息
这样子我们的错误就非常容易进行定位了。在这个应用中ContentNotAllowedException包含了一些不合时宜的词,
而UserNotFoundException仅仅只有一个错误信息。那么让我门写一个控制器来处理这些异常。

如何写异常切面

下面几点是在我们写异常切面需要考虑的点

创建你自己的异常类:

尽管在我们的应用程序中Spring提供了很多的公用的异常类,但是最好还是写一个自己应用程序的异常类或者是扩展自已存在的异常。

一个应用程序用一个切面类:

这是一个不错的想法让所有的异常处理都在单个类中,而不是把异常处理分散到多个类中。

写一个handleException方法:

被@ExceptionHandler注解的方法会处理所有定义的异常然后对进行进行分发到具体的处理方法,起异常代理的作用。

返回响应结果给客户端:

异常处理方法就是实现具体的异常处理逻辑,然后返回处理结果给客户端

GlobalExceptionHandler 代码演示

@ControllerAdvice
public class GlobalExceptionHandler {
    /** Provides handling for exceptions throughout this service. */
    @ExceptionHandler({ UserNotFoundException.class, ContentNotAllowedException.class })
    public final ResponseEntity handleException(Exception ex, WebRequest request) {
        HttpHeaders headers = new HttpHeaders();

        if (ex instanceof UserNotFoundException) {
            HttpStatus status = HttpStatus.NOT_FOUND;
            UserNotFoundException unfe = (UserNotFoundException) ex;

            return handleUserNotFoundException(unfe, headers, status, request);
        } else if (ex instanceof ContentNotAllowedException) {
            HttpStatus status = HttpStatus.BAD_REQUEST;
            ContentNotAllowedException cnae = (ContentNotAllowedException) ex;

            return handleContentNotAllowedException(cnae, headers, status, request);
        } else {
            HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
            return handleExceptionInternal(ex, null, headers, status, request);
        }
    }

    /** Customize the response for UserNotFoundException. */
    protected ResponseEntity handleUserNotFoundException(UserNotFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        List errors = Collections.singletonList(ex.getMessage());
        return handleExceptionInternal(ex, new ApiError(errors), headers, status, request);
    }

    /** Customize the response for ContentNotAllowedException. */
    protected ResponseEntity handleContentNotAllowedException(ContentNotAllowedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        List errorMessages = ex.getErrors()
                .stream()
                .map(contentError -> contentError.getObjectName() + " " + contentError.getDefaultMessage())
                .collect(Collectors.toList());

        return handleExceptionInternal(ex, new ApiError(errorMessages), headers, status, request);
    }

    /** A single place to customize the response body of all Exception types. */
    protected ResponseEntity handleExceptionInternal(Exception ex, ApiError body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
            request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
        }

        return new ResponseEntity<>(body, headers, status);
    }
}

第三种使用单独的方法处理异常

这样子就不用自己做统一的分发,由Spring进行异常的匹配
自动路由到对应的异常处理类上


@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    @Autowired
    private ActiveProperties activeProperties;

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public FailHttpApiResponse handle(Exception e) {


        return new FailHttpApiResponse("系统发生异常", "");

    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public FailHttpApiResponse methodArgumentNotValidException(MethodArgumentNotValidException e){
        BindingResult bindingResult =e.getBindingResult();
        String message = bindingResult.getFieldErrors().get(0).getDefaultMessage();
        return new FailHttpApiResponse(HttpServletResponse.SC_BAD_REQUEST, "参数非法: " +message , "");
    }

    @ExceptionHandler(BusinessException.class)
    @ResponseBody
    public FailHttpApiResponse businessException(BusinessException e){
        return new FailHttpApiResponse(e.getCode(), e.getMsg(), "");
    }

}

@ControllerAdvice类现在就能拦截所有来自控制器的异常,开发者需要做的就是把之前写在控制器中的异常处理
代码异常掉,仅此而已。

英文版地址:https://medium.com/@jovannypcg/understanding-springs-controlleradvice-cd96a364033f

你可能感兴趣的:(理解Spring @ControllerAdvice 国外文章翻译)