log4j2 自動刪除過期日誌檔案的配置及實現原理
日誌檔案自動刪除功能必不可少,當然你可以讓運維去做這事,只是這不地道。而日誌元件是一個必備元件,讓其多做一件刪除的工作,無可厚非。本文就來探討下 log4j 的日誌檔案自動刪除實現吧。
0.自動刪除配置參考樣例: (log4j2.xml)
<?xml version="1.0" encoding="UTF-8" ?> <Configuration status="warn" monitorInterval="30" strict="true" schema="Log4J-V2.2.xsd"> <Properties> <Property name="log_level">info</Property> </Properties> <Appenders> <!-- 輸出到控制檯 --> <Console name="Console" target="SYSTEM_OUT"> <ThresholdFilter level="${log_level}" onMatch="ACCEPT" onMismatch="DENY" /> <PatternLayout pattern="%d{yyyy-MM-dd'T'HH:mm:ss.SSS} [%t] %p - %c - %m%n" /> </Console> <!-- 與properties檔案中位置存在衝突,如有問題,請注意調整 --> <RollingFile name="logFile" fileName="logs/app/test.log" filePattern="logs/app/history/test-%d{MM-dd-yyyy}-%i.log.gz"> <ThresholdFilter level="${log_level}" onMatch="ACCEPT" onMismatch="DENY" /> <PatternLayout pattern="%d{yyyy-MM-dd'T'HH:mm:ss.SSS} [%p] [%c:%L] -- %m%n" /> <Policies> <!-- 按天遞計算頻率 --> <TimeBasedTriggeringPolicy interval="1" /> <SizeBasedTriggeringPolicy size="500 MB" /> <OnStartupTriggeringPolicy /> </Policies> <!-- 刪除策略配置 --> <DefaultRolloverStrategy max="5"> <Delete basePath="logs/app/history" maxDepth="1"> <IfFileName glob="*.log.gz"/> <IfLastModified age="7d"/> </Delete> <Delete basePath="logs/app/history" maxDepth="1"> <IfFileName glob="*.docx"/> </Delete> <Delete basePath="logs/app/history" maxDepth="1"> <IfFileName glob="*.vsdx"/> </Delete> </DefaultRolloverStrategy> </RollingFile> <Async name="Async" bufferSize="2000" blocking="false"> <AppenderRef ref="logFile"/> </Async> </Appenders> <Loggers> <Root level="${log_level}"> <AppenderRef ref="Console" /> <AppenderRef ref="Async" /> </Root> <!-- 配置個例 --> <Logger name="com.xx.filter" level="info" /> </Loggers> </Configuration>
如果僅想停留在使用層面,如上log4j2.xml配置檔案足矣!
不過,至少得注意一點,以上配置需要基於log4j2,而如果你是 log4j1.x,則需要做下無縫升級:主要就是換下jar包版本,換個橋接包之類的,比如下參考配置:
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency> <!-- 橋接:告訴commons logging使用Log4j2 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.26</version> </dependency> <!-- 此處老版本,需註釋掉 --> <!--<dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-compress</artifactId> <version>1.10</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.8.2</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.8.2</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.8.2</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-web</artifactId> <version>2.8.2</version> </dependency>
如果還想多瞭解一點其執行原理,就跟隨本文的腳步吧:
1.自動清理大體執行流程
自動刪除工作的執行原理大體流程如下。(大抵都是如此)
1. 載入log4j2.xml配置檔案;
2. 讀取appenders,並新增到log4j上下文中;
3. 載入 policy,載入 rollover 配置;
4. 寫入日誌時判斷是否滿足rollover配置,預設是一天執行一次,可自行新增各種執行測試,比如大小、啟動時;
所以,刪除策略的核心是每一次新增日誌時。程式碼驗證如下:
// 在每次新增日誌時判定 // org.apache.logging.log4j.core.appender.RollingRandomAccessFileAppender#append /** * Write the log entry rolling over the file when required. * * @param event The LogEvent. */ @Override public void append(final LogEvent event) { final RollingRandomAccessFileManager manager = getManager(); // 重點:直接檢查是否需要 rollover,如需要直接進行 manager.checkRollover(event); // Leverage the nice batching behaviour of async Loggers/Appenders: // we can signal the file manager that it needs to flush the buffer // to disk at the end of a batch. // From a user's point of view,this means that all log events are // _always_ available in the log file,without incurring the overhead // of immediateFlush=true. manager.setEndOfBatch(event.isEndOfBatch()); // FIXME manager's EndOfBatch threadlocal can be deleted // LOG4J2-1292 utilize gc-free Layout.encode() method: taken care of in superclass super.append(event); } // org.apache.logging.log4j.core.appender.rolling.RollingFileManager#checkRollover /** * Determines if a rollover should occur. * @param event The LogEvent. */ public synchronized void checkRollover(final LogEvent event) { // 由各觸發策略判定是否需要進行 rolling // 如需要,則呼叫 rollover() if (triggeringPolicy.isTriggeringEvent(event)) { rollover(); } }
所以,何時進行刪除?答案是在適當的時機,這個時機可以是任意時候。
2. log4j 日誌滾動
日誌滾動,可以是重新命名,也可以是刪除檔案。但總體判斷是否可觸發滾動的前提是一致的。我們這裡主要關注檔案刪除。我們以時間作為依據看下判斷過程。
// 1. 判斷是否是 觸發事件時機 // org.apache.logging.log4j.core.appender.rolling.TimeBasedTriggeringPolicy#isTriggeringEvent /** * Determines whether a rollover should occur. * @param event A reference to the currently event. * @return true if a rollover should occur. */ @Override public boolean isTriggeringEvent(final LogEvent event) { if (manager.getFileSize() == 0) { return false; } final long nowMillis = event.getTimeMillis(); // TimeBasedTriggeringPolicy,是基於時間判斷的,此處為每天一次 if (nowMillis >= nextRolloverMillis) { nextRolloverMillis = manager.getPatternProcessor().getNextTime(nowMillis,interval,modulate); return true; } return false; } // org.apache.logging.log4j.core.appender.rolling.RollingFileManager#rollover() public synchronized void rollover() { if (!hasOutputStream()) { return; } // strategy 是xml配置的策略 if (rollover(rolloverStrategy)) { try { size = 0; initialTime = System.currentTimeMillis(); createFileAfterRollover(); } catch (final IOException e) { logError("Failed to create file after rollover",e); } } } // RollingFileManager 統一管理觸發器 // org.apache.logging.log4j.core.appender.rolling.RollingFileManager#rollover private boolean rollover(final RolloverStrategy strategy) { boolean releaseRequired = false; try { // Block until the asynchronous operation is completed. // 上鎖保證執行緒安全 semaphore.acquire(); releaseRequired = true; } catch (final InterruptedException e) { logError("Thread interrupted while attempting to check rollover",e); return false; } boolean success = true; try { // 由各觸發器執行 rollover 邏輯 final RolloverDescription descriptor = strategy.rollover(this); if (descriptor != null) { writeFooter(); closeOutputStream(); if (descriptor.getSynchronous() != null) { LOGGER.debug("RollingFileManager executing synchronous {}",descriptor.getSynchronous()); try { // 先使用同步方法,改名,然後再使用非同步方法操作更多 success = descriptor.getSynchronous().execute(); } catch (final Exception ex) { success = false; logError("Caught error in synchronous task",ex); } } // 如果配置了非同步器,則使用非同步進行 rollover if (success && descriptor.getAsynchronous() != null) { LOGGER.debug("RollingFileManager executing async {}",descriptor.getAsynchronous()); // CompositeAction,使用非同步執行緒池執行使用者的 action asyncExecutor.execute(new AsyncAction(descriptor.getAsynchronous(),this)); // 在非同步執行action期間,鎖是不會被釋放的,以避免執行緒安全問題 // 直到非同步任務完成,再主動釋放鎖 releaseRequired = false; } return true; } return false; } finally { if (releaseRequired) { semaphore.release(); } } }
此處滾動有兩個處理點,1. 每個滾動策略可以自行處理業務; 2. RollingFileManager 統一管理觸發同步和非同步的滾動action;
3. DefaultRolloverStrategy 預設滾動策略驅動
DefaultRolloverStrategy 作為一個預設的滾動策略實現,可以配置多個 Action,然後處理刪除操作。
刪除有兩種方式: 1. 當次滾動的檔案數過多,會立即進行刪除; 2. 配置單獨的 DeleteAction,根據配置的具體策略進行刪除。(但該Action只會被返回給外部呼叫,自身則不會執行)
// org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy#rollover /** * Performs the rollover. * * @param manager The RollingFileManager name for current active log file. * @return A RolloverDescription. * @throws SecurityException if an error occurs. */ @Override public RolloverDescription rollover(final RollingFileManager manager) throws SecurityException { int fileIndex; // 預設 minIndex=1 if (minIndex == Integer.MIN_VALUE) { final SortedMap<Integer,Path> eligibleFiles = getEligibleFiles(manager); fileIndex = eligibleFiles.size() > 0 ? eligibleFiles.lastKey() + 1 : 1; } else { if (maxIndex < 0) { return null; } final long startNanos = System.nanoTime(); // 刪除case1: 獲取符合條件的檔案數,同時清理掉大於 max 配置的日誌檔案 // 如配置 max=5,當前只有4個滿足時,不會立即清理檔案,但也不會阻塞後續流程 // 只要沒有出現錯誤,fileIndex 不會小於0 fileIndex = purge(minIndex,maxIndex,manager); if (fileIndex < 0) { return null; } if (LOGGER.isTraceEnabled()) { final double durationMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos); LOGGER.trace("DefaultRolloverStrategy.purge() took {} milliseconds",durationMillis); } } // 進入此區域即意味著,必然有檔案需要滾動,重新命名了 final StringBuilder buf = new StringBuilder(255); manager.getPatternProcessor().formatFileName(strSubstitutor,buf,fileIndex); final String currentFileName = manager.getFileName(); String renameTo = buf.toString(); final String compressedName = renameTo; Action compressAction = null; FileExtension fileExtension = manager.getFileExtension(); if (fileExtension != null) { renameTo = renameTo.substring(0,renameTo.length() - fileExtension.length()); compressAction = fileExtension.createCompressAction(renameTo,compressedName,true,compressionLevel); } // 未發生檔案重新命名情況,即檔案未被重新命名未被滾動 // 該種情況應該不太會發生 if (currentFileName.equals(renameTo)) { LOGGER.warn("Attempt to rename file {} to itself will be ignored",currentFileName); return new RolloverDescriptionImpl(currentFileName,false,null,null); } // 新建一個重命令的 action,返回待用 final FileRenameAction renameAction = new FileRenameAction(new File(currentFileName),new File(renameTo),manager.isRenameEmptyFiles()); // 非同步處理器,會處理使用者配置的非同步action,如本文配置的 DeleteAction // 它將會在稍後被提交到非同步執行緒池中執行 final Action asyncAction = merge(compressAction,customActions,stopCustomActionsOnError); // 封裝Rollover返回,renameAction 是同步方法,其他使用者配置的動態action 則是非同步方法 // 刪除case2: 封裝非同步返回action return new RolloverDescriptionImpl(currentFileName,renameAction,asyncAction); } private int purge(final int lowIndex,final int highIndex,final RollingFileManager manager) { // 預設使用 accending 的方式進行清理檔案 return useMax ? purgeAscending(lowIndex,highIndex,manager) : purgeDescending(lowIndex,manager); } // org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy#purgeAscending /** * Purges and renames old log files in preparation for rollover. The oldest file will have the smallest index,the * newest the highest. * * @param lowIndex low index. Log file associated with low index will be deleted if needed. * @param highIndex high index. * @param manager The RollingFileManager * @return true if purge was successful and rollover should be attempted. */ private int purgeAscending(final int lowIndex,final RollingFileManager manager) { final SortedMap<Integer,Path> eligibleFiles = getEligibleFiles(manager); final int maxFiles = highIndex - lowIndex + 1; boolean renameFiles = false; // 依次迭代 eligibleFiles,刪除 while (eligibleFiles.size() >= maxFiles) { try { LOGGER.debug("Eligible files: {}",eligibleFiles); Integer key = eligibleFiles.firstKey(); LOGGER.debug("Deleting {}",eligibleFiles.get(key).toFile().getAbsolutePath()); // 呼叫nio的介面刪除檔案 Files.delete(eligibleFiles.get(key)); eligibleFiles.remove(key); renameFiles = true; } catch (IOException ioe) { LOGGER.error("Unable to delete {},{}",eligibleFiles.firstKey(),ioe.getMessage(),ioe); break; } } final StringBuilder buf = new StringBuilder(); if (renameFiles) { // 針對未完成刪除的檔案,繼續處理 // 比如使用 匹配的方式匹配檔案,則不能被正常刪除 // 還有些未超過maxFiles的檔案 for (Map.Entry<Integer,Path> entry : eligibleFiles.entrySet()) { buf.setLength(0); // LOG4J2-531: directory scan & rollover must use same format manager.getPatternProcessor().formatFileName(strSubstitutor,entry.getKey() - 1); String currentName = entry.getValue().toFile().getName(); String renameTo = buf.toString(); int suffixLength = suffixLength(renameTo); if (suffixLength > 0 && suffixLength(currentName) == 0) { renameTo = renameTo.substring(0,renameTo.length() - suffixLength); } Action action = new FileRenameAction(entry.getValue().toFile(),true); try { LOGGER.debug("DefaultRolloverStrategy.purgeAscending executing {}",action); if (!action.execute()) { return -1; } } catch (final Exception ex) { LOGGER.warn("Exception during purge in RollingFileAppender",ex); return -1; } } } // 此處返回的 findIndex 一定是 >=0 的 return eligibleFiles.size() > 0 ? (eligibleFiles.lastKey() < highIndex ? eligibleFiles.lastKey() + 1 : highIndex) : lowIndex; }
4. 符合過濾條件的檔案查詢
當配置了 max 引數,這個引數是如何匹配的呢?比如我某個資料夾下有很歷史檔案,是否都會匹配呢?
// 檔案查詢規則 // org.apache.logging.log4j.core.appender.rolling.AbstractRolloverStrategy#getEligibleFiles protected SortedMap<Integer,Path> getEligibleFiles(final RollingFileManager manager) { return getEligibleFiles(manager,true); } protected SortedMap<Integer,Path> getEligibleFiles(final RollingFileManager manager,final boolean isAscending) { final StringBuilder buf = new StringBuilder(); // 此處的pattern 即是在appender上配置的 filePattern,一般會受限於 MM-dd-yyyy-$i.log.gz String pattern = manager.getPatternProcessor().getPattern(); // 此處會將時間替換為當前,然後按照此規則進行匹配要處理的檔案 manager.getPatternProcessor().formatFileName(strSubstitutor,NotANumber.NAN); return getEligibleFiles(buf.toString(),pattern,isAscending); } // 細節匹配要處理的檔案 protected SortedMap<Integer,Path> getEligibleFiles(String path,String logfilePattern,boolean isAscending) { TreeMap<Integer,Path> eligibleFiles = new TreeMap<>(); File file = new File(path); File parent = file.getParentFile(); if (parent == null) { parent = new File("."); } else { parent.mkdirs(); } if (!logfilePattern.contains("%i")) { return eligibleFiles; } Path dir = parent.toPath(); String fileName = file.getName(); int suffixLength = suffixLength(fileName); if (suffixLength > 0) { fileName = fileName.substring(0,fileName.length() - suffixLength) + ".*"; } String filePattern = fileName.replace(NotANumber.VALUE,"(\\d+)"); Pattern pattern = Pattern.compile(filePattern); try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) { for (Path entry: stream) { // 該匹配相當精確 // 只會刪除當天或者在時間交替的時候刪除上一天的資料咯 // 如果在這個時候進行了重啟操作,就再也不會刪除此檔案了 Matcher matcher = pattern.matcher(entry.toFile().getName()); if (matcher.matches()) { Integer index = Integer.parseInt(matcher.group(1)); eligibleFiles.put(index,entry); } } } catch (IOException ioe) { throw new LoggingException("Error reading folder " + dir + " " + ioe.getMessage(),ioe); } return isAscending? eligibleFiles : eligibleFiles.descendingMap(); } // 此處會將 各種格式的檔名,替換為當前時間或者最後一次滾動的檔案的時間。所以匹配的時候,並不會匹配超時當前認知範圍的檔案 /** * Formats file name. * @param subst The StrSubstitutor. * @param buf string buffer to which formatted file name is appended,may not be null. * @param obj object to be evaluated in formatting,may not be null. */ public final void formatFileName(final StrSubstitutor subst,final StringBuilder buf,final boolean useCurrentTime,final Object obj) { // LOG4J2-628: we deliberately use System time,not the log4j.Clock time // for creating the file name of rolled-over files. final long time = useCurrentTime && currentFileTime != 0 ? currentFileTime : prevFileTime != 0 ? prevFileTime : System.currentTimeMillis(); formatFileName(buf,new Date(time),obj); final LogEvent event = new Log4jLogEvent.Builder().setTimeMillis(time).build(); final String fileName = subst.replace(event,buf); buf.setLength(0); buf.append(fileName); }
AsyncAction 是一個 Runnable 的實現,被直接提交到執行緒池執行. AsyncAction -> AbstractAction -> Action -> Runnable
它是一個統一管理非同步Action的包裝,主要是管理鎖和異常類操作。
// org.apache.logging.log4j.core.appender.rolling.RollingFileManager.AsyncAction /** * Performs actions asynchronously. */ private static class AsyncAction extends AbstractAction { private final Action action; private final RollingFileManager manager; /** * Constructor. * @param act The action to perform. * @param manager The manager. */ public AsyncAction(final Action act,final RollingFileManager manager) { this.action = act; this.manager = manager; } /** * Executes an action. * * @return true if action was successful. A return value of false will cause * the rollover to be aborted if possible. * @throws java.io.IOException if IO error,a thrown exception will cause the rollover * to be aborted if possible. */ @Override public boolean execute() throws IOException { try { // 門面呼叫 action.execute(),一般是呼叫 CompositeAction,裡面封裝了多個 action return action.execute(); } finally { // 任務執行完成,才會釋放外部的鎖 // 雖然不是很優雅,但是很準確很安全 manager.semaphore.release(); } } ... } // CompositeAction 封裝了多個 action 處理 // org.apache.logging.log4j.core.appender.rolling.action.CompositeAction#run /** * Execute sequence of actions. * * @return true if all actions were successful. * @throws IOException on IO error. */ @Override public boolean execute() throws IOException { if (stopOnError) { // 依次呼叫action for (final Action action : actions) { if (!action.execute()) { return false; } } return true; } boolean status = true; IOException exception = null; for (final Action action : actions) { try { status &= action.execute(); } catch (final IOException ex) { status = false; if (exception == null) { exception = ex; } } } if (exception != null) { throw exception; } return status; }
DeleteAction是我們真正關心的動作。
// CompositeAction 封裝了多個 action 處理 // org.apache.logging.log4j.core.appender.rolling.action.CompositeAction#run /** * Execute sequence of actions. * * @return true if all actions were successful. * @throws IOException on IO error. */ @Override public boolean execute() throws IOException { if (stopOnError) { // 依次呼叫action for (final Action action : actions) { if (!action.execute()) { return false; } } return true; } boolean status = true; IOException exception = null; for (final Action action : actions) { try { status &= action.execute(); } catch (final IOException ex) { status = false; if (exception == null) { exception = ex; } } } if (exception != null) { throw exception; } return status; } // DeleteAction 做真正的刪除動作 // org.apache.logging.log4j.core.appender.rolling.action.DeleteAction#execute() @Override public boolean execute() throws IOException { // 如果沒有script配置,則直接委託父類處理 return scriptCondition != null ? executeScript() : super.execute(); } org.apache.logging.log4j.core.appender.rolling.action.AbstractPathAction#execute() @Override public boolean execute() throws IOException { // 根據指定的basePath,和過濾條件,選擇相關檔案 // 呼叫 DeleteAction 的 createFileVisitor(),返回 DeletingVisitor return execute(createFileVisitor(getBasePath(),pathConditions)); } // org.apache.logging.log4j.core.appender.rolling.action.DeleteAction#execute(java.nio.file.FileVisitor<java.nio.file.Path>) @Override public boolean execute(final FileVisitor<Path> visitor) throws IOException { // 根據maxDepth設定,遍歷所有可能的檔案路徑 // 使用 Files.walkFileTree() 實現,新增到 collected 中 final List<PathWithAttributes> sortedPaths = getSortedPaths(); trace("Sorted paths:",sortedPaths); for (final PathWithAttributes element : sortedPaths) { try { // 依次呼叫 visitFile,依次判斷是否需要刪除 visitor.visitFile(element.getPath(),element.getAttributes()); } catch (final IOException ioex) { LOGGER.error("Error in post-rollover Delete when visiting {}",element.getPath(),ioex); visitor.visitFileFailed(element.getPath(),ioex); } } // TODO return (visitor.success || ignoreProcessingFailure) return true; // do not abort rollover even if processing failed }
最終,即和想像的一樣:找到要查詢的資料夾,遍歷各檔案,用多個條件判斷是否滿足。刪除符合條件的檔案。
只是這其中注意的點:如何刪除檔案的執行緒安全性;如何保證刪除工作不影響業務執行緒;很常見的鎖和多執行緒的應用。
5.真正的刪除
真正的刪除動作就是在DeleteAction中配置的,但上面可以看它是呼叫visitor的visitFile方法,所以有必要看看是如何真正處理刪除的。(實際上前面在purge時已經做過一次刪除操作了,所以別被兩個點迷惑了,建議儘量只依賴於Delete配置,可以將外部max設定很大以避免兩處生效)
// org.apache.logging.log4j.core.appender.rolling.action.DeletingVisitor#visitFile @Override public FileVisitResult visitFile(final Path file,final BasicFileAttributes attrs) throws IOException { for (final PathCondition pathFilter : pathConditions) { final Path relative = basePath.relativize(file); // 遍歷所有條件,只要有一個不符合,即不進行刪除。 // 所以,所以條件是 AND 關係, 沒有 OR 關係 // 如果想配置 OR 關係,只能配置多個DELETE if (!pathFilter.accept(basePath,relative,attrs)) { LOGGER.trace("Not deleting base={},relative={}",basePath,relative); return FileVisitResult.CONTINUE; } } // 直接刪除檔案 if (isTestMode()) { LOGGER.info("Deleting {} (TEST MODE: file not actually deleted)",file); } else { delete(file); } return FileVisitResult.CONTINUE; }
刪除策略配置比如:
<RollingFile name="logFile" fileName="logs/app/test.log" filePattern="logs/app/history/test-%d{MM-dd-yyyy}-%i.log.gz"> <ThresholdFilter level="${log_level}" onMatch="ACCEPT" onMismatch="DENY" /> <PatternLayout pattern="%d{yyyy-MM-dd'T'HH:mm:ss.SSS} [%p] [%c:%L] -- %m%n" /> <Policies> <!-- 按天遞計算頻率 --> <TimeBasedTriggeringPolicy interval="1" /> <SizeBasedTriggeringPolicy size="500 MB" /> <OnStartupTriggeringPolicy /> </Policies> <!-- 刪除策略配置 --> <DefaultRolloverStrategy max="5000"> <Delete basePath="logs/app/history" maxDepth="1"> <!-- 配置且關係 --> <IfFileName glob="*.log.gz"/> <IfLastModified age="7d"/> </Delete> <!-- 配置或關係 --> <Delete basePath="logs/app/history" maxDepth="1"> <IfFileName glob="*.docx"/> </Delete> <Delete basePath="logs/app/history" maxDepth="1"> <IfFileName glob="*.vsdx"/> </Delete> </DefaultRolloverStrategy> </RollingFile>
另外說明,之所以能夠無縫替換,是因為利用了不同實現版本的 org/slf4j/impl/StaticLoggerBinder.class,而外部都使用 slf4j 介面定義實現的,比如 org.apache.logging.log4j:log4j-slf4j-impl 包的實現。
總結
到此這篇關於log4j2 自動刪除過期日誌檔案的配置及實現原理解析的文章就介紹到這了,更多相關log4j2自動刪除過期日誌檔案內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!