异常梳理aaaa

异常梳理

你的理解非常准确,也是很多资深开发者的共识。"见洞就补"式的防御性编程(到处写 if (obj != null))是不可取的,这不仅会让代码变得臃肿、难以阅读,还可能掩盖真正的逻辑错误(Bug)。

以下是关于如何优雅处理空指针(NPE)的几点核心原则和建议:

  1. 核心原则:区分"异常情况"与"业务状态"
    必定有值(Contract/契约):如果业务逻辑上规定某个对象必须存在(例如:订单必须有关联的用户ID),那么不需要判空。
    为什么? 如果它为空,说明系统出现了严重的逻辑 Bug(数据不一致或上游代码写错了)。此时抛出 NullPointerException(或者用 Objects.requireNonNull 抛出带信息的异常)是好事,因为它能Fail Fast(快速失败),让你迅速定位到 Bug 的源头,而不是默默地吞掉错误导致后续数据错乱。
    可能为空(Optional/可选项):只有当业务上允许该字段为空(例如:用户的"备注"信息可能没填),或者你不确定外部传入的数据是否合法时,才需要进行判空处理。
  2. 实战中的处理建议
    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)的几点核心原则和建议:

  1. 核心原则:区分"异常情况"与"业务状态"
    必定有值(Contract/契约):如果业务逻辑上规定某个对象必须存在(例如:订单必须有关联的用户ID),那么不需要判空。
    为什么? 如果它为空,说明系统出现了严重的逻辑 Bug(数据不一致或上游代码写错了)。此时抛出 NullPointerException(或者用 Objects.requireNonNull 抛出带信息的异常)是好事,因为它能Fail Fast(快速失败),让你迅速定位到 Bug 的源头,而不是默默地吞掉错误导致后续数据错乱。
    可能为空(Optional/可选项):只有当业务上允许该字段为空(例如:用户的"备注"信息可能没填),或者你不确定外部传入的数据是否合法时,才需要进行判空处理。
  2. 实战中的处理建议
    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),而不是业务逻辑。

这听起来可能有点"冤",但从系统稳定性的角度解释如下:

  1. 为什么 NPE 默认是"系统异常"?
    代码健壮性缺失:NPE 在 Java 中是 RuntimeException,它代表了**"代码逻辑错误"或"预期之外的情况"**。
    监控的视角:监控系统看到你的服务在处理请求时"崩了"(抛出了异常栈),它无法区分是因为"上游传空了"还是"你代码写错了"。它只知道你没有成功处理完这个请求,也没有优雅地拒绝,而是直接抛出了异常。只要是 Uncaught Exception,通常都算作服务不可用(5xx 错误)。
  2. 你的理解 vs. 系统的理解
    你的理解:"这是上游给的数据不对,不关我代码的事,这应该是业务上的数据错误。" ------ 这是归因视角。
    系统的理解:"不管上游给什么垃圾数据,你作为下游服务,应该显式拦截并告诉上游数据错了。你没拦截住,反而在处理过程中自己爆了 NPE,那就是你的代码不够健壮。" ------ 这是结果视角。
  3. 怎样才能不影响可用率?
    要保住可用率,核心是将**"被动的崩溃(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 我不能理解

抱歉让你感到困惑了,这确实有点绕。我用一个核心区别来澄清这个"矛盾":

  1. 为什么说"不该抛 NPE"?
    因为 NPE (NullPointerException) 是 Java 语言层面的"Bug"信号。
    无论原因是什么(上游传空、逻辑漏洞),只要你的代码抛出了未被捕获的 NullPointerException,在监控系统和JVM看来,这就是你的程序崩溃了(Crash)。

后果:系统报警、可用率下跌、请求失败。

定性:这叫"代码质量问题"。

  1. 为什么说"要 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?

  1. 什么时候打 ERROR?(系统故障)
    定义:预期之外的、系统级的、不仅影响当前请求可能还影响后续请求的严重问题。

场景:

数据库连不上了。

核心配置加载失败。

未捕获的 NPE(代码逻辑错误)。

磁盘满了。

目的:ERROR 日志通常会配置监控告警(发短信/打电话)。打了 ERROR 就要有人负责去修。

  1. 什么时候打 WARN?(业务阻断)

定义:预期之内的、业务层面的、系统本身没坏但请求无法继续的问题。

场景:

上游传了空参数(你刚才问的场景)。

用户密码输错了。

库存不足导致下单失败。

目的:记录案发现场,方便排查问题(比如上游来撕逼说为什么调用失败,你甩出 WARN 日志证明是他们参数传空了)。这种事不需要开发人员去改代码,也不需要触发告警。

  1. 为什么要区分?

