V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
huangdaxian
V2EX  ›  Java

我能用异常及全局异常控制来设计异常返回的流程吗? Spring MVC

  •  
  •   huangdaxian · 2019-07-05 11:00:12 +08:00 · 6314 次点击
    这是一个创建于 1728 天前的主题,其中的信息可能已经有所发展或是发生改变。

    正常来说,如果一个 Service 因为数据验证未通过等原因,需要驳回请求,则只能多层 return error,然后在 Controller 里依据某些约定来返回对应的报错信息。

    我现在是通过全局异常处理来统一返回。在 Service 里需要中断请求、报错返回的情况下,直接抛出一个 RuntimeException,然后被全局处理捕获,按约定统一返回。

    一直以来,我都拒绝第一种方式,因为第一种方式需要冗杂的代码来进行返回的控制,并且难以达到最好的效果。 但是我今天在阅读 [阿里巴巴 Java 开发手册时] ,上面标注了:

    [强制] 异常不要用来做流程控制,条件控制。 说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。

    那么该如何抉择?

    如果抛出预加载的静态异常对象呢?

    第 1 条附言  ·  2019-08-07 16:57:20 +08:00
    最近比较忙,我会在闲下来后进行总结,给收藏的兄弟们一个交代
    39 条回复    2019-07-08 17:24:32 +08:00
    lengbumo
        1
    lengbumo  
       2019-07-05 11:16:26 +08:00
    目前用的是在 Service 层抛出自定义异常,然后在 controller 层捕获异常
    比如:
    if (null == user) {
    throw new BizException("E00001", "用户不能为空");
    }
    然后 controller 捕获异常
    catch (BizException e) {
    return AppRespUtil.fail(e.getErrCode(), e.getMessage());
    }
    JRay
        2
    JRay  
       2019-07-05 11:21:07 +08:00
    直接用的 @ControllerAdvice 处理
    Duluku
        3
    Duluku  
       2019-07-05 11:23:54 +08:00
    是的,阿里的手册就是这么要求的,当然这个是对阿里的 code 的强制要求,也可以不接受的。
    chendy
        4
    chendy  
       2019-07-05 11:33:09 +08:00
    错误的输入本来就是意外情况,应该抛异常的
    huangdaxian
        5
    huangdaxian  
    OP
       2019-07-05 11:34:40 +08:00
    huangdaxian
        6
    huangdaxian  
    OP
       2019-07-05 11:35:41 +08:00
    @JRay
    @lengbumo
    我是 @ExceptionHandler 捕获异常,@ControllerAdvice 封装正常返回。
    huangdaxian
        7
    huangdaxian  
    OP
       2019-07-05 11:36:21 +08:00
    @Duluku 如果是阿里特有的情况,那么他们是怎么设计报错流程以达到类似的功能的呢?
    huangdaxian
        8
    huangdaxian  
    OP
       2019-07-05 11:38:26 +08:00
    @chendy 但不可否认异常的开销比 return 大得多。我见过我们公司其他人负责的模块代码,层层 return,各种 if else 对 return 的内容进行判断,在我看来又臭又长,但终归人家觉得自己效率高。
    coolcfan
        9
    coolcfan  
       2019-07-05 11:50:57 +08:00
    我好像听人说过,在某开源项目里看到过用异常做流程控制——是真的流程控制,而不是因为出错而抛出或处理异常……
    Inside
        10
    Inside  
       2019-07-05 12:24:07 +08:00
    流程也分正常流程,异常流程。
    异常流程用异常来全局处理没毛病,spring 也支持在 @ControllerAdvice 中统一处理异常。

    另外,在有异常的语言里还用返回值做错误处理是嫌自己工作量不饱和非要增加心智和体力负担吗,能让机器累的为什么要让人累?

    再另外,意外情况如何定义?我认为数据输入错误就是意外情况,比如登录时把密码输入错了。我就喜欢定义一个 InvalidPasswordException。
    chendy
        11
    chendy  
       2019-07-05 12:27:32 +08:00
    @huangdaxian 全部手撸性能最高,但是大多数系统根本不在意这点性能,因为瓶颈大多数时候不会出现在这种地方
    huangdaxian
        12
    huangdaxian  
    OP
       2019-07-05 12:44:48 +08:00
    针对于意外情况:我觉得使程序无法正常执行结束的均为意外情况。

    1. 输入错误抛 400 BadRequest
    2. 访问的对象不存在 404 NotFound

    有时候还会抛出一些正常数据及逻辑下不可能出现的异常:

    3. 系统某项数据缺失的异常 500 InternalServerError

    目前我的系统中主要就是上面三种,其中 1 的情况最多,但更细的就没有划分下去了。
    目前预留了 `errorCode` 的字段,但是还没时间设计相关的对应关系。
    huangdaxian
        13
    huangdaxian  
    OP
       2019-07-05 12:45:30 +08:00
    @Inside 回复见楼上
    sampeng
        14
    sampeng  
       2019-07-05 12:57:27 +08:00 via iPhone
    嗯。然后满屏幕的日志是 unknowerror
    chengzh
        15
    chengzh  
       2019-07-05 13:20:51 +08:00
    `异常不要用来做流程控制,条件控制` 本来就是个伪命题, 像是 springaop 里面的事务回滚也是依赖抛异常来做的,
    参考 spring security oauth 这些框架当密码不匹配,没有权限,它们也是以异常的形式通知上层的。
    这没有什么问题。
    我认为阿里上面说的 `异常不要用来做流程控制` 是有前提的 应该是指的 dubbo rpc 接口 要 try 起来以 errcode 和 errormsg 的形式返回。这里面除了性能的考虑之外还有一个很重要的原因是方便理解定位错误的原因吧。

    而且楼上所说的 errorcode msg 就要层层 if else 一坨屎一样。其实这个也是很优雅的。
    比如 函数语言里的 Either
    ````scala
    def divideBy2(x: Int, y: Int): Either[String, Int] = {
    if(y == 0) Left("Dude, can't divide by 0")
    else Right(x / y)
    }

    divideBy2(1, 0) match {
    case Left(s) => println("Answer: " + s)
    case Right(i) => println("Answer: " + i)
    }
    ````
    如果是 java 也是可以做到类似效果的:
    ````java
    public class BaseController {

    private Logger logger = LoggerFactory.getLogger(BaseController.class);

    /****
    * 验证参数 执行函数返回及结果
    * @param vo 封装参数的 vo
    * @param fun 执行业务方法的函数
    * @param <V> vo 类型
    * @param <R> 业务函数返回类型
    * @return
    */
    public <V, R> Response<R> action(V vo, Function<V, R> fun) {

    return Try.of(() -> {
    if (vo != null) {
    String vstr = ValidatorUtils.validate(vo);
    if (StringUtils.isNotBlank(vstr)) {
    return new Response(ResponseStatus.BAD_REQUEST.getCode(), vstr);
    }
    }
    R result = fun.apply(vo);
    Response<R> response = new Response(ResponseStatus.OK.getCode(), ResponseStatus.OK.getMsg());
    response.setData(result);

    return response;
    }

    ).recover(e -> {
    logger.error("", e);
    Response<R> response = GlobalControllerExceptionHandler.resolveExceptionCustom(e);
    return response;

    }).get();


    }}
    ````
    chengzh
        16
    chengzh  
       2019-07-05 13:31:21 +08:00
    ````java
    @RequiresRoles(roles = {RoleEnum.ADMIN_CENTER, RoleEnum.ADMIN_AREA, RoleEnum.ADMIN_CITY})
    @ApiOperation(value = "新增费用录入")
    @PostMapping("/saveCost")
    public Response<Boolean> save(@ApiIgnore @ModelAttribute DmaSalaryEmpInfo empInfo, @RequestBody AddCostVO vo) {
    return action(vo, v -> costService.saveCost(empInfo, v));
    }
    ````
    这是我目前配合上面的函数接口的使用方式,这样子异常在 service 层出来以后直接就被 action 函数消化了,不然会一直往上面的调用站抛,直到 spring 回调你的异常处理器。 既避免了 性能问题,也解决了代码不优雅的问题。
    CRUD
        17
    CRUD  
       2019-07-05 13:40:48 +08:00
    我也是用 @ControllerAdvice 全局捕获,然后根据 restful 返回异常的 http 状态和 code、message。

    ```
    @ControllerAdvice
    @Slf4j
    public class ExceptionHandle {

    /**
    * 拦截业务异常
    *
    * @param e 异常信息
    * @param response Http 响应
    * @return 全局异常响应
    */
    @ExceptionHandler(ApiRequestException.class)
    @ResponseBody
    public ExceptionResponse handleApiRequestException(ApiRequestException e, HttpServletResponse response) {
    response.setStatus(e.getCode());
    ExceptionResponse exceptionResponse = new ExceptionResponse();
    exceptionResponse.setStatus(e.getCode());
    exceptionResponse.setMessage(e.getMessage());
    return exceptionResponse;
    }

    }
    ```
    chengzh
        18
    chengzh  
       2019-07-05 13:46:17 +08:00
    @CRUD 直接写一个这样的全局异常处理器的话 异常是从 service 抛出一直抛到 spring 的 disPathServlet 再回调过来,不如上层搞一个函数可以省去这个步骤。
    Amayadream
        19
    Amayadream  
       2019-07-05 13:48:34 +08:00
    我的理解是:
    hyl24
        20
    hyl24  
       2019-07-05 13:53:55 +08:00
    spring 哪个 resource 我记得都有用 try catch 控制流程的呢·· 之前看源码看到过
    Amayadream
        21
    Amayadream  
       2019-07-05 14:04:57 +08:00   ❤️ 1
    不要用异常做业务流程控制, 特别是 RuntimeException, 比如用户不存在返回 UserNotFoundException, 密码不正确返回 PasswordErrorException, 用户不存在和密码不正确都属于可预料的情况, 需要业务自己处理而不是过度依赖抛异常来统一处理

    如果 service 返回结果很复杂, 可以考虑声明一个复合返回类型, 类似接口层返回结果, 将可能产生的结果包装在内, 调用者看一下返回值就能知道所有可能的情况, 而不是去阅读你的代码才能知道会有多少个分支

    异常做流程控制一时爽, 后续维护和调用者就要骂娘了
    no1xsyzy
        22
    no1xsyzy  
       2019-07-05 14:46:51 +08:00
    参考一下 erlang 的思想,出现 Exception 的时候应该直接认为程序无法继续,自身已经无法处理的情况。
    大致上就是当作 Exception 的处理都是直接记录并重启。
    chocotan
        23
    chocotan  
       2019-07-05 14:57:47 +08:00
    阿里的这个所谓规范看看就行了
    之前围观 spring security 的时候看到就是用异常来控制的
    wysnylc
        24
    wysnylc  
       2019-07-05 14:59:03 +08:00
    异常不要用来做流程控制,条件控制。 应当改为 不要用异常来控制程序的正常流程
    https://www.iteye.com/topic/857443 这是一个 8 年前的讨论,时至今日该问题还是在重复
    zzxzzxhao
        25
    zzxzzxhao  
       2019-07-05 15:43:08 +08:00
    @Amayadream 我赞同你说的,不要过度依赖统一处理异常。以前我也写过全局处理异常的,但是实际业务中的抛出的异常不一定能考虑到,那就代表无法精准定位问题,反而耗费更多的精力
    limuyan44
        26
    limuyan44  
       2019-07-05 15:54:53 +08:00 via Android
    异常流程一种是直接 over 的流程,一种是分支流程,比如 savepoint ?合适才是最好了没必要一刀切。
    liuhuansir
        27
    liuhuansir  
       2019-07-05 16:06:30 +08:00
    我一直都是用全局捕获自定义异常,但是有个问题请教大家,因为某些 service 中的业务逻辑是共用的,导致返回给客户端的提示信息描述不太合适,有没有好的办法?
    MotherShip
        28
    MotherShip  
       2019-07-05 16:25:48 +08:00   ❤️ 1
    你看看上面那条

    1. [强制] Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过
    catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
    说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,不得不通过 catch
    NumberFormatException 来实现。
    正例:if (obj != null) {...}
    反例:try { obj.method(); } catch (NullPointerException e) {…}


    2. [强制] 异常不要用来做流程控制,条件控制。
    说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式
    要低很多。

    第二条其实和第一条的意思是一样的,能判空的情况就不要去 catch NPE,类似的还有能判断 List 长度就不要去 catch 下标越界异常。。
    huangdaxian
        29
    huangdaxian  
    OP
       2019-07-07 16:00:18 +08:00
    @liuhuansir 异常类里定义 errorCode,使用中间层或者前端维护 message。
    YzSama
        30
    YzSama  
       2019-07-08 09:07:58 +08:00
    我的做法是

    1. 全局定义统一异常处理并封装异常信息。
    2. 使用 HttpStatus 来做 顶层异常类,例如 401 无权限、400 参数错误或其他、404 资源找不到。包装的时候,直接就包装这类异常。
    3. 业务异常里,使用 Code、Message 来处理。

    例如:

    ```json

    {
    "status": 401,
    "error": "Unauthorized",
    "message": "用户未登录",
    "code": 4001,
    "path": "/example/example-xjaldkskskal",
    "exception": "com.example.exception.UserNotLoginException",
    "errors": [
    {
    "code": 20009,
    "message": "token 为空"
    }
    ],
    "timestamp": 1556536629108
    }

    ```

    errors 是具体的业务错误信息。errors 包含多个 error。

    我们采用了 Restful api 设计,所以使用 httpstatus 来做顶层的异常类,为了更好的针对业务异常进一步的处理和展现,就加了一个 errors。。 只有两层。

    个人理解和想法。
    YzSama
        31
    YzSama  
       2019-07-08 09:18:17 +08:00
    对了,我认为异常来控制流程 这一边

    因为,我认为流程一旦出错,分两种,可执行 和 不可执行。

    可执行,是即使出错了,也无所谓。

    不可执行,是一旦数据校验不正确,不可能走下去,应该往外抛。

    正常流程,是啥事都没有,这就是正确的并且是按照 研发人员设计的流程走下去的。

    如果,全局异常处理的设计去考虑性能问题,我觉得对,也不对。你想想,你用的大部分第三方依赖库,它们不也往外抛异常吗?
    huangdaxian
        32
    huangdaxian  
    OP
       2019-07-08 12:07:38 +08:00
    @YzSama 大体流程相同,也是包装 NotFoundException 这种。但是 errors 这里没看懂。errors 里面是 Exception 的错误链吗?你封装的 NotFoundException 不是 RuntimeException ?
    YzSama
        33
    YzSama  
       2019-07-08 15:19:45 +08:00
    @huangdaxian #32

    errors 可以理解具体错误的行为表现。

    if(Objects.isNull(token)){
    // result 就是 Errors 的体现错误的信息
    throws new NotFoundException(ErrorResult.TOKEN_IS_NULL);
    }

    所以,增加了 NotFoundException(ErrorResult errorResult) ,来体现 业务主要错误的具体信息。

    所有自定义异常都是继承 RuntimeException。
    huangdaxian
        34
    huangdaxian  
    OP
       2019-07-08 15:35:11 +08:00
    @YzSama 那么什么情况下会有多个 eroor 的 eroors ? code 为 4001 的 Unauthorized error 是在哪一层被添加上的?
    huangdaxian
        35
    huangdaxian  
    OP
       2019-07-08 15:40:00 +08:00
    @chengzh 16 楼没有看到 v
    YzSama
        36
    YzSama  
       2019-07-08 16:09:15 +08:00
    @huangdaxian

    封装自定义异常初始化的时候,默认生成。

    errors 一般没有多个 error 的存在。

    主要是方便后面拓展的时候,客户端不用从 JsonObject 改成 JsonArray。 直接就是用 JsonArray。哈哈
    huangdaxian
        37
    huangdaxian  
    OP
       2019-07-08 16:17:37 +08:00
    @YzSama 眼看着一个饶有创造的设计揭开神秘的面纱后让我吐血满地
    YzSama
        38
    YzSama  
       2019-07-08 17:01:27 +08:00
    @huangdaxian #37 😂...
    ourslay
        39
    ourslay  
       2019-07-08 17:24:32 +08:00 via iPhone
    继承 DefaultErrorAttributes
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3330 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 37ms · UTC 11:23 · PVG 19:23 · LAX 04:23 · JFK 07:23
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.