Java案例怎么抛出自定义异常?

wen java案例 9

Java案例:如何优雅地抛出自定义异常?从入门到项目实战

目录导读

  • 为什么需要自定义异常?——业务逻辑的“告警灯”

    Java案例怎么抛出自定义异常?

  • 自定义异常的核心语法:extends Exception 与构造函数

  • 实战案例一:用户注册时的密码强度校验

  • 实战案例二:库存扣减时的超卖检查

  • 自定义异常与全局异常处理器(Spring Boot 集成)

  • 常见误区与性能考量

  • Q&A 高频问题详解


为什么需要自定义异常?

在Java开发中,内置异常(如NullPointerExceptionArithmeticException)只能描述“代码层面”的错误,无法表达“业务层面”的语义。

  • 用户注册时密码太短(业务规则)
  • 商品库存不足(业务状态)
  • 订单金额超过单日限额(风控规则)

自定义异常的价值在于:

  • 语义清晰:异常名称直接反映业务问题(如PasswordTooWeakException
  • 分类处理:前端可根据不同异常类型展示不同提示
  • 统一管理:配合全局异常处理器,避免重复的try-catch代码

一句口诀:内置异常管“系统”,自定义异常管“业务”


自定义异常的核心语法

1 继承体系选择

父类 适用场景 是否需要显式处理
Exception 强制调用者处理(checked异常) 必须try-catch或throws
RuntimeException 可选择处理(unchecked异常) 可选

推荐:大部分业务场景选择RuntimeException,减少调用方代码污染。

2 标准结构

public class BusinessException extends RuntimeException {
    private Integer code; // 业务错误码
    private String message; // 错误描述
    public BusinessException(String message) {
        super(message);
        this.code = 400;
    }
    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
    }
    public Integer getCode() {
        return code;
    }
}

关键点

  • 提供多个构造函数,支持不同参数组合
  • 保留getCode()方法便于前端解析
  • 建议覆盖toString()getMessage()展示完整信息

实战案例一:用户注册时的密码强度校验

场景描述

用户注册时,密码必须包含大写字母、小写字母、数字、特殊字符,且长度≥8位,不符合时抛出PasswordWeakException

异常类定义

public class PasswordWeakException extends RuntimeException {
    private static final int DEFAULT_CODE = 2001;
    public PasswordWeakException(String message) {
        super(message);
        this.code = DEFAULT_CODE;
    }
    public PasswordWeakException(Integer code, String message) {
        super(message);
        this.code = code;
    }
}

业务逻辑实现

public class RegisterService {
    public void register(String username, String password) {
        // 密码强度校验
        if (password.length() < 8) {
            throw new PasswordWeakException("密码长度不能少于8位");
        }
        if (!password.matches(".*[A-Z].*")) {
            throw new PasswordWeakException("密码必须包含大写字母");
        }
        if (!password.matches(".*[a-z].*")) {
            throw new PasswordWeakException("密码必须包含小写字母");
        }
        // 剩余校验省略...
        // 通过校验后写入数据库
        userRepository.save(new User(username, password));
    }
}

前端调用处理

// 假设使用Spring Boot + Ajax
try {
    // 调用注册接口
} catch (PasswordWeakException e) {
    alert(e.getMessage()); // 直接展示业务友好的提示
}

实战案例二:库存扣减时的超卖检查

高并发场景下的设计要点

电商系统中,库存扣减必须防止超卖,自定义异常InsufficientStockException需要携带当前库存量和请求量。

带业务数据的异常

public class InsufficientStockException extends RuntimeException {
    private Long productId;
    private Integer requestQuantity;
    private Integer availableQuantity;
    public InsufficientStockException(Long productId, Integer requestQuantity, Integer availableQuantity) {
        super(String.format("商品%s库存不足:请求%s件,可用库存%s件", 
                productId, requestQuantity, availableQuantity));
        this.productId = productId;
        this.requestQuantity = requestQuantity;
        this.availableQuantity = availableQuantity;
    }
    // getters用于日志或监控
}

扣减逻辑

public void deductStock(Long productId, Integer quantity) {
    Product product = productRepository.findById(productId)
            .orElseThrow(() -> new ProductNotFoundException(productId));
    if (product.getStock() < quantity) {
        throw new InsufficientStockException(productId, quantity, product.getStock());
    }
    // 使用乐观锁或分布式锁执行扣减
    product.setStock(product.getStock() - quantity);
    productRepository.save(product);
}

