乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

自定义异常

本节定位:掌握自定义异常的设计方法——选择合适的父类(Exception vs RuntimeException)、设计规范的构造方法、以及在企业级项目中构建分层异常体系。理解自定义异常在业务语义表达和异常分类中的核心价值。


场景引入

飞翔科技的小崔刚刚完成学生管理系统的开发。代码审查时,架构师白歌发现每个模块都在抛 RuntimeException("出错啦")。

"你这样写,调用方怎么区分是'学生不存在'还是'学分不足'还是'数据库挂了'?"白歌指着屏幕,"一个好的异常体系应该让异常自己会说话。"

李眉好奇地问:"Java 不是已经有很多异常了吗?为什么还要自己定义?"

大翔解释道:"JDK 的异常是通用的。IllegalArgumentException 可以表示一万种参数非法的情况,但业务上你需要区分'学号重复'和'年龄不合法'——这就是自定义异常的价值。"


为什么需要自定义异常

使用 JDK 通用异常使用自定义异常
throw new RuntimeException("错了")throw new StudentNotFoundException(studentId)
调用方只能 catch RuntimeException调用方可以精确 catch StudentNotFoundException
无法根据异常类型做差异化处理可以根据不同业务异常走不同处理分支
日志排查困难,只能靠字符串匹配通过类名即可快速定位问题类型

核心价值:自定义异常让你的异常具有业务语义,让代码自文档化。


设计自定义异常

第一步:选择父类

继承的父类异常类型适用场景
Exception受检异常调用方必须处理的异常(如业务规则违反)
RuntimeException非受检异常调用方可选处理的异常(如参数校验失败)

推荐:如果你使用 Spring 框架,继承 RuntimeException 是一个好选择,因为 Spring 的事务管理默认只对非受检异常回滚。

第二步:设计构造方法

一个规范的自定义异常应该提供三个标准构造方法:

/**
 * 自定义异常的标准构造方法模板
 */
public class StudentServiceException extends RuntimeException {

    /**
     * 构造方法一:仅异常消息
     */
    public StudentServiceException(String message) {
        super(message);
    }

    /**
     * 构造方法二:异常消息 + 根因
     * (最常用,支持异常链)
     */
    public StudentServiceException(String message, Throwable cause) {
        super(message, cause);
    }

    /**
     * 构造方法三:仅根因
     */
    public StudentServiceException(Throwable cause) {
        super(cause);
    }

    // 可选:无参构造方法
    public StudentServiceException() {
        super();
    }
}
构造方法参数使用场景
()无参极少使用,不推荐(缺少错误信息)
(String message)错误消息业务逻辑异常,如 "学号已存在"
(String message, Throwable cause)消息 + 根因最常用,包装底层异常时
(Throwable cause)仅根因纯包装,不需要额外消息

第三步:遵循命名规范

规范说明正例反例
以 Exception 结尾受检异常StudentNotFoundExceptionStudentNotFound
以 Exception 结尾非受检异常StudentImportExceptionImportStudentError
明确表达业务语义一看就知道什么情况DuplicateStudentExceptionMyException1
分层命名不同层用不同后缀DaoException, ServiceException全部叫 BizException

飞翔科技实战:构建分层异常体系

业务场景

架构师白歌为飞翔科技学生管理系统设计了如下异常体系:

基类异常设计

/**
 * 飞翔科技学生管理系统 - 业务异常基类
 *
 * 所有业务异常继承此类,统一携带错误码,便于:
 * 1. 前端根据错误码展示不同的提示信息
 * 2. 国际化(i18n)通过错误码查消息模板
 * 3. 监控系统按错误码统计异常频率
 */
public class StudentSystemException extends RuntimeException {

    /** 错误码 */
    private final int errorCode;

    /** 错误码常量 */
    public static final int STUDENT_NOT_FOUND = 1001;
    public static final int DUPLICATE_STUDENT = 1002;
    public static final int INVALID_STUDENT = 1003;
    public static final int INSUFFICIENT_CREDITS = 1004;
    public static final int IMPORT_FAILED = 2001;
    public static final int DB_ERROR = 9001;

    public StudentSystemException(int errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public StudentSystemException(int errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }

    public int getErrorCode() {
        return errorCode;
    }

    /**
     * 获取格式化的错误信息(用于日志和 API 响应)
     */
    public String getFormattedMessage() {
        return String.format("[错误码: %d] %s", errorCode, getMessage());
    }
}

具体异常子类

/**
 * 学生不存在异常
 */
public class StudentNotFoundException extends StudentSystemException {

    public StudentNotFoundException(String studentId) {
        super(STUDENT_NOT_FOUND,
              String.format("学生不存在: %s", studentId));
    }

    public StudentNotFoundException(String studentId, Throwable cause) {
        super(STUDENT_NOT_FOUND,
              String.format("查询学生 %s 时发生错误", studentId), cause);
    }
}

/**
 * 学号重复异常
 */
public class DuplicateStudentException extends StudentSystemException {

