間隔を設定してログを定期的に印刷する
log4j を通じてログを定期的に印刷するというリクエストを受け取りました。その需要は次のように説明されています。ログは定期的に印刷する必要があり、時間間隔は設定可能です。タイミングと言えば、最初に思い浮かぶのは DailyRollingFileAppender クラスです。 datePattern によれば、これは SimpleDateFormat クラスを参照できます。
'.'yyyy-MM: 毎月。
'.'yyyy-ww : 毎週
'.'yyyy-MM-dd: 毎日
'.'yyyy-MM-dd-a: 1 日 2 回
'.'yyyy-MM-dd-HH : 毎時
'. 'yyyy-MM-dd-HH-mm: 毎分
n 分の同様の日付形式がないことが観察されたため、DailyRollingFileAppender クラスに基づいてカスタム クラスが作成されました。プロセスは次のとおりです:
1) DailyRollingFileAppender クラスのソース コードをコピーし、名前を MinuteRollingAppender に変更します。これを log4j.xml に設定するには、設定項目 intervalTime を追加し、set メソッドと get メソッドを追加します。 DailyRollingFileAppender クラスは次の間隔時間を計算するために RollingCalendar クラスを使用するため、intervalTime パラメーターを渡す必要があります。そのため、RollingCalendar クラスは、次のロールオーバー アクションの時間を計算するメソッドであるため、内部クラスとして変更されます。 datePattern、現時点では他の時間パターンは必要ありません。変更方法は次のとおりです:
private int intervalTime = 10;
3) 分に従って構成できる場合は、時間モードを無効にし、静的な最終モードに変更する必要があります。同様に、get、set メソッド、および MinuteRollingAppender コンストラクターの datePattern パラメーターを削除します
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(); }
同様に、さまざまな datePattern メソッド computeCheckPeriod() も削除できます この時点で、変換は完了し、完成した製品クラスは次のようになります。 :
private static String DATEPATTERN = "'.'yyyy-MM-dd-HH-mm'.log'";
テスト設定ファイルは次のとおりです:
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; /** * 按分钟可配置定时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'"; /** * 间隔时间,单位:分钟 */ 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 aMinuteRollingAppender
and open the file * designated byfilename
. 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. * ** 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(); } } }
タイミングの実装については、Java が提供する Timer を使用することもできます。実装により、ログを記録するときに毎回時間を計算して比較する必要がなくなります。違いは、実際に自分でスレッドを開始し、rollOver メソッドを呼び出すことです。実装は次のとおりです:
<?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>
ただし、上記の実装には 2 つの問題があります:
1 ) 同時実行性
同時実行性の問題が発生する可能性がある場所の 1 つは、次のとおりです。 run() で closeFile(); を呼び出した後、subAppend() メソッドがログを書き込むため、この時点でファイルが閉じられ、次のエラーが報告されます。 run() メソッド全体を同期し、synchronized キーワードを追加します。ただし、作成者は、実際に書き込む必要があり、書き込み速度が十分に速い場合にログが失われる問題を現在解決していません。 2) パフォーマンス
Timer を使用して実装するのは比較的簡単ですが、Timer 内のタスクの実行時間が長すぎると、Timer オブジェクトが占有され、後続のタスクが実行できなくなります。解決策も、スレッド プール バージョンを使用することで比較的簡単です。 timer クラス ScheduledExecutorService であり、実装は次のとおりです。
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'"; /** * 间隔时间,单位:分钟 */ 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."); } } } }
タイミングの実装に関しては、デフォルトでは 10 分ごとに新しいログ ファイルを生成することになっています。設定中に自分で設定できます。ただし、設定の場合、時間間隔が分であることを認識せず、600 を割り当ててデバッグをオンにすると、間違いなく GB のログ ファイルが生成されます。次の変換は、RollingFileAppender の最大サイズとバックアップ ファイルの最大数を組み合わせることであり、次回も変換プロセスを説明します。
モジュール名の設定を追加します
以前、log4j スケジュールされた印刷のカスタム クラスの実装について説明しましたが、バックアップ ファイルのサイズと数の指定については説明しませんでした。RollingFileAppender クラスからコードをコピーして、前のコードに追加するだけです。解決する必要がある唯一のことは、同時実行性です。つまり、ファイルが閉じられ、名前変更ファイルも閉じられ、ログ イベントが発生すると、出力ストリームのクローズ エラーが報告されます。
現在、このようなアプリケーション シナリオがあり、それがよく起こります。
1. プロジェクトには複数の異なるプロジェクトが含まれています。
2. 同じプロジェクトに異なるモジュールが含まれています。
最初のケースでは、log4jjava.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)
..............................
/** * */ 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实现定时配置打印日志 * <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'"; /** * 间隔时间,单位:分钟 */ 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."); } } } }
Log4j のスケジュールされたログ出力とモジュール名設定の追加に関する Java コード例については、PHP 中国語 Web サイトに注目してください。