項目需要:
ORDER|用戶下單成功),便於多模塊混合部署時快速篩選。DailyRollingFileAppender 基於 DatePattern 滾動,最小粒度為分鐘(yyyy-MM-dd-HH-mm),但間隔不可配。通過覆寫 RollingCalendar 實現可配間隔:
public class MinuteRollingAppender extends FileAppender {
private static final String DATE_PATTERN = "'.'yyyy-MM-dd-HH-mm'.log'";
private int intervalMinutes = 10; // 可配間隔,默認 10 分鐘
private final RollingCalendar calendar = new RollingCalendar();
private String scheduledFilename;
public void setIntervalMinutes(int intervalMinutes) { this.intervalMinutes = intervalMinutes; }
@Override
public void activateOptions() {
super.activateOptions();
calendar.setIntervalMinutes(intervalMinutes);
scheduledFilename = fileName + new SimpleDateFormat(DATE_PATTERN).format(new Date());
}
@Override
protected void subAppend(LoggingEvent event) {
long now = System.currentTimeMillis();
if (now >= calendar.getNextCheckMillis()) {
rollOver();
calendar.updateNextCheckMillis(now);
}
super.subAppend(event);
}
void rollOver() {
// 關閉當前文件、重命名、重新打開...
}
// 內部類 RollingCalendar 維護下次滾動時間點
class RollingCalendar extends GregorianCalendar {
private int intervalMinutes = 10;
public void setIntervalMinutes(int m) { intervalMinutes = m; }
public long getNextCheckMillis() { return getNextCheckDate().getTime(); }
public Date getNextCheckDate() {
setTime(new Date());
set(Calendar.SECOND, 0);
set(Calendar.MILLISECOND, 0);
add(Calendar.MINUTE, intervalMinutes);
return getTime();
}
}
}
<appender name="minuteRoll" class="com.example.MinuteRollingAppender">
<param name="File" value="logs/app.log"/>
<param name="Append" value="true"/>
<param name="IntervalMinutes" value="2"/> <!-- 每 2 分鐘滾動 -->
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%p %d %c - %m%n"/>
</layout>
</appender>
避免在日誌寫入路徑上計算時間,改用獨立調度線程,性能更優、邏輯更清晰:
public class ScheduledRollingAppender extends FileAppender {
private static final String DATE_PATTERN = "'.'yyyy-MM-dd-HH-mm'.log'";
private int intervalMinutes = 10;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final SimpleDateFormat sdf = new SimpleDateFormat(DATE_PATTERN);
public void setIntervalMinutes(int m) { this.intervalMinutes = m; }
@Override
public void activateOptions() {
super.activateOptions();
scheduler.scheduleAtFixedRate(this::doRollOver, intervalMinutes, intervalMinutes, TimeUnit.MINUTES);
}
private synchronized void doRollOver() {
String dated = fileName + sdf.format(new Date());
closeFile();
File target = new File(dated);
if (target.exists()) target.delete();
boolean ok = new File(fileName).renameTo(target);
if (!ok) LogLog.error("Rename failed: " + fileName);
try { setFile(fileName, true, bufferedIO, bufferSize); }
catch (IOException e) { errorHandler.error("Reopen failed", e); }
}
@Override
public void close() {
scheduler.shutdown();
super.close();
}
}
並發安全:doRollOver 加 synchronized,防止滾動與寫入競爭。生產環境建議使用 ReentrantReadWriteLock 進一步優化。
繼承 RollingFileAppender,覆寫 subAppend 向日誌事件注入模塊名:
public class ModuleAppender extends RollingFileAppender {
private String moduleName; // 配置文件注入
public void setModuleName(String moduleName) { this.moduleName = moduleName; }
@Override
public void subAppend(LoggingEvent event) {
if (moduleName != null) {
String original = event.getRenderedMessage();
String enriched = moduleName + "|" + original;
// 創建新事件攜帶豐富消息
LoggingEvent newEvent = new LoggingEvent(
event.getFQNOfLoggerClass(),
event.getLogger(),
event.getTimeStamp(),
event.getLevel(),
enriched,
event.getThreadName(),
event.getThrowableInformation(),
event.getNDC(),
event.getLocationInformation(),
event.getProperties()
);
super.subAppend(newEvent);
} else {
super.subAppend(event);
}
}
}
<appender name="orderLog" class="com.example.ModuleAppender">
<param name="File" value="logs/order.log"/>
<param name="ModuleName" value="ORDER"/>
<param name="MaxFileSize" value="100MB"/>
<param name="MaxBackupIndex" value="10"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n"/>
</layout>
</appender>
<logger name="com.example.order">
<appender-ref ref="orderLog"/>
</logger>
日誌輸出效果:
2024-01-15 10:30:45 [http-nio-8080-exec-1] INFO OrderService - ORDER|用戶 1001 下單成功,訂單號 20240115001
| 問題 | 原因 | 解決 |
|---|---|---|
| 滾動時丟失日誌 | rename 與寫入競爭 | synchronized / ReadWriteLock 保護滾動關鍵區 |
| 定時任務堆積 | Timer 單線程,任務耗時導致延遲 | 改用 ScheduledExecutorService 線程池 |
| 磁盤寫滿 | 無大小/個數限制 | 配置 MaxFileSize、MaxBackupIndex |
| 模塊名未生效 | Logger 未綁定對應 Appender | logger 配置 additivity="false" 並指定 appender-ref |
Log4j 1.x 已停止維護,強烈建議遷移至 Logback(原生支持 TimeBasedRollingPolicy 分鐘級滾動、SiftingAppender 動態分模塊文件)或 Log4j2(異步日誌、GC-free、插件化架構)。Logback 配置片段:
<appender name="MINUTE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd_HH-mm}.log</fileNamePattern>
<maxHistory>720</maxHistory> <!-- 保留 720 個分鐘 = 5 天 -->
</rollingPolicy>
<encoder><pattern>%d %p %c - %m%n</pattern></encoder>
</appender>