    private final String studentId;
    private final String existingName;

    public DuplicateStudentException(String studentId, String existingName) {
        super(DUPLICATE_STUDENT,
              String.format("学号 %s 已被学生 %s 占用", studentId, existingName));
        this.studentId = studentId;
        this.existingName = existingName;
    }

    public String getStudentId() { return studentId; }
    public String getExistingName() { return existingName; }
}

/**
 * 学分不足异常(携带业务数据,便于调用方精确了解情况)
 */
public class InsufficientCreditsException extends StudentSystemException {

    private final String studentId;
    private final int required;
    private final int current;

    public InsufficientCreditsException(String studentId, int required, int current) {
        super(INSUFFICIENT_CREDITS,
              String.format("学生 %s 学分不足:需要 %d,当前 %d(缺 %d)",
                            studentId, required, current, required - current));
        this.studentId = studentId;
        this.required = required;
        this.current = current;
    }

    public String getStudentId() { return studentId; }
    public int getRequired() { return required; }
    public int getCurrent() { return current; }
    public int getShortfall() { return required - current; }
}

业务代码中使用自定义异常

@Service
public class EnrollmentService {

    private final StudentDao studentDao;
    private final CourseDao courseDao;

    public EnrollmentService(StudentDao studentDao, CourseDao courseDao) {
        this.studentDao = studentDao;
        this.courseDao = courseDao;
    }

    /**
     * 学生选课
     */
    public void enroll(String studentId, String courseId) {
        // 1. 查找学生
        Student student = studentDao.findById(studentId);
        if (student == null) {
            throw new StudentNotFoundException(studentId);  // 自定义异常,语义明确
        }

        // 2. 查找课程
        Course course = courseDao.findById(courseId);
        if (course == null) {
            throw new CourseNotFoundException(courseId);     // 另一个自定义异常
        }

        // 3. 检查学分
        if (student.getCredits() < course.getRequiredCredits()) {
            throw new InsufficientCreditsException(
                studentId, course.getRequiredCredits(), student.getCredits());
        }

        // 4. 检查是否已选过
        if (courseDao.isEnrolled(studentId, courseId)) {
            throw new DuplicateEnrollmentException(studentId, courseId);
        }

        // 5. 执行选课
        try {
            courseDao.enroll(studentId, courseId);
            student.setCredits(student.getCredits() - course.getRequiredCredits());
            studentDao.update(student);
            logger.info("选课成功: {} -> {}", studentId, courseId);
        } catch (DataAccessException e) {
            throw new ServiceException(DB_ERROR, "选课数据操作失败", e);
        }
    }
}

/**
 * 全局异常处理器(在 Controller 层统一处理)
 */
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(StudentNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleStudentNotFound(StudentNotFoundException e) {
        return ResponseEntity.status(404)
            .body(new ErrorResponse(e.getErrorCode(), e.getMessage()));
    }

    @ExceptionHandler(InsufficientCreditsException.class)
    public ResponseEntity<ErrorResponse> handleInsufficientCredits(
            InsufficientCreditsException e) {
        return ResponseEntity.status(400)
            .body(new ErrorResponse(e.getErrorCode(), e.getMessage()));
    }

    @ExceptionHandler(StudentSystemException.class)
    public ResponseEntity<ErrorResponse> handleSystemException(StudentSystemException e) {
        return ResponseEntity.status(500)
            .body(new ErrorResponse(e.getErrorCode(), "系统内部错误"));
    }
}
运行输出(学生不存在):
HTTP 404 Not Found
{
    "errorCode": 1001,
    "message": "学生不存在: STU-2024-0001",
    "timestamp": "2024-01-15T10:30:00"
}

运行输出(学分不足):
HTTP 400 Bad Request
{
    "errorCode": 1004,
    "message": "学生 STU-2024-0002 学分不足:需要 5,当前 2(缺 3)",
    "timestamp": "2024-01-15T10:31:00"
}

核心原理:Throwable 的构造方法链

serialVersionUID 的作用

public class StudentSystemException extends RuntimeException {

    // 为什么要显式定义 serialVersionUID?
    private static final long serialVersionUID = 1L;

    // ...
}
问题说明
为什么要定义?Throwable 实现了 Serializable 接口,所以异常对象可以被序列化(如在 RMI 或分布式系统中跨 JVM 传输)
不定义会怎样?编译器自动生成一个,但不同编译器版本可能生成不同的值,导致反序列化失败
值写什么?通常是 1L,只要保证类结构不变时值不变即可

为什么构造方法要调用 super()

// Throwable 内部的构造方法链
public class Throwable {
    private String detailMessage;
    private Throwable cause;

    public Throwable(String message) {
        this.detailMessage = message;  // 存储到私有字段
    }

