不该抛 runtimeexception,而应抛出自定义领域异常;异常需反映领域语义、置于领域层包下、禁止继承检查型异常;repository 不声明 throws;application service 仅记录日志并控制回滚;controller 映射为语义化 http 响应。

领域模型里该不该 throw RuntimeException?
不该,但绝大多数人其实已经在用了——关键不是“能不能抛”,而是“抛什么、谁来捕获、怎么恢复”。DDD 要求异常必须反映领域语义,IllegalArgumentException 或 NullPointerException 这类 JVM 底层异常,对领域建模毫无意义。
实操建议:
- 每个聚合根或实体内部只抛出自定义的领域异常,如
InsufficientBalanceException、InvalidOrderStatusTransitionException - 异常类必须放在领域层包下(如
com.example.banking.domain.exception),不能混在 infrastructure 或 application 层 - 禁止继承
RuntimeException以外的异常基类——检查型异常(Exception子类)会污染领域对象的 API,强迫调用方处理技术细节 - 构造函数里不做复杂校验;校验逻辑应封装在工厂方法或静态构造器中,失败时直接抛出领域异常
Repository 方法要不要声明 throws?
不要。Repository 是领域层契约,它的接口必须干净,不泄露数据访问细节。
常见错误现象:在 OrderRepository.findById() 上声明 throws SQLException 或 throws DataAccessException ——这等于把 JPA/Hibernate 的实现泄漏进了领域层。
立即学习“Java免费学习笔记(深入)”;
实操建议:
- Repository 接口方法一律不写
throws,所有底层异常都由 infrastructure 层拦截并转换为领域异常或静默处理(如返回Optional.empty()) - 当查询必然应存在却没查到时(例如根据 ID 加载已确认存在的聚合),抛
EntityNotFoundException(领域异常),而非让上层处理EmptyResultDataAccessException - 批量操作失败需区分:部分失败用
PartialUpdateException包含成功/失败 ID 列表;全部失败才按常规领域异常处理
Application Service 中怎么处理领域异常?
只做两件事:记录上下文、决定是否回滚。别在这里“修复”业务逻辑。
使用场景:用户提交转账请求,领域层抛出 InsufficientBalanceException,Application Service 不该尝试去充值或提示“请先存钱”,那是 UI 或 Domain Service 的事。
实操建议:
- 用 try-catch 捕获明确的领域异常类型,避免 catch
RuntimeException大而化之 - 记录日志时必须带上聚合 ID、用例输入参数(脱敏后),例如:
log.warn("Transfer failed for accountId={}, targetId={}: {}", accountId, targetId, e.getMessage()) - Spring 环境下,在 service 方法上加
@Transactional,并在@Transactional(rollbackFor = DomainException.class)显式指定回滚条件 - 不要在 catch 块里重新包装成
RuntimeException向上传——上层(如 Controller)需要知道这是领域规则拒绝,不是系统故障
Controller 层收到领域异常后怎么响应?
映射成 HTTP 语义明确的状态码和错误体,而不是堆栈或泛化消息。
容易踩的坑:把 InsufficientBalanceException 统一转成 500 Internal Server Error,或者返回 “Operation failed” 这种前端无法解析的字符串。
实操建议:
- 用 Spring 的
@ControllerAdvice+@ExceptionHandler做全局映射,例如将InsufficientBalanceException映射为 409 Conflict,body 包含code: "BALANCE_INSUFFICIENT"和message: "账户余额不足" - 错误 code 必须是机器可读的常量字符串(如
"ORDER_STATUS_INVALID"),不能拼接变量或带空格 - 敏感字段(如账户号、金额)不出现在 error message 中;message 只用于前端展示,详情走单独的 debug_id 日志追踪
- 避免在 Controller 里 new 异常再 throw——领域异常必须由领域对象自己抛出,否则边界就模糊了
最常被忽略的一点:领域异常的构造函数里别放业务计算逻辑。比如在 InsufficientBalanceException 的构造里调用 account.getAvailableBalance(),万一这个 getter 本身又抛异常,你就掉进双重异常陷阱了。











