学习笔记-异常
一、 异常顶层架构与生命周期 (底层逻辑)
Java 异常体系的根节点是 Throwable,由此向下衍生出两条泾渭分明的支线:Error 和 Exception。区分它们的核心在于:谁该为这个报错负责,以及系统还能不能活。
1. Error (系统级灾难)
代表 JVM 层面的严重故障,应用程序原则上不应该也无法捕获。系统是否会彻底宕机,取决于 Error 消耗的是私有资源还是公共资源。
-
StackOverflowError (栈溢出): 通常由无限递归引起。因为栈内存是线程私有的,所以仅会导致当前子线程崩溃,主线程和其他工作线程依然存活。(实战启示:高风险/深调用的操作可丢入独立线程池,防止主业务挂掉。 ) -
OutOfMemoryError (OOM 堆溢出): 因为堆内存是所有线程共享的,一旦内存耗尽爆发 OOM,意味着全局资源枯竭,整个应用程序会立刻宕机,属于 P0 级致命故障。
2. Exception (程序级异常)
这是我们在日常编码中需要打交道的主力军。
RuntimeException (非受检异常/运行时异常): 如
NullPointerException (空指针)、IndexOutOfBoundsException(数组越界)。- 本质: 程序员的代码逻辑存在 Bug。
- 处理原则: 编译器不强制检查(不报红)。绝不能用
try-catch 去兜底,而必须在代码逻辑层面通过前置判断(如检查边界、判空)来彻底规避。
Checked Exception (受检异常): 如
IOException、SQLException。- 本质: 外部环境不可控因素导致的预期内异常。
- 处理原则: 编译器强制要求检查(报红)。必须通过
try-catch 捕获处理,或者通过throws在方法签名上显式声明,将处理责任上抛。
二、 核心实战规范 (团队协作底线)
在企业级开发中,不规范的异常处理会让日志成为灾难,导致线上 Bug 难以排查。必须严守以下“军规”:
- 🚫 严禁异常控制业务流程: 不要为了终止
for循环而故意触发数组越界并捕获,异常机制极其消耗性能,只能用于处理“非正常情况”。 - 🚫 严禁吞掉异常:
catch 块绝对不能留空!至少要输出完整的日志(包括当前方法、入参、异常堆栈信息e.printStackTrace()转化为日志形式)。 - 🚫 严禁“既打日志又 throw”: 捕获异常后,要么内部消化掉(打日志记录),要么封装后继续往上抛。两者同时做会导致在不同调用层级打印出多条一模一样的错误日志,造成日志污染。
- 🚫 严禁在
finally 中使用 return : 会覆盖掉try块中的正常返回值或异常信息,导致排查方向完全错误。 - ✅ 异常捕获粒度要细: 针对不同的代码逻辑,分别捕获具体的异常(如
TimeoutException、NumberFormatException),坚决抵制用一个顶级的catch (Exception e)把上千行代码全部包住。
三、 优雅编码与工程化落地 (高级感体现)
高级开发人员不仅追求功能跑通,更追求代码的健壮性和优雅度。
1. 彻底消灭满屏的判空 (NPE 防御)
放弃传统的 if (obj != null) 嵌套,全面拥抱 JDK 8 新特性 Optional。
- 最佳实践: 使用
Optional.ofNullable().map().orElse() 或orElseThrow()。 - 优势: 实现安全的链式调用。如果对象为空,可以直接赋予默认值,或者抛出带有明确语义的自定义业务异常。
2. 资源自动释放
在处理文件 IO 或数据库连接时,放弃在 finally 块中手动写冗长的 close() 判断。
- 最佳实践: 使用 JDK 7 新特性
try-with-resources。 - 优势: 只要是实现了
AutoCloseable接口的资源,代码块执行完毕后会自动且安全地释放,避免资源泄露。
3. API 业务异常的统一化设计
面对系统正常运行但业务逻辑不符的情况(例如:根据 ID 查不到部门、IP 异常高频攻击接口),我们需要构建自定义的异常体系。
- 不跨层传递: 尽量在 Service 层或适配层消化掉底层的技术异常,向上封装为业务异常。
- 枚举 + Error Code: 配合枚举类使用。统一定义错误码(Error Code)和错误信息。
- 大数据赋能: 通过 Flink 等实时计算组件,可以监听日志中特定的 Error Code 频率。一旦某类业务异常(如频繁密码错误)触发阈值,可以直接推送到监控大屏并联动黑名单机制进行自动封禁。
阿里巴巴的规范
【强制】 Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
- 说明: 无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过 catch NumberFormatException 来实现。
- 正例:
if (obj != null) {...} - 反例:
try { obj.method(); } catch (NullPointerException e) {…} - 我的理解简单来说就是:如果有一个对象可能是空指针,那我们就用 Optional 去处理一下,而不是用 try-catch 去处理这种问题。
【强制】 异常不要用来做流程控制,条件控制。
- 说明: 异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
【强制】 catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
说明: 对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。
正例: 用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
这里是有 best practice 的,例如我们处理一个业务逻辑的时候:
- 首先 validate,我们可以单独写一个方法。
- 每遇到一段不稳定的代码,我们可以进行一次try catch;如果是本地处理,那可以不用加 try-catch。
- 有替代方案/降级方案 比如某个本地解析失败了,你可以走默认值。这里的本地方法也是可以使用 try catch 的
【强制】 捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
- 很经典的一个就是把这个给 print 出来了,但没有任何作用,完全是初学者的代码。e.printStackTrace();
【强制】 有 try 块放到了事务代码中,catch 异常后,如果需要回滚事务,一定要注意手动回滚事务。
- 事务代码通常是将事务丢给代理对象去进行异常的 try-catch 回滚的。而如果我们在代码里面提前进行了 try-catch,外部那一层隐藏的 回滚代码 根本不知道里面的异常已经被处理掉了,就会导致事务的回滚代码失效。
【强制】 finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。
- 说明: 如果 JDK7 及以上,可以使用 try-with-resources 方式。
【强制】 不要在 finally 块中使用 return。
- 说明: try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在 return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点。
【强制】 捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
- 说明: 如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
【强制】 在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable 类来进行拦截。
- 说明: 通过反射机制来调用方法,如果找不到方法,抛出 NoSuchMethodException。什么情况会抛出 NoSuchMethodError 呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出 NoSuchMethodError。
- 这个在 ATS 项目里遇到过。就是开了一个子程序去调用 C++ 的方法,偶尔会出现一些无法处理的异常,而这个时候就需要用 Throwable类 去拦截这个异常。
【推荐】 方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。
- 说明: 本手册明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。
【推荐】 防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。反例:
public int f() { return Integer对象; }, 如果为 null,自动解箱抛 NPE。- 数据库列非 null → 可以用
Integer 或int,但很多 ORM 场景里还是常见Integer,一般来说包装类型通常更稳妥 - DTO / 请求对象里:大多数可选字段用 Integer(避免产生说不清的脏数据,int 默认是0);外部返回的 Integer 禁止直接参与计算,先判空再计算
-
Integer 用来接不确定,int 用来处理确定。
- 数据库列非 null → 可以用
数据库的查询结果可能为 null。
集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
级联调用
obj.getA().getB().getC();一连串调用,易产生 NPE。正例: 使用 JDK8 的 Optional 类来防止 NPE 问题。
【推荐】 定义时区分 unchecked / checked 异常,避免直接抛出
new RuntimeException(),更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException 等。【参考】 对于公司外的 http/api 开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封装
isSuccess()方法、“错误码”、“错误简短信息”。说明: 关于 RPC 方法返回方式使用 Result 方式的理由:
- 使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
- 如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
【参考】 避免出现重复的代码(Don't Repeat Yourself),即 DRY 原则。
- 说明: 随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。
- 正例: 一个类中有多个 public 方法,都需要进行数行相同的参数校验操作,这个时候请抽取
- 所以后面我写代码的时候,尽量能做成公共就做成公共,因为很多校验在很多场景下都是通用的。
异常提示词
# 🤖 AI Agent 代码生成规范:Java 异常处理与防御性编程
**【指令背景】**
作为代码生成与审查 Agent,你必须克服大语言模型(LLM)在生成 Java 代码时常见的“偷懒”和“上下文遗忘”缺陷。本规范直接针对 AI 易犯的低级错误(如过度捕获、异常吞噬、遗漏回滚等)设定强制约束。在输出代码前,严格按此规范进行自检。
---
## ⚠️ 一、 规范豁免与特殊声明机制
**【强制声明】**
如果在生成或审查代码时,遇到**必须打破本规范**的特殊实现(例如:为了兼容老旧历史系统、处理特定底层框架的反射机制、极端性能优化等),Agent 必须严格执行以下两步:
1. **主动分析与告知:** 在输出代码之前,必须单独向用户明确指出打破了哪条规范,并详细分析必须这样写的底层逻辑与原因。
2. **强制增加特殊注释:** 如果确认该违规实现的合理性,必须在生成的对应代码上方添加醒目的特殊说明注释,否则一律视为生成不合格。
- *正例参考注释:* `// 特殊说明:此处因对接老旧且无错误码的第三方 SOAP 接口,无法精确捕获业务异常,故暂不遵守【严禁宽泛捕获】规范。`
---
## 🚫 二、 严禁大模型常见的“偷懒”模式(反模式)
1. **【强制拦截】严禁宽泛捕获(Lazy Catching):**
- **AI 易犯错误:** 喜欢无脑生成 `catch (Exception e)` 或 `catch (Throwable t)`。
- **规范:** 必须捕获能精确推断的具体异常类型(如 `IOException`、`TimeoutException`)。仅在全局异常处理器(Global Exception Handler)中才允许捕获顶级 `Exception`。
2. **【强制拦截】严禁吞噬异常(Exception Swallowing):**
- **AI 易犯错误:** 生成 `catch (Exception e) { log.error(e.getMessage()); }`,导致堆栈丢失且业务静默失败。
- **规范:** 捕获异常后,要么**包含完整堆栈**打印并中止当前流程,要么将其包装为自定义业务异常向上抛出。严禁只打印 message。
- **正例:** `log.error("执行失败,参数: {}", param, e); throw new BizException(ErrorCode.SYSTEM_ERROR, e);`
3. **【强制拦截】严禁忽略 Spring 事务上下文:**
- **AI 易犯错误:** 在 `@Transactional` 标注的方法内使用 `try-catch` 消化了异常,导致事务未能按预期回滚。
- **规范:** 若捕获异常但不向外抛出,必须手动标记事务回滚:`TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();`。
4. **【强制拦截】严禁在 finally 中 return:**
- **规范:** 绝对禁止在 `finally` 块中生成 `return` 语句,这会导致抛出的异常被强行抹除。
---
## 🛡️ 三、 强制的防御性编程范式(AI 生成标准)
1. **【强制范式】彻底杜绝级联 NPE(空指针防御):**
- **AI 易犯错误:** 生成缺乏安全检查的级联调用 `a.getB().getC()`,或生成冗长陈旧的 `if (a != null)` 嵌套。
- **规范:** 面对可能为空的对象,必须强制使用 `Optional` 进行链式处理。
- **正例:** `Optional.ofNullable(dto).map(DTO::getDetail).orElseThrow(() -> new BizException(ErrorCode.DATA_NOT_FOUND));`
2. **【强制范式】资源流自动化管理:**
- **AI 易犯错误:** 在 `finally` 中手写臃肿的 `if (in != null) in.close();`,且容易遗漏报错处理。
- **规范:** 所有实现了 `AutoCloseable` 的资源(流、连接、Socket),必须且只能使用 **`try-with-resources`** 语法块生成。
3. **【强制范式】精准抛出业务异常:**
- **AI 易犯错误:** 遇到业务校验失败时,随意生成 `throw new RuntimeException("参数错误")`。
- **规范:** 业务阻断必须使用指定的业务异常类(如 `BizException`),并强制传入对应的枚举错误码(Error Code)。严禁硬编码中文错误信息。
---
## ⚙️ 四、 接口与 RPC 边界规范
1. **【规范】RPC 调用结果的强制校验:**
- **AI 易犯错误:** 盲目信任 RPC/外部 HTTP 接口的返回对象,直接进行属性提取。
- **规范:** 接收远程调用返回的 `Result<T>` 或 `Response` 对象后,第一行代码必须校验 `isSuccess()` 或响应状态码,失败时立即转化为当前系统的业务异常抛出。
2. **【规范】反射与代理环境的异常拦截:**
- 仅在动态代理、反射调用(如 `Method.invoke()`)等特定场景下,生成的代码必须捕获 `InvocationTargetException` 并通过 `getTargetException()` 提取真实异常,防止真实业务错误被隐藏。
---
**【System Check】** Agent 在输出最终 Java 代码前,必须检查代码树中是否包含违规反模式代码。如存在,立即内部重试并修正。对于确需打破常规的实现,检查是否已主动分析告知,并附带了合规的“特殊说明”注释。
