Log4j DailyRollingFileAppender源码详读

瞎扯

Log4j对Java开发者来说是经常使用到的日志框架,我每次使用都对它的配置文件头大,网上搜一个别人的例子自己改巴改巴,草草了事。再次使用时,又忘了怎么回事了。这次突然来了兴趣,想看看它具体是怎么做的,做个笔记,加深一下印象。

目前的版本是 log4j:log4j:1.2.17

依赖结构

clipboard.png

Appender接口

Log4j的输出类都需要实现的接口,为了用户自定义log输出策略,抽象出了以下几点功能

  • 过滤链

  • log输出

  • 错误处理

  • log格式

clipboard.png

OptionHandler接口

这个接口只定义了一个方法 void activateOptions();,用于按需初始化一些配置。

AppenderSkeleton抽象类

既然是Skeleton,那它必须是最核心的骨架。这个类主要做了以下几个事

  • 过滤链(链表)增删操作

    protected Filter headFilter;
    protected Filter tailFilter;
    
    public void addFilter(Filter newFilter) {
    if(headFilter == null) {
      headFilter = tailFilter = newFilter;
    } else {
      tailFilter.setNext(newFilter);
      tailFilter = newFilter;    
    }
    }
    
    public void clearFilters() {
    headFilter = tailFilter = null;
    }
  • 定义了日志优先级 threshold “门槛”,实现日志的分级输出

    protected Priority threshold;//默认为空
    
    public boolean isAsSevereAsThreshold(Priority priority) {
        return ((threshold == null) || priority.isGreaterOrEqual(threshold));
    }
  • log的输出核心逻辑

    public synchronized void doAppend(LoggingEvent event) {
    if(closed) {
      LogLog.error("Attempted to append to closed appender named ["+name+"].");
      return;
    }
    //日志级别拦截
    if(!isAsSevereAsThreshold(event.getLevel())) {
      return;
    }
    
    Filter f = this.headFilter;
    
    //结合Filter实现类自身的优先级[停止输出、立即输出、依次过滤后输出]进行过滤,
    FILTER_LOOP:
    while(f != null) {
      switch(f.decide(event)) {
      case Filter.DENY: return;
      case Filter.ACCEPT: break FILTER_LOOP;
      case Filter.NEUTRAL: f = f.getNext();
      }
    }
    //具体的输出开放给子类实现
    this.append(event);    
    }
  • 下放的权限

    //子类只需要关心日志具体的输出方式
    abstract protected void append(LoggingEvent event);
    //配置方法,子类可以按自己的需求覆写
    public void activateOptions() {}

WriteAppender

继承AppenderSkeleton,用户可选择将log按字符流或字节流输出。增加了以下特性

  • 提供了写入刷新控制

  • 可配置编码方式

  • 提供了静态字符流QuitWriter,异常不会抛出,会交给ErrorHandler去处理

    //默认实时刷新,效率低但可保证每次输出均可写入,设为false时,若程序崩溃,尾部log可能丢失
    protected boolean immediateFlush = true;
    protected String encoding;
    protected QuietWriter qw;
  • 提供了字节流->字符流的转换

  • log输出 官方注释说明了在log输出之前做的检查或过滤操作[检查日志级别->过滤->检查当前输出状况(Appender状态、输出流、格式是否均具备)->输出]

    public void append(LoggingEvent event) {
    
    // Reminder: the nesting of calls is:
    //
    //    doAppend()
    //      - check threshold
    //      - filter
    //      - append();
    //        - checkEntryConditions();
    //        - subAppend();
    
    if(!checkEntryConditions()) {
      return;
    }
    subAppend(event);
     }
    
    protected void subAppend(LoggingEvent event) {
    this.qw.write(this.layout.format(event));//将日志格式化后输出
    //依次输出异常栈
    if(layout.ignoresThrowable()) {
      String[] s = event.getThrowableStrRep();
      if (s != null) {
        int len = s.length;
        for(int i = 0; i < len; i++) {
          this.qw.write(s[i]);
          this.qw.write(Layout.LINE_SEP);
        }
      }
    }
    //写入刷新控制
    if(shouldFlush(event)) {
      this.qw.flush();
    }
    }
  • 还有一些Header、Footer的写入和输出流的关闭操作

