背景

软件开发过程中,不可避免的是需要处理各种异常,所以代码中就会出现大量的 try {...} catch {...} finally {...} 代码块,不仅有大量的冗余代码,而且还影响代码的可读性。

另一个就是面对业务异常的情况,我们经常需要将业务异常结果组装成统一的信息返回给前端进行提示。

假如我们在每个接口中都去包装异常信息进行返回就会让代码变得很冗余且混乱。在我司的实际项目开发过程中,我们会巧用断言去简化代码。

业务异常处理示例

假设我们定义的标准接口响应实体为 ApiResult:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResult<T> implements Serializable {
private static final long serialVersionUID = 411731814484355577L;
private int responseCode;
private String responseMsg;
private boolean isSuccess;
private T data;

public String toString() {
return "ApiResult(responseCode=" + this.getResponseCode() + ", responseMsg=" + this.getResponseMsg() + ", isSuccess=" + this.isSuccess() + ", data=" + this.getData() + ")";
}
}

那么我们接口处理业务逻辑时代码就会变成这样,看起来非常多代码:

public ApiResult cancelService(@PathVariable Long serviceOrderId){
ServiceOrder serviceOrder = serviceOrderMapper.selectByPrimaryKey(serviceOrderId);
ApiResult result = new ApiResult<>();
if (ObjectUtil.isNull(serviceOrder)) {
result.setSuccess(false);
result.setResponseCode(ErrorCodeEnum.FAIL.getCode());
result.setResponseMsg("查无此服务单");
return result;
}
if(serviceOrder.getOrderStatus().equals(cancelOrderStatus)){
result.setSuccess(false);
result.setResponseCode(ErrorCodeEnum.FAIL.getCode());
result.setResponseMsg("已取消的服务单不允许再次取消");
return result;
}
if(serviceOrder.getSortOrderId() != null){
result.setSuccess(false);
result.setResponseCode(ErrorCodeEnum.FAIL.getCode());
result.setResponseMsg("已配置物料的服务单不允许取消");
return result;
}
// ...other check

// ...do something
return result;
}

然后在上面这个代码基础上,我们可以观察到,里面其实有非常多的重复代码,完全可以把它们装到 ApiResult 里面。

这也是我看到很多开源框架的处理方式(PS:所以我第一个自己写的框架也是这么处理的)

在原 ApiResult 实体中增加一些公用的处理方法:

public static ApiResult<String> success() {
return success("success");
}

public static <T> ApiResult<T> success(T data) {
return (new ApiResult()).setResponseCode(0).setResponseMsg("操作成功").setSuccess(true).setData(data);
}

public static ApiResult<String> fail() {
return fail(-1);
}

public static ApiResult<String> fail(int code) {
return fail(code, "fail");
}

public static <T> ApiResult<T> fail(T data) {
return fail(-1, data);
}

public static <T> ApiResult<T> fail(int code, T data) {
return (new ApiResult()).setResponseCode(code).setResponseMsg("操作失败").setSuccess(false).setData(data);
}

public static <T> ApiResult<T> success(int code, String message, T data) {
return (new ApiResult()).setResponseCode(code).setResponseMsg(message).setSuccess(true).setData(data);
}

public static <T> ApiResult<T> fail(int code, String message, T data) {
return (new ApiResult()).setResponseCode(code).setResponseMsg(message).setSuccess(false).setData(data);
}

然后业务逻辑处理就变成这样了,看起来还不错是不是:

/**
* 取消服务单(不用断言)
*/
public ApiResult cancelService(Long serviceOrderId){
ServiceOrder serviceOrder = serviceOrderMapper.selectByPrimaryKey(serviceOrderId);
ApiResult result = new ApiResult<>();
if (ObjectUtil.isNull(serviceOrder)) {
result = ApiResult.fail(ErrorCodeEnum.FAIL.getCode(), "查无此服务单");
return result;
}
if(serviceOrder.getOrderStatus().equals(cancelOrderStatus)){
result = ApiResult.fail(ErrorCodeEnum.FAIL.getCode(), "已取消的服务单不允许再次取消");
return result;
}
if(serviceOrder.getSortOrderId() != null){
result = ApiResult.fail(ErrorCodeEnum.FAIL.getCode(), "已配置物料的服务单不允许取消");
return result;
}
// ...other check

// ...do something
return result;
}

但是我们可以用异常处理类+断言处理得更加简化。

增加异常处理类:

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = BusinessException.class)
@ResponseBody
public ResponseBean businessExceptionHandler(BusinessException e) {
log.info("business error : {}",e.getMessage(),e);
if (e.getCode() == -1) {
return ResponseBean.error(ApiCode.SERVICE_ERROR.getValue(), ApiCode.SERVICE_ERROR.getMessage());
}
return ResponseBean.error(e.getCode(), e.getMessage());
}
}

增加异常类 BusinessException:

/**
* 业务异常,异常信息会返回到前端展示给用户
*
* @date 2020/12/15 14:18
*/
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = -5770538329754222306L;

private int code = 1;
private Level level;

public BusinessException(int code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}

public BusinessException(String message) {
super(message);
}

public BusinessException(Level level, String message) {
super(message);
this.level = level;
}

public BusinessException(Throwable cause) {
super(cause);
}

public BusinessException(int code, String message) {
super(message);
this.code = code;
}

public int getCode() {
return this.code;
}

public final Level getLevel() {
return this.level;
}
}

增加断言工具类 AssertUtil:

public class AssertUtil extends cn.com.bluemoon.common.web.exception.AssertUtil  {
public AssertUtil() {
}

/**
* 服务调用异常
* @param expression
* @param message
*/
public static void isTrueServiceInvoke(boolean expression, String message) {
if (!expression) {
throw new ServiceInvokeException(message);
}
}

/**
* 抛出异常(默认错误1000)
* @param message
*/
public static void businessInvalid(String message) {
throw new BusinessException(ApiCode.SERVICE_ERROR.getValue(), message);
}

/**
* 表达式为真即抛出异常(默认错误1000)
*
* @param expression
* @param message
*/
public static void businessInvalid(boolean expression, String message) {
if (expression) {
throw new BusinessException(ApiCode.SERVICE_ERROR.getValue(), message);
}
}

/**
* 表达式为真即抛出异常
*
* @param expression
* @param message
*/
public static void businessInvalid(boolean expression, int code, String message) {
if (expression) {
throw new BusinessException(code, message);
}
}
}

最后优化的结果:

/**
* 取消服务单
*/
public ApiResult cancelService(@PathVariable Long serviceOrderId){
ServiceOrder serviceOrder = serviceOrderMapper.selectByPrimaryKey(serviceOrderId);
AssertUtil.businessInvalid(ObjectUtil.isNull(serviceOrder),"查无此服务单");
AssertUtil.businessInvalid(serviceOrder.getOrderStatus().equals(cancelOrderStatus),"查无此服务单");
AssertUtil.businessInvalid(serviceOrder.getSortOrderId() != null,"查无此服务单");
// ...other check

// ...do something
return ApiResult.success();
}

最后,我们可以看到我们的接口由 19 行的业务检查代码简化到了 3 行。这只是单接口的情况下,在业务多且复杂的情况下能给我们节省更多的开发时间,把精力集中在核心业务上。

附上代码

统一异常处理类:

/**
* 统一异常处理
*/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(value = AssertException.class)
@ResponseBody
public ResponseBean bootExceptionHandler(AssertException e) {
ApiCode apiCode = ApiCode.getObjectByValue(e.getCode());
log.error("business error : {}", e.getMessage(), e);
if (e.getCode() == -1) {
return ResponseBean.error(ApiCode.SERVICE_ERROR.getValue(), ApiCode.SERVICE_ERROR.getMessage());
}
return ResponseBean.error(apiCode.getValue(), e.getMessage());
}

@ExceptionHandler(value = com.alibaba.fastjson.JSONException.class)
public ResponseBean alibabaJsonExceptionHandler(com.alibaba.fastjson.JSONException e) {
ResponseBean response = new ResponseBean(false, ApiCode.PARAM_FORMAT_INCORR.getValue(), ApiCode.PARAM_FORMAT_INCORR.getMessage() + e.getMessage(), null);
log.error("1102", e);
return response;
}

@ExceptionHandler(value = JSONException.class)
@ResponseBody
public ResponseBean jsonExceptionHandler(JSONException e) {
ResponseBean response = new ResponseBean(false, ApiCode.PARAM_FORMAT_INCORR.getValue(), ApiCode.PARAM_FORMAT_INCORR.getMessage() + e.getMessage(), null);
log.error(ApiCode.PARAM_FORMAT_INCORR.getValue() + "", e);
return response;
}

@ExceptionHandler(value = JsonParseException.class)
@ResponseBody
public ResponseBean jsonParseExceptionHandler(JsonParseException e) {
ResponseBean response = new ResponseBean(false, ApiCode.PARAM_FORMAT_INCORR.getValue(), String.format(ApiCode.PARAM_FORMAT_INCORR.getMessage() + ":%s", e.getMessage()), null);
log.error(ApiCode.PARAM_FORMAT_INCORR.getValue() + "", e);
return response;
}

