从登录失败到订单校验:5个真实场景解锁BusinessException实战技巧
在Java开发中,我们经常遇到两类问题:一类是代码执行时出现的技术异常(如NullPointerException),另一类则是业务规则校验失败(如密码错误、库存不足)。传统处理方式往往将两者混为一谈,导致代码充斥着大量if-else和Result对象返回。今天,我们就通过5个真实业务场景,看看如何用BusinessException让代码更优雅。
1. 用户登录:告别Result对象嵌套
假设我们有一个用户登录功能,传统实现可能是这样的:
public Result<User> login(String username, String password) { User user = userRepository.findByUsername(username); if (user == null) { return Result.error("用户不存在"); } if (!passwordEncoder.matches(password, user.getPassword())) { return Result.error("密码错误"); } if (user.getStatus() == UserStatus.LOCKED) { return Result.error("账户已锁定"); } return Result.success(user); }这种写法有三个明显问题:
- 业务逻辑与技术处理耦合
- 方法签名暴露了返回包装类型
- 错误处理分散在各个if条件中
使用BusinessException改造后:
public User login(String username, String password) { User user = userRepository.findByUsername(username); if (user == null) { throw new BusinessException(ErrorCode.USER_NOT_FOUND); } if (!passwordEncoder.matches(password, user.getPassword())) { throw new BusinessException(ErrorCode.PASSWORD_MISMATCH); } if (user.getStatus() == UserStatus.LOCKED) { throw new BusinessException(ErrorCode.ACCOUNT_LOCKED); } return user; }关键改进点:
- 方法签名更干净,直接返回User类型
- 业务规则校验集中抛出异常
- 错误码和消息通过枚举统一管理
提示:在Controller层可以通过
@ExceptionHandler统一捕获BusinessException,转换为对应的HTTP状态码和错误信息。
2. 参数校验:超越JSR-303的灵活控制
JSR-303注解(如@NotBlank)适合简单校验,但遇到复杂业务规则时往往力不从心。比如商品创建接口需要校验:
- 商品名称不能包含特殊字符
- 价格必须大于成本价
- 上架时间不能早于当前时间
传统实现:
public Result createProduct(ProductCreateDTO dto) { if (containsSpecialChars(dto.getName())) { return Result.error("商品名称含非法字符"); } if (dto.getPrice() <= dto.getCostPrice()) { return Result.error("售价必须高于成本价"); } if (dto.getOnlineTime().isBefore(LocalDateTime.now())) { return Result.error("上架时间不能早于当前时间"); } // 保存逻辑... }BusinessException版本:
public void createProduct(ProductCreateDTO dto) { if (containsSpecialChars(dto.getName())) { throw new BusinessException(ErrorCode.INVALID_PRODUCT_NAME); } if (dto.getPrice() <= dto.getCostPrice()) { throw new BusinessException(ErrorCode.PRICE_TOO_LOW); } if (dto.getOnlineTime().isBefore(LocalDateTime.now())) { throw new BusinessException(ErrorCode.INVALID_ONLINE_TIME); } // 保存逻辑... }对比优势:
| 校验方式 | 可读性 | 灵活性 | 错误处理 |
|---|---|---|---|
| JSR-303 | 高 | 低 | 统一但不够具体 |
| Result返回 | 中 | 高 | 分散在各处 |
| BusinessException | 高 | 高 | 集中且类型明确 |
3. 库存管理:原子操作与异常处理
电商系统中,扣减库存需要保证原子性。传统实现可能会这样:
public Result deductStock(Long productId, int quantity) { Product product = productRepository.findById(productId); if (product == null) { return Result.error("商品不存在"); } if (product.getStock() < quantity) { return Result.error("库存不足"); } // 非原子操作存在并发问题 product.setStock(product.getStock() - quantity); productRepository.save(product); return Result.success(); }更完善的BusinessException版本:
@Transactional public void deductStock(Long productId, int quantity) { int updated = productRepository.reduceStock(productId, quantity); if (updated == 0) { throw new BusinessException(ErrorCode.STOCK_NOT_ENOUGH); } }对应的Repository方法:
@Modifying @Query("UPDATE Product p SET p.stock = p.stock - :quantity WHERE p.id = :productId AND p.stock >= :quantity") int reduceStock(@Param("productId") Long productId, @Param("quantity") int quantity);关键设计点:
- 使用数据库乐观锁保证原子性
- 通过update返回影响行数判断是否成功
- 失败时抛出业务异常
4. 权限校验:分层拦截与异常统一处理
在订单详情查询接口中,我们需要校验:
- 订单是否存在
- 当前用户是否有权限查看
传统嵌套校验:
public Result<Order> getOrderDetail(Long orderId, Long userId) { Order order = orderRepository.findById(orderId); if (order == null) { return Result.error("订单不存在"); } if (!order.getUserId().equals(userId)) { return Result.error("无权查看该订单"); } return Result.success(order); }使用BusinessException结合Spring AOP的优雅方案:
// 业务方法 public Order getOrderDetail(Long orderId) { return orderRepository.findById(orderId) .orElseThrow(() -> new BusinessException(ErrorCode.ORDER_NOT_FOUND)); } // AOP切面 @Before("@annotation(requirePermission) && args(orderId,..)") public void checkPermission(RequirePermission requirePermission, Long orderId) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new BusinessException(ErrorCode.ORDER_NOT_FOUND)); if (!order.getUserId().equals(SecurityUtils.getCurrentUserId())) { throw new BusinessException(ErrorCode.PERMISSION_DENIED); } }架构优势:
- 业务方法只需关注核心逻辑
- 权限校验通过注解+AOP实现
- 异常类型可以精确区分"订单不存在"和"权限不足"
5. 状态流转:用异常替代状态判断
考虑订单发货流程,业务规则包括:
- 只有"待发货"状态的订单可以发货
- 发货必须填写物流单号
- 物流单号必须符合格式
传统状态判断:
public Result deliverOrder(Long orderId, String trackingNumber) { Order order = orderRepository.findById(orderId); if (order == null) { return Result.error("订单不存在"); } if (order.getStatus() != OrderStatus.PENDING_SHIPMENT) { return Result.error("订单状态不正确"); } if (!isValidTrackingNumber(trackingNumber)) { return Result.error("物流单号格式错误"); } // 发货逻辑... }BusinessException实现:
public void deliverOrder(Long orderId, String trackingNumber) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new BusinessException(ErrorCode.ORDER_NOT_FOUND)); if (order.getStatus() != OrderStatus.PENDING_SHIPMENT) { throw new BusinessException(ErrorCode.INVALID_ORDER_STATUS); } if (!isValidTrackingNumber(trackingNumber)) { throw new BusinessException(ErrorCode.INVALID_TRACKING_NUMBER); } // 发货逻辑... }状态机对比:
| 方法 | 可维护性 | 可扩展性 | 错误定位 |
|---|---|---|---|
| if-else返回Result | 低 | 低 | 需要查看每个条件 |
| BusinessException | 高 | 高 | 异常类型即说明问题 |
| 状态模式 | 最高 | 最高 | 需要额外设计成本 |
在实际项目中,我通常会根据业务复杂度选择方案:简单流程用BusinessException,复杂状态机才引入专门的状态模式。