FileAppender

继承了WriteAppender,将log输出到文件。这个比较简单,主要就是将父类中的输出流封装指向到文件。

protected boolean fileAppend = true;//是否覆盖
protected String fileName = null;//目标文件名
protected boolean bufferedIO = false;//是否缓冲 
protected int bufferSize = 8*1024;//默认缓冲区大小

public synchronized void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize)
                                                        throws IOException {
LogLog.debug("setFile called: "+fileName+", "+append);

// It does not make sense to have immediate flush and bufferedIO.
if(bufferedIO) {
  setImmediateFlush(false);//既然缓冲了,那意味着父类中的刷新控制为false-不进行同步刷新
}

reset();
FileOutputStream ostream = null;
try {
      ostream = new FileOutputStream(fileName, append);
} catch(FileNotFoundException ex) {
      String parentName = new File(fileName).getParent();
      if (parentName != null) {
         File parentDir = new File(parentName);
         if(!parentDir.exists() && parentDir.mkdirs()) {
            ostream = new FileOutputStream(fileName, append);
         } else {
            throw ex;
         }
      } else {
         throw ex;
      }
}
Writer fw = createWriter(ostream);//利用父类中的字节流->字符流转换方法
if(bufferedIO) {
  fw = new BufferedWriter(fw, bufferSize);
}
this.setQWForFiles(fw);//实例化父类中的QuitWriter(实际在上面指向了文件输出流)
this.fileName = fileName;
this.fileAppend = append;
this.bufferedIO = bufferedIO;
this.bufferSize = bufferSize;
writeHeader();
LogLog.debug("setFile ended");
  }

protected void setQWForFiles(Writer writer) {
 this.qw = new QuietWriter(writer, errorHandler);
}

DailyRollingFileAppender

继承FileAppender,将log文件进行日常转存。我们常用的日志处理类,官方注释里说已证实有并发和数据丢失的问题,可惜我看不出来...
可以自定义转存日期表达式datePattern(格式需遵循SimpleDateFormat的约定),如

'.'yyyy-MM
'.'yyyy-ww
'.'yyyy-MM-dd
'.'yyyy-MM-dd-a
'.'yyyy-MM-dd-HH
'.'yyyy-MM-dd-HH-mm

注意不要包含任何冒号

它根据用户提供的日期表达式datePattern,通过内部类RollingCalendar计算得到对应的日期检查周期rc.type,每次log输出之前,计算下次检查时间nextCheck,对比当前时间,判断是否进行文件转存。

主要方法有

//各级检查周期对应的常量
// The code assumes that the following constants are in a increasing sequence.
  static final int TOP_OF_TROUBLE=-1;
  static final int TOP_OF_MINUTE = 0;
  static final int TOP_OF_HOUR   = 1;
  static final int HALF_DAY      = 2;
  static final int TOP_OF_DAY    = 3;
  static final int TOP_OF_WEEK   = 4;
  static final int TOP_OF_MONTH  = 5;

//初始化配置项
public void activateOptions() {
    super.activateOptions();
    if(datePattern != null && fileName != null) {
      now.setTime(System.currentTimeMillis());
      sdf = new SimpleDateFormat(datePattern);
      int type = computeCheckPeriod();//计算datePattern对应的检查周期
      printPeriodicity(type);//打印当前检查周期
      rc.setType(type);//内部RollingCalendar会在log输出之前根据type计算出下次检查时间
      File file = new File(fileName);//log输出文件名
      scheduledFilename = fileName+sdf.format(new Date(file.lastModified()));//log转存文件名

    } else {
      LogLog.error("Either File or DatePattern options are not set for appender ["
           +name+"].");
    }
  }

