在上一篇 [global response]即全局统一返回中,我们已经定义了使用 CommonResult 全局统一返回,并且看到了成功返回的示例与代码。这一小节,我们主要是来全局异常处理,最终能也是通过 CommonResult 返回。
3.1 ServiceExceptionEnum
创建 [ServiceExceptionEnum]枚举类,枚举项目中的错误码。代码如下
package com.erbadagang.springboot.springwebflux.globalresponse.constants;
/**
* 业务异常枚举
*/
public enum ServiceExceptionEnum {
// ========== 系统级别 ==========
SUCCESS(0, "成功"),
SYS_ERROR(2001001000, "服务端发生异常"),
MISSING_REQUEST_PARAM_ERROR(2001001001, "参数缺失"),
// ========== 用户模块 ==========
USER_NOT_FOUND(1001002000, "用户不存在"),
// ========== 订单模块 ==========
// ========== 商品模块 ==========
;
/**
* 错误码
*/
private final int code;
/**
* 错误提示
*/
private final String message;
ServiceExceptionEnum(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
3.2 ServiceException
我们在一起讨论下 Service 逻辑异常的时候,如何进行返回。这里的逻辑异常,我们指的是,例如说用户名已经存在,商品库存不足等。一般来说,常用的方案选择,有两种:
- 封装统一的业务异常类 ServiceException ,里面有错误码和错误提示,然后进行
throws
抛出。 - 封装通用的返回类 CommonResult ,里面有错误码和错误提示,然后进行
return
返回。
一开始,我们选择了 CommonResult ,结果发现如下情况:
- 因为 Spring
@Transactional
声明式事务,是基于异常进行回滚的,如果使用 CommonResult 返回,则事务回滚会非常麻烦。 - 当调用别的方法时,如果别人返回的是 CommonResult 对象,还需要不断的进行判断,写起来挺麻烦的。
所以,后来我们采用了抛出业务异常 ServiceException 的方式。
创建 [ServiceException]异常类,继承 RuntimeException 异常类,用于定义业务异常。代码如下:
package com.erbadagang.springboot.springwebflux.globalresponse.core.exception;
import com.erbadagang.springboot.springwebflux.globalresponse.constants.ServiceExceptionEnum;
/**
* 服务异常
*
* 参考 https://www.kancloud.cn/onebase/ob/484204 文章
*
* 一共 10 位,分成四段
*
* 第一段,1 位,类型
* 1 - 业务级别异常
* 2 - 系统级别异常
* 第二段,3 位,系统类型
* 001 - 用户系统
* 002 - 商品系统
* 003 - 订单系统
* 004 - 支付系统
* 005 - 优惠劵系统
* ... - ...
* 第三段,3 位,模块
* 不限制规则。
* 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子:
* 001 - OAuth2 模块
* 002 - User 模块
* 003 - MobileCode 模块
* 第四段,3 位,错误码
* 不限制规则。
* 一般建议,每个模块自增。
*/
public final class ServiceException extends RuntimeException {
/**
* 错误码
*/
private final Integer code;
public ServiceException(ServiceExceptionEnum serviceExceptionEnum) {
// 使用父类的 message 字段
super(serviceExceptionEnum.getMessage());
// 设置错误码
this.code = serviceExceptionEnum.getCode();
}
public Integer getCode() {
return code;
}
}
提供传入 serviceExceptionEnum 参数的构造方法。具体的处理,看下代码和注释。
3.3 GlobalExceptionHandler
创建 [GlobalExceptionHandler]类,全局统一返回的处理器。代码如下:
package com.erbadagang.springboot.springwebflux.globalresponse.core.web;
import com.erbadagang.springboot.springwebflux.globalresponse.constants.ServiceExceptionEnum;
import com.erbadagang.springboot.springwebflux.globalresponse.core.exception.ServiceException;
import com.erbadagang.springboot.springwebflux.globalresponse.core.vo.CommonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.server.ServerWebInputException;
@ControllerAdvice(basePackages = "com.erbadagang.springboot.springwebflux.globalresponse.controller")
public class GlobalExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 处理 ServiceException 异常
*/
@ResponseBody
@ExceptionHandler(value = ServiceException.class)
public CommonResult serviceExceptionHandler(ServiceException ex) {
logger.debug("[serviceExceptionHandler]", ex);
// 包装 CommonResult 结果
return CommonResult.error(ex.getCode(), ex.getMessage());
}
/**
* 处理 ServerWebInputException 异常
*
* WebFlux 参数不正确
*/
@ResponseBody
@ExceptionHandler(value = ServerWebInputException.class)
public CommonResult serverWebInputExceptionHandler(ServerWebInputException ex) {
logger.debug("[ServerWebInputExceptionHandler]", ex);
// 包装 CommonResult 结果
return CommonResult.error(ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getCode(),
ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getMessage());
}
/**
* 处理其它 Exception 异常
*/
@ResponseBody
@ExceptionHandler(value = Exception.class)
public CommonResult exceptionHandler(Exception e) {
// 记录异常日志
logger.error("[exceptionHandler]", e);
// 返回 ERROR CommonResult
return CommonResult.error(ServiceExceptionEnum.SYS_ERROR.getCode(),
ServiceExceptionEnum.SYS_ERROR.getMessage());
}
}
- 在 WebFlux 中,可以使用通过实现 ResponseBodyAdvice 接口,并添加
@ControllerAdvice
接口,拦截 Controller 的返回结果。注意,我们这里@ControllerAdvice
注解,设置了basePackages
属性,只拦截"com.erbadagang.springboot.springwebflux.globalresponse.controller"
包,也就是我们定义的 Controller 。为什么呢?因为在项目中,我们可能会引入 Swagger 等库,也使用 Controller 提供 API 接口,那么我们显然不应该让 GlobalResponseBodyHandler 去拦截这些接口,毕竟它们并不需要我们去替它们做全局统一的返回。 - 我们定义了三个方法,通过添加
@ExceptionHandler
注解,定义每个方法对应处理的异常。并且,也添加了@ResponseBody
注解,标记直接使用返回结果作为 API 的响应。 -
#serviceExceptionHandler(...)
方法,拦截处理 ServiceException 业务异常,直接使用该异常的code
+message
属性,构建出 CommonResult 对象返回。 -
#serverWebInputExceptionHandler(...)
方法,拦截处理 ServerWebInputException 请求参数异常,构建出错误码为ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR
的 CommonResult 对象返回。 -
#exceptionHandler(...)
方法,拦截处理 Exception 异常,构建出错误码为ServiceExceptionEnum.SYS_ERROR
的 CommonResult 对象返回。这是一个兜底的异常处理,避免有一些其它异常,我们没有在 GlobalExceptionHandler 中,提供自定义的处理方式。
注意,在 #exceptionHandler(...)
方法中,我们还多使用 logger
打印了错误日志,方便我们接入 ELK 等日志服务,发起告警,通知我们去排查解决。如果胖友的系统里暂时没有日志服务,可以记录错误日志到数据库中,也是不错的选择。而其它两个方法,因为是更偏业务的,相对正常的异常,所以无需记录错误日志。
3.4 UserController
在 [UserController]类中,我们添加两个 API 接口,抛出异常,方便我们测试全局异常处理的效果。代码如下:
// UserController.java
/**
* 测试抛出 NullPointerException 异常
*/
@GetMapping("/exception-01")
public UserVO exception01() {
throw new NullPointerException("没有粗面鱼丸");
}
/**
* 测试抛出 ServiceException 异常
*/
@GetMapping("/exception-02")
public UserVO exception02() {
throw new ServiceException(ServiceExceptionEnum.USER_NOT_FOUND);
}
在#exception01()
方法,抛出NullPointerException
异常。这样,异常会被 GlobalExceptionHandler#exceptionHandler(...)
方法来拦截,包装成CommonResult
类型返回。请求结果如下:
{
"code": 2001001000,
"message": "服务端发生异常",
"data": null
}
在#exception02()
方法,抛出ServiceException
异常。这样,异常会被 GlobalExceptionHandler#serviceExceptionHandler(...)
方法来拦截,包装成 CommonResult 类型返回。请求结果如下:
{
"code": 1001002000,
"message": "用户不存在",
"data": null
}
3.5 简单小结
采用 ControllerAdvice + @ExceptionHandler 注解的方式,可以很方便的实现 WebFlux 的全局异常处理。不过这种方案存在一个弊端,不支持 WebFlux 的基于函数式编程方式。不过考虑到,绝大多数情况下,我们并不会采用基于函数式编程方式,所以这种方案还是没问题的。
底线
本文源代码使用 Apache License 2.0开源许可协议,这里是本文源码Gitee地址,可通过命令git clone+地址
下载代码到本地,也可直接点击链接通过浏览器方式查看源代码。