应明确区分checked与unchecked异常:checked用于外部依赖失败且可恢复的场景,unchecked用于程序逻辑错误;禁用自定义checked异常做业务校验;各层需按约定转换异常;自定义异常须继承统一父类、命名体现动作结果、构造函数强制携带关键字段;通过静态检查、日志规范和文档同步保障落地。

在大型Java项目中,Checked与Unchecked异常的使用不能靠经验或随意决定,而需建立明确、可落地、团队共识的规范。核心原则是:让异常类型传递语义,而不是增加维护负担。
按异常性质划分责任边界
区分“调用者能否预见并恢复”是制定规范的第一步:
-
必须用Checked异常:外部依赖失败且业务上允许重试、降级或提示用户的情况。例如:
IOException(文件读写)、SQLException(数据库连接超时)、TimeoutException(RPC调用超时)。 -
必须用Unchecked异常:程序逻辑错误、参数校验失败、状态非法等本不该发生的问题。例如:
IllegalArgumentException(订单金额为负)、IllegalStateException(支付状态已终态却再次调用确认)、NullPointerException(未判空导致NPE)。 -
禁止自定义Checked异常用于业务规则校验:比如“用户名已存在”“库存不足”这类业务拒绝场景,应统一抛出带错误码的
BusinessException(继承RuntimeException),而非UsernameExistException extends Exception——否则每个校验都要强制try-catch,破坏API简洁性。
分层约定:谁抛、谁捕、谁转
异常不应跨层裸传,各层有明确职责:
-
DAO/Client层:原始异常(如
SQLException、FeignException)必须转换。不直接抛出底层Checked异常,而是包装为项目统一的DataAccessException(Unchecked)或RemoteCallException(Checked,仅当上层需感知网络稳定性时)。 -
Service层:只抛出业务语义清晰的异常。Checked异常仅用于“系统暂时不可用但用户可稍后重试”的场景;其余一律用自定义Unchecked异常(如
OrderInvalidException),并在构造时注入错误码、日志上下文ID、traceId。 - Controller/Web层:统一拦截所有异常。Checked异常转为HTTP 503或408响应;Unchecked异常根据错误码映射为400(业务拒绝)或500(系统异常),并脱敏堆栈信息。
自定义异常的设计约束
避免异常泛滥,通过命名和继承结构强化语义:
-
所有自定义异常必须继承明确父类:业务类异常统一继承
BaseBusinessException extends RuntimeException;系统级可恢复异常继承BaseSystemException extends Exception(极少使用)。 -
异常类名必须体现动作+结果:如
PaymentFailedException、InventoryDeductFailedException,禁用PaymentException这类模糊命名。 -
构造函数强制携带关键字段:至少包含
errorCode: String、message: String、cause: Throwable,避免无参构造器;支持传入Map<string object> context</string>用于日志追踪。
工具与治理保障
规范落地需技术手段兜底:
-
静态检查:用SonarQube规则禁止
catch (Exception e)、禁止throws Exception、禁止在Service方法签名中声明Checked异常(除非极特殊场景)。 - 日志规范:Unchecked异常默认记录ERROR级别+完整堆栈;Checked异常仅记录WARN级别+业务上下文,不打堆栈(因它本就是预期路径)。
-
文档同步:所有对外提供的API接口文档(如OpenAPI)中,
4xx错误码对应哪些Unchecked异常,5xx中哪些属于可重试的Checked异常,必须显式列出。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











