Spring Boot动态切换日志文件报错原因排查与解决求助
Spring Boot动态切换Log4j2日志文件报错分析与解决
问题场景
在Spring Boot应用中动态修改日志输出文件后,日志能成功写入新文件,但持续触发报错,同时需要保留日志输出到控制台的能力。相关配置、代码及报错信息如下:
日志配置文件(log4j2.xml)
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="ConsoleAppender" target="SYSTEM_OUT"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/> </Console> <File name="FileAppender" fileName="logs/api.log" append="false"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </File> </Appenders> <Loggers> <Root level="INFO"> <AppenderRef ref="ConsoleAppender" /> <AppenderRef ref="FileAppender" /> </Root> <Logger name="myPackage" level="DEBUG" additivity="false"> <AppenderRef ref="ConsoleAppender" /> <AppenderRef ref="FileAppender" /> </Logger> </Loggers> </Configuration>
动态切换日志文件的方法
public static void updateLogger(String fileName, String appenderName, String packageName) { LoggerContext context = (LoggerContext) LogManager.getContext(false); Configuration configuration = context.getConfiguration(); Layout<? extends Serializable> oldLayout = configuration.getAppender(appenderName).getLayout(); // 删除旧的appender/logger configuration.getAppender(appenderName).stop(); configuration.removeLogger(packageName); // 创建新的appender/logger LoggerConfig loggerConfig = new LoggerConfig(packageName, Level.ALL, false); FileAppender appender = FileAppender.newBuilder() .withFileName(fileName) .withAppend(true) .setName(appenderName) .setLayout(oldLayout) .build(); appender.start(); loggerConfig.addAppender(appender, Level.ALL, null); configuration.addLogger(packageName, loggerConfig); context.updateLoggers(); }
方法调用代码
FileUtils.updateLogger("logs/logs/log" + ".txt", "FileAppender", "mypackage");
报错信息
2023-08-09 15:52:23,418 MessageBroker-8 ERROR Attempted to append to non-started appender FileAppender 2023-08-09 15:52:23,422 MessageBroker-8 ERROR Unable to write to stream logs/api.log for appender FileAppender org.apache.logging.log4j.core.appender.AppenderLoggingException: Error writing to stream logs/api.log at org.apache.logging.log4j.core.appender.OutputStreamManager.writeToDestination(OutputStreamManager.java:252) at org.apache.logging.log4j.core.appender.FileManager.writeToDestination(FileManager.java:277) at org.apache.logging.log4j.core.appender.OutputStreamManager.flushBuffer(OutputStreamManager.java:283) at org.apache.logging.log4j.core.appender.OutputStreamManager.flush(OutputStreamManager.java:294) at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.directEncodeEvent(AbstractOutputStreamAppender.java:217) at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.tryAppend(AbstractOutputStreamAppender.java:208) at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.append(AbstractOutputStreamAppender.java:199) at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:161) at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:134) at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:125) at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:89) at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:675) at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:633) at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:616) at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:552) at org.apache.logging.log4j.core.config.AwaitCompletionReliabilityStrategy.log(AwaitCompletionReliabilityStrategy.java:82) at org.apache.logging.log4j.core.Logger.log(Logger.java:161) at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2205) at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2159) at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2142) at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2022) at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:1875) at org.apache.commons.logging.LogAdapter$Log4jLog.log(LogAdapter.java:266) at org.apache.commons.logging.LogAdapter$Log4jLog.info(LogAdapter.java:230) at org.springframework.web.socket.config.WebSocketMessageBrokerStats.lambda$initLoggingTask$0(WebSocketMessageBrokerStats.java:128) at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.runAndReset$$$capture(FutureTask.java:308) at java.util.concurrent.FutureTask.runAndReset(FutureTask.java) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Caused by: java.io.IOException: Stream Closed at java.io.FileOutputStream.writeBytes(Native Method) at java.io.FileOutputStream.write(FileOutputStream.java:326) at org.apache.logging.log4j.core.appender.OutputStreamManager.writeToDestination(OutputStreamManager.java:250) ... 33 more
报错原因分析
- Root Logger未更新Appender引用:配置中Root Logger关联了
FileAppender,但代码仅停止旧Appender、更新自定义Logger,未处理Root Logger的引用。当Root Logger输出日志时,会调用已停止的旧Appender,触发流关闭报错。 - 旧Appender未彻底移除:代码仅停止旧Appender,未从Configuration中删除,导致同名新Appender创建后,内部仍存在旧Appender的残留引用,部分Logger会尝试调用已停止的实例。
解决方案
修复后的动态切换方法
public static void updateLogger(String fileName, String appenderName, String packageName) { LoggerContext context = (LoggerContext) LogManager.getContext(false); Configuration configuration = context.getConfiguration(); Layout<? extends Serializable> oldLayout = configuration.getAppender(appenderName).getLayout(); // 1. 彻底移除旧Appender Appender oldAppender = configuration.getAppender(appenderName); if (oldAppender != null) { oldAppender.stop(); configuration.removeAppender(appenderName); } // 2. 更新Root Logger,清除旧Appender引用并添加新引用 LoggerConfig rootConfig = configuration.getRootLogger(); rootConfig.removeAppender(appenderName); // 3. 创建并初始化新Appender FileAppender newAppender = FileAppender.newBuilder() .withFileName(fileName) .withAppend(true) .setName(appenderName) .setLayout(oldLayout) .build(); newAppender.start(); configuration.addAppender(newAppender); // 4. 更新自定义Logger配置 LoggerConfig loggerConfig = configuration.getLoggerConfig(packageName); if (loggerConfig != null) { loggerConfig.removeAppender(appenderName); } else { loggerConfig = new LoggerConfig(packageName, Level.ALL, false); configuration.addLogger(packageName, loggerConfig); } loggerConfig.addAppender(newAppender, Level.ALL, null); // 5. 将新Appender重新关联到Root Logger,保留控制台+文件双输出 rootConfig.addAppender(newAppender, Level.INFO, null); // 6. 刷新日志上下文 context.updateLoggers(); }
关键修复点说明
- 彻底清理旧Appender:停止旧Appender后调用
configuration.removeAppender(appenderName),避免同名冲突。 - 同步更新Root Logger:移除Root Logger对旧Appender的引用,并关联新Appender,确保全局日志输出正常。
- 安全处理Logger实例:先检查自定义Logger是否存在,存在则更新配置,不存在再创建,避免重复添加。
验证效果
修改后,日志会同时输出到控制台和指定的新日志文件,且不会再出现流关闭或Appender未启动的报错。
内容的提问来源于stack exchange,提问作者Magic Developement
相关产品推荐
相关产品推荐

