slf4j: The Simple Logging Facade for java即 java简单的日志门面。
统一定义了一系列的日志接口,使用户可以使用统一的接口来记录日志。logback,log4j等框架都实现了这些接口,启动时动态地决定真正的日志框架。本文以slf4j+logback的源码来讲解整个绑定和打印日志的流程。手动阅读目录如下:
- 绑定日志框架
- 解析配置文件获取LoggerFactory:对于logback而言就是LoggerContext
- 获取Logger:在LoggerAction的begin方法中会将创建的logger对象缓存到loggerCache中and logger的childList结构中;后续的AppenderRefAction中,会将appender添加进来
- 打印日志:分为同步的Appender和异步的Appender
可用的日志框架依赖如下:
12 6org.slf4j 3slf4j-simple 41.7.5 57 11 12org.slf4j 8slf4j-log4j12 91.7.25 1013 17org.slf4j 14slf4j-jdk14 151.7.25 1618 ch.qos.logback 19logback-classic 20runtime 21
如何使用slf4j的示例代码如下(配合logback的配置文件):
1 import org.slf4j.Logger; 2 import org.slf4j.LoggerFactory; 3 4 5 public class LoggerTest { 6 7 private static final Logger logger = LoggerFactory.getLogger(LoggerTest.class); 8 9 public static void main(String[] args){10 logger.info("hello world!");11 }12 13 }
本文以[示例]logback.xml的配置文件为例:
1 23 13 14 154 5 9/Users/tanling/saveDir/logs/%d{yyyy-MM-dd}/debug-log1.log 6 760 810 12%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 1116 17 26 27 280 18 192048 20true 21true 223000 23 2425 29 30 34 3531 32 33 36 37 38
绑定日志框架
点开LoggerFactory.getLogger的源码:
1 //LoggerFactory 2 public static Logger getLogger(String name) { 3 ILoggerFactory iLoggerFactory = getILoggerFactory(); 4 return iLoggerFactory.getLogger(name); 5 } 6 public static ILoggerFactory getILoggerFactory() { 7 if (INITIALIZATION_STATE == UNINITIALIZED) { 8 INITIALIZATION_STATE = ONGOING_INITIALIZATION; 9 // 初始化 这里是后续分析代码的核心10 performInitialization();11 }12 switch (INITIALIZATION_STATE) {13 case SUCCESSFUL_INITIALIZATION:14 // 初始化成功以后,返回StaticLoggerBinder的单例的loggerfactory15 return StaticLoggerBinder.getSingleton().getLoggerFactory();16 case NOP_FALLBACK_INITIALIZATION:17 return NOP_FALLBACK_FACTORY;18 case FAILED_INITIALIZATION:19 throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);20 case ONGOING_INITIALIZATION:21 // support re-entrant behavior.22 // See also http://bugzilla.slf4j.org/show_bug.cgi?id=10623 return TEMP_FACTORY;24 }25 throw new IllegalStateException("Unreachable code");26 }2728 private final static void performInitialization() {29 // 绑定,这里是绑定真正的日志框架30 bind();31 if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {32 versionSanityCheck();33 }34 }3536 // ⭐️ 绑定是重点37 private final static void bind() {38 try {39 // 通过类加载器加载 org/slf4j/impl/StaticLoggerBinder.class 40 SetstaticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();41 reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);42 // the next line does the binding43 // ⭐️ 解析logback配置文件,获取真正的LoggerFactory44 StaticLoggerBinder.getSingleton();45 INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;46 reportActualBinding(staticLoggerBinderPathSet);47 fixSubstitutedLoggers();48 } catch (NoClassDefFoundError ncde) {49 // 代码略去50 } catch (java.lang.NoSuchMethodError nsme) {51 // 代码略去52 }53 }
源码解析:
当运行getILoggerFactory方法的时候,首先会执行初始化获取到真正的LoggerFactory,然后返回。bind是重点。bind里面做了以下几件事情:- 通过类加载器加载classpath中的org/slf4j/impl/StaticLoggerBinder.class
- StaticLoggerBinder.getSingleton 解析logback配置文件,获取真正的LoggerFactory
- 检查classpath中是否导入了多个日志框架
slf4j api旨在一次只与一个基础日志框架绑定。如果类路径上存在多个绑定,则slf4j将发出警告,列出这些绑定的位置。如果类路径上有多个绑定,请选择一个且只能使用一个绑定,并删除其他绑定。即使存在多个绑定,slf4j也会选择一个日志框架实现并与之绑定。https://www.slf4j.org/codes.html#multiple_bindings
获取LoggerContext
下面来介绍StaticLoggerBinder的过程(以logback为例)
1 void init() { 2 try { 3 try { 4 // 通过ContextInitializer来解析logback的配置文件,解析的内容记录到LoggerContext中 5 new ContextInitializer(defaultLoggerContext).autoConfig(); 6 } catch (JoranException je) { 7 Util.report("Failed to auto configure default logger context", je); 8 } 9 // logback-29210 if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {11 StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);12 }13 // 将LoggerContext存储到 contextSelectorBinder.getContextSelector()中14 contextSelectorBinder.init(defaultLoggerContext, KEY);15 initialized = true;16 } catch (Throwable t) {17 // we should never get here18 Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);19 }20 }2122public ILoggerFactory getLoggerFactory() {23 if (!initialized) {24 return defaultLoggerContext;25 }2627 if (contextSelectorBinder.getContextSelector() == null) {28 throw new IllegalStateException("contextSelector cannot be null. See also " + NULL_CS_URL);29 }30 // 这里获取到的LoggerFactroy实际上就是LoggerContext31 return contextSelectorBinder.getContextSelector().getLoggerContext();32 }
代码解析:
上面两个方法,我们结合起来看。- init()方法中,通过ContextInitializer来解析logback的配置文件,解析的内容入LoggerContext中。并且将该内容存储到了contextSelectorBinder.getContextSelector()中。
- getLoggerFactory实际返回的就是LoggerContext。
接着来分析是如何解析logback配置文件
1// ContextInitializer 2 public void autoConfig() throws JoranException { 3 StatusListenerConfigHelper.installIfAsked(loggerContext); 4 // 获取到logback的配置文件 5 URL url = findURLOfDefaultConfigurationFile(true); 6 if (url != null) { 7 // 存在配置文件的时候解析 8 configureByResource(url); 9 } else {10 // 不存在配置文件的时候的处理11 Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);12 if (c != null) {13 try {14 c.setContext(loggerContext);15 c.configure(loggerContext);16 } catch (Exception e) {17 throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass()18 .getCanonicalName() : "null"), e);19 }20 } else {21 BasicConfigurator basicConfigurator = new BasicConfigurator();22 basicConfigurator.setContext(loggerContext);23 basicConfigurator.configure(loggerContext);24 }25 }26 }
代码解析:
获取logback配置文件,获取到则进行配置文件的解析。获取的顺序是这样的:1 // ②2 final public static String GROOVY_AUTOCONFIG_FILE = "logback.groovy";3 // ④4 final public static String AUTOCONFIG_FILE = "logback.xml";5 // ③6 final public static String TEST_AUTOCONFIG_FILE = "logback-test.xml";7 // ①8 final public static String CONFIG_FILE_PROPERTY = "logback.configurationFile";
最终落到代码EventPlayer上
1// ContextInitializer2else if (urlString.endsWith("xml")) {3 JoranConfigurator configurator = new JoranConfigurator();4 configurator.setContext(loggerContext);5 configurator.doConfigure(url);6 } 1 public void play(ListaSaxEventList) { 2 eventList = aSaxEventList; 3 SaxEvent se; 4 for (currentIndex = 0; currentIndex < eventList.size(); currentIndex++) { 5 se = eventList.get(currentIndex); 6 7 if (se instanceof StartEvent) { 8 interpreter.startElement((StartEvent) se); 9 // invoke fireInPlay after startElement processing10 interpreter.getInterpretationContext().fireInPlay(se);11 }12 if (se instanceof BodyEvent) {13 // invoke fireInPlay before characters processing14 interpreter.getInterpretationContext().fireInPlay(se);15 interpreter.characters((BodyEvent) se);16 }17 if (se instanceof EndEvent) {18 // invoke fireInPlay before endElement processing19 interpreter.getInterpretationContext().fireInPlay(se);20 interpreter.endElement((EndEvent) se);21 }2223 }24 }
代码解析:
将logback配置文件中的标签会被解析成List。是如何解析的:标签以"<"为开头的,会解析成StartEven事件。 标签以"<杠"为开头的,会被解析成EndEvent事件。 在"<"xx>"body"<杠xxx>"标签中的body内容,会被解析成BodyEvent事件。如图参见debug的结果,每个<的标签都是成对出现的,中间是BodyEvent。对于不同的标签,均会有StartEvent和EndEvent事件。每个标签都有一个对应的Action,其中有begin和end方法,分别对应上面StartEvent和EndEvent事件的处理。
其中BodyEvent对应于Action中的body方法。标签 | 处理标签的StartEvent和EndEvent的Action |
---|---|
appender | AppenderAction |
root | RootLoggerAction |
logger | LoggerAction |
appender-ref | AppenderRefAction |
属性标签(比如:rollingPolicy) | NestedComplexPropertyIA |
属性标签(比如:fileNamePattern) | NestedBasicPropertyIA |
下面主要来介绍下各标签的处理:
其中以示例logback.xml为例:红色标的数字代表appender=DEBUG_LOG的执行路径。青色数字顺序代表appender=DEBUG_LOG_ASYNC的执行顺序。黄色的数字代表logger=org.foo.demo的执行顺序。紫色的数字代表root的执行顺序。
核心思想就是利用栈的特性,标签开始的是将对象压入栈,结束的时候将对象从栈中出栈,处理内部的标签时只需要从栈中peek一个对象即是其父对象,处理完了后作为属性设置进去。最终所有key=loggerName,value=logger对象都会缓存到LoggerContext中。获取Logger
1 public final Logger getLogger(final String name) { 2 3 if (name == null) { 4 throw new IllegalArgumentException("name argument cannot be null"); 5 } 6 7 // if we are asking for the root logger, then let us return it without 8 // wasting time 9 if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {10 return root;11 }1213 int i = 0;14 Logger logger = root;1516 // check if the desired logger exists, if it does, return it17 // without further ado.18 // 最终所有的key=loggerName,value=logger实例都会缓存在LoggerContext中的loggerCache中19 Logger childLogger = (Logger) loggerCache.get(name);20 // if we have the child, then let us return it without wasting time21 if (childLogger != null) {22 return childLogger;23 }2425 // if the desired logger does not exist, them create all the loggers26 // in between as well (if they don't already exist)27 String childName;28 // 获取Logger的逻辑29 while (true) {30 int h = LoggerNameUtil.getSeparatorIndexOf(name, i);31 if (h == -1) {32 childName = name;33 } else {34 childName = name.substring(0, h);35 }36 // move i left of the last point37 i = h + 1;38 synchronized (logger) {39 childLogger = logger.getChildByName(childName);40 if (childLogger == null) {41 childLogger = logger.createChildByName(childName);42 loggerCache.put(childName, childLogger);43 incSize();44 }45 }46 logger = childLogger;47 if (h == -1) {48 return childLogger;49 }50 }51 }
代码解析:
当指定的loggerName中没有点号的时候,从rootLogger中获取childLogger,没有的场合,则新建一个Logger对象,其parentLogger为rootLogger。指定一个类名的时候,loggerName为该类的全类名。比如org.foo.demo.LoggerTest,先查找loggerName=org的logger对象;再查找loggerName=org.foo的logger实例;直至创建loggerName=org.foo.demo.LoggerTest的logger对象。有的场合直接从logger的chidlist结构中获取,这是在解析LoggerAction的时候缓存起来的。剧透一下,执行打印的时候,会追溯logger的父logger执行appender。利用Logger打印日志(同步&异步)
1 private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params, 2 final Throwable t) { 3 // 会将方法的调用参数level 消息 params 异常封装成LoggerEvent 4 LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params); 5 le.setMarker(marker); 6 callAppenders(le); 7 } 8 9 public void callAppenders(ILoggingEvent event) {10 int writes = 0;11 // 这里是上述的剧透内容,会遍历执行logger.parent的appender12 // additive为配置文件中配置的内容,默认为true,当配置为false的时候,会终止追溯其父logger13 for (Loogger l = this; l != null; l = l.parent) {14 writes += l.appendLoopOnAppenders(event);15 if (!l.additive) {16 break;17 }18 }19 // No appenders in hierarchy20 if (writes == 0) {21 loggerContext.noAppenderDefinedWarning(this);22 }23 }24 // logger的aai是AppenderAttachableImpl对象,其中持有多个Appender的list25 private int appendLoopOnAppenders(ILoggingEvent event) {26 if (aai != null) {27 return aai.appendLoopOnAppenders(event);28 } else {29 return 0;30 }31 }32 public int appendLoopOnAppenders(E e) {33 int size = 0;34 // 找到logger对应的Appender,执行器logger方法35 for (Appenderappender : appenderList) {36 appender.doAppend(e);37 size++;38 }39 return size;40 }
代码解析:
将方法的参数封装成LoggingEvent对象,从指定名称的loggery追溯logger的parentLogger,通过logger中持有的appender来执行。AppenderAction的end方法中会执行启动:
1 public void end(InterpretationContext ec, String name) { 2 if (inError) { 3 return; 4 } 5 // 这里启动appender 6 if (appender instanceof LifeCycle) { 7 ((LifeCycle) appender).start(); 8 } 910 Object o = ec.peekObject();1112 if (o != appender) {13 addWarn("The object at the of the stack is not the appender named [" + appender.getName() + "] pushed earlier.");14 } else {15 ec.popObject();16 }17 }
RollingFileAppender(同步)
1 public void start() {2 // 部分代码略3 // 进行一堆check,然后appender的文件名创建文件,然后打开文件流,将日志以append的方式追加到文件中4 currentlyActiveFile = new File(getFile());5 addInfo("Active log file name: " + getFile());6 super.start();7 }
AsyncAppender(异步)
1// AsyncAppenderBase 2 @Override 3 public void start() { 4 if (isStarted()) 5 return; 6 if (appenderCount == 0) { 7 addError("No attached appenders found."); 8 return; 9 }10 if (queueSize < 1) {11 addError("Invalid queue size [" + queueSize + "]");12 return;13 }14 blockingQueue = new ArrayBlockingQueue(queueSize);1516 if (discardingThreshold == UNDEFINED)17 discardingThreshold = queueSize / 5;18 addInfo("Setting discardingThreshold to " + discardingThreshold);19 worker.setDaemon(true);20 worker.setName("AsyncAppender-Worker-" + getName());21 // make sure this instance is marked as "started" before staring the worker Thread22 super.start();23 worker.start();24 }2526 @Override27 protected void append(E eventObject) {28 // 当队列空间所剩无几的时候 & 日志级别(INFO,DEBU,TRACE)<=INFO的场合,消息直接丢弃29 if (isQueueBelowDiscardingThreshold() && isDiscardable(eventObject)) {30 return;31 }32 preprocess(eventObject);33 put(eventObject);34 }3536 private void put(E eventObject) {37 if (neverBlock) {38 blockingQueue.offer(eventObject);39 } else {40 try {41 blockingQueue.put(eventObject);42 } catch (InterruptedException e) {43 // Interruption of current thread when in doAppend method should not be consumed44 // by AsyncAppender45 Thread.currentThread().interrupt();46 }47 }48 }
代码解析:
- 创建一个ArrayBlockingQueue队列,用于存放创建的LoggingEvent,其中neverBlock为配置文件中指定,true的场合,满了也不阻塞直接返回; false的场合,满了则阻塞,直至event被消费。
- 创建了一个工作线程Worker
1 class Worker extends Thread { 2 3 public void run() { 4 AsyncAppenderBaseparent = AsyncAppenderBase.this; 5 AppenderAttachableImpl aai = parent.aai; 6 7 // loop while the parent is started 8 while (parent.isStarted()) { 9 try {10 E e = parent.blockingQueue.take();11 aai.appendLoopOnAppenders(e);12 } catch (InterruptedException ie) {13 break;14 }15 }1617 addInfo("Worker thread will flush remaining events before exiting. ");1819 for (E e : parent.blockingQueue) {20 aai.appendLoopOnAppenders(e);21 parent.blockingQueue.remove(e);22 }2324 aai.detachAndStopAllAppenders();25 }26 }
代码解析:
asyncAppender中的aai就是同步的appender,这里只要appender开启中,会从blockingQueue队列中取出LoggingEvent交给普通的appender来执行。以上。
更多源码分析,关注公众号???