Log4j对Java开发者来讲是常常使用到的日志框架,我每次使用都对它的配置文件头大,网上搜一个别人的例子本身改巴改巴,草草了事。再次使用时,又忘了怎么回事了。此次忽然来了兴趣,想看看它具体是怎么作的,作个笔记,加深一下印象。并发
目前的版本是 log4j:log4j:1.2.17app
Log4j的输出类都须要实现的接口,为了用户自定义log输出策略,抽象出了如下几点功能框架
这个接口只定义了一个方法 void activateOptions();
,用于按需初始化一些配置。ide
既然是Skeleton,那它必须是最核心的骨架。这个类主要作了如下几个事ui
过滤链(链表)增删操做this
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);
}spa
* 下放的权限
//子类只须要关心日志具体的输出方式 abstract protected void append(LoggingEvent event); //配置方法,子类能够按本身的需求覆写 public void activateOptions() {} ````
继承AppenderSkeleton,用户可选择将log按字符流或字节流输出。增长了如下特性线程
提供了静态字符流QuitWriter,异常不会抛出,会交给ErrorHandler去处理debug
//默认实时刷新,效率低但可保证每次输出都可写入,设为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,下次再更新。 还有上面几个关键方法中的同步关键字,我还没搞懂应该怎么解释。