如何按需将程序控制台全量输出(含用户输入)追加写入日志文件
低侵入控制台全量日志捕获实现方案
需求梳理
要实现的能力边界如下:
- 自定义日志方法触发时,捕获触发节点前的所有控制台内容,以追加模式写入用户指定路径的文件,不得覆盖文件原有内容
- 严格按照控制台实际展示顺序记录两类内容:
- 程序运行产生的所有输出、提示消息
- 多轮控制台交互过程中的所有用户输入
- 保留原有控制台交互逻辑:键盘输入、控制台打印的正常行为不受影响
- 最大程度降低代码侵入性,避免在每一处输入、输出代码点位手动插入日志收集逻辑,降低维护成本
现有方案问题
- 已实现部分:通过重定向
System.out到字节输出流的方式,完成了程序输出内容的统一收集、按需落盘 - 待解决问题:未找到合理的用户输入记录方案,要求不破坏原有输入流逻辑,同时保证输入内容和输出内容的记录顺序与控制台实际顺序完全一致
- 初始方案缺陷:最早计划用全局
StringBuilder逐次追加所有输入输出内容,日志方法触发时落盘后清空缓存,但该方案需要在所有涉及打印、输入的代码位置手动追加收集逻辑,侵入性极强,后续代码迭代很容易漏加埋点,维护成本很高。
具体实现方案
核心思路是用装饰器模式分别包装标准输出流和标准输入流,在不改变原有流行为的前提下,把所有经过流的内容同步写入统一的日志缓存,不需要修改任何业务侧的输入输出代码。
1. 流包装核心逻辑
- 对标准输出流
System.out做分流包装:所有输出内容一边正常打印到控制台,一边同步写入全局日志缓存,替代原有重定向逻辑,避免重定向后控制台看不到输出的问题 - 对标准输入流
System.in做代理包装:所有读取操作先调用原始输入流的读取方法拿到用户输入的内容,一边正常返回给业务调用方,一边把读到的输入内容同步写入全局日志缓存,不需要重定向原始输入流,键盘交互逻辑完全不受影响
顺序一致性说明:程序输出内容在打印时写入缓存,用户输入内容在按回车被程序读取时写入缓存,这个时机和内容实际展示在控制台的时机完全对齐,不会出现顺序错乱的问题。
2. 日志落盘逻辑
日志方法触发时按以下流程处理:
- 提示用户选择日志文件保存路径
- 读取全局日志缓存中累计的所有内容,以追加写入模式打开目标文件,将缓存内容写入文件
- 写入完成后重置日志缓存,继续收集后续的控制台输入输出内容
- 多次调用日志方法时如果用户选择了同一个文件,直接追加新收集的内容即可,不会覆盖文件原有内容。
3. 核心实现代码
import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; public class ConsoleLogger { // 全局统一日志缓存 private static final ByteArrayOutputStream LOG_CACHE = new ByteArrayOutputStream(); private static boolean initialized = false; /** * 初始化流包装,程序启动时调用一次即可 */ public static void init() throws IOException { if (initialized) return; // 包装标准输出流:分流到控制台和日志缓存 OutputStream teeOutputStream = new FilterOutputStream(System.out) { @Override public void write(int b) throws IOException { super.write(b); LOG_CACHE.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { super.write(b, off, len); LOG_CACHE.write(b, off, len); } }; System.setOut(new PrintStream(teeOutputStream, true, "UTF-8")); // 包装标准输入流:读取输入时同步记录到缓存 InputStream teeInputStream = new FilterInputStream(System.in) { @Override public int read() throws IOException { int readByte = super.read(); if (readByte != -1) { LOG_CACHE.write(readByte); } return readByte; } @Override public int read(byte[] b, int off, int len) throws IOException { int readCount = super.read(b, off, len); if (readCount > 0) { LOG_CACHE.write(b, off, readCount); } return readCount; } }; System.setIn(teeInputStream); initialized = true; } /** * 日志保存方法,需要记录日志时调用 * @param targetPath 日志文件保存路径 */ public static synchronized void saveToFile(String targetPath) throws IOException { byte[] cachedContent = LOG_CACHE.toByteArray(); if (cachedContent.length == 0) return; // 追加模式写入,文件不存在则自动创建 Files.write(Paths.get(targetPath), cachedContent, StandardOpenOption.CREATE, StandardOpenOption.APPEND); // 写入完成后清空缓存,继续收集后续内容 LOG_CACHE.reset(); } }
方案优势
- 侵入性极低:只需要在程序启动入口调用一次
ConsoleLogger.init(),所有业务代码里的System.out.println、Scanner/BufferedReader读取控制台输入的逻辑完全不需要修改 - 顺序准确:所有内容写入缓存的时机和控制台实际展示时机完全一致,不会出现输入输出顺序错乱
- 无功能影响:所有控制台的交互行为和加日志逻辑前完全一致,不会出现输入不回显、控制台无输出的问题
- 符合写入要求:落盘时采用追加模式,不会覆盖文件已有历史内容。
内容的提问来源于stack exchange,提问作者vdmclcv
相关产品推荐
相关产品推荐

