Skip to content

自定义异常

概述

当标准异常(如 IllegalArgumentExceptionIOException)无法准确表达业务含义时,可以自定义异常类。自定义异常能传达领域语义(如「用户不存在」「订单状态非法」)、统一错误码或上下文信息,并可通过异常链保留底层原因,便于排查。本文说明如何正确设计受检/非受检自定义异常、构造方法约定以及常见实践。


何时需要自定义异常

  • 业务语义明确:如 UserNotFoundExceptionInsufficientBalanceException,比通用的 IllegalArgumentException 更易读、易处理。
  • 统一错误码或上下文:在异常中封装错误码、字段名、业务 ID 等,便于上层做国际化或统一响应。
  • 保留异常链:包装底层异常(如 IO、SQL)时,通过 cause 传递根因,不丢失堆栈信息。

若标准异常已能表达含义(如参数不合法用 IllegalArgumentException),不必强行自定义;过度细分异常类会增加调用方的 catch 负担。

提示

自定义异常前先确认 异常体系:需要调用方必须处理的用受检异常(继承 Exception);表示编程错误或不可恢复状态、可不强制处理的用非受检异常(继承 RuntimeException)。


基本语法:继承 Exception 或 RuntimeException

  • 受检异常:继承 Exception(不要带 RuntimeException 的父类)。方法抛出后,调用方必须 try-catch 或 throws。
  • 非受检异常:继承 RuntimeException。编译器不强制处理,适合前置条件不满足、非法状态等。

不要继承 Error:Error 表示 JVM/系统级严重错误,业务异常应归入 Exception 体系。

java
// 受检异常:调用方必须处理(如「文件格式不支持」)
public class UnsupportedFormatException extends Exception {
    public UnsupportedFormatException(String message) {
        super(message);
    }
}

// 非受检异常:可不强制处理(如「用户不存在」由上层统一处理)
public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
}

构造方法约定

Throwable 及其子类常用以下形式的构造方法,自定义异常应至少提供「消息」和「原因」两种,以便日志与异常链完整。

构造方法说明
无参使用较少,消息可由子类固定
(String message)最常用,携带错误描述
(String message, Throwable cause)包装底层异常,保留原因链
(Throwable cause)仅包装原因,消息可用 cause.getMessage()

重写时用 super(message)super(cause)super(message, cause) 传给父类,不要吞掉 cause。

示例:标准自定义异常类

java
/**
 * 业务含义:用户不存在。
 * 继承 RuntimeException,调用方可不强制 try-catch,由上层或全局异常处理器统一处理。
 */
public class UserNotFoundException extends RuntimeException {

    private final String userId;  // 可选:携带业务上下文,便于日志与接口返回

    public UserNotFoundException(String message) {
        super(message);
        this.userId = null;
    }

    public UserNotFoundException(String message, String userId) {
        super(message);
        this.userId = userId;
    }

    public UserNotFoundException(String message, Throwable cause) {
        super(message, cause);
        this.userId = null;
    }

    public String getUserId() {
        return userId;
    }
}

使用示例

示例 1:非受检业务异常(推荐用于「查无此人」类错误)

java
public class UserService {
    public User findById(String id) {
        User user = userRepository.findById(id);
        if (user == null) {
            throw new UserNotFoundException("用户不存在: " + id, id);
        }
        return user;
    }
}

// 调用方:可不捕获,由上层或全局异常处理器统一返回 404
// 或按需捕获做分支逻辑
try {
    User user = userService.findById("U001");
    return user;
} catch (UserNotFoundException e) {
    return ResponseEntity.status(404).body(e.getMessage());
}

示例 2:受检异常(调用方必须处理)

适用于「可恢复或调用方必须知晓」的场景,如配置缺失、格式不支持等。

java
public class InvalidConfigException extends Exception {
    public InvalidConfigException(String message) {
        super(message);
    }

    public InvalidConfigException(String message, Throwable cause) {
        super(message, cause);
    }
}

// 使用:调用方必须 try-catch 或 throws
public void loadConfig(String path) throws InvalidConfigException {
    try {
        String content = Files.readString(Path.of(path));
        parseConfig(content);
    } catch (IOException e) {
        throw new InvalidConfigException("无法读取配置文件: " + path, e);
    }
}

示例 3:异常链(保留 cause)

包装底层异常时,务必使用带 cause 的构造方法,便于排查根因。

java
try {
    return jdbcTemplate.queryForObject(sql, rowMapper, id);
} catch (DataAccessException e) {
    // 包装为业务异常,保留底层 SQL/连接异常信息
    throw new OrderQueryException("查询订单失败, id=" + id, e);
}

捕获方可通过 getCause()printStackTrace() 看到完整链式堆栈。


进阶用法

错误码与统一响应

在异常中封装错误码,便于 REST 接口或前端统一处理。

java
public class BusinessException extends RuntimeException {
    private final String code;  // 如 "USER_NOT_FOUND", "INSUFFICIENT_BALANCE"

    public BusinessException(String code, String message) {
        super(message);
        this.code = code;
    }

    public String getCode() {
        return code;
    }
}

// 使用
throw new BusinessException("INSUFFICIENT_BALANCE", "余额不足,当前余额: " + balance);

序列化(可选)

若异常需要跨 JVM 传递(如 RPC、分布式调用),应实现 Serializable 并显式声明 serialVersionUID,避免版本变更导致反序列化失败。

java
public class RemoteServiceException extends Exception implements java.io.Serializable {
    private static final long serialVersionUID = 1L;

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

说明

仅当异常实例会跨进程/网络传输时才需考虑序列化;多数 Web 应用在单机内抛出、由全局异常处理器转为 JSON 响应,不必须实现 Serializable。


注意事项

注意

受检 vs 非受检:若希望调用方必须处理(如重试、降级、提示用户),用受检异常(继承 Exception);若表示编程错误或由上层统一处理即可,用非受检异常(继承 RuntimeException)。不要为了「少写 throws」而把所有自定义异常都做成 RuntimeException,否则调用方容易忽略。

注意

务必保留异常链:包装底层异常时使用 super(message, cause),不要 new MyException(message) 而丢弃 cause,否则排查问题时难以定位根因。

不要继承 Error

业务异常一律继承 ExceptionRuntimeException,不要继承 Error。Error 留给 JVM 与系统级错误使用。

  • 命名:类名应能表达业务含义,通常以 Exception 结尾,如 UserNotFoundExceptionInvalidOrderStateException
  • 避免过度细分:同一模块内异常类不宜过多,否则调用方需要 catch 大量类型;可适当用「错误码 + 通用业务异常」替代过多子类。

相关链接

基于 VitePress 构建