资讯专栏INFORMATION COLUMN

Log4j DailyRollingFileAppender源码初探

warkiz / 2650人阅读

摘要:是否覆盖目标文件名是否缓冲默认缓冲区大小既然缓冲了,那意味着父类中的刷新控制为不进行同步刷新利用父类中的字节流字符流转换方法实例化父类中的实际在上面指向了文件输出流继承,将文件进行日常转存。

瞎扯

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

目前的版本是 log4j:log4j:1.2.17

依赖结构

Appender接口

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

过滤链

log输出

错误处理

log格式

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;

* 提供了字节流->字符流的转换
* 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,下次再更新。
还有上面几个关键方法中的同步关键字,我还没搞懂应该怎么解释。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/65203.html

相关文章

  • LOG4J和SLF4J的使用和原理

    摘要:和在通常系统中,日志功能使用了,对于这两者的区别不甚了解,通过实践,并深入源代码层次的分析,希望能够讲的清晰一些。基本原理在项目中使用到的包主要有和三个。 LOG4J和SLF4J 在通常系统中,日志功能使用了log4j+slf4j,对于这两者的区别不甚了解,通过实践,并深入源代码层次的分析,希望能够讲的清晰一些。 基本原理 在项目中使用到的jar包主要有log4j.jar , slf4...

    1treeS 评论0 收藏0
  • spring进步 -- log4j的学习

    摘要:建议只使用四个级别,优先级从高到低分别是。比如在这里定义了级别,只有等于及高于这个级别的才进行处理,则应用程序中所有级别的日志信息将不被打印出来。可同时指定多个输出目的地。 一直感觉到log4j是使用比较混乱,今天抽空整理一下,以后方便使用 一、引用apache.log4j 使用maven进行lo4j的引用 log4j log4j 1.2.17 其他版本也...

    edgardeng 评论0 收藏0
  • LogBack与Log4j配置与日志分模块打印

    摘要:如果日志级别等于配置级别,过滤器会根据和接收或拒绝日志。例如过滤掉所有低于级别的日志。有个子标签,用于配置求值条件。 没时间解释了,快上车,老司机先看代码 LogBack.xml DEBUG ${MESSAGE_FILE_PATTERN} ...

    kycool 评论0 收藏0
  • spring-springmvc-mybatis-shiro项目介绍

    摘要:项目介绍在之前的整合项目之后,新增日志简单集成,之前的代码不予展示与介绍,想了解的请参考整合项目项目代码获取项目结构代码控制层,,主要包含登录及几个页面跳转会跳到我们自定义的中登录用户名或密码错误业务处理层,包含一个包,以接口类型存在 spring-springmvc-mybatis-shiro项目介绍 在之前的mybatis整合项目之后,新增日志、简单集成shiro,之前的代码不予展...

    fanux 评论0 收藏0

发表评论

0条评论

warkiz

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<