可通过替换System.out/err为自定义PrintStream来安全捕获静默异常:在println(Object)中判断Throwable并结构化记录,在println(String)中关键词兜底;需缓存原始流、保障线程安全、异步上报,并配合UncaughtExceptionHandler与日志框架形成防御纵深。

不能直接“重写”System类(它是final且由JVM硬编码管理的),但可以通过替换System.out和System.err对应的PrintStream,来**拦截并安全捕获原本会被吞没的异常输出**——尤其是那些因日志、调试或遗留代码中未显式处理而被静默丢弃的异常堆栈。
用自定义PrintStream包装标准输出流
核心思路是创建一个继承PrintStream的子类,在println(String)、print(Object)等关键方法中识别异常对象或堆栈字符串,再将其转为结构化日志或抛出可追踪的警告。
- 不要覆盖
write(byte[])等底层方法——容易破坏格式,优先拦截高层语义方法 - 在
println(Object obj)中判断obj instanceof Throwable,若成立则记录完整堆栈并触发告警(如发送到监控系统或写入专用错误日志文件) - 对
println(String s)做简单关键词检测(如包含"Exception"、"Caused by:"、"at "且含类名),作为兜底策略
替换前保存原始流并确保线程安全
调用System.setOut()或System.setErr()前,务必先缓存原始流,以便在拦截逻辑中仍能转发非异常内容;同时注意多线程环境下PrintStream本身是线程安全的,但你的包装逻辑若涉及共享状态(如计数器、日志队列),需加锁或用线程安全容器。
- 使用
final PrintStream originalOut = System.out;保存引用,避免循环代理 - 在自定义
PrintStream构造时传入originalOut,仅在非异常路径下调用originalOut.println(...) - 若需异步上报异常,用
ExecutorService提交任务,避免阻塞控制台输出线程
配合安全管理器与日志框架增强可靠性
仅靠重定向out/err无法捕获所有被动吞没场景(例如catch (Exception e) { }空块)。需结合其他机制形成防御纵深:
- 启用
SecurityManager(Java 17前)限制RuntimePermission("setSecurityManager"),防止恶意代码恢复原始流 - 在应用启动时,用
Thread.setDefaultUncaughtExceptionHandler兜底捕获未处理异常 - 将捕获到的异常统一桥接到SLF4J或Log4j,利用MDC注入请求ID、线程名等上下文,避免信息孤岛
验证与灰度上线建议
上线前必须验证拦截逻辑不影响正常业务输出(如JSON日志、进度条、ANSI颜色码),尤其注意字符编码、换行符兼容性及缓冲区刷新行为。
- 编写单元测试:构造
Throwable实例调用yourPrintStream.println(t),断言是否触发预期日志/告警 - 灰度阶段开启采样开关(如只拦截1%的异常输出),观察性能损耗与误报率
- 禁止在生产环境直接
System.setErr(new YourPrintStream(System.err))而不设熔断——万一包装逻辑抛异常会导致死循环

