    public Throwable(String message, Throwable cause) {
        this.detailMessage = message;
        this.cause = cause;            // 建立异常链
    }
}

// 你的自定义异常
public class MyException extends Exception {
    public MyException(String message, Throwable cause) {
        super(message, cause);  // 必须调用!否则 message 和 cause 都不会被设置
        // 不调用 super,getMessage() 返回 null,getCause() 返回 null
    }
}

原理:getMessage() 和 getCause() 是 Throwable 的方法,数据存储在 Throwable 的私有字段中。如果不调用 super(message, cause),这些方法将返回 null。


易错场景

反例一:用一个异常类表示所有情况

// ❌ 极差的设计:所有错误都用同一个类,靠字符串区分
public class MyException extends RuntimeException {
    public MyException(String message) {
        super(message);
    }
}

// 使用方
throw new MyException("学生不存在");
throw new MyException("学分不足");
throw new MyException("数据库错误");

// 调用方无法根据异常类型做差异化处理
catch (MyException e) {
    if (e.getMessage().contains("学生不存在")) {       // 丑陋的字符串判断!
        // ...
    } else if (e.getMessage().contains("学分不足")) {
        // ...
    }
}
// ✅ 正确:每种业务异常一个类
throw new StudentNotFoundException(studentId);
throw new InsufficientCreditsException(studentId, required, current);
throw new ServiceException(DB_ERROR, "数据库错误", e);

// 调用方可以精确捕获
catch (StudentNotFoundException e) { /* 404 */ }
catch (InsufficientCreditsException e) { /* 400 */ }
catch (ServiceException e) { /* 500 */ }

反例二:自定义异常没有提供 cause 构造方法

// ❌ 不完整的自定义异常
public class BadException extends RuntimeException {
    public BadException(String message) {
        super(message);
    }
    // 缺少 (String, Throwable) 和 (Throwable) 构造方法
}

// 使用时无法保留根因
try {
    studentDao.save(student);
} catch (DataAccessException e) {
    throw new BadException("保存失败");  // 根因 DataAccessException 丢失!
}
// ✅ 规范的自定义异常
public class GoodException extends RuntimeException {

    public GoodException(String message) {
        super(message);
    }

    public GoodException(String message, Throwable cause) {  // 关键!
        super(message, cause);
    }

    public GoodException(Throwable cause) {
        super(cause);
    }
}

反例三:过度使用受检异常

// ❌ 全部定义为受检异常,调用链每层都要处理或声明
public class StudentNotFoundException extends Exception { }  // 受检

// 造成"异常污染"——每一层调用都必须声明或捕获
public void methodA() throws StudentNotFoundException {
    methodB();
}
public void methodB() throws StudentNotFoundException {
    methodC();
}
public void methodC() throws StudentNotFoundException {
    throw new StudentNotFoundException("id");
}
// ✅ 对于 Spring 管理的业务系统,推荐使用非受检异常
// Spring 默认只对 RuntimeException 回滚事务
public class StudentNotFoundException extends RuntimeException { }

面试考点

Q1:什么时候应该自定义异常?JDK 的异常不够用吗?

JDK 的异常是通用的,无法表达具体业务语义。当需要以下情况时应该自定义异常:① 需要根据异常类型执行不同的处理逻辑(如区分"学生不存在"和"学分不足");② 需要携带额外的业务数据(如错误码、涉及的实体 ID);③ 需要建立分层的异常体系(如 DAO 层异常、Service 层异常、Controller 层异常)。核心原则是:让异常自己说话,而不是靠字符串匹配区分。

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

两种选择都有合理场景:继承 Exception 表示受检异常,调用方必须显式处理,适合"调用方应该有恢复策略"的场景(如文件不存在)。继承 RuntimeException 表示非受检异常,调用方可选处理,适合"程序逻辑问题"的场景。在 Spring 生态中,推荐继承 RuntimeException,因为 Spring 事务默认只对非受检异常回滚。现代 Java 开发的趋势是优先使用非受检异常。

Q3:自定义异常的三个标准构造方法是什么?

① (String message):最基础,仅传递错误消息。② (String message, Throwable cause):最常用,传递消息和根因,构建异常链。③ (Throwable cause):仅传递根因,用于纯包装场景。此外有时还会提供无参构造方法,但不推荐。每个构造方法都应该调用 super() 对应版本,否则 getMessage() / getCause() 将返回 null。

Q4:自定义异常中应该包含哪些额外字段?

根据业务需要,常见的有:① errorCode(错误码),便于 API 返回和国际化;② 业务实体 ID(如 studentId),便于日志定位;③ 上下文数据(如 required/current),便于调用方精确了解情况。但不要过度设计,加的字段应该确实有用。

Q5:异常类的 serialVersionUID 是必须的吗?

技术上不是必须的(编译器会自动生成),但强烈推荐显式定义。因为 Throwable 实现了 Serializable,异常对象可能在分布式系统(RMI、序列化缓存等)中传输。如果依赖编译器自动生成,不同编译器版本可能产生不同的值,导致反序列化时 InvalidClassException。显式定义为 private static final long serialVersionUID = 1L; 可避免此问题。

上一页
throws 与 throw