注意:在分布式场景中,自定义异常可以携带溯源ID(traceId)便于定位问题。


自定义异常与全局异常处理器(Spring Boot 集成)

定义统一响应模型

@RestControllerAdvice
public class GlobalExceptionHandler {
    // 处理自定义业务异常
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<Result> handleBusinessException(BusinessException e) {
        Result result = new Result(
            e.getCode() != null ? e.getCode() : 400,
            e.getMessage(),
            null
        );
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
    }
    // 处理校验异常(如@Valid)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Result> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .map(error -> error.getField() + ":" + error.getDefaultMessage())
                .collect(Collectors.joining(", "));
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(new Result(400, message, null));
    }
    // 兜底处理
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Result> handleUnknownException(Exception e) {
        // 记录日志
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new Result(500, "系统繁忙", null));
    }
}

效果

  • 前端只需解析统一的codemessage
  • 后端不再需要在每个控制器中写try-catch

常见误区与性能考量

误区1:异常用于流程控制

// 错误示例
try {
    validatePassword(password);
} catch (PasswordWeakException e) {
    // 正常业务流程,不应使用异常控制
}

建议:校验逻辑应返回布尔值或使用Validation框架,异常只用于“意外但不希望发生”的情况。

误区2:异常层次过深

不要为每个业务场景都创建新异常类,建议采用“分类+错误码”模式:

public class BusinessException extends RuntimeException {
    private ErrorCode errorCode;
    public enum ErrorCode {
        PASSWORD_WEAK(2001, "密码强度不足"),
        STOCK_SHORTAGE(3001, "库存不足"),
        ORDER_REPEAT(4001, "重复提交");
    }
}

这样只需要一个自定义异常类,通过错误码区分类型。

性能考量

  • 异常创建会生成栈追踪信息,高频调用中避免使用异常
  • 对于“预期内”的边界检查(如库存不足),可先用if判断,只在“不应发生”的场景下抛异常
  • 生产环境可配置关闭部分栈追踪(-XX:-StackTrace)减轻性能损耗

Q&A 高频问题详解

Q1:自定义异常应该继承Exception还是RuntimeException

A根据谁负责处理来决定,如果希望调用方必须显式处理(如IO操作),用Exception;如果是业务校验(如密码强度),用RuntimeException,现代Spring项目几乎统一使用RuntimeException,配合@RestControllerAdvice

Q2:自定义异常可以不带错误码吗?

A:可以,但建议带,错误码便于前端国际化展示、系统监控、跨服务调用时精确识别问题,前端根据code=2001显示“密码太弱”的翻译文本,而不是直接展示后端的英文消息。

Q3:多个自定义异常类如何管理?

A:推荐两种模式:

  1. 枚举模式:一个异常类+枚举错误码,适合中小型项目
  2. 多类模式:每个业务域一个异常(如UserExceptionOrderException),适合大型项目

Q4:自定义异常中是否应该包含完整的数据对象?

A:建议包含最小必要信息,避免序列化整个对象(如User实体),因为异常对象可能被序列化传递到远程,通常传递ID、数量等简单字段即可。

Q5:全局异常处理器能处理所有自定义异常吗?

A:是的,需要确保:

  • 异常处理器被Spring管理(添加@RestControllerAdvice
  • 自定义异常被@ExceptionHandler捕获(按父子类顺序匹配)
  • 如果存在多个异常处理器,优先级低的会被覆盖(可通过@Order控制)

自定义异常的最佳实践清单

  1. 继承RuntimeException,除非有特殊要求
  2. 包含错误码,方便国际化和监控
  3. 提供构造函数重载,适应不同场景
  4. 消息要业务友好,避免技术堆栈暴露
  5. 配合全局异常处理器,减少模板代码
  6. 控制异常粒度,一个业务域一个异常类
  7. 高频路径避免异常,用条件判断替代

通过本文的案例与问答,你应该已经掌握如何在实际Java项目中定义和抛出业务语义清晰的异常。异常是给开发者看的,错误码是给系统看的,而友好的消息是给用户看的——三者缺一不可。

抱歉,评论功能暂时关闭!