理解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<User> get(@PathVariable String username) throws UserNotFoundException {
        // More logic on User

        throw UserNotFoundException.createWith(username);
    }

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ApiError> handleContentNotAllowedException(UserNotFoundException unfe) {
        List<String> 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<Post> create(@PathVariable String username, @RequestBody Post post)
            throws ContentNotAllowedException {
        List<ObjectError> 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<ApiError> handleContentNotAllowedException(ContentNotAllowedException cnae) {
        List<String> 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<Comment> create(@PathVariable String username,
                                          @PathVariable(name = "post_id") Long postId,
                                          @RequestBody Comment comment)
            throws ContentNotAllowedException{
        List<ObjectError> 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<ApiError> handleContentNotAllowedException(ContentNotAllowedException cnae) {
        List<String> 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<ApiError> 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<ApiError> handleUserNotFoundException(UserNotFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        List<String> errors = Collections.singletonList(ex.getMessage());
        return handleExceptionInternal(ex, new ApiError(errors), headers, status, request);
    }

    /** Customize the response for ContentNotAllowedException. */
    protected ResponseEntity<ApiError> handleContentNotAllowedException(ContentNotAllowedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        List<String> 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<ApiError> 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容