前言
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家:https://www.captainbed.cn/z
文章目录
1. 错误场景复现
场景1:吞掉异常(Silent Swallow)
public void processOrder(Order order) {
try {
validate(order);
saveToDatabase(order);
} catch (Exception e) {
// 吞掉异常,无日志、无处理
}
}
后果:订单处理失败时无任何痕迹,导致后续流程数据错乱且难以排查。
场景2:捕获过于宽泛的异常
try {
config = loadConfig(); // 可能抛出IOException
startService(config); // 可能抛出IllegalStateException
} catch (Exception e) { // 捕获所有异常
if (e instanceof IOException) {
log.error("配置加载失败");
}
// 但IllegalStateException未被处理!
}
隐患:将非受检异常(如空指针)与业务异常混为一谈,掩盖核心问题。
场景3:重复日志与堆栈轰炸
try {
parseJson(data);
} catch (JsonException e) {
log.error("JSON解析失败: " + e.getMessage()); // 记录1次
throw new ServiceException("数据处理失败", e); // 上层又记录堆栈
}
// 上层调用
try {
service.process(data);
} catch (ServiceException e) {
log.error("业务处理失败", e); // 堆栈重复打印
}
问题:同一异常链被多次记录,日志文件体积暴涨,关键信息被淹没。
2. 原理解析
异常处理的核心原则
- 明确性:异常应反映具体问题(如
NetworkException
vsDatabaseException
) - 完整性:保留原始异常信息(cause链传递)
- 适度性:在能处理的层级捕获,否则抛出
Checked vs Unchecked异常
Checked Exception | Unchecked Exception | |
---|---|---|
继承关系 | 继承自Exception | 继承自RuntimeException |
处理要求 | 必须显式捕获或声明抛出 | 可选择性处理 |
适用场景 | 可预见的业务错误(如参数校验) | 系统级错误(如NPE、越界) |
日志滥用引发的性能问题
- 同步写入磁盘:每个
log.error()
触发I/O操作 - 堆栈深度:打印堆栈需遍历线程栈,消耗CPU
- 日志序列化:异常对象转换为字符串的计算开销
3. 正确解决方案
方案1:精准捕获 + 异常传递
public void processFile(String path) {
try {
readContent(path);
} catch (FileNotFoundException e) { // 精确捕获
log.error("文件不存在: {}", path);
throw new BusinessException("FILE_MISSING", "文件未找到", e); // 封装并传递
} catch (IOException e) { // 较宽泛但必要
log.error("文件读取失败", e);
throw new BusinessException("IO_ERROR", "系统错误", e);
}
}
要点:
- 优先捕获具体异常类型
- 自定义业务异常携带错误码和上下文
- 始终传递原始异常(
e
作为cause)
方案2:全局异常处理器(Spring示例)
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理业务异常(返回结构化的错误信息)
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException e) {
return ResponseEntity.status(500)
.body(new ErrorResponse(e.getCode(), e.getMessage()));
}
// 兜底处理未捕获异常(记录堆栈,但屏蔽细节)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpectedException(Exception e) {
log.error("系统未知错误", e); // 仅在此处记录完整堆栈
return ResponseEntity.status(500)
.body(new ErrorResponse("SYSTEM_ERROR", "服务器繁忙"));
}
}
优势:
- 避免Controller层重复try-catch
- 统一异常响应格式
- 敏感信息过滤(如不返回SQL异常给前端)
方案3:日志优化策略
try {
riskyOperation();
} catch (SpecificException e) {
// 正确做法:携带上下文 + 仅记录一次堆栈
log.error("操作失败,ID={}, 原因={}", id, e.getMessage(), e); // 最后一个参数传递异常对象
// 避免冗余信息:
// 错误示例:log.error("失败:" + e); (字符串拼接消耗性能)
}
日志规范:
- 使用占位符(
{}
)而非字符串拼接 - 仅在最外层记录完整堆栈
- 敏感信息脱敏(如手机号、密码)
4. 工具与最佳实践
静态分析工具
-
SonarQube规则:
"java:S1166"
- 异常应携带详细信息"java:S2221"
- 避免捕获Throwable"java:S1452"
- 禁止暴露泛型异常
-
IntelliJ插件:
@Nonnull
/@Nullable
注解辅助空安全- 自动检测未被处理的异常
自定义异常设计模板
public class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String errorCode, String message, Throwable cause) {
super(message, cause); // 传递cause链
this.errorCode = errorCode;
}
// 生成标准化错误响应
public ErrorResponse toResponse() {
return new ErrorResponse(errorCode, getMessage());
}
}
扩展建议:
- 错误码分类:
A-BB-CCC
格式(A: 系统, BB: 模块, CCC: 具体错误) - 自动生成文档:通过注解关联错误码与解决方案
5. Code Review检查清单
检查项 | 正确做法 |
---|---|
是否捕获Throwable? | 禁止!可能捕获Error(如OOM),导致JVM无法正常恢复 |
是否在合适层级处理异常? | DAO层抛异常,Service层转换,Controller层处理 |
日志是否重复记录堆栈? | 确保异常链中每个堆栈只打印一次 |
自定义异常是否携带错误码? | 便于前端和监控系统分类处理 |
6. 真实案例
某支付系统在资金划转时捕获异常但未记录:
try {
account.transfer(amount);
} catch (InsufficientBalanceException e) {
// 无日志、无处理
}
后果:用户余额充足却提示扣款失败,客诉量激增。
分析:底层抛出的是NetworkException
,但被宽泛的catch (Exception e)
拦截后未记录,误判为余额不足。
修复:
- 引入自定义异常
PaymentException
,区分业务与系统异常 - 关键路径添加审计日志(含请求ID、账户、金额)
- 配置ELK日志系统实时报警
总结
- 绝不吞掉异常:至少记录日志,必要时传递
- 精准捕获:避免
catch (Exception e)
的懒惰写法 - 日志是黄金:在关键位置记录上下文,但避免冗余
- 统一管理:全局处理器 + 自定义异常 = 优雅的错误交互
下期预告:《并发编程初学者的噩梦:线程安全与锁机制误区》——从死锁到数据竞争,彻底拆解多线程开发的隐蔽陷阱。
联系作者
职场经验分享,Java面试,简历修改,求职辅导尽在科技泡泡
思维导图面试视频合集