1. 概述
本文深入探讨 Java 中的全局异常处理机制。我们会先回顾异常的基本概念和处理方式,然后重点分析如何通过 UncaughtExceptionHandler
实现全局异常捕获。
对于异常处理的更多基础知识,可参考 Java 异常处理详解。
如果你在项目中经常遇到线上报一堆堆栈却不知道从哪下手,或者想统一收集未捕获异常做监控报警——那这篇文章就是为你准备的。✅
2. 什么是异常?
异常是在程序运行或编译期间发生的非正常状态,通常是由于代码违反了 Java 语言的语义规则所导致。
Java 中的异常主要分为两类:
- ✅ 检查异常(Checked Exceptions):继承自
Exception
类但非RuntimeException
子类。编译器强制要求必须处理,否则无法通过编译。 - ✅ 非检查异常(Unchecked Exceptions):即运行时异常,继承自
RuntimeException
。编译器不强制捕获或声明,容易被忽略,也是我们踩坑的高发区 ❗
⚠️ 虽然运行时异常不需要显式处理,但这不代表它们不重要。相反,很多生产问题都源于未妥善处理的
NullPointerException
、ArithmeticException
等。
3. 异常处理机制
Java 的健壮性很大程度上得益于其完善的异常处理框架。它允许程序在出错时优雅降级,而不是直接崩溃退出。
当异常发生时,JVM 或当前执行的方法会创建一个 Exception
对象,其中包含了异常类型、堆栈信息等关键数据。我们的任务就是“接住”这个对象并决定如何应对。
3.1 try-catch 块
最基础的异常捕获方式是使用 try-catch
:
String string = "01, , 2010";
DateFormat format = new SimpleDateFormat("MM, dd, yyyy");
Date date;
try {
date = format.parse(string);
} catch (ParseException e) {
System.out.println("ParseException caught!");
}
上面这段代码尝试解析一个格式错误的日期字符串,parse()
方法会抛出 ParseException
,我们用 catch
捕获并打印提示。
这是局部处理的经典模式,适用于你能预知异常场景的情况。
3.2 throw 与 throws 关键字
有时候你不打算自己处理异常,而是选择向上抛出,交由调用方处理:
public class ExceptionHandler {
public static void main(String[] args) {
String strDate = "01, , 2010";
String dateFormat = "MM, dd, yyyy";
try {
Date date = new DateParser().getParsedDate(strDate, dateFormat);
} catch (ParseException e) {
System.out.println("The calling method caught ParseException!");
}
}
}
class DateParser {
public Date getParsedDate(String strDate, String dateFormat) throws ParseException {
DateFormat format = new SimpleDateFormat(dateFormat);
try {
return format.parse(strDate);
} catch (ParseException parseException) {
throw parseException;
}
}
}
这里 getParsedDate()
方法声明了 throws ParseException
,表示它可能抛出该异常。main()
方法作为调用者负责捕获处理。
这种方式适合分层架构中将异常传递到更高层统一处理的场景。
4. 全局异常处理器
尽管我们可以用 try-catch
处理已知异常,但总有漏网之鱼——尤其是那些未被捕获的运行时异常。这时候如果程序直接崩掉,用户体验极差,也难以定位问题。
为此,Java 提供了 Thread.UncaughtExceptionHandler
接口,用于捕获线程中未处理的异常。
核心机制
✅
Thread.UncaughtExceptionHandler
是一个函数式接口,定义了一个方法:void uncaughtException(Thread t, Throwable e)
✅ Java 5 起,
Thread
类提供了静态方法设置全局处理器:public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh)
✅ 当某个线程因未捕获异常而终止时,JVM 会自动调用该处理器的
uncaughtException()
方法
实战示例
下面是一个典型的全局异常处理器实现:
public class GlobalExceptionHandler {
public static void main(String[] args) {
Handler globalExceptionHandler = new Handler();
Thread.setDefaultUncaughtExceptionHandler(globalExceptionHandler);
new GlobalExceptionHandler().performArithmeticOperation(10, 0);
}
public int performArithmeticOperation(int num1, int num2) {
return num1 / num2; // 会抛出 ArithmeticException
}
}
class Handler implements Thread.UncaughtExceptionHandler {
private static Logger LOGGER = LoggerFactory.getLogger(Handler.class);
public void uncaughtException(Thread t, Throwable e) {
LOGGER.info("Unhandled exception caught in thread: " + t.getName(), e);
// 可以在这里做日志记录、上报监控系统、发送告警邮件等
}
}
输出结果类似:
INFO Handler - Unhandled exception caught in thread: main
java.lang.ArithmeticException: / by zero
at com.example.GlobalExceptionHandler.performArithmeticOperation(...)
at com.example.GlobalExceptionHandler.main(...)
也能处理检查异常?
你可能会问:全局处理器不是只处理运行时异常吗?其实不然。
只要异常最终没有被 catch
,即使它是检查异常(如 IOException
),也会被 UncaughtExceptionHandler
捕获:
public static void main(String[] args) throws Exception {
Handler globalExceptionHandler = new Handler();
Thread.setDefaultUncaughtExceptionHandler(globalExceptionHandler);
Path file = Paths.get("/nonexistent/file.txt");
Files.delete(file); // 抛出 IOException
}
虽然 main
方法声明了 throws Exception
,但由于没有调用者处理,该异常最终仍被视为“未捕获”,从而触发全局处理器。
注意事项 ⚠️
- 全局异常处理器虽然强大,但它打破了“就近处理”的设计原则。
- 它更像是最后一道防线,而不是替代
try-catch
的方案。 - 不建议在处理器中做复杂逻辑(如网络请求),因为此时线程已处于异常状态,资源可能不可用。
5. 总结
本文系统介绍了 Java 中的异常分类、基本处理方式以及全局异常处理器的使用。
关键点回顾:
- ✅ 异常分 checked 和 unchecked 两类,后者更易被忽视但危害更大
- ✅
try-catch
和throws
是常规处理手段 - ✅
UncaughtExceptionHandler
是处理未捕获异常的终极武器 - ✅ 通过
Thread.setDefaultUncaughtExceptionHandler()
可设置全局处理器 - ✅ 即使是检查异常,若最终未被捕获,也能被全局处理器捕获
💡 实际项目中建议结合使用:业务层做好异常处理,同时配置全局处理器用于兜底日志记录和告警。
本文示例代码可在 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-exceptions-2