Log4j定時(shí)打印日志及添加模塊名配置的Java代碼實(shí)例
配置間隔時(shí)間,定時(shí)打印日志
接到個(gè)需求,通過(guò)log4j定時(shí)打印日志,需求描述如下:需要能夠定時(shí)打印日志,時(shí)間間隔可配。說(shuō)到定時(shí),首先想到了DailyRollingFileAppender類,各種定時(shí),根據(jù)datePattern,這個(gè)可以參考類SimpleDateFormat類,常見的一些定時(shí)設(shè)置如下:
- '.'yyyy-MM: 每月
- '.'yyyy-ww: 每周
- '.'yyyy-MM-dd: 每天
- '.'yyyy-MM-dd-a: 每天兩次
- '.'yyyy-MM-dd-HH: 每小時(shí)
- '.'yyyy-MM-dd-HH-mm: 每分鐘
通過(guò)觀察發(fā)現(xiàn)沒(méi)有n分鐘類似的日期格式,因此,在DailyRollingFileAppender類基礎(chǔ)上進(jìn)行自定義類的編寫。過(guò)程如下:
1)拷貝DailyRollingFileAppender類源碼并并改名MinuteRollingAppender,為了在log4j.xml中配置,增加配置項(xiàng)intervalTime并添加set、get方法;
private int intervalTime = 10;
2)由于DailyRollingFileAppender類使用了RollingCalendar類來(lái)計(jì)算下一次間隔時(shí)間,而需要傳遞參數(shù)intervalTime,因此修改RollingCalendar類為內(nèi)部類;由于其方法就是根據(jù)datePattern來(lái)計(jì)算下一次rollOver動(dòng)作的時(shí)間,此時(shí)不需要其他的時(shí)間模式,修改方法如下:
public Date getNextCheckDate(Date now) { this.setTime(now); this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); this.add(Calendar.MINUTE, intervalTime); return getTime(); }
3)按照分鐘可配時(shí),時(shí)間模式就需要禁用了,將其改為static final,響應(yīng)的去掉其get、set方法和MinuteRollingAppender構(gòu)造函數(shù)中的datePattern參數(shù)
private static String DATEPATTERN = "'.'yyyy-MM-dd-HH-mm'.log'";
同樣,服務(wù)于多種datePattern的方法computeCheckPeriod()也可以刪除; 至此改造就完成了,成品類如下:
package net.csdn.blog; import java.io.File; import java.io.IOException; import java.io.InterruptedIOException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import org.apache.log4j.FileAppender; import org.apache.log4j.Layout; import org.apache.log4j.helpers.LogLog; import org.apache.log4j.spi.LoggingEvent; /** * 按分鐘可配置定時(shí)appender * * @author coder_xia * */ public class MinuteRollingAppender extends FileAppender { /** * The date pattern. By default, the pattern is set to "'.'yyyy-MM-dd" * meaning daily rollover. */ private static String DATEPATTERN = "'.'yyyy-MM-dd-HH-mm'.log'"; /** * 間隔時(shí)間,單位:分鐘 */ private int intervalTime = 10; /** * The log file will be renamed to the value of the scheduledFilename * variable when the next interval is entered. For example, if the rollover * period is one hour, the log file will be renamed to the value of * "scheduledFilename" at the beginning of the next hour. * * The precise time when a rollover occurs depends on logging activity. */ private String scheduledFilename; /** * The next time we estimate a rollover should occur. */ private long nextCheck = System.currentTimeMillis() - 1; Date now = new Date(); SimpleDateFormat sdf; RollingCalendar rc = new RollingCalendar(); /** * The default constructor does nothing. */ public MinuteRollingAppender() { } /** * Instantiate a <code>MinuteRollingAppender</code> and open the file * designated by <code>filename</code>. The opened filename will become the * ouput destination for this appender. */ public MinuteRollingAppender(Layout layout, String filename) throws IOException { super(layout, filename, true); activateOptions(); } /** * @return the intervalTime */ public int getIntervalTime() { return intervalTime; } /** * @param intervalTime * the intervalTime to set */ public void setIntervalTime(int intervalTime) { this.intervalTime = intervalTime; } @Override public void activateOptions() { super.activateOptions(); if (fileName != null) { now.setTime(System.currentTimeMillis()); sdf = new SimpleDateFormat(DATEPATTERN); File file = new File(fileName); scheduledFilename = fileName + sdf.format(new Date(file.lastModified())); } else { LogLog .error("Either File or DatePattern options are not set for appender [" + name + "]."); } } /** * Rollover the current file to a new file. */ void rollOver() throws IOException { String datedFilename = fileName + sdf.format(now); // It is too early to roll over because we are still within the // bounds of the current interval. Rollover will occur once the // next interval is reached. if (scheduledFilename.equals(datedFilename)) { return; } // close current file, and rename it to datedFilename this.closeFile(); File target = new File(scheduledFilename); if (target.exists()) { target.delete(); } File file = new File(fileName); boolean result = file.renameTo(target); if (result) { LogLog.debug(fileName + " -> " + scheduledFilename); } else { LogLog.error("Failed to rename [" + fileName + "] to [" + scheduledFilename + "]."); } try { // This will also close the file. This is OK since multiple // close operations are safe. this.setFile(fileName, true, this.bufferedIO, this.bufferSize); } catch (IOException e) { errorHandler.error("setFile(" + fileName + ", true) call failed."); } scheduledFilename = datedFilename; } /** * This method differentiates MinuteRollingAppender from its super class. * * <p> * Before actually logging, this method will check whether it is time to do * a rollover. If it is, it will schedule the next rollover time and then * rollover. * */ @Override protected void subAppend(LoggingEvent event) { long n = System.currentTimeMillis(); if (n >= nextCheck) { now.setTime(n); nextCheck = rc.getNextCheckMillis(now); try { rollOver(); } catch (IOException ioe) { if (ioe instanceof InterruptedIOException) { Thread.currentThread().interrupt(); } LogLog.error("rollOver() failed.", ioe); } } super.subAppend(event); } /** * RollingCalendar is a helper class to MinuteRollingAppender. Given a * periodicity type and the current time, it computes the start of the next * interval. * */ class RollingCalendar extends GregorianCalendar { private static final long serialVersionUID = -3560331770601814177L; RollingCalendar() { super(); } public long getNextCheckMillis(Date now) { return getNextCheckDate(now).getTime(); } public Date getNextCheckDate(Date now) { this.setTime(now); this.set(Calendar.SECOND, 0); this.set(Calendar.MILLISECOND, 0); this.add(Calendar.MINUTE, intervalTime); return getTime(); } } }
測(cè)試配置文件如下:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> <appender name="myFile" class="net.csdn.blog.MinuteRollingAppender"> <param name="File" value="log4jTest.log" /> <param name="Append" value="true" /> <param name="intervalTime" value="2"/> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%p %d (%c:%L)- %m%n" /> </layout> </appender> <root> <priority value="debug"/> <appender-ref ref="myFile"/> </root> </log4j:configuration>
關(guān)于定時(shí)實(shí)現(xiàn),還可以采用java提供的Timer實(shí)現(xiàn),也就免去了每次記錄日志時(shí)計(jì)算并且比較時(shí)間,區(qū)別其實(shí)就是自己起個(gè)線程與調(diào)用rollOver方法,實(shí)現(xiàn)如下:
package net.csdn.blog; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Timer; import java.util.TimerTask; import org.apache.log4j.FileAppender; import org.apache.log4j.Layout; import org.apache.log4j.helpers.LogLog; public class TimerTaskRollingAppender extends FileAppender { /** * The date pattern. By default, the pattern is set to "'.'yyyy-MM-dd" * meaning daily rollover. */ private static final String DATEPATTERN = "'.'yyyy-MM-dd-HH-mm'.log'"; /** * 間隔時(shí)間,單位:分鐘 */ private int intervalTime = 10; SimpleDateFormat sdf = new SimpleDateFormat(DATEPATTERN); /** * The default constructor does nothing. */ public TimerTaskRollingAppender() { } /** * Instantiate a <code>TimerTaskRollingAppender</code> and open the file * designated by <code>filename</code>. The opened filename will become the * ouput destination for this appender. */ public TimerTaskRollingAppender(Layout layout, String filename) throws IOException { super(layout, filename, true); activateOptions(); } /** * @return the intervalTime */ public int getIntervalTime() { return intervalTime; } /** * @param intervalTime * the intervalTime to set */ public void setIntervalTime(int intervalTime) { this.intervalTime = intervalTime; } @Override public void activateOptions() { super.activateOptions(); Timer timer = new Timer(); timer.schedule(new LogTimerTask(), 1000, intervalTime * 60000); } class LogTimerTask extends TimerTask { @Override public void run() { String datedFilename = fileName + sdf.format(new Date()); closeFile(); File target = new File(datedFilename); if (target.exists()) target.delete(); File file = new File(fileName); boolean result = file.renameTo(target); if (result) LogLog.debug(fileName + " -> " + datedFilename); else LogLog.error("Failed to rename [" + fileName + "] to [" + datedFilename + "]."); try { setFile(fileName, true, bufferedIO, bufferSize); } catch (IOException e) { errorHandler.error("setFile(" + fileName + ", true) call failed."); } } } }
不過(guò),以上實(shí)現(xiàn),存在2個(gè)問(wèn)題:
1)并發(fā)
并發(fā)問(wèn)題可能發(fā)生的一個(gè)地方在run()中調(diào)用closeFile();后,正好subAppend()方法寫日志,此刻文件已關(guān)閉,則會(huì)報(bào)以下錯(cuò)誤:
java.io.IOException: Stream closed at sun.nio.cs.StreamEncoder.ensureOpen(Unknown Source) at sun.nio.cs.StreamEncoder.write(Unknown Source) at sun.nio.cs.StreamEncoder.write(Unknown Source) at java.io.OutputStreamWriter.write(Unknown Source) at java.io.Writer.write(Unknown Source) ..............................
2)性能
使用Timer實(shí)現(xiàn)比較簡(jiǎn)單,但是Timer里面的任務(wù)如果執(zhí)行時(shí)間太長(zhǎng),會(huì)獨(dú)占Timer對(duì)象,使得后面的任務(wù)無(wú)法幾時(shí)的執(zhí)行,解決方法也比較簡(jiǎn)單,采用線程池版定時(shí)器類ScheduledExecutorService,實(shí)現(xiàn)如下:
/** * */ package net.csdn.blog; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.apache.log4j.FileAppender; import org.apache.log4j.Layout; import org.apache.log4j.helpers.LogLog; /** * @author coder_xia * <p> * 采用ScheduledExecutorService實(shí)現(xiàn)定時(shí)配置打印日志 * <p> * */ public class ScheduledExecutorServiceAppender extends FileAppender { /** * The date pattern. By default, the pattern is set to "'.'yyyy-MM-dd" * meaning daily rollover. */ private static final String DATEPATTERN = "'.'yyyy-MM-dd-HH-mm'.log'"; /** * 間隔時(shí)間,單位:分鐘 */ private int intervalTime = 10; SimpleDateFormat sdf = new SimpleDateFormat(DATEPATTERN); /** * The default constructor does nothing. */ public ScheduledExecutorServiceAppender() { } /** * Instantiate a <code>ScheduledExecutorServiceAppender</code> and open the * file designated by <code>filename</code>. The opened filename will become * the ouput destination for this appender. */ public ScheduledExecutorServiceAppender(Layout layout, String filename) throws IOException { super(layout, filename, true); activateOptions(); } /** * @return the intervalTime */ public int getIntervalTime() { return intervalTime; } /** * @param intervalTime * the intervalTime to set */ public void setIntervalTime(int intervalTime) { this.intervalTime = intervalTime; } @Override public void activateOptions() { super.activateOptions(); Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate( new LogTimerTask(), 1, intervalTime * 60000, TimeUnit.MILLISECONDS); } class LogTimerTask implements Runnable { @Override public void run() { String datedFilename = fileName + sdf.format(new Date()); closeFile(); File target = new File(datedFilename); if (target.exists()) target.delete(); File file = new File(fileName); boolean result = file.renameTo(target); if (result) LogLog.debug(fileName + " -> " + datedFilename); else LogLog.error("Failed to rename [" + fileName + "] to [" + datedFilename + "]."); try { setFile(fileName, true, bufferedIO, bufferSize); } catch (IOException e) { errorHandler.error("setFile(" + fileName + ", true) call failed."); } } } }
關(guān)于定時(shí)的實(shí)現(xiàn),差不多就到這里了,采用的都默認(rèn)是10分鐘產(chǎn)生一個(gè)新的日志文件,在配置時(shí)可以自行設(shè)置,不過(guò)存在的一個(gè)隱患,萬(wàn)一配置的人不知道時(shí)間間隔是分鐘,如果以為是秒,配了個(gè)600,又開了debug,產(chǎn)生上G的日志文件,這肯定是個(gè)災(zāi)難,下面的改造就是結(jié)合RollingFileAppender的最大大小和最多備份文件個(gè)數(shù)可配,再次進(jìn)行完善,下次繼續(xù)描述改造過(guò)程。
添加模塊名配置
在前面講到了log4j定時(shí)打印的定制類實(shí)現(xiàn),就不講指定大小和指定備份文件個(gè)數(shù)了,從RollingFileAppender類copy代碼到前面的定制類中添加即可,唯一需要解決的是并發(fā)問(wèn)題,即文件關(guān)閉rename文件時(shí),發(fā)生了記錄日志事件時(shí),會(huì)報(bào)output stream closed的錯(cuò)誤。
現(xiàn)在有這樣一種應(yīng)用場(chǎng)景,而且經(jīng)常有:
1.項(xiàng)目包含有多個(gè)不同的工程;
2.同一工程包含不同的模塊。
對(duì)第一種情況,可以通過(guò)配置log4j<catogery=“Test”>,再在產(chǎn)生Logger時(shí)使用類似如下方式:
Logger logger=Logger.getLogger("Test");
對(duì)第二種情況,我們希望能夠?qū)⒉煌K打印到同一個(gè)日志文件中,不過(guò)希望能夠在日志中打印出模塊名以便出問(wèn)題時(shí)定位問(wèn)題,因此便有了本文需要的在Appender類中添加配置ModuleName,下面開始改造,與定時(shí)打印不同,我們采用RollingFileAppender類為基類進(jìn)行改造。
首先,添加配置項(xiàng)moduleName,并增加get、set方法;
由于繼承自RollingFileAppender,所以只需要在subAppend()中格式化LoggingEvent中的數(shù)據(jù),添加formatInfo方法格式化數(shù)據(jù),代碼略;
最終的成品類如下:
package net.csdn.blog; import org.apache.log4j.Category; import org.apache.log4j.RollingFileAppender; import org.apache.log4j.spi.LoggingEvent; /** * @author coder_xia * */ public class ModuleAppender extends RollingFileAppender { private String moduleName; /** * @return the moduleName */ public String getModuleName() { return moduleName; } /** * @param moduleName * the moduleName to set */ public void setModuleName(String moduleName) { this.moduleName = moduleName; } /** * 格式化打印內(nèi)容 * * @param event * event * @return msg */ private String formatInfo(LoggingEvent event) { StringBuilder sb = new StringBuilder(); if (moduleName != null) { sb.append(moduleName).append("|"); sb.append(event.getMessage()); } return sb.toString(); } @Override public void subAppend(LoggingEvent event) { String msg = formatInfo(event); super.subAppend(new LoggingEvent(Category.class.getName(), event .getLogger(), event.getLevel(), msg, null)); } }
相關(guān)文章
SpringBoot使用@Async注解可能會(huì)遇到的8大坑點(diǎn)匯總
SpringBoot中,@Async注解可以實(shí)現(xiàn)異步線程調(diào)用,用法簡(jiǎn)單,體驗(yàn)舒適,但是你一定碰到過(guò)異步調(diào)用不生效的情況,今天,我就列出90%的人都可能會(huì)遇到的8大坑點(diǎn),需要的朋友可以參考下2023-09-09Java開啟JMX遠(yuǎn)程監(jiān)控服務(wù)配置
這篇文章主要為大家介紹了Java開啟JMX遠(yuǎn)程監(jiān)控的服務(wù)配置,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(61)
下面小編就為大家?guī)?lái)一篇Java基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧,希望可以幫到你2021-08-08利用棧使用簡(jiǎn)易計(jì)算器(Java實(shí)現(xiàn))
這篇文章主要為大家詳細(xì)介紹了Java利用棧實(shí)現(xiàn)簡(jiǎn)易計(jì)算器,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09JAVA后端學(xué)習(xí)精華之網(wǎng)絡(luò)通信項(xiàng)目進(jìn)階
不同項(xiàng)目之間的通信方式分為,http、socket、webservice;其中socket通信的效率最高,youtube就采用的是原始的socket通信,他們信奉的原則是簡(jiǎn)單有效2021-09-09簡(jiǎn)單了解Java方法的定義和使用實(shí)現(xiàn)詳解
這篇文章主要介紹了簡(jiǎn)單了解Java方法的定義和使用實(shí)現(xiàn)詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12