这篇文章笔者准备和大家介绍一下日志框架Log4j,Log4j日志框架已经很老了,但是后面的日志框架和Log4j都差不多,所以Log4j还是一个很好的学习对象。大家在写代码的过程中肯定会使用到日志框架。大家应该都会用,但是用的对不对,等这个专题完成了相信读者就心里有数了。我们先看一张图
我们在使用的时候接触到的应该是Logger,然后调用Logger的方法输出日志。笔者先把图上出现的对象介绍一些以及捋一捋他们之间的关系。(下面很重要)
1.在我们打印日志的时候会输入需要打印的信息,这个信息就是我们的信息源,也就是上图中的LoggerEvent。
2.我们获取Logger的时候会传入对应的类的全限定名称,其实是通过这个名称去LoggerReposity中取对应的Logger(也就是说LoggerReposity是我们的一个Logger的容器)
3.获取到Logger之后我们要去输出日志,输出的目的地很多可能是控制台、文件、远程对象。可能一行日志我们要输出到多个地方,这个时候我们就要获取到Logger中所有的Appender的集合,然后遍历去调用输出方法。
4.输出的时候我们会指定日志的格式,比如说时间、线程、所属类。这里我们就需要格式化每个Appender都会有对应的格式化也就是Layout(对应一个)。
5.有可能在打印日志的时候需要一些自定义的处理,那我们就需要拦截需要打印的消息,根据消息做一些操作,这个就是Filter(可能有多个也可能没有)。
6.可以看到在我们输出日志的时候LoggerEvent实际上是在整个流程中传递的,可以看成这个是上下文。
在实际使用的时候我们更多的是使用slf4j来获取Logger对象,因为在项目中途可能需要换日志框架比如从log4j换成logback,如果我们使用的对应日志框架中的API,那就尴尬了,我的项目那么多类都得改代码。所以slf4j就出来了,它主要使用的是适配器模式。在项目中调用的API是slf4j的API,它在会根据实际使用的日志框架适配成统一的调用格式。也就是方便项目在中途换日志框架了。在log4j分析差不多之后,笔者就简单分析slf4j。
Log4j的主要源码还是比较少的。笔者就直接根据上面的图进行介绍,首先我们来看一下Logger类。
首先看一下Logger的结构关系,继承了Category实现了AppenderAttachable。从类名来看Category这个应该是根据不同种类打印日志,AppenderAttachable这个应该是连接Appender。我们从AppenderAttachable开始看
确实有一个对应的实现类。这个里面还有另外一些成员变量,
name:Logger对应的名称
level:日志级别
parent:Logger的父亲(继承关系,如果子Logger没有指定Appender默认使用父亲的)
FQCN:全限名称指定了当前类
resourceBundle:国际化相关
repository:所属的容器,
aai:相关的Appender
additve:这个变量就是控制是否使用父类的Appender
成员变量介绍完了,这个类的方法太多了。Logger主要的方法也是在这个类里面,我们先看看Logger的相关方法,然后在回来介绍这个
方法不多,主要就是获取Logger变量(从Logger容器中获取)。我们回去分析它的父类。先来介绍关联Appender类AppenderAttachableImpl实现了AppenderAttachable。这个类只有一个成员变量。
protected Vector appenderList; //Appender集合
然后我们在看实现的方法,这里的方法笔者不打算全部介绍,因为就是简单的操作Vector。
public
void addAppender(Appender newAppender) { //添加Appender
if(newAppender == null)
return;
if(appenderList == null) { //如果appenderList为空初始化
appenderList = new Vector(1);
}
if(!appenderList.contains(newAppender)) //如果不包含添加
appenderList.addElement(newAppender);
}
其他的方法也都差不多就是操作这个集合,到这里读者肯定已经发现了Logger和Appender之间的关系是一对多。我们后面通过方法来进行分析,就先分析常用的方法
public
void debug(Object message) {
if(repository.isDisabled(Level.DEBUG_INT)) //判断配置的根日志级别是否可以输出
return;
if(Level.DEBUG.isGreaterOrEqual(this.getEffectiveLevel())) { //Debug级别是否可以在当前日志输出
forcedLog(FQCN, Level.DEBUG, message, null); //输出日志
}
}
public
Level getEffectiveLevel() {
for(Category c = this; c != null; c=c.parent) { //如果当前类没有级别获取父类的
if(c.level != null)
return c.level;
}
return null; // If reached will cause an NullPointerException.
}
首先判断根日志级别可否输出,默认是debug的,然后我们在调用getEffectiveLevel方法获取Logger设置的输出日志级别,如果当前的Logger没有设置则获取父类的级别。如果日志级别大于或者等于则说明可以输出日志调用forcedLog输出日志。
protected
void forcedLog(String fqcn, Priority level, Object message, Throwable t) {
callAppenders(new LoggingEvent(fqcn, this, level, message, t)); //创建日志并appender
}
public
void callAppenders(LoggingEvent event) {
int writes = 0;
for(Category c = this; c != null; c=c.parent) { //遍历类本身以及父类
// Protected against simultaneous call to addAppender, removeAppender,...
synchronized(c) {
if(c.aai != null) { //如果有Appender则调用输出
writes += c.aai.appendLoopOnAppenders(event);
}
if(!c.additive) { //如果不能使用父类的Appender则直接退出循环
break;
}
}
}
if(writes == 0) { //处理没有Appender的情况
repository.emitNoAppenderWarning(this);
}
}
这里创建了一个LoggerEvent并调用方法获取对应的Appender进行输出,
public
int appendLoopOnAppenders(LoggingEvent event) { //循环Appender
int size = 0;
Appender appender;
if(appenderList != null) {
size = appenderList.size();
for(int i = 0; i < size; i++) {
appender = (Appender) appenderList.elementAt(i); //获取所有Appender
appender.doAppend(event); //执行输出
}
}
return size; //返回appender的个数
}
到这里我们就跟踪完了Logger.debug()的方法,后面doAppend就是Appender的方法了,后面再介绍。这样其实我们已经把Logger介绍完了,其他的输出方法都差不多。后面我们的注意点应该就是在Appender、LoggerRepository了。我们来看看
Appender先来看接口的方法
根据方法名我们可以看到有setFilter、setErrorHandler、setLayout。读者应该就可以猜到Appdener的组成了,包括Filter、Layout、以及ErrorHandler。我们来看实现类AppenderSkeleton,这个类是一个抽象类。这个类用了模板模式,将公共的方法抽出来在父类,父类调用子类的具体实现。我们先来看看对应的成员变量
protected Layout layout; //布局
protected String name; //名称
protected Priority threshold; //日志级别比这个大才会打出
protected ErrorHandler errorHandler = new OnlyOnceErrorHandler();
protected Filter headFilter; //第一个拦截器
protected Filter tailFilter;//最后一个拦截器
protected boolean closed = false; //Appender是否关闭
成员变量应该不难理解,成员变量对应一些set、get方法这里就不做介绍。咱直接接着Logger调用的方法来分析。
public
synchronized
void doAppend(LoggingEvent event) {
if(closed) {
LogLog.error("Attempted to append to closed appender named ["+name+"].");
return;
}
if(!isAsSevereAsThreshold(event.getLevel())) { //判断日志级别,如果不满足直接return
return;
}
Filter f = this.headFilter; //拦截器
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);
}
这里先判断对应的日志级别是否能输出,如果不能输出就直接返回,能输出就先获取拦截器。拦截器有三种状态
DENY:拒绝 即不处理直接return。后面的拦截器也不会处理了,就是说不符合条件直接返回了
ACCEPT:接受 即满足条件我直接使用Appender去处理,不会再调用后面的拦截器了
NEUTRAL:中立 拦截器处理了一些事情,后面的拦截器继续处理。
上面拦截器应该很好理解,这里用了责任链模式,责任链模式是常用的设计模式之一,一个拦截器一个拦截器往下处理。最后调用了append方法这个方法是一个抽象方法也就是子类实现了。模板方法的设计模式就体现出来了,不关心子类怎么实现(也是多态的体现)。到这里我们就要去子类里面看对应的实现我们看WriterAppender这个子类,这个子类还有对应的子类,是我们常用的Console、File模式。我们看下成员变量。
protected boolean immediateFlush = true; //写完一条日志立刻刷新标志
protected String encoding; //编码方式
protected QuietWriter qw; //写者
三个成员变量,主要注意的是qw这个是实际的写者,我们可以根据不同的输出创建不同的写者,比如文件,那就是使用输出流创建一个写文件的写者,如果是Console那就是System.out创建写者
public
void append(LoggingEvent event) {
if(!checkEntryConditions()) {
return;
}
subAppend(event);
}
protected
boolean checkEntryConditions() { //检查
if(this.closed) {
LogLog.warn("Not allowed to write to a closed appender.");
return false;
}
if(this.qw == null) {
errorHandler.error("No output stream or file set for the appender named ["+
name+"].");
return false;
}
if(this.layout == null) {
errorHandler.error("No layout set for the appender named ["+ name+"].");
return false;
}
return true;
}
首先检查变量,检查是否关闭、写者是否为空、布局是否为空、三个条件一个不满足就不写了。
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(); //刷新流
}
}
这里就把日志格式化之后输出了。整个大体流程就串完了,还是比较简单的,后面就是具体的一些实现了,比如解析配置文件初始化容器、初始化Logger等一系列操作,下一篇就介绍一下初始化容器吧。码字不易、右下角点个好看哈.
笔者主要还是在微信公众号写文章,记得关注哈