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

2019-07-05 11:00:12 +08:00
 huangdaxian

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

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

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

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

那么该如何抉择?

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

6379 次点击
所在节点    Java
39 条回复
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
2019-07-05 11:21:07 +08:00
直接用的 @ControllerAdvice 处理
Duluku
2019-07-05 11:23:54 +08:00
是的,阿里的手册就是这么要求的,当然这个是对阿里的 code 的强制要求,也可以不接受的。
chendy
2019-07-05 11:33:09 +08:00
错误的输入本来就是意外情况,应该抛异常的
huangdaxian
2019-07-05 11:34:40 +08:00
huangdaxian
2019-07-05 11:35:41 +08:00
@JRay
@lengbumo
我是 @ExceptionHandler 捕获异常,@ControllerAdvice 封装正常返回。
huangdaxian
2019-07-05 11:36:21 +08:00
@Duluku 如果是阿里特有的情况,那么他们是怎么设计报错流程以达到类似的功能的呢?
huangdaxian
2019-07-05 11:38:26 +08:00
@chendy 但不可否认异常的开销比 return 大得多。我见过我们公司其他人负责的模块代码,层层 return,各种 if else 对 return 的内容进行判断,在我看来又臭又长,但终归人家觉得自己效率高。
coolcfan
2019-07-05 11:50:57 +08:00
我好像听人说过,在某开源项目里看到过用异常做流程控制——是真的流程控制,而不是因为出错而抛出或处理异常……
Inside
2019-07-05 12:24:07 +08:00
流程也分正常流程,异常流程。
异常流程用异常来全局处理没毛病,spring 也支持在 @ControllerAdvice 中统一处理异常。

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

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

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

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

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

目前我的系统中主要就是上面三种,其中 1 的情况最多,但更细的就没有划分下去了。
目前预留了 `errorCode` 的字段,但是还没时间设计相关的对应关系。
huangdaxian
2019-07-05 12:45:30 +08:00
@Inside 回复见楼上
sampeng
2019-07-05 12:57:27 +08:00
嗯。然后满屏幕的日志是 unknowerror
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
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
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
2019-07-05 13:46:17 +08:00
@CRUD 直接写一个这样的全局异常处理器的话 异常是从 service 抛出一直抛到 spring 的 disPathServlet 再回调过来,不如上层搞一个函数可以省去这个步骤。
Amayadream
2019-07-05 13:48:34 +08:00
我的理解是:
hyl24
2019-07-05 13:53:55 +08:00
spring 哪个 resource 我记得都有用 try catch 控制流程的呢·· 之前看源码看到过

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/580229

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX