# 使用Assert让代码更优雅

# 简介

在web服务的controller层和service层,随处可见使用if/else去做一些业务逻辑判断,然后通过抛异常/retrun方式中断业务,如下伪代码:

	if(isEmpty(x)){
        return Response.error("x is empty");
	}
	or
	if(isEmpty(x)){
        throw new ValidateException("x is empty");
	}
	or
	try{
        xxx
	}catch(Exception e){
        log.error("....");
        return Response.error("biz handle error");
	}

对于一些有经验的程序员来说,太多的if else , try catch代码让人看的很难受,总是想方设法的想消灭它们。相信很多人已经在使用Assert,配合Spring的统一异常处理机制来消灭他们了,如下伪代码:

	Assert.notEmpty(x, 'x is empty');

有没有感觉整个世界都干净多了,下面介绍一种更容易理解、让人更舒服的写法。

# Assert Elegant Check

先看下最终调用效果:

	ResponseEnum.PARAM_EMPTY_ERROR.notBlank(pwd);
	ResponseEnum.USER_EXITS.isTrue(exist);
  • 我们先定义一个返回前端的统一模型Response
@Getter
public class Response<T> {
    private static final int SUC_CODE = 0;
    private static final String SUC_MSG = "成功";

    private int code;
    private String msg;
    private T data;

    private Response(T data) {
        this.code = SUC_CODE;
        this.msg = SUC_MSG;
        this.data = data;
    }

    private Response(int code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public static final <T> Response ok() {
        return new Response<>(null);
    }

    public static final <T> Response ok(T data) {
        return new Response<>(data);
    }

    public static final <T> Response error(int code, String msg) {
        return new Response<>(code, msg, null);
    }

    public static final <T> Response res(int code, String msg, T data) {
        return new Response<>(code, msg, data);
    }
}
  • 定义一个自定义异常的基类,扩展code属性,一般前端需要一个code和msg
@Getter
public class BaseException extends RuntimeException{
    private int code;
    private String msg;

    public BaseException(int code, String msg){
        super(msg);
        this.code = code;
        this.msg = msg;
    }
}

//细化异常类型
@Getter
public class ValidateException extends BaseException {
    private int code;
    private String msg;

    public ValidateException(int code, String msg) {
        super(code, msg);
    }
}
  • Spring全局异常处理
@ControllerAdvice
@ConditionalOnWebApplication
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(BaseException.class)
    @ResponseBody
    public Response busErrorException(BaseException e) {
        return Response.error(e.getCode(), e.getMsg());
    }

    @ExceptionHandler(BindException.class)
    @ResponseBody
    public Response handleBindException(BindException e) {
        return wrapperBindingResult(e.getBindingResult());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public Response handleValidException(MethodArgumentNotValidException e) {
        return wrapperBindingResult(e.getBindingResult());
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Response exceptionHandler(HttpServletRequest request, Exception ex) {
        log.error("GlobalException Catch Exception :{}", ex.getMessage(), ex);
        return Response.error(ResponseEnum.SYSTEM_ERROR.getCode(), ResponseEnum.SYSTEM_ERROR.getMsg());
    }

    private Response wrapperBindingResult(BindingResult bindingResult) {
        StringBuilder msg = new StringBuilder();

        for (ObjectError error : bindingResult.getAllErrors()) {
            msg.append(", ");
            if (error instanceof FieldError) {
                msg.append(((FieldError) error).getField()).append(": ");
            }
            msg.append(error.getDefaultMessage() == null ? "" : error.getDefaultMessage());

        }

        return Response.error(ResponseEnum.VALID_ERROR.getCode(), msg.substring(2));
    }
}

上面全局异常处理类中,使用了统一定义的全局错误码枚举ResponseEnum,对于一些通用的错误码定义,我们可以通过一个枚举单独列出来,各个业务模块共用。

  • 定义统一错误码枚举类
@Getter
public enum ResponseEnum implements Assert {
    SYSTEM_ERROR(10000, "Server Error!"),
    VALID_ERROR(10001, "Parameters Validate Error!"),

    PARAM_EMPTY_ERROR(20000, "Missing necessary parameters!"),

    USER_EXITS(20001, "User already exists!"),
    PASS_STRENGTH_LOW(20002, "Password strength is low!");

    private int code;
    private String msg;

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

    @Override
    public BaseException ofException(Object... args) {
        String msg = MessageFormat.format(this.getMsg(), args);
        return new BaseException(getCode(), msg);
    }
}

为了实现我们最上面定义的效果,我们需要让枚举类具备Assert的断言能力,枚举无法继承,但是可以实现接口,所以我们需要将Assert定义成一个接口,然后由枚举类来实现。

  • Assert接口类
public interface Assert {

    BaseException ofException(Object... args);

    default void notNull(Object object, Object... args) {
        if (object == null) {
            throw ofException(args);
        }
    }

    default void notBlank(String str, Object... args) {
        if (StrUtils.isBlank(str)) {
            throw ofException(args);
        }
    }

    default void isAnyBlank(CharSequence... css){
        if(StrUtils.isAnyBlank(css)){
            throw  ofException();
        }
    }

    default void isTrue(boolean expression, Object... args) {
        if (!expression) {
            throw ofException(args);
        }
    }

    default void isFalse(boolean expression, Object... args) {
        if (expression) {
            throw ofException(args);
        }
    }
}

Assert只负责断言,断言后要返回哪个error info,其实是ResponseEnum定义的,所以提供一个未实现的ofException方法,由ResponseEnum来实现。

以上我们已实现了通过对断言和统一异常处理的组合封装,让代码更优雅。

# 业务调用

我们通过一个用户注册的demo,演示业务侧调用

@RestController
public class UserRegistController {
    @Autowired
    private UserRegistService registService;
	
	//为方便演示,pwd改成非必传
    @GetMapping("/user/regist")
    public Response regist(@RequestParam String usname, @RequestParam(required = false) String pwd) {
        return registService.regist(usname, pwd);
    }
}

@Service
public class UserRegistService {

    public Response regist(String usname, String pwd) {

        ResponseEnum.PARAM_EMPTY_ERROR.isAnyBlank(usname, pwd);

        boolean exist = "abc".equalsIgnoreCase(usname);
        ResponseEnum.USER_EXITS.isTrue(!exist);

        boolean weakPass = Pattern.matches(".*[A-Za-z0-9]+.*", pwd);
        ResponseEnum.PASS_STRENGTH_LOW.isTrue(!weakPass);

        return Response.ok();
    }
}

# 总结

其实就是异常信息和断言的组合,如果按常用的写法,异常信息和断言其实是耦合的或者属于硬编码的,这种写法让这个组合更灵活,异常信息和断言本身没有耦合,异常信息只是负责统一错误码的定义,断言只负责断言。

Demo源码:https://github.com/pengls/dragon-demo/tree/master/assert-except