在logback日志框架中,每一个Logger都会去关联一个Appender,Appender是真正把日志内容输出到控制台,文件等,可以说在日常应用中非常重要和拓展的一个组件。
- 首先看一下
Appender
接口
public interface Appender<E> extends LifeCycle, ContextAware, FilterAttachable<E> {
/**
* Get the name of this appender. The name uniquely identifies the appender.
*/
String getName();
/**
* This is where an appender accomplishes its work. Note that the argument
* is of type Object.
* @param event
*/
void doAppend(E event) throws LogbackException;
/**
* Set the name of this appender. The name is used by other components to
* identify this appender.
*
*/
void setName(String name);
}
其中最重要的就是doAppend()
方法,每一个日志事件输出,都会调用到这个接口。
- 接着继续看实现
Appender
的接口的继承图
其中,有我们熟悉的ConsoleAppender
和FileAppender
,还有RollingFileAppender
。另外,有一个很重要的AsyncAppender
接口,异步输出,这也是logback日志受欢迎的一个非常重要的特性。他们都继承与抽象类UnsynchronizedAppenderBase
类。
在UnsynchronizedAppenderBase
中,我们看一下其中实现了最重要的一个方法doAppend()
public void doAppend(E eventObject) {
// WARNING: The guard check MUST be the first statement in the
// doAppend() method.
// prevent re-entry.
if (Boolean.TRUE.equals(guard.get())) {
return;
}
try {
guard.set(Boolean.TRUE);
if (!this.started) {
if (statusRepeatCount++ < ALLOWED_REPEATS) {
addStatus(new WarnStatus("Attempted to append to non started appender [" + name + "].", this));
}
return;
}
if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
return;
}
// ok, we now invoke derived class' implementation of append
this.append(eventObject);
} catch (Exception e) {
if (exceptionCount++ < ALLOWED_REPEATS) {
addError("Appender [" + name + "] failed to append.", e);
}
} finally {
guard.set(Boolean.FALSE);
}
}
其实这个方法没有东西也没有做,只是使用模版方法,让具体的子类交给子类的append()
方法实现具体的逻辑。
- 接着继续看
OutputStreamAppender
类
在OutputStreamAppender
类中,有几个比较重要的成员变量。
protected Encoder<E> encoder;
private OutputStream outputStream;
其中encoder
最主要的作用就是把日志事件要输出的内容变成字节数组,然后把字节数组写入到outputStream
。
其中outputStream
可以是控制台或者文件等。
接着看append()
方法
@Override
protected void append(E eventObject) {
if (!isStarted()) {
return;
}
subAppend(eventObject);
}
这个方法也是没有做什么,接着具体看subAppend()
方法。
protected void subAppend(E event) {
if (!isStarted()) {
return;
}
try {
// this step avoids LBCLASSIC-139
if (event instanceof DeferredProcessingAware) {
((DeferredProcessingAware) event).prepareForDeferredProcessing();
}
// the synchronization prevents the OutputStream from being closed while we
// are writing. It also prevents multiple threads from entering the same
// converter. Converters assume that they are in a synchronized block.
// lock.lock();
// 这一步就是encoder的作用
byte[] byteArray = this.encoder.encode(event);
// 然后接着把字节数组写入到outputStream
writeBytes(byteArray);
} catch (IOException ioe) {
// as soon as an exception occurs, move to non-started state
// and add a single ErrorStatus to the SM.
this.started = false;
addStatus(new ErrorStatus("IO failure in appender", this, ioe));
}
}
在这个方法中,可以看到encoder
的作用,同时还有一个重要的方法writeBytes()
private void writeBytes(byte[] byteArray) throws IOException {
if(byteArray == null || byteArray.length == 0)
return;
lock.lock();
try {
this.outputStream.write(byteArray);
if (immediateFlush) {
this.outputStream.flush();
}
} finally {
lock.unlock();
}
}
这个方法就是把字节数组写入到outputStream中。
总结一下:一个日志事件记录的流程就是,要需要输出的内容通过encoder变成字节数组,然后写入到具体的输出流中。
- 接着看一下最简单的
ConsoleAppender
ConsoleAppender
继承了OutputStreamAppender
,唯一不一样的就是输出的位置,ConsoleAppender
是输出到控制台,outputStream输出流也应该是控制台。
protected ConsoleTarget target = ConsoleTarget.SystemOut;
@Override
public void start() {
OutputStream targetStream = target.getStream();
// enable jansi only on Windows and only if withJansi set to true
if (EnvUtil.isWindows() && withJansi) {
targetStream = getTargetStreamForWindows(targetStream);
}
setOutputStream(targetStream);
super.start();
}
其中,start()
方法会在初始化logback.xml配置的时候去调用,在start()
方法中就设置了outputStream输出流——控制台。
- 接下来看
FileAppender
FileAppender
,如果一个日志事件需要输出到文件中,那么它就需要一个文件输出流FileOutStream。
protected String fileName = null;
需要设置一个文件名。在看看start()
方法对FileAppender
进行初始化工作。
public void start() {
int errors = 0;
if (getFile() != null) {
addInfo("File property is set to [" + fileName + "]");
if (prudent) {
if (!isAppend()) {
setAppend(true);
addWarn("Setting \"Append\" property to true on account of \"Prudent\" mode");
}
}
if (checkForFileCollisionInPreviousFileAppenders()) {
addError("Collisions detected with FileAppender/RollingAppender instances defined earlier. Aborting.");
addError(MORE_INFO_PREFIX + COLLISION_WITH_EARLIER_APPENDER_URL);
errors++;
} else {
// file should be opened only if collision free
try {
openFile(getFile());
} catch (java.io.IOException e) {
errors++;
addError("openFile(" + fileName + "," + append + ") call failed.", e);
}
}
} else {
errors++;
addError("\"File\" property not set for appender named [" + name + "].");
}
if (errors == 0) {
super.start();
}
}
主要看openFile()
方法
public void openFile(String file_name) throws IOException {
lock.lock();
try {
File file = new File(file_name);
boolean result = FileUtil.createMissingParentDirectories(file);
if (!result) {
addError("Failed to create parent directories for [" + file.getAbsolutePath() + "]");
}
ResilientFileOutputStream resilientFos = new ResilientFileOutputStream(file, append, bufferSize.getSize());
resilientFos.setContext(context);
setOutputStream(resilientFos);
} finally {
lock.unlock();
}
}
在这个方法中,通过文件名去new File()
文件,然后封装到ResilientFileOutputStream
中,然后设置setOutputStream()
,这样子就可以把日志输出到文件中了。
- 接着看异步的
AsyncAppenderBase
异步输出,也就意味着会把日志事件先放入到一个队列中,然后由专门的线程去队列中拿日志事件消费。
看类AsyncAppenderBase
中几个重要的成员变量
AppenderAttachableImpl<E> aai = new AppenderAttachableImpl<E>();
BlockingQueue<E> blockingQueue;
Worker worker = new Worker();
AppenderAttachableImpl
里面包含一个Appender列表,列表是具体的Appender,如上面讲到的ConsoleAppender和FileAppender。
BlockingQueue是一个队列,Worker是一个消费线程。
例如,在配置文件中定义一个异步的Appender
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- 日志输出编码 -->
<Encoding>UTF-8</Encoding>
<encoder>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
</appender>
<appender name="asyncStdoutAppender" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref = "STDOUT"/>
</appender>
接下来看看具体是如何实现异步输出的。
public void start() {
if (isStarted())
return;
if (appenderCount == 0) {
addError("No attached appenders found.");
return;
}
if (queueSize < 1) {
addError("Invalid queue size [" + queueSize + "]");
return;
}
blockingQueue = new ArrayBlockingQueue<E>(queueSize);
if (discardingThreshold == UNDEFINED)
discardingThreshold = queueSize / 5;
addInfo("Setting discardingThreshold to " + discardingThreshold);
worker.setDaemon(true);
worker.setName("AsyncAppender-Worker-" + getName());
// make sure this instance is marked as "started" before staring the worker Thread
super.start();
worker.start();
}
start()
方法初始化中,定义了一个ArrayBlockingQueue
队列,以及启动了一个worker线程。
接着看append()
方法
@Override
protected void append(E eventObject) {
if (isQueueBelowDiscardingThreshold() && isDiscardable(eventObject)) {
return;
}
preprocess(eventObject);
put(eventObject);
}
在append()
方法中,并没有马上处理日志事件,而是调用put()
方法
private void put(E eventObject) {
if (neverBlock) {
blockingQueue.offer(eventObject);
} else {
putUninterruptibly(eventObject);
}
}
接着把日志事件放进去消息队列中。然后由Worker
线程去处理
class Worker extends Thread {
public void run() {
AsyncAppenderBase<E> parent = AsyncAppenderBase.this;
AppenderAttachableImpl<E> aai = parent.aai;
// loop while the parent is started
while (parent.isStarted()) {
try {
E e = parent.blockingQueue.take();
aai.appendLoopOnAppenders(e);
} catch (InterruptedException ie) {
break;
}
}
addInfo("Worker thread will flush remaining events before exiting. ");
for (E e : parent.blockingQueue) {
aai.appendLoopOnAppenders(e);
parent.blockingQueue.remove(e);
}
aai.detachAndStopAllAppenders();
}
}
Worker
线程从队列中take()取出事件,然后由具体的Appender去处理
public int appendLoopOnAppenders(E e) {
int size = 0;
final Appender<E>[] appenderArray = appenderList.asTypedArray();
final int len = appenderArray.length;
for (int i = 0; i < len; i++) {
appenderArray[i].doAppend(e);
size++;
}
return size;
}
看到调用doAppend()
,已经和之前的Appender是一样的啦。
本文是笔者个人的理解,若有不正确指出请多多纠正。
网友评论