1. 异常处理基础认知
刚接触Java那会儿,我最怕在控制台看到红色异常堆栈。直到有次线上服务因为NullPointerException崩溃,才真正理解异常处理不是应付编译器的形式主义,而是保障程序健壮性的生命线。Java异常体系就像程序世界的免疫系统,我们需要学会识别不同类型的"病原体",并建立相应的防御机制。
所有异常都继承自Throwable类,其下分为Error和Exception两大分支。Error代表JVM无法处理的致命问题(如OutOfMemoryError),通常我们无能为力;而Exception才是我们需要重点关注的业务异常。Exception又分为检查型异常(Checked Exception)和非检查型异常(Unchecked Exception),这个设计体现了Java"强制安全"的哲学:
java复制// 检查型异常示例 - 必须处理
try {
Files.readAllBytes(Paths.get("config.ini"));
} catch (IOException e) { // 必须捕获或声明抛出
logger.error("读取配置文件失败", e);
}
// 非检查型异常示例 - 可选择性处理
String[] arr = {"a", "b"};
System.out.println(arr[2]); // 运行时可能抛出ArrayIndexOutOfBoundsException
关键认知:检查型异常强制处理的设计常引发争议。我的经验法则是——如果调用者能合理恢复的异常就定义为检查型(如文件不存在),而编程错误(如空指针)应该用非检查型异常快速暴露问题。
2. 异常处理实战策略
2.1 try-catch-finally的正确姿势
新手常犯的错误是过度捕获异常。我曾见过这样的代码:
java复制try {
// 几十行业务代码
} catch (Exception e) {
e.printStackTrace();
}
这种"一网打尽"式的捕获会掩盖真正的问题。更合理的做法是:
- 按异常类型精细捕获,从具体到抽象排序
- 永远保留原始异常堆栈(使用带cause参数的构造器)
- finally块只放资源清理代码,不要包含可能抛出异常的语句
java复制Connection conn = null;
try {
conn = dataSource.getConnection();
// 执行SQL...
} catch (SQLException e) {
throw new ServiceException("数据库操作失败", e); // 保留原始异常
} finally {
if (conn != null) {
try {
conn.close(); // 关闭资源也可能抛出异常
} catch (SQLException e) {
logger.warn("关闭连接失败", e);
}
}
}
2.2 异常封装的艺术
直接抛出底层异常会暴露实现细节。合理的封装应该:
- 定义业务相关的异常体系
- 包含足够的上下文信息
- 实现统一的错误码规范
java复制public class PaymentException extends RuntimeException {
private ErrorCode code;
public PaymentException(ErrorCode code, String message) {
super(message);
this.code = code;
}
// 使用示例
void processPayment() {
if (balance < amount) {
throw new PaymentException(ErrorCode.INSUFFICIENT_BALANCE,
"用户余额不足,当前余额:" + balance);
}
}
}
踩坑记录:曾经因为异常消息过于简单(如"操作失败"),导致线上问题排查耗时数小时。现在我会在异常中记录当时的关键参数值(注意脱敏)。
3. 性能与设计进阶
3.1 异常处理的性能陷阱
异常处理是有成本的。测试数据显示,创建异常对象比普通对象慢100倍以上。在性能敏感场景要注意:
- 避免在循环中使用异常做流程控制
- 预检查优于捕获异常(如先checkNotNull)
- 重用静态异常对象(仅适用于不可变异常)
java复制// 反面教材 - 用异常实现业务逻辑
try {
while (true) {
list.remove(0);
}
} catch (IndexOutOfBoundsException e) {
// 结束循环
}
// 正确做法
while (!list.isEmpty()) {
list.remove(0);
}
3.2 函数式编程中的异常处理
Lambda表达式中的检查型异常需要特殊处理。推荐方案:
- 使用ThrowingFunction等三方库
- 定义自己的函数式接口
- 将异常转换为非检查型异常
java复制// 自定义函数式接口
@FunctionalInterface
interface ThrowingFunction<T, R> {
R apply(T t) throws Exception;
}
// 使用示例
List<String> fileContents = files.stream()
.map(wrap(file -> Files.readString(file.toPath())))
.collect(Collectors.toList());
// 包装方法
static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> fn) {
return t -> {
try {
return fn.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
4. 生产环境最佳实践
4.1 异常日志规范
混乱的异常日志是排查问题的噩梦。我们团队现在遵守以下规则:
- 使用SLF4J的error(String msg, Throwable t)方法
- 在日志消息中包含业务标识(如订单ID)
- 避免重复记录相同异常
- 敏感信息脱敏处理
java复制try {
processOrder(order);
} catch (PaymentException e) {
// 错误示例:仅打印异常对象
// logger.error("支付失败: " + e);
// 正确示例
logger.error("订单[{}]支付失败,错误码:{}",
order.getId(), e.getCode(), e);
}
4.2 全局异常处理
Spring Boot项目中推荐使用@ControllerAdvice统一处理异常:
java复制@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(
BusinessException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(ex.getCode(), ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpectedException(
Exception ex) {
logger.error("系统异常", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("SYSTEM_ERROR", "系统繁忙"));
}
}
// 统一错误响应体
@Data
@AllArgsConstructor
class ErrorResponse {
private String code;
private String message;
private long timestamp = System.currentTimeMillis();
}
5. 疑难问题排查指南
5.1 异常消失之谜
有时候异常会神秘消失,常见原因包括:
- 线程池未捕获异常
- Future.get()未处理ExecutionException
- CompletableFuture未使用exceptionally处理
java复制// 线程池异常处理方案
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.submit(() -> {
try {
riskyOperation();
} catch (Exception e) {
logger.error("子任务执行失败", e);
throw e;
}
});
// CompletableFuture异常处理
CompletableFuture.runAsync(this::riskyOperation)
.exceptionally(ex -> {
logger.error("异步操作失败", ex);
return null;
});
5.2 堆栈信息丢失
当看到"Something went wrong"而堆栈只有一行时,可以:
- 添加JVM参数-XX:-OmitStackTraceInFastThrow
- 检查是否重复抛出相同异常(JIT会优化)
- 使用异常分析工具(如ErrorProne)
java复制// 重现堆栈丢失的示例
for (int i = 0; i < 100000; i++) {
try {
String s = null;
s.length();
} catch (NullPointerException e) {
// 前几次有完整堆栈,之后会被JIT优化
System.out.println(e.getStackTrace().length);
}
}
在项目实践中,我逐渐形成了自己的异常处理原则:对可预见的错误使用明确的错误码,对意外情况使用异常;永远假设调用者会忽略文档说明,在异常消息中包含足够上下文;重要的业务异常要有监控报警。这些经验帮助我们将生产环境的异常定位时间缩短了70%以上。