基于全局统一返回的基础上
摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/SpringMVC/ 「芋道源码」欢迎转载,保留摘要,谢谢!
5. 全局异常处理
示例代码对应仓库:lab-springmvc-23-02 。
在 「4. 全局统一返回」 中,我们已经定义了使用 CommonResult 全局统一返回,并且看到了成功返回的示例与代码。这一小节,我们主要是来全局异常处理,最终能也是通过 CommonResult 返回。
那么,我们就不哔哔,直接看着示例代码,遨游起来。
友情提示:该示例,基于 「4. 全局统一返回」 的 lab-springmvc-23-02 的基础上,继续改造。
5.1 ServiceExceptionEnum
在 cn.iocoder.springboot.lab23.springmvc.constants
包路径,创建 ServiceExceptionEnum 枚举类,枚举项目中的错误码。代码如下:
// ServiceExceptionEnum.java public enum ServiceExceptionEnum { // ========== 系统级别 ========== SUCCESS(0, "成功"), SYS_ERROR(2001001000, "服务端发生异常"), MISSING_REQUEST_PARAM_ERROR(2001001001, "参数缺失"), // ========== 用户模块 ========== USER_NOT_FOUND(1001002000, "用户不存在"), // ========== 订单模块 ========== // ========== 商品模块 ========== ; /** * 错误码 */ private int code; /** * 错误提示 */ private String message; ServiceExceptionEnum(int code, String message) { this.code = code; this.message = message; } // ... 省略 getting 方法 } |
-
因为错误码是全局的,最好按照模块来拆分。如下是艿艿在 onemall 项目的实践:
/** * 服务异常 * * 参考 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 位,错误码 * 不限制规则。 * 一般建议,每个模块自增。 */
5.2 ServiceException
我们在一起讨论下 Service 逻辑异常的时候,如何进行返回。这里的逻辑异常,我们指的是,例如说用户名已经存在,商品库存不足等。一般来说,常用的方案选择,有两种:
- 封装统一的业务异常类 ServiceException ,里面有错误码和错误提示,然后进行
throws
抛出。 - 封装通用的返回类 CommonResult ,里面有错误码和错误提示,然后进行
return
返回。
一开始,我们选择了 CommonResult ,结果发现如下情况:
- 因为 Spring
@Transactional
声明式事务,是基于异常进行回滚的,如果使用 CommonResult 返回,则事务回滚会非常麻烦。 - 当调用别的方法时,如果别人返回的是 CommonResult 对象,还需要不断的进行判断,写起来挺麻烦的。
所以,后来我们采用了抛出业务异常 ServiceException 的方式。
在 cn.iocoder.springboot.lab23.springmvc.core.exception
包路径,创建 ServiceException 异常类,继承 RuntimeException 异常类,用于定义业务异常。代码如下:
// ServiceException.java public final class ServiceException extends RuntimeException { /** * 错误码 */ private final Integer code; public ServiceException(ServiceExceptionEnum serviceExceptionEnum) { // 使用父类的 message 字段 super(serviceExceptionEnum.getMessage()); // 设置错误码 this.code = serviceExceptionEnum.getCode(); } // ... 省略 getting 方法 } |
- 提供传入
serviceExceptionEnum
参数的构造方法。具体的处理,看下代码和注释。
5.3 GlobalExceptionHandler
在 cn.iocoder.springboot.lab23.springmvc.core.web
包路径,创建 GlobalExceptionHandler 类,全局统一返回的处理器。代码如下:
// GlobalExceptionHandler.java @ControllerAdvice(basePackages = "cn.iocoder.springboot.lab23.springmvc.controller") public class GlobalExceptionHandler { private Logger logger = LoggerFactory.getLogger(getClass()); /** * 处理 ServiceException 异常 */ @ResponseBody @ExceptionHandler(value = ServiceException.class) public CommonResult serviceExceptionHandler(HttpServletRequest req, ServiceException ex) { logger.debug("[serviceExceptionHandler]", ex); // 包装 CommonResult 结果 return CommonResult.error(ex.getCode(), ex.getMessage()); } /** * 处理 MissingServletRequestParameterException 异常 * * SpringMVC 参数不正确 */ @ResponseBody @ExceptionHandler(value = MissingServletRequestParameterException.class) public CommonResult missingServletRequestParameterExceptionHandler(HttpServletRequest req, MissingServletRequestParameterException ex) { logger.debug("[missingServletRequestParameterExceptionHandler]", 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(HttpServletRequest req, Exception e) { // 记录异常日志 logger.error("[exceptionHandler]", e); // 返回 ERROR CommonResult return CommonResult.error(ServiceExceptionEnum.SYS_ERROR.getCode(), ServiceExceptionEnum.SYS_ERROR.getMessage()); } } |
- 在类上,添加
@ControllerAdvice
注解。这一点,和 「4.4 GlobalResponseBodyHandler」 是一样的。不过,不会实现 ResponseBodyAdvice 接口,因为咱不需要拦截接口返回结果,进行修改。 - 我们定义了三个方法,通过添加
@ExceptionHandler
注解,定义每个方法对应处理的异常。并且,也添加了@ResponseBody
注解,标记直接使用返回结果作为 API 的响应。 #serviceExceptionHandler(...)
方法,拦截处理 ServiceException 业务异常,直接使用该异常的code
+message
属性,构建出 CommonResult 对象返回。#missingServletRequestParameterExceptionHandler(...)
方法,拦截处理 MissingServletRequestParameterException 请求参数异常,构建出错误码为ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR
的 CommonResult 对象返回。#exceptionHandler(...)
方法,拦截处理 Exception 异常,构建出错误码为ServiceExceptionEnum.SYS_ERROR
的 CommonResult 对象返回。这是一个兜底的异常处理,避免有一些其它异常,我们没有在 GlobalExceptionHandler 中,提供自定义的处理方式。
注意,在 #exceptionHandler(...)
方法中,我们还多使用 logger
打印了错误日志,方便我们接入 ELK 等日志服务,发起告警,通知我们去排查解决。如果胖友的系统里暂时没有日志服务,可以记录错误日志到数据库中,也是不错的选择。而其它两个方法,因为是更偏业务的,相对正常的异常,所以无需记录错误日志。
5.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 }