如果你把所有的 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)的保留。

  1. throw new BusinessException("...", e); (推荐)
    含义:我捕获了一个底层异常 e(比如 NPE),然后把它包装成了一个新的 BusinessException 抛出去。
    关键点:构造函数里的 e 作为 cause(原因)被传进去了。
    效果:当最外层打印日志时,不仅能看到"构建华润状态DTO失败",还能看到最原始的那个 NPE 的堆栈信息。
    好处:方便排查问题。你知道是因为业务失败了,也知道导致业务失败的具体代码行数(NPE 发生在哪一行)。
  2. throw new BusinessException("..."); (不推荐)
    含义:我捕获了异常 e,但我把它扔了,重新抛出了一个全新的异常。
    关键点:丢失了原始异常 e。
    效果:外层只能看到"构建华润状态DTO失败",原来的 NPE 堆栈彻底消失了。
    坏处:排查问题时会非常痛苦。你只知道出错了,但不知道具体是哪行代码空指针了,还得去猜。
    总结
    这两种写法在控制流程上是一模一样的(都会抛出异常,都会被外层 catch)。
    但在问题排查上,第一种(带 e 的)要优越得多,因为它保留了"案发现场"。

异常体系架构

这是一个非常专业且架构层面的问题。构建一套优秀的异常体系,核心目标是:分类明确、处理统一、语义清晰、监控友好。

基于现代 Java 业务开发(特别是 Spring Boot 微服务架构),我为你梳理了一套贴合实战的异常体系架构。

一、 核心架构设计图

异常体系不应过于扁平,也不应过于深层。推荐采用 "三层两类" 架构:

根基:BaseRuntimeException (所有自定义异常的父类)

两大分类:

BusinessException (业务异常):预期内的、可理解的、通常不需要半夜修Bug的(如余额不足、参数错误)。-> Log WARN

SystemException (系统异常):预期外的、技术层面的、需要介入修复的(如数据库挂了、NPE、配置错误)。-> Log ERROR

二、 详细异常类定义

  1. 基础接口:错误码接口 (IErrorCode)
    异常不仅仅是抛出 Message,更重要的是抛出 ErrorCode(便于前端展示和监控统计)。

public interface IErrorCode {

String getCode(); // 错误码,如 "A0001"

String getMessage(); // 错误描述,如 "参数校验失败"

}

  1. 根异常 (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...

}

  1. 核心业务异常 (BusinessException)

这是你代码里出现频率最高的异常。

场景:用户操作不当、违反业务规则。

特点:不需要打 Error 日志,不扣系统可用率。

public class BusinessException extends BaseException {

// 构造方法参照父类

}

  1. 核心系统异常 (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

二、 详细异常类定义

  1. 基础接口:错误码接口 (IErrorCode)
    异常不仅仅是抛出 Message,更重要的是抛出 ErrorCode(便于前端展示和监控统计)。

public interface IErrorCode {

String getCode(); // 错误码,如 "A0001"

String getMessage(); // 错误描述,如 "参数校验失败"

}

  1. 根异常 (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...

}

  1. 核心业务异常 (BusinessException)

这是你代码里出现频率最高的异常。

场景:用户操作不当、违反业务规则。

特点:不需要打 Error 日志,不扣系统可用率。

public class BusinessException extends BaseException {

// 构造方法参照父类

}

  1. 核心系统异常 (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

这套体系既能区分业务与系统故障,又避免了创建几百个异常类的繁琐,是目前互联网大厂中最主流的实践。

相关推荐
黎雁·泠崖2 小时前
Java面向对象:对象内存图+成员与局部变量
java·开发语言
sunfove2 小时前
实战篇:用 Python 徒手实现模拟退火算法解决 TSP 问题
开发语言·python·模拟退火算法
jiunian_cn2 小时前
【C++】IO流
开发语言·c++
froginwe112 小时前
C 语言输入与输出详解
开发语言
_童年的回忆_3 小时前
【PHP】关于守护进程报错:SQLSTATE[HY000]: General error: 2006 MySQL server has gone away
开发语言·oracle·php
我是菜鸟0713号3 小时前
Qt + Python 算法集成的一种低耦合实践:FastAPI 服务化方案
python·qt·fastapi
少林码僧3 小时前
2.30 传统行业预测神器:为什么GBDT系列算法在企业中最受欢迎
开发语言·人工智能·算法·机器学习·ai·数据分析
CoderCodingNo3 小时前
【GESP】C++六级考试大纲知识点梳理, (7) 栈与队列
开发语言·c++