SEO外包平台,我们为您提供专业的企业网站SEO整站优化外包服务 SEO设置

SEO外包平台

专注于企业网站SEO整站优化外包服务

Logback日志框架-日志打印过程及Logger继承特性源码分析

作者:jcmp      发布时间:2021-05-06      浏览量:0
摘要上一篇 《Logback日志框架

摘要

上一篇 《Logback日志框架初始化全过程源码解析》 分析了Logback的整个初始化过程以及Logger的创建,这篇文章将继续分析Logger的打印过程,上篇文章没有介绍Logback的架构,所以我们先会简单介绍下Logback的基础组成模块;然后再介绍下logback.xml 配置文件中的主要元素和他们之间的协作关系,重点说明下Logger的继承特性;接着梳理下整个日志打印过程,带着这些概念进入源码去一一验证,最后做个简单的总结。

正文

Logback基础组成模块

Logback主要分为3个模块:

一般使用Logback时我们会引用slf4j-api、logback-core和logback-classic等3个依赖。

Logback配置

logback 提供多种配置方式:

配置文件其实也提供2类:logback.grovy和logback.xml。

两者大同小异,只是语法不同而已,在这里我们选择比较熟悉和常用的logback.xml进行简单解释。

${pattern} ${fileName:-demo.log} ${pattern}

logback.xml中我们主要看6个xml元素:

appender

appender主要用来指定日志信息最终输出到什么位置,console、file或者socket等。

layout

layout用来根据pattern规则格式化日志内容,将日志event转换为String类型,然后通过java.io.Writer输出。

encoder

encoder是0.9.19版本之后出现的,她和layout作用本质上是相同的,都会通过pattern规则将日志内容格式化,不同点是,encoder将日志event转化为byte数组,然后写到OutputStream。

0.9.19版本之后官方推荐encoder来代替layout,具体原因,官方给出如下解释:

Why the breaking change?Layouts, as discussed in detail in the next chapter, are only able to transform an event into a String. Moreover, given that a layout has no control over when events get written out, layouts cannot aggregate events into batches. Contrast this with encoders which not only have total control over the format of the bytes written out, but also control when (and if) those bytes get written out.

意思是说,Layouts只能将日志event转化为String,不能够控制何时将日志event写出,无法将多个event集合到一组。而encoder不仅能控制日志bytes数据的格式,还能控制是否以及何时写出数据。简单来讲就是encoder比layout牛逼,能用encoder就不用layout。当然对于之前的使用layout的老代码,官方也提供一些适配class例如 LayoutWrappingEncoder。

pattern

pattern主要用来定义日志的输出格式等,详见 PatternLayout 。

logger

logger可以与我代码中LoggerFactory.getLogger产生的Logger对象相对应,她有三个属性:

appender-ref

appender-ref作用是将一个logger对象和具体的appender绑定,指定该logger将通过哪个appender将日志输出到何处。一个logger可存在多个 appender-ref ,也就是说可以同时绑定多个appender。

root

root其实是个特殊的logger,看名字就知道她是一个根节点,是所有logger的祖先,配置文件中必须得有一个root。

日志打印源码分析

过滤和创建LogEvent

这里我们以我们常用的logger.info() 为入口,info()中实际是调用filterAndLog_1()方法,

这个方法做2件事,过滤和创建LoggingEvent并推送给实际绑定的Appender对象。