@ExceptionHandler(value = Exception.class)
@ResponseBody
public ResponseBean exceptionHandler(Exception e) {
ResponseBean response = new ResponseBean(false, ApiCode.SERVICE_ERROR.getValue(), ApiCode.SERVICE_ERROR.getMessage(), null);
log.error(ApiCode.SERVICE_ERROR.getValue() + "", e);
return response;
}

@ExceptionHandler(value = MethodArgumentTypeMismatchException.class)
@ResponseBody
public ResponseBean exceptionHandle(MethodArgumentTypeMismatchException e) {
ResponseBean response = new ResponseBean(false, ApiCode.PARAM_FORMAT_INCORR.getValue(), String.format(ApiCode.PARAM_FORMAT_INCORR.getMessage() + ":%s", e.getMessage()), null);
log.error(ApiCode.PARAM_FORMAT_INCORR.getValue() + "", e);
return response;
}

@ExceptionHandler(value = WebException.class)
@ResponseBody
public ResponseBean exceptionHandler(WebException e) {
ResponseBean response = new ResponseBean(e.getIsSuccess(), e.getResponseCode(), e.getResponseMsg(), null);
log.error(e.getResponseCode() + "", e);
return response;
}

@ExceptionHandler(value = IllegalArgumentException.class)
@ResponseBody
public ResponseBean exceptionHandler(IllegalArgumentException e) {
log.error("illegal request : {}", e.getMessage(), e);
return ResponseBean.error(ApiCode.PARAM_INVALID.getValue(), ApiCode.PARAM_INVALID.getMessage());
}

@ExceptionHandler(value = ServiceInvokeException.class)
@ResponseBody
public ResponseBean exceptionHandler(ServiceInvokeException e) {
log.error("serviceInvoke error request : {}", e.getMessage(), e);
return ResponseBean.error(ApiCode.SERVICE_ERROR.getValue(), ApiCode.SERVICE_ERROR.getMessage());
}

@ExceptionHandler(value = BusinessException.class)
@ResponseBody
public ResponseBean businessExceptionHandler(BusinessException e) {
log.info("business error : {}",e.getMessage(),e);
if (e.getCode() == -1) {
return ResponseBean.error(ApiCode.SERVICE_ERROR.getValue(), ApiCode.SERVICE_ERROR.getMessage());
}
return ResponseBean.error(e.getCode(), e.getMessage());
}

@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseBean exceptionHandler(MethodArgumentNotValidException e) {
log.info("req params error", e);
String message = e.getBindingResult().getFieldError().getDefaultMessage();
if (StringUtils.isNotBlank(message) && !"不能为空".equals(message)) {
return ResponseBean.error(ApiCode.PARAM_INVALID.getValue(), message);
}
return ResponseBean.error(ApiCode.PARAM_INVALID.getValue(), ApiCode.PARAM_INVALID.getMessage());
}

@ExceptionHandler(value = TokenErrorException.class)
@ResponseBody
public ResponseBean tokenErrorExceptionHandler(TokenErrorException e) {
log.info("登录失效 : {}",e.getMessage(),e);
return ResponseBean.error(ApiCode.SERVICE_ERROR.getValue(), "登录已失效,请重新登录!");
}

@ExceptionHandler(value = ServiceException.class)
@ResponseBody
public ResponseBean businessExceptionHandler(ServiceException e) {
log.info("service error : {}",e.getMessage(),e);
return ResponseBean.error(ApiCode.SERVICE_ERROR.getValue(), e.getMessage());
}
}

异常情况枚举,仅作参考:

public enum ErrorCodeEnum implements EnumBase{

FAIL(-1, "网络异常,请稍后再试"),

SUCCESS(0, "请求成功"),

MAX_UPLOAD_SIZE_ERROR(1000, "上传文件不能超过20M"),

SERVICE_BUSY_ERROR(1000, "服务器正在繁忙,请稍后再试哦~"),

REQUEST_PARAMS_FAIL(1001, "参数错误"),

USER_NOT_LOGIN(1002, "用户未登录,请重新登录"),

USER_HAS_EXIST_LOGIN(1007, "用户已经存在,请检查!"),

USER_CODE_NOT_EXIST(1008, "用户编码不存在,请检查!"),

REQUEST_PARAMS_FORMAT_ERROR(1102, "请求参数格式异常"),

PASSWORD_SAFETY_ERROE(2204, "密码不符合安全规则,请通过忘记密码重新设置8-18位数字+字母组合密码"),

TOKEN_EXPIRED(2301, "token过期"),

TOKEN_ERROR(2302, "token验证失败"),

INTERFACE_ERROR(10000, "接口服务器异常");

private final int code;
private final String msg;

ErrorCodeEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}

@Override
public int getCode() {
return this.code;
}

@Override
public String getMsg() {
return this.msg;
}
}