一、参数校验的痛点
在业务系统开发中,我们经常需要编写这样的防御性代码:
public void updateUserStatus(String userId, Integer status) {if (userId == null || userId.isBlank()) {throw new BizException("USER_ID_EMPTY");}if (status < 0 || status > 5) {throw new BizException("STATUS_INVALID");}// 业务逻辑...
}
传统写法的弊端:
- 重复的
if-throw
代码块 - 异常消息分散在业务代码中
- 错误码缺乏统一管理
二、解决方案:设计断言工具类
2.1 核心代码实现
public class AssertUtils {/*** 参数校验断言方法* @param expression 校验条件* @param errorCode 错误码(需事先定义)* @throws BizException 当条件不满足时抛出业务异常*/public static void checkArgument(boolean expression, String errorCode) {if (!expression) {throw new BizException(errorCode);}}
}
2.2 设计亮点
- 统一异常类型:所有校验失败都抛出
BizException
- 错误码标准化:强制要求传入预定义的错误码
- 代码极简化:单行调用替代多行校验逻辑
三、实战应用案例
3.1 基础校验场景
public void login(String username, String password) {// 非空校验AssertUtils.checkArgument(username != null, "USERNAME_EMPTY");AssertUtils.checkArgument(password != null, "PASSWORD_EMPTY");// 长度校验AssertUtils.checkArgument(username.length() >= 6, "USERNAME_TOO_SHORT");AssertUtils.checkArgument(password.length() >= 8, "PASSWORD_TOO_SHORT");// 业务逻辑...
}
3.2 业务规则校验
public void transferMoney(BigDecimal amount, Account from, Account to) {// 金额校验AssertUtils.checkArgument(amount.compareTo(BigDecimal.ZERO) > 0, "AMOUNT_INVALID");// 账户状态校验AssertUtils.checkArgument(from.isActive(), "FROM_ACCOUNT_INACTIVE");AssertUtils.checkArgument(to.isActive(), "TO_ACCOUNT_INACTIVE");// 转账逻辑...
}
四、配套设计:构建完整校验体系
4.1 业务异常类设计
public class BizException extends RuntimeException {private final String errorCode;public BizException(String errorCode) {super("Business Exception [" + errorCode + "]");this.errorCode = errorCode;}public String getErrorCode() {return errorCode;}
}
4.2 错误码规范(推荐枚举管理)
public enum ErrorCode {// 用户相关USERNAME_EMPTY("U001", "用户名不能为空"),PASSWORD_EMPTY("U002", "密码不能为空"),// 支付相关AMOUNT_INVALID("P001", "金额必须大于零");private final String code;private final String message;ErrorCode(String code, String message) {this.code = code;this.message = message;}// 使用示例AssertUtils.checkArgument(..., ErrorCode.USERNAME_EMPTY.code());
}
五、进阶优化方向
5.1 支持错误消息格式化
public static void checkArgument(boolean expr, String code, Object... args) {if (!expr) {String message = MessageFormat.format(getMessage(code), args);throw new BizException(code, message);}
}// 使用示例
AssertUtils.checkArgument(age >= 18, "AGE_LIMIT", 18); // 错误消息:年龄必须满{0}岁
5.2 链式调用支持
public class AssertChain {private boolean valid = true;public AssertChain check(boolean condition, String errorCode) {if (valid) {valid = condition;if (!condition) {throw new BizException(errorCode);}}return this;}
}// 使用示例
new AssertChain().check(username != null, "USERNAME_EMPTY").check(password != null, "PASSWORD_EMPTY");
六、最佳实践建议
-
1.错误码管理规范:
- 使用前缀区分模块(如
U
开头表示用户模块) - 维护全局错误码文档
- 禁止硬编码错误码字符串
-
2.性能优化技巧:
- 避免在条件表达式中执行复杂计算
// 不推荐
AssertUtils.checkArgument(calculateRiskScore() < 100, "RISK_TOO_HIGH");// 推荐
int riskScore = calculateRiskScore();
AssertUtils.checkArgument(riskScore < 100, "RISK_TOO_HIGH");
-
3.日志记录策略:
- 在全局异常处理器中统一记录错误日志
- 记录错误码和上下文信
@ControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(BizException.class)public ResponseEntity<ErrorResponse> handleBizException(BizException ex) {log.error("业务异常 [{}]:{}", ex.getErrorCode(), ex.getMessage());return ResponseEntity.badRequest().body(new ErrorResponse(ex.getErrorCode()));}
}
七、单元测试方案
使用JUnit 5进行异常测试:
class AssertUtilsTest {@Testvoid shouldThrowWhenConditionFalse() {// 准备无效参数String invalidUsername = null;// 验证异常抛出BizException exception = assertThrows(BizException.class,() -> AssertUtils.checkArgument(invalidUsername != null, "USERNAME_EMPTY"));// 验证错误码assertEquals("USERNAME_EMPTY", exception.getErrorCode());}
}
八、对比其他方案
方案 | 优点 | 缺点 |
---|---|---|
原生if-throw | 简单直接 | 代码冗余,维护困难 |
Apache Commons Lang | 功能丰富 | 需要额外依赖 |
Spring Assert | 与Spring生态集成好 | 依赖Spring框架 |
本文方案 | 轻量灵活,定制化强 | 需要自行维护错误码 |
九、总结
通过实现自定义断言工具类,开发者可以:
✅ 统一参数校验标准
✅ 减少重复代码量(约60%)
✅ 提升代码可读性和可维护性
✅ 方便实现全局异常处理
建议根据项目需求逐步扩展功能,形成完善的参数校验体系。对于大型项目,可以结合注解校验和AOP实现声明式校验。