在平常开发中常常经过打印日志记录程序执行的步骤或者排查问题,以下代码相似不少,可是,它是如何执行的呢?java
package chapters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; // 省略... Logger logger = LoggerFactory.getLogger(LogbackTest.class); logger.info(" {} is best player in world", "Greizmann");
本文以Logback日志框架来分析以上代码的实现。数据库
现在日志框架经常使用的有log4j、log4j二、jul(common-log)以及logback。假如项目中用的是jul,现在想改为用log4j,若是直接引用java.util.logging包中Logger,须要修改大量代码,为了解决这个麻烦的事情,Ceki Gülcü 大神开发了slf4j(Simple Logging Facade for Java) 。slf4j 是众多日志框架抽象的门面接口,有了slf4j 想要切换日志实现,只须要把对应日志jar替换和添加对应的适配器。数组
图片来源: 一个著名的日志系统是怎么设计出来的?缓存
从图中就能够知道咱们开始的代码为何引 slf4j 包。在阿里的开发手册上一条并发
强制:应用中不可直接使用日志系统(log4j、logback)中的 API ,而应依赖使用日志框架 SLF4J 中的 API 。使用门面模式的日志框架,有利于维护和各个类的日志处理方式的统一。app
Logback 实现了 SLF4J ,少了中间适配层, Logback也是Ceki Gülcü 大神开发的。框架
Logback 主要的三个类 logger,appender和layouts。这三个组件一块儿做用能够知足咱们开发中根据消息的类型以及日志的级别打印日志到不一样的地方。socket
ch.qos.logback.classic.Logger类结构:ide
Logger 依附在LoggerContext上,LoggerContext负责生产Logger,经过一个树状的层次结构来进行管理。Logger 维护着当前节点的日志级别及level值。logger按 "." 分代(层级),日志级别有继承能力,如:名字为 chapters.LogbackTest 若是没有设置日志级别,会继承它的父类chapters 日志级别。全部日志的老祖宗都是ROOT名字的Logger,默认DEBUG级别。当前节点设置了日志级别不会考虑父类的日志级别。Logger 经过日志级别控制日志的启用和禁用。日志级别 TRACE < DEBUG < INFO < WARN < ERROR。oop
接下来咱们结合配置文件看一下Logger属性对应的配置标签:
<configuration> <turboFilter class="ch.qos.logback.classic.turbo.MDCFilter"> <MDCKey>username</MDCKey> <Value>sebastien</Value> <OnMatch>ACCEPT</OnMatch> </turboFilter> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>/Users/wolf/study/logback/logback-examples/myApp.log</file> <encoder> <pattern>%msg%n</pattern> </encoder> </appender> <logger name="chapters.LogbackTest" level="DEBUG"></logger> <root> <appender-ref ref="FILE"/> </root> </configuration>
name:logger 标签中 name 属性值。
level:logger 标签中 level 属性值。
parent:封装了父类 "chapters",以及"chapters"的父类“ROOT”的logger对象。
aai:appender-ref 标签,及这里对应 FileAppender 的实现类对象。若是没有appender-ref标签该值为null。
loggerContext:维护着过滤器,如 turbo 过滤器等。
Appender 做用是控制日志输出的目的地。日志输出的目的地是多元化,你能够把日志输出到console、file、remote socket server、MySQL、PostgreSQL、Oracle 或者其它的数据库、JMS、remote UNIX Syslog daemons 中。一个日志能够输出到多个目的地。如
<configuration> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>/Users/wolf/study/logback/logback-examples/myApp.log</file> <encoder> <pattern>%msg%n</pattern> </encoder> </appender> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%msg%n</pattern> </encoder> </appender> <root> <appender-ref ref="STDOUT" /> <appender-ref ref="FILE"/> </root> </configuration>
该xml配置把日志输出到了myApp.log文件和console中。
有上面Logger和Appender两大组件,日志已经输出到目的地了,可是这样打印的日志对咱们这种凡人不太友好,读起来费劲。凡人就要作到美观,那就用Layouts或Encoder美化一下日志输出格式吧。Encoder 在 logback 0.9.19 版本引进。在以前的版本中,大多数的 appender 依赖 layout 将日志事件转换为 string,而后再经过 java.io.Writer
写出。在以前的版本中,用户须要在 FileAppender
中内置一个 PatternLayout
。在 0.9.19 以后的版本中,FileAppender
以及子类须要一个 encoder 而不是 layout。
Logger logger = LoggerFactory.getLogger(LogbackTest.class);
接下来咱们根据源码分析一下logger的初始化。分析源码以前仍是按照老规矩来一张接口调用时序图吧。
第步:org.slf4j.LoggerFactory#getLogger(java.lang.String)
public static Logger getLogger(String name) { ILoggerFactory iLoggerFactory = getILoggerFactory(); return iLoggerFactory.getLogger(name); }
获取一个ILoggerFactory,即LoggerContext。而后从其获取到Logger对象。
第3步:org.slf4j.LoggerFactory#getILoggerFactory
public static ILoggerFactory getILoggerFactory() { return getProvider().getLoggerFactory(); }
第4步:org.slf4j.LoggerFactory#getProvider
static SLF4JServiceProvider getProvider() { if (INITIALIZATION_STATE == UNINITIALIZED) { synchronized (LoggerFactory.class) { if (INITIALIZATION_STATE == UNINITIALIZED) { INITIALIZATION_STATE = ONGOING_INITIALIZATION; performInitialization(); } } } switch (INITIALIZATION_STATE) { case SUCCESSFUL_INITIALIZATION: return PROVIDER; case NOP_FALLBACK_INITIALIZATION: return NOP_FALLBACK_FACTORY; case FAILED_INITIALIZATION: throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG); case ONGOING_INITIALIZATION: return SUBST_PROVIDER; } throw new IllegalStateException("Unreachable code"); }
对SLF4JServiceProvider初始化,即LogbackServiceProvider对象。而后检查初始化状态,若是成功就返回PROVIDER。
第5步:org.slf4j.LoggerFactory#performInitialization
private final static void performInitialization() { bind(); if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) { versionSanityCheck(); } }
第6步:org.slf4j.LoggerFactory#bind
private final static void bind() { try { // 加载 SLF4JServiceProvider List<SLF4JServiceProvider> providersList = findServiceProviders(); reportMultipleBindingAmbiguity(providersList); if (providersList != null && !providersList.isEmpty()) { PROVIDER = providersList.get(0); // SLF4JServiceProvider.initialize() PROVIDER.initialize(); INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION; reportActualBinding(providersList); fixSubstituteLoggers(); replayEvents(); SUBST_PROVIDER.getSubstituteLoggerFactory().clear(); } else { // 省略代码。。。 } } catch (Exception e) { // 失败,设置状态值,上报 failedBinding(e); throw new IllegalStateException("Unexpected initialization failure", e); } }
经过ServiceLoader加载LogbackServiceProvider,而后进行初始化相关字段。初始化成功后把初始化状态设置成功状态值。
第7步:ch.qos.logback.classic.spi.LogbackServiceProvider#initialize
public void initialize() { // 初始化默认的loggerContext defaultLoggerContext = new LoggerContext(); defaultLoggerContext.setName(CoreConstants.DEFAULT_CONTEXT_NAME); initializeLoggerContext(); markerFactory = new BasicMarkerFactory(); mdcAdapter = new LogbackMDCAdapter(); }
建立名字为default的LoggerContext对象,并初始化一些字段默认值。
ch.qos.logback.classic.LoggerContext#LoggerContext
public LoggerContext() { super(); this.loggerCache = new ConcurrentHashMap<String, Logger>(); this.loggerContextRemoteView = new LoggerContextVO(this); this.root = new Logger(Logger.ROOT_LOGGER_NAME, null, this); this.root.setLevel(Level.DEBUG); loggerCache.put(Logger.ROOT_LOGGER_NAME, root); initEvaluatorMap(); size = 1; this.frameworkPackages = new ArrayList<String>(); }
初始化LoggerContext时设置了ROOT的Logger,日志级别为DEBUG。
第8步:ch.qos.logback.classic.spi.LogbackServiceProvider#initializeLoggerContext
private void initializeLoggerContext() { try { try { new ContextInitializer(defaultLoggerContext).autoConfig(); } catch (JoranException je) { Util.report("Failed to auto configure default logger context", je); } // 省略代码。。。 } catch (Exception t) { // see LOGBACK-1159 Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t); } }
把第7步初始化好的LoggerContext当作参数传入ContextInitializer,构建其对象。而后解析配置文件。
第9步:ch.qos.logback.classic.util.ContextInitializer#autoConfig
public void autoConfig() throws JoranException { StatusListenerConfigHelper.installIfAsked(loggerContext); // (1) 从指定路径获取 URL url = findURLOfDefaultConfigurationFile(true); if (url != null) { configureByResource(url); } else { // (2) 从运行环境中获取 Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class); if (c != null) { // 省略代码。。。 } else { // (3)设置默认的 BasicConfigurator basicConfigurator = new BasicConfigurator(); basicConfigurator.setContext(loggerContext); basicConfigurator.configure(loggerContext); } } }
首先从指定的路径获取资源URL,若是存在就进行解析;若是不存在再从运行环境中获取配置;若是以上都没有最后会构建一个BasicConfigurator看成默认的。
ch.qos.logback.classic.util.ContextInitializer#findURLOfDefaultConfigurationFile
public URL findURLOfDefaultConfigurationFile(boolean updateStatus) { ClassLoader myClassLoader = Loader.getClassLoaderOfObject(this); // 启动参数中获取 URL url = findConfigFileURLFromSystemProperties(myClassLoader, updateStatus); if (url != null) { return url; } // logback-test.xml url = getResource(TEST_AUTOCONFIG_FILE, myClassLoader, updateStatus); if (url != null) { return url; } //logback.groovy url = getResource(GROOVY_AUTOCONFIG_FILE, myClassLoader, updateStatus); if (url != null) { return url; } // logback.xml return getResource(AUTOCONFIG_FILE, myClassLoader, updateStatus); }
先从启动参数中查找logback.configurationFile参数值,若是没有再从classpath中一次查找logback-test.xml -> logback.groovy -> logback.xml 。由此可知文件的优先级是 启动参数 -> logback-test.xml -> logback.groovy -> logback.xml
第10步:ch.qos.logback.classic.util.ContextInitializer#configureByResource
public void configureByResource(URL url) throws JoranException { if (url == null) { throw new IllegalArgumentException("URL argument cannot be null"); } final String urlString = url.toString(); if (urlString.endsWith("groovy")) { // 省略代码。。。 } else if (urlString.endsWith("xml")) { JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(loggerContext); configurator.doConfigure(url); } else { // 省略代码。。。 } }
根据文件后缀判断是 groovy或者xml,而后交给不一样的配置解析器处理。这里也是把第7步中的LoggerContext传进去,继续封装它的字段值。
第12步:ch.qos.logback.core.joran.GenericConfigurator#doConfigure(org.xml.sax.InputSource)
public final void doConfigure(final InputSource inputSource) throws JoranException { long threshold = System.currentTimeMillis(); SaxEventRecorder recorder = new SaxEventRecorder(context); recorder.recordEvents(inputSource); // 处理配置文件,封装到 LoggerContext 中 playEventsAndProcessModel(recorder.saxEventList); StatusUtil statusUtil = new StatusUtil(context); if (statusUtil.noXMLParsingErrorsOccurred(threshold)) { registerSafeConfiguration(recorder.saxEventList); } }
真正解析配置文件的逻辑在playEventsAndProcessModel方法中,这里就不展开分析了。到这一步LoggerContext基本初始化完成了。
第13步:ch.qos.logback.classic.LoggerContext#getLogger(java.lang.String)
@Override public Logger getLogger(final String name) { // 省略代码。。。 if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) { return root; } int i = 0; Logger logger = root; // 从缓存中获取, 有直接返回 Logger childLogger = (Logger) loggerCache.get(name); if (childLogger != null) { return childLogger; } // if the desired logger does not exist, them create all the loggers // in between as well (if they don't already exist) String childName; while (true) { int h = LoggerNameUtil.getSeparatorIndexOf(name, i); if (h == -1) { childName = name; } else { childName = name.substring(0, h); } // move i left of the last point i = h + 1; synchronized (logger) { childLogger = logger.getChildByName(childName); if (childLogger == null) { childLogger = logger.createChildByName(childName); loggerCache.put(childName, childLogger); incSize(); } } logger = childLogger; if (h == -1) { return childLogger; } } }
通过前面漫长的对LoggerContext进行初始化工做,这一步就是从LoggerContext获取Logger对象。若是缓存中直接返回。不然经过“.”分代构建层次结构。
上一节Logger建立完成,接下来分析一下打日志的流程。
logger.info(" {} is best player in world", "Greizmann");
第1步:ch.qos.logback.classic.Logger#info(java.lang.String, java.lang.Object)
public void info(String format, Object arg) { filterAndLog_1(FQCN, null, Level.INFO, format, arg, null); }
把接口的日志级别(Level.INFO)传到下一个方法。
第2步:ch.qos.logback.classic.Logger#filterAndLog_1
private void filterAndLog_1(final String localFQCN, final Marker marker, final Level level, final String msg, final Object param, final Throwable t) { // 先经过turboFilter过滤 final FilterReply decision = loggerContext.getTurboFilterChainDecision_1(marker, this, level, msg, param, t); // 判断日志级别 if (decision == FilterReply.NEUTRAL) { if (effectiveLevelInt > level.levelInt) { return; } } else if (decision == FilterReply.DENY) { return; } buildLoggingEventAndAppend(localFQCN, marker, level, msg, new Object[] { param }, t); }
若是TurboFilter过滤器存在就会执行相关操做,并返回FilterReply。若是结果是FilterReply.DENY本条日志消息直接丢弃;若是是FilterReply.NEUTRAL会继续判断日志级别是否在该方法级别之上;若是是FilterReply.ACCEPT直接跳到下一步。
第3步:ch.qos.logback.classic.Logger#buildLoggingEventAndAppend
private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params, final Throwable t) { LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params); le.setMarker(marker); callAppenders(le); }
建立了LoggingEvent对象,该对象包含日志请求全部相关的参数,请求的 logger,日志请求的级别,日志信息,与日志一同传递的异常信息,当前时间,当前线程,以及当前类的各类信息和 MDC。其实打印日志就是一个事件,因此这个对象是相关重要,下面所有是在操做该对象。
第4步:ch.qos.logback.classic.Logger#callAppenders
public void callAppenders(ILoggingEvent event) { int writes = 0; // 从本身往父辈查找知足 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); } }
第5步:ch.qos.logback.classic.Logger#appendLoopOnAppenders
private int appendLoopOnAppenders(ILoggingEvent event) { if (aai != null) { return aai.appendLoopOnAppenders(event); } else { return 0; } }
从当前Logger到父节点遍历,直到AppenderAttachableImpl不为空(有appender-ref 标签)。
第6步:ch.qos.logback.core.spi.AppenderAttachableImpl#appendLoopOnAppenders
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; }
若是设置了多个日志输出目的地,这里就是循环调用对应的Appender进行输出。
第7步:ch.qos.logback.core.UnsynchronizedAppenderBase#doAppend
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; } this.append(eventObject); } catch (Exception e) { if (exceptionCount++ < ALLOWED_REPEATS) { addError("Appender [" + name + "] failed to append.", e); } } finally { guard.set(Boolean.FALSE); } }
经过ThreadLocal控制递归致使的重复提交
第8步:ch.qos.logback.core.OutputStreamAppender#append
protected void append(E eventObject) { if (!isStarted()) { return; } subAppend(eventObject); }
第9步:ch.qos.logback.core.OutputStreamAppender#subAppend
protected void subAppend(E event) { if (!isStarted()) { return; } try { if (event instanceof DeferredProcessingAware) { // 拼接日志信息(填充占位符),设置当前线程以及MDC等信息 ((DeferredProcessingAware) event).prepareForDeferredProcessing(); } byte[] byteArray = this.encoder.encode(event); writeBytes(byteArray); } catch (IOException ioe) { this.started = false; addStatus(new ErrorStatus("IO failure in appender", this, ioe)); } }
Encoder在这里惨淡登场,返回byte数组。
第10步:ch.qos.logback.core.encoder.LayoutWrappingEncoder#encode
public byte[] encode(E event) { String txt = layout.doLayout(event); return convertToBytes(txt); }
Encoder先把LoggerEvent交给Layout,Layout组装日志信息,在每条信息后加上换行符。
第11步:ch.qos.logback.core.OutputStreamAppender#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(); } }
使用AQS锁控制并发问题。这也是Logback性能不如 Log4j2的缘由。后面有时间分析一下Log4j2。
本文到此结束了,还有两天就要放假了,祝你们新年快乐。