1. 异常类型与分类
1.1 检查型异常与运行时异常
在 Java 中,异常分为检查型异常和运行时异常,前者需要在方法签名中通过 throws 声明,调用方必须处理或继续抛出;后者为运行时异常,通常用于编程错误或不可预期的条件,编译器不强制处理。理解这两类异常的差异有助于设计更清晰的错误处理路径。
例子展示:IOException是检查型异常,需要 catch 或声明;NullPointerException、IllegalArgumentException 属于运行时异常,往往指出代码逻辑缺陷。下面给出具体示例。
public void readFile(String path) throws IOException {try (BufferedReader br = new BufferedReader(new FileReader(path))) {String line = br.readLine();// 处理...}
}1.2 自定义异常设计原则
自定义异常应具备清晰的意义、适当的层级、以及可扩展性。理解业务语义与技术异常的分离,通过统一的异常层次来传递错误信息。
常见做法是创建一个基本自定义异常类,如 AppException,再派生出具体领域的异常,如 DataNotFoundException、ValidationException 等。
public class AppException extends RuntimeException {private final int errorCode;public AppException(int errorCode, String message) {super(message);this.errorCode = errorCode;}public int getErrorCode() { return errorCode; }
}1.3 常见错误类型
在日常开发中,常见的错误类型包括空指针、越界、类型转换异常等。通过梳理错误类型和原因,可以提早设置防御性检查。
比如对外部输入应有边界校验,对集合操作应注意空集合与空引用的差异。
2. 排查异常的系统化流程
2.1 栈轨迹的解读要点
堆栈轨迹揭示了异常发生的调用链路,定位的第一步是关注最外层异常的类型和消息。核心信息通常来自首个抛出的异常,后续的 caused by 可能提供深层原因。
在实际排查中,结合日志时间线和相关调用栈,可以快速缩小范围。
try {// 业务逻辑
} catch (IOException e) {// 记录日志并重新抛出throw e;
}2.2 日志策略与异常链分析
良好的日志包括时间、线程、错误码、堆栈与消息。保持异常链完整性,便于后续分析。
避免只记录简单文本,应在日志中保留关键字段以辅助排错。
catch (DataAccessException e) {logger.error("DB failure at {}: {}", Instant.now(), e.getMessage(), e);throw new AppException(1001, "数据库访问失败", e);
}2.3 使用断言和预检减小异常发生
在边界情况下使用简单的前置条件检查,代替让异常在深层抛出。断言与前置校验是排错的前线武器。
例如在方法入口对参数进行合理性检查,能显著降低后续异常。
3. 常用的异常处理技巧
3.1 try-catch 与多捕获
Java 7 引入的多捕获允许在同一个 catch 中处理多种类型的异常,避免重复代码。使用多捕获可以统一处理策略,并提升可读性。
示例展示了对 IO、SQL 异常在同一分支处理日志和抛出自定义异常。
try (Connection conn = dataSource.getConnection()) {// 数据操作
} catch (IOException | SQLException ex) {logger.error("Operation failed: {}", ex.toString(), ex);throw new AppException(1002, "操作失败", ex);
}3.2 资源管理与 try-with-resources
通过 try-with-resources,自动关闭实现了 AutoCloseable 的资源,避免资源泄露。这是一项关键的异常防御机制。
将释放资源的代码从 finally 中转移到 try-with-resources,减少错误点。
try (BufferedReader br = new BufferedReader(new FileReader(path))) {String line;while ((line = br.readLine()) != null) {// 处理}
} catch (IOException e) {throw new AppException(1003, "读取文件失败", e);
}3.3 异常链与重新抛出
保留原始异常的原因是诊断问题的关键。在重新抛出时附带 cause,能够避免信息丢失。
推荐做法是在新异常中包含原始异常作为 cause。
catch (SQLException e) {throw new DataAccessException("Query failed", e);
}3.4 自定义错误码与消息
结合统一的错误码系统,可将客户端的错误处理从文本消息中解耦。错误码提升了跨系统的可观测性。
示例:定义错误码枚举,结合异常携带。
public enum ErrorCode {DB_FAILURE(1001),INVALID_INPUT(1004);private final int code;ErrorCode(int code) { this.code = code; }public int code() { return code; }
}3.5 避免吞掉异常与忽略中断
空的 catch 块或对 InterruptedException 的错误处理,会隐藏真实状态。总是对异常有意义的处理路径。
对于中断应恢复线程中断状态,必要时传播中断信号。
4. 常见错误与坑点
4.1 捕获过度泛化
使用 catch (Exception e) 或 Catch-all 的做法可能掩盖不可预见的错误,导致日志中无重点信息。限定捕获范围有助于快速定位。
适当时将异常重新抛出或转换为业务异常,以保持语义清晰。
4.2 空的 catch 块与吞噬异常
空的 catch 块会让故障隐匿,应用状态可能在后续阶段崩溃。务必记录日志并处理,或将异常向上抛出。

try {// 操作
} catch (IOException e) {// 仅吞掉
}4.3 finally 中抛出新异常覆盖原始异常
在 finally 中抛出异常会覆盖原来正在传播的异常,导致诊断困难。优先在 try 块内处理/抛出,最终在 finally 最小化逻辑。
4.4 中断吞并与忽略线程中断
忽略 InterruptedException 会破坏线程的中断机制,影响上层调度与协作。应保存并重新中断或合理处理。
4.5 异步场景中的异常处理
任务在未来执行时抛出的异常,需要被显式捕获与汇报。使用 CompletableFuture 的异常处理方法,避免静默失败。
CompletableFuture.supplyAsync(() -> {// 任务return compute();
}).exceptionally(ex -> {logger.error("Async task failed", ex);return null;
});5. 最佳实践与设计模式
5.1 统一异常层次结构
打造统一的异常基类和分层结构,确保错误从业务逻辑层到表现层的传递一致性。清晰的层次结构便于维护与扩展。
示例:在数据访问、服务、控制层各自自定义异常。
// Service层
public class UserService {public User getUser(String id) {//...throw new DataNotFoundException(id);}
}5.2 服务端错误响应设计
服务端返回的错误响应应包含错误码、消息、以及可选的字段信息,便于前端解析并给出友好提示。避免暴露内部实现细节。
前后端契合的错误格式提升了系统可用性。
5.3 异常监控与告警
将异常事件聚合到集中日志和监控系统,支持告警阈值、趋势分析等。观察驱动的告警机制,提高运维效率。
// 日志示例
logger.error("Service failed with code {}: {}", errCode, message, e);
5.4 单元测试与异常路径覆盖
测试异常路径是确保健壮性的关键。通过断言异常类型和异常信息来验证行为。覆盖边界与失败场景。
@Test
void testGetUser_NotFound() {when(repo.findById("123")).thenReturn(Optional.empty());assertThrows(DataNotFoundException.class, () -> service.getUser("123"));
} 

