美文网首页logback
logback之Appender源码解读

logback之Appender源码解读

作者: 曾泽浩 | 来源:发表于2019-11-16 16:44 被阅读0次

    在logback日志框架中,每一个Logger都会去关联一个Appender,Appender是真正把日志内容输出到控制台,文件等,可以说在日常应用中非常重要和拓展的一个组件。

    1. 首先看一下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()方法,每一个日志事件输出,都会调用到这个接口。

    1. 接着继续看实现Appender的接口的继承图
    Appender接口继承图

    其中,有我们熟悉的ConsoleAppenderFileAppender,还有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()方法实现具体的逻辑。

    1. 接着继续看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变成字节数组,然后写入到具体的输出流中。

    1. 接着看一下最简单的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输出流——控制台。

    1. 接下来看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(),这样子就可以把日志输出到文件中了。

    1. 接着看异步的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是一样的啦。
    本文是笔者个人的理解,若有不正确指出请多多纠正。

    相关文章

      网友评论

        本文标题:logback之Appender源码解读

        本文链接:https://www.haomeiwen.com/subject/lciyictx.html