public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable, Serializable { public void info(String format, Object arg) { filterAndLog_1(FQCN, null, Level.INFO, format, arg, null); } private void filterAndLog_1(final String localFQCN, final Marker marker, final Level level, final String msg, final Object param, final Throwable t) { /** * 这里先经过一串过滤器处理,根据传入的marker满足的条件不同返回不同的结果, * FilterReply.NEUTRAL 表示中立的意思 */ final FilterReply decision = loggerContext.getTurboFilterChainDecision_1(marker, this, level, msg, param, t); /** * 过滤后返回NEUTRAL则进一步判断当前想要打印的日志等级"INFO"是否高于或者等于有效日志级别。 * 如果小于有效日志级别,则表示该日志无效,直接返回,不做具体输出动作。 */ if (decision == FilterReply.NEUTRAL) { if (effectiveLevelInt > level.levelInt) { return; } } else if (decision == FilterReply.DENY) { return; } buildLoggingEventAndAppend(localFQCN, marker, level, msg, new Object[] { param }, t); } /** * 创建日志Event, 推送到与logger绑定的所有appender中 */ private void buildLoggingEventAndAppend( final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params, final Throwable t) { // 创建日志Event LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params); le.setMarker(marker); //推送到与logger绑定的所有appender中 callAppenders(le); } public void callAppenders(ILoggingEvent event) { int writes = 0; /** * 这里有个熟悉的单词additive,上面我们介绍logger的时候说到, * 她是logger的一个属性,默认为true * 结合这段代码我们看下,for循环中,先将event, 推给当前logger的Appenders中,并且累加 * writes,紧接着判断additive, * 如果为true则,取logger的父级,继续将event推给父级logger的Appender中, * 以此类推,直至遇到某个父级或者祖先的additive为false,则break跳出循环, * 或者 送到root日志根节点为止。 * 这几解释了additive的作用了。 */ for (Logger l = this; l != null; l = l.parent) { writes += l.appendLoopOnAppenders(event); if (!l.additive) { break; } } // No appenders in hierarchy if (writes == 0) { loggerContext.noAppenderDefinedWarning(this); } } private int appendLoopOnAppenders(ILoggingEvent event) { if (aai != null) { return aai.appendLoopOnAppenders(event); } else { return 0; } } /** * appenderList是与Logger绑定的Appender * 例如上述logback.xml中name="FILE"的logger绑定的 * Appender 就是ch.qos.logback.core.FileAppender类。 * 因为每个logger可以同时绑定多个Appender所以这里用数组appenderList来存储这些Appender。 */ public int appendLoopOnAppenders(E e) { int size = 0; final Appender[] appenderArray = appenderList.asTypedArray(); final int len = appenderArray.length; for (int i = 0; i < len; i++) { appenderArray[i].doAppend(e); size++; } return size; }}

通过FileAppender打印到文件

logback.xml中name="FILE"的logger绑定的 Appender 是ch.qos.logback.core.FileAppender类,那么我们就以FileAppender为例分析下日志写入到文件的整个流程。

FIleAppender类的继承关系图:

整个流程如下:

FileAppender 指定日志打印的目标文件,并指定文件的输出流OutputStream给 OutputStreamAppender ,

目标文件的指定动作是发生在logger的初始化过程中,通过加载解析logback.xml找到name="FILE"的元素,读取子元素的值来获取文件;

OutputStreamAppender 提供一个基础服务,她继承 UnsynchronizedAppenderBase 抽象类并实现其中的。

append(E eventObject) 抽象方法,进行具体的日志格式化和打印动作,其实就是向 FileAppender 提供的输出流中输入格式化后的日志;

通过名称可看出 UnsynchronizedAppenderBase 并未做线程安全方面的措施,而是将线程安全方面的问题交给子类自己去处理, OutputStreamAppender 确实也自己处理了,她通过在 writeBytes(byte[] byteArray) 方法中加入 ReentrantLock 重入锁来保证日志写入的线程安全。

接下来我们从下到上依次来分析下几个类中重要的方法:

FileAppender

public class FileAppender extends OutputStreamAppender { ... /** * */ public void start() { int errors = 0; if (getFile() != null) { ... /** * 检查该日志文件是否以及被其他Appender绑定了,如果没被绑定则返回false */ if (checkForFileCollisionInPreviousFileAppenders()) { addError("Collisions detected with FileAppender/RollingAppender instances defined earlier. Aborting."); addError(MORE_INFO_PREFIX + COLLISION_WITH_EARLIER_APPENDER_URL); errors++; } else { try { /** * 打开未被其他Appender绑定的文件输出流给OutputStreamAppender, * 用于后续的日志输入。 */ 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(); } } /** * 检查该日志文件是否以及被其他Appender绑定了,如果是的话就返回true * 否则记录appender的名称和日志文件名称,表示当前文件已经被Appender绑定了, * 不能再和别的Appender绑定,然后返回false */ protected boolean checkForFileCollisionInPreviousFileAppenders() { boolean collisionsDetected = false; if (fileName == null) { return false; } @SuppressWarnings("unchecked") Map map = (Map) context.getObject(CoreConstants.FA_FILENAME_COLLISION_MAP); if (map == null) { return collisionsDetected; } for (Entry entry : map.entrySet()) { if (fileName.equals(entry.getValue())) { addErrorForCollision("File", entry.getValue(), entry.getKey()); collisionsDetected = true; } } if (name != null) { map.put(getName(), fileName); } return collisionsDetected; } /** * 创建个ResilientFileOutputStream输出流, * 给OutputStreamAppender中的,outputStream成员变量; * 文件必须是可写的; * 虽然该方法是public的但是不要直接调用, * 而是通过start方法一步步设置还成员变量的后再调用 */ 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(); } } ...}

OutputStreamAppender

public class OutputStreamAppender extends UnsynchronizedAppenderBase { protected Encoder encoder; protected final ReentrantLock lock = new ReentrantLock(false); private OutputStream outputStream; boolean immediateFlush = true; @Override protected void append(E eventObject) { if (!isStarted()) { return; } subAppend(eventObject); } /** * 真正的的写入操作 *

* Most subclasses of WriterAppender will need to override this * method. */ protected void subAppend(E event) { if (!isStarted()) { return; } try { // this step avoids LBCLASSIC-139 if (event instanceof DeferredProcessingAware) { ((DeferredProcessingAware) event).prepareForDeferredProcessing(); } // 格式化日志文件 byte[] byteArray = this.encoder.encode(event); // 写入日志到特定的输出流中,使用ReentrantLock保证线程安全 writeBytes(byteArray); } catch (IOException ioe) { this.started = false; addStatus(new ErrorStatus("IO failure in appender", this, ioe)); } } /** * 传入一个已经打开的outputStream, * 例如FileAppender 中打开的ResilientFileOutputStream */ public void setOutputStream(OutputStream outputStream) { lock.lock(); try { // close any previously opened output stream closeOutputStream(); this.outputStream = outputStream; if (encoder == null) { addWarn("Encoder has not been set. Cannot invoke its init method."); return; } encoderInit(); } finally { lock.unlock(); } } ... /** * 写入日志到特定的输出流中,使用ReentrantLock保证线程安全 */ 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(); } }}

UnsynchronizedAppenderBase

abstract public class UnsynchronizedAppenderBase extends ContextAwareBase implements Appender { protected boolean started = false; /** * guard 用来防止appender 重复调用自己的doAppend方法 */ private ThreadLocal guard = new ThreadLocal(); /** * Appenders are named. */ protected String name; private FilterAttachableImpl fai = new FilterAttachableImpl(); private int statusRepeatCount = 0; private int exceptionCount = 0; static final int ALLOWED_REPEATS = 3; public void doAppend(E eventObject) { // 判断当前线程是否正在打印日志,如果不是才进行打印动作。 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; } /** * append是个抽象方法,所以此处会调用append方法的具体实现 * 例如OutputStreamAppender类中实现的该方法 */ this.append(eventObject); } catch (Exception e) { if (exceptionCount++ < ALLOWED_REPEATS) { addError("Appender [" + name + "] failed to append.", e); } } finally { guard.set(Boolean.FALSE); } } abstract protected void append(E eventObject);

至此整个日志打印过程结束了。

总结

ConsoleAppender: 打印日志到控制台RollingFileAppender: 可以根据一定的规则对日志进行切割备份,如:日期、日志大小等规则。

文中就不一一介绍了,他们的流程都大同小异,留个感兴趣的同学自己去分析。