//初始化配置时,计算检查周期
int computeCheckPeriod() {
    RollingCalendar rollingCalendar = new RollingCalendar(gmtTimeZone, Locale.getDefault());
    // set sate to 1970-01-01 00:00:00 GMT
    Date epoch = new Date(0);
    if(datePattern != null) {
      for(int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
        simpleDateFormat.setTimeZone(gmtTimeZone); // do all date formatting in GMT
        String r0 = simpleDateFormat.format(epoch);
        rollingCalendar.setType(i);
        Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
        String r1 =  simpleDateFormat.format(next);
        //r0、r1均以datePattern格式来转换日期,若type小于datePattern表示的最小范围,对应日期next的变化不会影响格式化后的r1的值
        //每循环一次,type(也就是i) 增1,最终得到的type就是datePattern表示的最小范围
        if(r0 != null && r1 != null && !r0.equals(r1)) {
          return i;
        }
      }
    }
    return TOP_OF_TROUBLE; // Deliberately head for trouble...
  }

  //log输出
  protected void subAppend(LoggingEvent event) {
    //在每次调用父类subAppend方法输出文件之前,进行周期计算
    //若当前时间晚于'检查点时间',调用rollOver()方法进行日志转存,将当前log文件转存为指定日期结尾的文件,然后将父类的QuietWriter指向新的log文件
    //当然在转存之前,需要再次计算并刷新'检查点时间',rc内部type会影响计算结果(在初始化配置时已根据datePattern计算得到)
    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);
   }

RollingFileAppender

同样继承于FileAppender,由文件大小来转存log文件

ExternallyRolledFileAppender

继承于RollingFileAppender,通过Socket监听转存消息来进行转存操作,后台运行着一个Socket监听线程,每次收到转存消息,会新起一个线程进行日志转存,并将转存结果信息返回。

不足

只是介绍了关键的一些类,但他们的生命周期,相关的属性类和辅助类还没提到,主要是Filter和Layout,下次再更新。
还有上面几个关键方法中的同步关键字,我还没搞懂应该怎么解释。

log4j是一个日志管理工具,其中DailyRollingFileAppender和RollingFileAppender都是log4j中的文件输出器,用于将日志输出到文件。 DailyRollingFileAppender按照时间切割日志文件,例如每天或每小时生成一个新的日志文件,以避免单个日志文件过大。使用DailyRollingFileAppender时,需要指定日志文件的文件名、日期格式和日志文件最大大小等参数。 例如,下面的配置将在每天生成一个新的日志文件,并将日志文件名设置为"app.log": ``` log4j.appender.myAppender=org.apache.log4j.DailyRollingFileAppender log4j.appender.myAppender.File=app.log log4j.appender.myAppender.DatePattern='.'yyyy-MM-dd log4j.appender.myAppender.Threshold=DEBUG log4j.appender.myAppender.layout=org.apache.log4j.PatternLayout log4j.appender.myAppender.layout.ConversionPattern=%d [%t] %-5p %c - %m%n ``` RollingFileAppender也是按照一定的规则切割日志文件,但是与DailyRollingFileAppender不同的是,RollingFileAppender是按照日志文件大小来切割日志文件。当一个日志文件达到指定的大小时,将自动创建一个新的日志文件,并将日志输出到新的文件中。使用RollingFileAppender时,需要指定日志文件的文件名、最大文件大小、最大备份数量等参数。 例如,下面的配置将在每个日志文件达到100MB时生成一个新的日志文件,最多保留5个备份: ``` log4j.appender.myAppender=org.apache.log4j.RollingFileAppender log4j.appender.myAppender.File=app.log log4j.appender.myAppender.MaxFileSize=100MB log4j.appender.myAppender.MaxBackupIndex=5 log4j.appender.myAppender.Threshold=DEBUG log4j.appender.myAppender.layout=org.apache.log4j.PatternLayout log4j.appender.myAppender.layout.ConversionPattern=%d [%t] %-5p %c - %m%n ``` 总的来说,DailyRollingFileAppender和RollingFileAppender都是log4j中非常实用的日志文件输出器,可以根据实际需求选择使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值