目录
为什么需要全局统一异常处理
我们平时在编写代码时,不管是对底层的数据库操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到各种可预知的、不可预知的异常需要处理。每个过程都单独处理异常,系统的代码耦合度高,工作量大且不好统一,维护的工作量也很大。
对于刚学习java的同学来说,对异常的处理只停留在try-catch和throws的层面,这样一方面是造成代码冗余,可阅读性差。另外一方面,异常散落在不同层面的代码下,不好统一管理异常和快速定位异常进行解决。
因此,对异常进行全局的同一处理就显得非常关键。
我们以springboot的全局异常处理来处理校验逻辑为例,下面具体的讲一下springboot的全局异常处理框架
异常处理逻辑
-
定义一个CommonResult实体类,用于封装data,code,message等信息。用于返回成功或失败的消息
-
使用@ControllerAdvice注解定义一个全局异常的处理类GlobalExceptionHandler
-
自定义一个异常类ApiException,用于包含我们异常的信息(如错误信息,错误码等)
-
创建一个断言处理类
Asserts
,用于抛出各种ApiException里面所包含的信息
使用方法
通用结果实体类CommonResult
package com.kkb.project.common.api; /** * @Description: 通用返回对象 * @date: 2021/04/07 */ public class CommonResult<T> { private long code; private String message; private T data; protected CommonResult() { } protected CommonResult(long code, String message, T data) { this.code = code; this.message = message; this.data = data; } /** * 成功返回结果 * * @param data 获取的数据 */ public static <T> CommonResult<T> success(T data) { return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); } /** * 成功返回结果 * * @param data 获取的数据 * @param message 提示信息 */ public static <T> CommonResult<T> success(T data, String message) { return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data); } /** * 失败返回结果 * * @param errorCode 错误码 */ public static <T> CommonResult<T> failed(IErrorCode errorCode) { return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null); } /** * 失败返回结果 * * @param errorCode 错误码 * @param message 错误信息 */ public static <T> CommonResult<T> failed(IErrorCode errorCode, String message) { return new CommonResult<T>(errorCode.getCode(), message, null); } /** * 失败返回结果 * * @param message 提示信息 */ public static <T> CommonResult<T> failed(String message) { return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null); } /** * 失败返回结果 */ public static <T> CommonResult<T> failed() { return failed(ResultCode.FAILED); } /** * 参数验证失败返回结果 */ public static <T> CommonResult<T> validateFailed() { return failed(ResultCode.VALIDATE_FAILED); } /** * 参数验证失败返回结果 * * @param message 提示信息 */ public static <T> CommonResult<T> validateFailed(String message) { return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null); } /** * 未登录返回结果 */ public static <T> CommonResult<T> unauthorized(T data) { return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); } /** * 未授权返回结果 */ public static <T> CommonResult<T> forbidden(T data) { return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data); } public long getCode() { return code; } public void setCode(long code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getData() { return data; } public void setData(T data) { this.data = data; } }
全局异常处理GlobalExceptionHandler
创建我们的全局异常处理类
GlobalExceptionHandler
,用于处理全局异常,并返回封装好的CommonResult对象;
package com.kkb.project.common.exception; import com.kkb.project.common.api.CommonResult; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; /** * @Description: 全局异常处理 ,并返回封装好的CommonResult对象; * @date: 2021/04/07 */ @ControllerAdvice public class GlobalExceptionHandler { /** * 自定义 API 异常处理方法 * @param e 自定义异常对象 * @return 返回用户可理解的错误消息对象 */ @ResponseBody @ExceptionHandler(value = ApiException.class) public CommonResult handle(ApiException e) { if (e.getErrorCode() != null) { return CommonResult.failed(e.getErrorCode()); } return CommonResult.failed(e.getMessage()); } /** * 方法参数无效异常 处理方法 * @param e 方法参数无效对象 * @return 返回用户可理解的错误消息对象 */ @ResponseBody @ExceptionHandler(value = MethodArgumentNotValidException.class) public CommonResult handleValidException(MethodArgumentNotValidException e) { BindingResult bindingResult = e.getBindingResult(); String message = null; if (bindingResult.hasErrors()) { FieldError fieldError = bindingResult.getFieldError(); if (fieldError != null) { message = fieldError.getField()+fieldError.getDefaultMessage(); } } return CommonResult.validateFailed(message); } /** * 绑定错误异常 * @param e 抛出异常对象 * @return 返回用户可理解的错误消息对象 */ @ResponseBody @ExceptionHandler(value = BindException.class) public CommonResult handleValidException(BindException e) { BindingResult bindingResult = e.getBindingResult(); String message = null; if (bindingResult.hasErrors()) { FieldError fieldError = bindingResult.getFieldError(); if (fieldError != null) { message = fieldError.getField()+fieldError.getDefaultMessage(); } } return CommonResult.validateFailed(message); } }
自定义异常类ApiException
自定义一个异常类ApiException,用于包含我们异常的信息(如错误信息,错误码等)
package com.kkb.project.common.exception; import com.kkb.project.common.api.IErrorCode; /** * @Description: 自定义API异常 , 效验失败抛出改异常 * @date: 2021/04/07 */ public class ApiException extends RuntimeException { private IErrorCode errorCode; public ApiException(IErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; } public ApiException(String message) { super(message); } public ApiException(Throwable cause) { super(cause); } public ApiException(String message, Throwable cause) { super(message, cause); } public IErrorCode getErrorCode() { return errorCode; } }
断言处理类Asserts
断言处理类
Asserts
,用于抛出各种ApiException里面所包含的信息
package com.kkb.project.common.exception; import com.kkb.project.common.api.IErrorCode; /** * @Description: 断言处理类,用于抛出各种API异常 * @author: peng.ni * @date: 2021/04/07 */ public class Asserts { /** * * @param message 错误消息 */ public static void fail(String message) { throw new ApiException(message); } /** * * @param errorCode 错误码 */ public static void fail(IErrorCode errorCode) { throw new ApiException(errorCode); } }
使用案例
我们看一下使用全局异常处理方法前后,service层和controller层的改变
controller
这里拿用户领取优惠券的代码为例,我们先对比下改进前后的代码,首先看Controller层代码。改进后只要Service中的方法执行成功就表示领取优惠券成功,因为领取不成功的话会直接抛出ApiException从而返回错误信息;
/** * 用户优惠券管理Controller * Created by macro on 2018/8/29. */ @Controller @Api(tags = "UmsMemberCouponController", description = "用户优惠券管理") @RequestMapping("/member/coupon") public class UmsMemberCouponController { @Autowired private UmsMemberCouponService memberCouponService; //改进前 @ApiOperation("领取指定优惠券") @RequestMapping(value = "/add/{couponId}", method = RequestMethod.POST) @ResponseBody public CommonResult add(@PathVariable Long couponId) { return memberCouponService.add(couponId); } //改进后 @ApiOperation("领取指定优惠券") @RequestMapping(value = "/add/{couponId}", method = RequestMethod.POST) @ResponseBody public CommonResult add(@PathVariable Long couponId) { memberCouponService.add(couponId); return CommonResult.success(null,"领取成功"); } }
serviceImpl
看下Service实现类中的代码,可以看到原先校验逻辑中返回CommonResult的逻辑都改成了调用Asserts的fail方法来实现;
/** * 会员优惠券管理Service实现类 * Created by macro on 2018/8/29. */ @Service public class UmsMemberCouponServiceImpl implements UmsMemberCouponService { @Autowired private UmsMemberService memberService; @Autowired private SmsCouponMapper couponMapper; @Autowired private SmsCouponHistoryMapper couponHistoryMapper; @Autowired private SmsCouponHistoryDao couponHistoryDao; //改进前 @Override public CommonResult add(Long couponId) { UmsMember currentMember = memberService.getCurrentMember(); //获取优惠券信息,判断数量 SmsCoupon coupon = couponMapper.selectByPrimaryKey(couponId); if(coupon==null){ return CommonResult.failed("优惠券不存在"); } if(coupon.getCount()<=0){ return CommonResult.failed("优惠券已经领完了"); } Date now = new Date(); if(now.before(coupon.getEnableTime())){ return CommonResult.failed("优惠券还没到领取时间"); } //判断用户领取的优惠券数量是否超过限制 SmsCouponHistoryExample couponHistoryExample = new SmsCouponHistoryExample(); couponHistoryExample.createCriteria().andCouponIdEqualTo(couponId).andMemberIdEqualTo(currentMember.getId()); long count = couponHistoryMapper.countByExample(couponHistoryExample); if(count>=coupon.getPerLimit()){ return CommonResult.failed("您已经领取过该优惠券"); } //省略领取优惠券逻辑... return CommonResult.success(null,"领取成功"); } //改进后 @Override public void add(Long couponId) { UmsMember currentMember = memberService.getCurrentMember(); //获取优惠券信息,判断数量 SmsCoupon coupon = couponMapper.selectByPrimaryKey(couponId); if(coupon==null){ Asserts.fail("优惠券不存在"); } if(coupon.getCount()<=0){ Asserts.fail("优惠券已经领完了"); } Date now = new Date(); if(now.before(coupon.getEnableTime())){ Asserts.fail("优惠券还没到领取时间"); } //判断用户领取的优惠券数量是否超过限制 SmsCouponHistoryExample couponHistoryExample = new SmsCouponHistoryExample(); couponHistoryExample.createCriteria().andCouponIdEqualTo(couponId).andMemberIdEqualTo(currentMember.getId()); long count = couponHistoryMapper.countByExample(couponHistoryExample); if(count>=coupon.getPerLimit()){ Asserts.fail("您已经领取过该优惠券"); } //省略领取优惠券逻辑... } }
优缺点
使用全局异常来处理校验逻辑的优点是比较灵活,可以处理复杂的校验逻辑。缺点是我们需要重复编写校验代码。