# 使用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