异常梳理
你的理解非常准确,也是很多资深开发者的共识。"见洞就补"式的防御性编程(到处写 if (obj != null))是不可取的,这不仅会让代码变得臃肿、难以阅读,还可能掩盖真正的逻辑错误(Bug)。
以下是关于如何优雅处理空指针(NPE)的几点核心原则和建议:
- 核心原则:区分"异常情况"与"业务状态"
必定有值(Contract/契约):如果业务逻辑上规定某个对象必须存在(例如:订单必须有关联的用户ID),那么不需要判空。
为什么? 如果它为空,说明系统出现了严重的逻辑 Bug(数据不一致或上游代码写错了)。此时抛出 NullPointerException(或者用 Objects.requireNonNull 抛出带信息的异常)是好事,因为它能Fail Fast(快速失败),让你迅速定位到 Bug 的源头,而不是默默地吞掉错误导致后续数据错乱。
可能为空(Optional/可选项):只有当业务上允许该字段为空(例如:用户的"备注"信息可能没填),或者你不确定外部传入的数据是否合法时,才需要进行判空处理。 - 实战中的处理建议
A. 边界处严防死守,内部互信
对外接口(Controller/RPC):对于外部传进来的参数,必须做校验(可以使用 @NotNull 等 Validated 注解,或者手动检查)。永远不要信任外部输入。
内部调用(Service/Private方法):一旦数据进入了受信赖的内部逻辑,尽量减少防御性判空。
B. 利用工具类和语法糖
你的项目中引入了 commons-lang3 和 commons-collections4,善用它们可以避免显式的 if 判断:
字符串:StringUtils.isBlank(str) (安全处理 null 和空串)
集合:CollectionUtils.isEmpty(list) (安全处理 null 和空集合)
Java 8+ Optional:对于可能为空的返回值,推荐返回 Optional,强制调用方思考"如果没有值该怎么办"。
// 优雅写法
Optional.ofNullable(order.getUser())
.map(User::getAddress)
.orElse("默认地址");
C. 避免"级联调用"导致的问题
即使你确信 order 不为空,但 order.getUser().getAddress() 这种连点操作很容易在中间环节出问题。
推荐:使用 Optional 或者 MapStruct 等工具进行各种 DTO 转换,尽量把数据扁平化,减少深层嵌套的 get().get().get()。
D. 使用断言(Assertions)
如果你非常确定某个值不能为空,但又怕万一,可以使用 Java 标准库的断言来替代 if:
// 抛出明确的异常信息,比单纯的 NPE 更容易排查
this.userId = Objects.requireNonNull(userId, "用户ID不能为空");
总结
你的理解是完全正确的。
不要为了"不报错"而盲目判空,那样只会制造"掩耳盗铃"的代码。
要在业务允许为空,或涉及外部输入的地方进行判空。
对于"绝对不应该为空"的地方,如果真的空了,让它报错,因为那是 Bug,需要被发现和修复。
已深度思考
你的理解非常准确,也是很多资深开发者的共识。"见洞就补"式的防御性编程(到处写 if (obj != null))是不可取的,这不仅会让代码变得臃肿、难以阅读,还可能掩盖真正的逻辑错误(Bug)。
以下是关于如何优雅处理空指针(NPE)的几点核心原则和建议:
- 核心原则:区分"异常情况"与"业务状态"
必定有值(Contract/契约):如果业务逻辑上规定某个对象必须存在(例如:订单必须有关联的用户ID),那么不需要判空。
为什么? 如果它为空,说明系统出现了严重的逻辑 Bug(数据不一致或上游代码写错了)。此时抛出 NullPointerException(或者用 Objects.requireNonNull 抛出带信息的异常)是好事,因为它能Fail Fast(快速失败),让你迅速定位到 Bug 的源头,而不是默默地吞掉错误导致后续数据错乱。
可能为空(Optional/可选项):只有当业务上允许该字段为空(例如:用户的"备注"信息可能没填),或者你不确定外部传入的数据是否合法时,才需要进行判空处理。 - 实战中的处理建议
A. 边界处严防死守,内部互信
对外接口(Controller/RPC):对于外部传进来的参数,必须做校验(可以使用 @NotNull 等 Validated 注解,或者手动检查)。永远不要信任外部输入。
内部调用(Service/Private方法):一旦数据进入了受信赖的内部逻辑,尽量减少防御性判空。
B. 利用工具类和语法糖
你的项目中引入了 commons-lang3 和 commons-collections4,善用它们可以避免显式的 if 判断:
字符串:StringUtils.isBlank(str) (安全处理 null 和空串)
集合:CollectionUtils.isEmpty(list) (安全处理 null 和空集合)
Java 8+ Optional:对于可能为空的返回值,推荐返回 Optional,强制调用方思考"如果没有值该怎么办"。
// 优雅写法
Optional.ofNullable(order.getUser())
.map(User::getAddress)
.orElse("默认地址");
C. 避免"级联调用"导致的问题
即使你确信 order 不为空,但 order.getUser().getAddress() 这种连点操作很容易在中间环节出问题。
推荐:使用 Optional 或者 MapStruct 等工具进行各种 DTO 转换,尽量把数据扁平化,减少深层嵌套的 get().get().get()。
D. 使用断言(Assertions)
如果你非常确定某个值不能为空,但又怕万一,可以使用 Java 标准库的断言来替代 if:
// 抛出明确的异常信息,比单纯的 NPE 更容易排查
this.userId = Objects.requireNonNull(userId, "用户ID不能为空");
总结
你的理解是完全正确的。
不要为了"不报错"而盲目判空,那样只会制造"掩耳盗铃"的代码。
要在业务允许为空,或涉及外部输入的地方进行判空。
对于"绝对不应该为空"的地方,如果真的空了,让它报错,因为那是 Bug,需要被发现和修复。
还有一个问题,比如我确信某个对象,他不会为空,但是因为上游的问题,这里就是获取到的是空值导致了空指针,又以为没有使用业务异常处理 最终导致上报了可用率,我理解空指针是业务异常,他不是因为系统调用接口调用不到,或者网络异常等系统原因的异常。但是他确最终报的是系统异常,影响了可用率这个怎么理解
已深度思考
这其实是一个视角的错位问题:在监控系统眼里,只要代码抛出未捕获的 NullPointerException (NPE),它就是系统故障(Bug),而不是业务逻辑。
这听起来可能有点"冤",但从系统稳定性的角度解释如下:
- 为什么 NPE 默认是"系统异常"?
代码健壮性缺失:NPE 在 Java 中是 RuntimeException,它代表了**"代码逻辑错误"或"预期之外的情况"**。
监控的视角:监控系统看到你的服务在处理请求时"崩了"(抛出了异常栈),它无法区分是因为"上游传空了"还是"你代码写错了"。它只知道你没有成功处理完这个请求,也没有优雅地拒绝,而是直接抛出了异常。只要是 Uncaught Exception,通常都算作服务不可用(5xx 错误)。 - 你的理解 vs. 系统的理解
你的理解:"这是上游给的数据不对,不关我代码的事,这应该是业务上的数据错误。" ------ 这是归因视角。
系统的理解:"不管上游给什么垃圾数据,你作为下游服务,应该显式拦截并告诉上游数据错了。你没拦截住,反而在处理过程中自己爆了 NPE,那就是你的代码不够健壮。" ------ 这是结果视角。 - 怎样才能不影响可用率?
要保住可用率,核心是将**"被动的崩溃(NPE)"转化为"主动的拒绝(业务异常)"**。
错误做法(影响可用率):
// 上游传了 null,这里直接爆 NPE,监控抓取到异常栈 -> 系统异常 -> 扣可用率
String name = inputObject.getName();
正确做法(不影响可用率):
// 显式检查
if (inputObject == null) {
// 主动抛出业务异常(BusinessException)
throw new BusinessException(ErrorCode.PARAM_ERROR, "上游数据不能为空");
}
// 或者使用断言
// Objects.requireNonNull(inputObject, "数据异常");
总结
NPE 只要抛出来,就是代码的锅。
如果你希望它被视为"业务异常",你必须在代码里显式地 catch 住它(或者预先 check),然后以**"错误码"或者"受控的业务异常"**形式返回。
一句话心法:
上游传空是上游的错,但因为上游传空导致你报了 NPE,那就是你的错(缺乏防御性)。
你这不是自相矛盾吗,刚才你还说确信不为null的不抛,现在又说要catch 我不能理解
抱歉让你感到困惑了,这确实有点绕。我用一个核心区别来澄清这个"矛盾":
- 为什么说"不该抛 NPE"?
因为 NPE (NullPointerException) 是 Java 语言层面的"Bug"信号。
无论原因是什么(上游传空、逻辑漏洞),只要你的代码抛出了未被捕获的 NullPointerException,在监控系统和JVM看来,这就是你的程序崩溃了(Crash)。
后果:系统报警、可用率下跌、请求失败。
定性:这叫"代码质量问题"。
- 为什么说"要 catch"或者"显式校验"?
为了把这个"Bug 信号"转换成"业务信号"。
如果你确信这个字段必须有值,但上游违约传了空,这时候你有两种选择:
选择 A(你的做法 - 也是最容易被扣可用率的): 直接使用,让它爆 NPE。
结果:监控抓到 NPE -> 判定为系统异常 -> 扣可用率。
选择 B(推荐做法 - 保护可用率): 虽然我确信它应该有值,但作为对外接口,我还是加一行校验:
if (obj == null) {
throw new BusinessException("参数错误"); // 或者 return Result.fail("参数错误");
}
结果:监控抓到 BusinessException -> 判定为业务校验失败(预期内) -> 不扣可用率。
总结
矛盾点在于**"确信"**这个词。
逻辑上的确信:即使你逻辑上确信它不为空,如果它来自外部输入(上游、数据库、RPC),为了系统的鲁棒性和监控指标,你必须做防腐层(校验并转换异常)。
真正的不用判空:只有在你的系统内部(比如 Service A 调 Service B,或者私有方法),数据已经在入口处被校验过了,这种时候才不需要重复判空。
结论:为了不背"系统异常"的锅,在数据入口处(Controller/Listener),即使你认为必传的参数,也要为了"监控好看"而显式校验并抛出业务异常,而不是任由它爆 NPE。
我还有一个问题 只要catch异常 一定要答应error日志么 为什么
已深度思考
答案是:绝对不是。甚至在大多数"业务异常"的场景下,强烈建议不要打 ERROR 日志。
区分的关键在于:这件事是否需要开发/运维人员半夜起来修 Bug?
- 什么时候打 ERROR?(系统故障)
定义:预期之外的、系统级的、不仅影响当前请求可能还影响后续请求的严重问题。
场景:
数据库连不上了。
核心配置加载失败。
未捕获的 NPE(代码逻辑错误)。
磁盘满了。
目的:ERROR 日志通常会配置监控告警(发短信/打电话)。打了 ERROR 就要有人负责去修。
- 什么时候打 WARN?(业务阻断)
定义:预期之内的、业务层面的、系统本身没坏但请求无法继续的问题。
场景:
上游传了空参数(你刚才问的场景)。
用户密码输错了。
库存不足导致下单失败。
目的:记录案发现场,方便排查问题(比如上游来撕逼说为什么调用失败,你甩出 WARN 日志证明是他们参数传空了)。这种事不需要开发人员去改代码,也不需要触发告警。
- 为什么要区分?
如果你把所有的 catch 都打成 ERROR:
狼来了:监控群里全屏报警,大家习惯性忽略,真正数据库挂了的时候反而没人看到了。
污染大盘:很多公司的监控大盘是统计 ERROR 数量来计算健康度的。业务校验失败打 ERROR 会导致健康度虚低。
总结
Catch 到的业务异常 (BusinessException) -> 打 WARN (知晓即可,不扣可用率,不报警)。
Catch 到的系统异常 (SystemException/Exception) -> 打 ERROR (需要修复,扣可用率,报警)。
查看最新变更
我理解就是 ERROR 是系统界别的异常,warn是业务级别的异常。比如我通过if 判断空指针对的时候 我是不是就可以连日志也不打印,然后只是抛一个业务异常,然后配上关键字告警就可以了
是的,完全可以。 你的理解非常到位。
标准做法:不打日志(或只打 WARN),只抛业务异常
你说的这种处理方式其实是最高效的,既不影响监控,也不产生垃圾日志。
public void processOrder(OrderInfo order) {
// 1. 显式判空(防御式编程)
if (order == null || order.getOrderNo() == null) {
// 2. 抛出业务异常(BusinessException)
// 这里的异常信息(message)就是给上游看的,不需要再额外打日志
throw new BusinessException(ErrorCode.PARAM_ERROR, "订单信息或订单号不能为空");
}
// 3. 正常业务逻辑...
}
为什么可以不打日志?
异常对象本身就携带了信息:抛出的 BusinessException 包含了错误码和错误描述。
上层会统一处理:通常在 Web 层(ControllerAdvice)或 RPC 入口处,会有一个全局异常处理器。它会捕获这个 BusinessException,然后把它转换成一个标准的错误响应(Result)返回给调用方。
关键字告警:你可以在全局异常处理的地方,针对某些特定的 ErrorCode 配置关键字监控(如果需要的话)。或者直接让上游系统去监控他们收到的错误码。
唯一的例外
如果你没有全局异常处理机制,或者你的上游是MQ(消息队列)。
MQ 场景:如果消费者抛异常,消息可能会重试。对于这种"不可恢复的参数错误"(重试一万次也是空),你需要 catch 住它,打个 WARN 日志记录一下("收到一条脏数据,丢弃"),然后正常返回(消费成功),以避免死循环重试。
我还有一个问题,就是如果我只打warn 没有抛异常,这样即使是业务异常,代码还是会继续向后执行,所以我想不到什么场景可以只打warn 不抛异常
非常敏锐的发现!确实,如果只打日志(Warn)而不抛异常,程序会继续往下跑,这在很多时候是危险的。
但是,"只打 Warn,不抛异常" 实际上是非常经典的处理方式,通常用于**"非核心/旁路业务"或"尽力而为(Best Effort)"**的场景。
以下是几个常见的场景,我们故意只打 Warn 而不中断主流程:
场景 1:非关键旁路逻辑(如:埋点、通知)
假设你在处理一个核心的"下单"流程,下单成功后需要发一个非关键的通知(比如发个营销短信,或者记个 BI 埋点)。
// 核心:创建订单
orderService.create(order);
// 旁路:发营销短信(如果发短信挂了,或者用户没手机号,不应该影响用户下单成功)
try {
smsService.send(user.getPhone(), "下单成功");
} catch (Exception e) {
// 这里的异常我们不想抛出去,因为下单已经成功了,抛出去会让整个事务回滚,用户会看到下单失败
// 所以这里只打 Warn 日志,吞掉异常
log.warn("营销短信发送失败,但不影响主流程, userId: {}", user.getId(), e);
}
场景 2:降级处理(默认值策略)
当获取某个非必须的数据失败时,我们可以给个默认值继续跑,而不是报错。
// 获取用户头像
String avatarUrl;
try {
avatarUrl = remoteUserService.getAvatar(userId);
} catch (Exception e) {
// 获取头像失败不算大事,给个默认头像,让页面能显示出来
log.warn("获取用户头像失败,降级使用默认头像, userId: {}", userId);
avatarUrl = DEFAULT_AVATAR;
}
// 继续后续逻辑...
场景 3:批量处理中的"部分成功"
假设你要批量处理 100 个订单的状态同步。
for (Order order : orderList) {
try {
syncStatus(order);
} catch (Exception e) {
// 如果第 5 个订单同步失败了,我不希望后面 95 个都不跑了
// 所以我只打 Warn,记录这个订单失败了,然后 continue 继续跑下一个
log.warn("订单同步失败, orderId: {}", order.getId(), e);
}
}
总结
核心链路(如校验参数、扣库存、写数据库):必须抛异常,阻断流程。
非核心链路/可降级逻辑(如发通知、查非关键信息、批量任务):可以(且应该)只打 Warn,吞掉异常,保全大局。
throw new BusinessException("构建华润状态DTO失败", e); 为什么这样写就可以不在方法上抛出去异常,
throw new BusinessException("构建华润状态DTO失败");这样写就要抛出去呢
HuaRunStatusCallbackServiceImpl.java: L140#4-L140#50
这两种写法都会抛出异常,都会中断当前方法的执行。
区别在于:异常链(Exception Chaining)的保留。
- throw new BusinessException("...", e); (推荐)
含义:我捕获了一个底层异常 e(比如 NPE),然后把它包装成了一个新的 BusinessException 抛出去。
关键点:构造函数里的 e 作为 cause(原因)被传进去了。
效果:当最外层打印日志时,不仅能看到"构建华润状态DTO失败",还能看到最原始的那个 NPE 的堆栈信息。
好处:方便排查问题。你知道是因为业务失败了,也知道导致业务失败的具体代码行数(NPE 发生在哪一行)。 - throw new BusinessException("..."); (不推荐)
含义:我捕获了异常 e,但我把它扔了,重新抛出了一个全新的异常。
关键点:丢失了原始异常 e。
效果:外层只能看到"构建华润状态DTO失败",原来的 NPE 堆栈彻底消失了。
坏处:排查问题时会非常痛苦。你只知道出错了,但不知道具体是哪行代码空指针了,还得去猜。
总结
这两种写法在控制流程上是一模一样的(都会抛出异常,都会被外层 catch)。
但在问题排查上,第一种(带 e 的)要优越得多,因为它保留了"案发现场"。
异常体系架构
这是一个非常专业且架构层面的问题。构建一套优秀的异常体系,核心目标是:分类明确、处理统一、语义清晰、监控友好。
基于现代 Java 业务开发(特别是 Spring Boot 微服务架构),我为你梳理了一套贴合实战的异常体系架构。
一、 核心架构设计图
异常体系不应过于扁平,也不应过于深层。推荐采用 "三层两类" 架构:
根基:BaseRuntimeException (所有自定义异常的父类)
两大分类:
BusinessException (业务异常):预期内的、可理解的、通常不需要半夜修Bug的(如余额不足、参数错误)。-> Log WARN
SystemException (系统异常):预期外的、技术层面的、需要介入修复的(如数据库挂了、NPE、配置错误)。-> Log ERROR
二、 详细异常类定义
- 基础接口:错误码接口 (IErrorCode)
异常不仅仅是抛出 Message,更重要的是抛出 ErrorCode(便于前端展示和监控统计)。
public interface IErrorCode {
String getCode(); // 错误码,如 "A0001"
String getMessage(); // 错误描述,如 "参数校验失败"
}
- 根异常 (BaseException)
所有自定义异常的基类,继承自 RuntimeException(切记:业务开发中尽量不要用 Checked Exception)。
public abstract class BaseException extends RuntimeException {
private final String errCode;
public BaseException(IErrorCode errCode) {
super(errCode.getMessage());
this.errCode = errCode.getCode();
}
public BaseException(String code, String msg) {
super(msg);
this.errCode = code;
}
public BaseException(String code, String msg, Throwable e) {
super(msg, e); // 保留原始堆栈
this.errCode = code;
}
// getter...
}
- 核心业务异常 (BusinessException)
这是你代码里出现频率最高的异常。
场景:用户操作不当、违反业务规则。
特点:不需要打 Error 日志,不扣系统可用率。
public class BusinessException extends BaseException {
// 构造方法参照父类
}
- 核心系统异常 (SystemException)
这是防御性编程的兜底异常。
场景:技术组件故障、代码逻辑Bug、不可恢复的错误。
特点:必须打 Error 日志,必须报警。
public class SystemException extends BaseException {
// 构造方法参照父类
}
三、 高频通用异常封装(细分领域)
除了上述两个大类,为了代码语义更清晰,建议封装以下 5 个高频细分异常(它们通常继承自 BusinessException 或 BaseException,视情况而定):
| 异常类名 | 继承自 | 场景描述 | HTTP 状态码建议 |
|---|---|---|---|
| ParamException | Business | 参数校验异常。手动校验参数为空、格式不对时抛出。全局异常处理器捕获后返回 400。 | 400 |
| UnauthorizedException | Business | 认证异常。用户未登录、Token 过期、Token 伪造。 | 401 |
| ForbiddenException | Business | 权限异常。用户登录了,但没权限操作该资源(如普通用户删管理员数据)。 | 403 |
| NotFoundException | Business | 资源不存在异常。查询数据库为空,且业务逻辑要求必须存在时抛出。 | 404 |
| RpcException | System | 远程调用异常。调用 Feign/Dubbo 接口超时、失败、熔断。这是系统级故障。 | 500 |
四、 全局异常处理器 (GlobalExceptionHandler)
这是异常体系的执行中枢,在这里决定了是打 WARN 还是 ERROR。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 1. 处理业务异常 -> 预期内 -> WARN -> 正常返回 Result
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
// 关键:只打 WARN,不报警,方便排查即可
log.warn("业务异常: code={}, msg={}", e.getErrCode(), e.getMessage());
return Result.fail(e.getErrCode(), e.getMessage());
}
// 2. 处理特定细分异常 (如参数异常)
@ExceptionHandler(ParamException.class)
public Result<?> handleParamException(ParamException e) {
log.warn("参数校验失败: {}", e.getMessage());
return Result.fail("400", e.getMessage());
}
// 3. 处理系统异常 -> 预期外 -> ERROR -> 报警
@ExceptionHandler(SystemException.class)
public Result<?> handleSystemException(SystemException e) {
// 关键:打 ERROR,且把堆栈 e 打印出来
log.error("系统严重异常", e);
return Result.fail("500", "系统繁忙,请稍后重试");
}
// 4. 兜底处理所有未知的 RuntimeException (如 NPE)
@ExceptionHandler(Exception.class)
public Result<?> handleUnknownException(Exception e) {
log.error("未知系统异常", e);
return Result.fail("999", "系统内部错误,请联系管理员");
}
}
五、 实战使用规范(开发手册)
不要定义太多的 XxxNotFoundException:
❌ 不要写 UserNotFoundException, OrderNotFoundException, GoodsNotFoundException... 这样会导致类爆炸。
✅ 统一用 throw new BusinessException(UserErrors.USER_NOT_FOUND),通过错误码枚举来区分业务。
必须保留堆栈:
当 catch 住一个异常并 rethrow 时,务必使用 throw new SystemException("描述", e),千万别漏了 e,否则案发现场就丢了。
优先使用标准异常:
如果能用 Java 自带的 IllegalArgumentException 表达清楚的,也可以用,但在全局处理时需要把它归类到"系统异常"或"参数异常"中。建议统一封装成 ParamException 以便前端统一识别。
六、 总结推荐的目录结构
com.company.project
├── common
│ ├── exception
│ │ ├── BaseException.java
│ │ ├── BusinessException.java <-- 最常用
│ │ ├── SystemException.java <-- 即使 catch 也要报警
│ │ ├── ParamException.java <-- 参数校验失败
│ │ ├── RpcException.java <-- 外部接口挂了
│ │ └── UnauthorizedException.java
│ ├── errorcode
│ │ ├── IErrorCode.java
│ │ └── GlobalErrorCodeEnum.java <-- 全局通用错误码
│ └── handler
│ └── GlobalExceptionHandler.java
这套体系既能区分业务与系统故障,又避免了创建几百个异常类的繁琐,是目前互联网大厂中最主流的实践。
这是一个非常专业且架构层面的问题。构建一套优秀的异常体系,核心目标是:分类明确、处理统一、语义清晰、监控友好。
基于现代 Java 业务开发(特别是 Spring Boot 微服务架构),我为你梳理了一套贴合实战的异常体系架构。
一、 核心架构设计图
异常体系不应过于扁平,也不应过于深层。推荐采用 "三层两类" 架构:
根基:BaseRuntimeException (所有自定义异常的父类)
两大分类:
BusinessException (业务异常):预期内的、可理解的、通常不需要半夜修Bug的(如余额不足、参数错误)。-> Log WARN
SystemException (系统异常):预期外的、技术层面的、需要介入修复的(如数据库挂了、NPE、配置错误)。-> Log ERROR
二、 详细异常类定义
- 基础接口:错误码接口 (IErrorCode)
异常不仅仅是抛出 Message,更重要的是抛出 ErrorCode(便于前端展示和监控统计)。
public interface IErrorCode {
String getCode(); // 错误码,如 "A0001"
String getMessage(); // 错误描述,如 "参数校验失败"
}
- 根异常 (BaseException)
所有自定义异常的基类,继承自 RuntimeException(切记:业务开发中尽量不要用 Checked Exception)。
public abstract class BaseException extends RuntimeException {
private final String errCode;
public BaseException(IErrorCode errCode) {
super(errCode.getMessage());
this.errCode = errCode.getCode();
}
public BaseException(String code, String msg) {
super(msg);
this.errCode = code;
}
public BaseException(String code, String msg, Throwable e) {
super(msg, e); // 保留原始堆栈
this.errCode = code;
}
// getter...
}
- 核心业务异常 (BusinessException)
这是你代码里出现频率最高的异常。
场景:用户操作不当、违反业务规则。
特点:不需要打 Error 日志,不扣系统可用率。
public class BusinessException extends BaseException {
// 构造方法参照父类
}
- 核心系统异常 (SystemException)
这是防御性编程的兜底异常。
场景:技术组件故障、代码逻辑Bug、不可恢复的错误。
特点:必须打 Error 日志,必须报警。
public class SystemException extends BaseException {
// 构造方法参照父类
}
三、 高频通用异常封装(细分领域)
除了上述两个大类,为了代码语义更清晰,建议封装以下 5 个高频细分异常(它们通常继承自 BusinessException 或 BaseException,视情况而定):
| 异常类名 | 继承自 | 场景描述 | HTTP 状态码建议 |
|---|---|---|---|
| ParamException | Business | 参数校验异常。手动校验参数为空、格式不对时抛出。全局异常处理器捕获后返回 400。 | 400 |
| UnauthorizedException | Business | 认证异常。用户未登录、Token 过期、Token 伪造。 | 401 |
| ForbiddenException | Business | 权限异常。用户登录了,但没权限操作该资源(如普通用户删管理员数据)。 | 403 |
| NotFoundException | Business | 资源不存在异常。查询数据库为空,且业务逻辑要求必须存在时抛出。 | 404 |
| RpcException | System | 远程调用异常。调用 Feign/Dubbo 接口超时、失败、熔断。这是系统级故障。 | 500 |
四、 全局异常处理器 (GlobalExceptionHandler)
这是异常体系的执行中枢,在这里决定了是打 WARN 还是 ERROR。
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 1. 处理业务异常 -> 预期内 -> WARN -> 正常返回 Result
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
// 关键:只打 WARN,不报警,方便排查即可
log.warn("业务异常: code={}, msg={}", e.getErrCode(), e.getMessage());
return Result.fail(e.getErrCode(), e.getMessage());
}
// 2. 处理特定细分异常 (如参数异常)
@ExceptionHandler(ParamException.class)
public Result<?> handleParamException(ParamException e) {
log.warn("参数校验失败: {}", e.getMessage());
return Result.fail("400", e.getMessage());
}
// 3. 处理系统异常 -> 预期外 -> ERROR -> 报警
@ExceptionHandler(SystemException.class)
public Result<?> handleSystemException(SystemException e) {
// 关键:打 ERROR,且把堆栈 e 打印出来
log.error("系统严重异常", e);
return Result.fail("500", "系统繁忙,请稍后重试");
}
// 4. 兜底处理所有未知的 RuntimeException (如 NPE)
@ExceptionHandler(Exception.class)
public Result<?> handleUnknownException(Exception e) {
log.error("未知系统异常", e);
return Result.fail("999", "系统内部错误,请联系管理员");
}
}
五、 实战使用规范(开发手册)
不要定义太多的 XxxNotFoundException:
❌ 不要写 UserNotFoundException, OrderNotFoundException, GoodsNotFoundException... 这样会导致类爆炸。
✅ 统一用 throw new BusinessException(UserErrors.USER_NOT_FOUND),通过错误码枚举来区分业务。
必须保留堆栈:
当 catch 住一个异常并 rethrow 时,务必使用 throw new SystemException("描述", e),千万别漏了 e,否则案发现场就丢了。
优先使用标准异常:
如果能用 Java 自带的 IllegalArgumentException 表达清楚的,也可以用,但在全局处理时需要把它归类到"系统异常"或"参数异常"中。建议统一封装成 ParamException 以便前端统一识别。
六、 总结推荐的目录结构
com.company.project
├── common
│ ├── exception
│ │ ├── BaseException.java
│ │ ├── BusinessException.java <-- 最常用
│ │ ├── SystemException.java <-- 即使 catch 也要报警
│ │ ├── ParamException.java <-- 参数校验失败
│ │ ├── RpcException.java <-- 外部接口挂了
│ │ └── UnauthorizedException.java
│ ├── errorcode
│ │ ├── IErrorCode.java
│ │ └── GlobalErrorCodeEnum.java <-- 全局通用错误码
│ └── handler
│ └── GlobalExceptionHandler.java
这套体系既能区分业务与系统故障,又避免了创建几百个异常类的繁琐,是目前互联网大厂中最主流的实践。