追踪一下slf4j的工作原理
slf4j
是一个java
界非常流行的日志门面框架,使用它,你可以任意更换底层实现,如logback
、log4j
,而不需要修改代码。那他是如何做到的。
引入slf4j
的包
这里采用最新稳定版,因为只引入slf4j
无法打印日志,这里选择logback
作为底层实现。
1 2 3 4 5 6 7 8 9 10 11
| <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.28</version> </dependency>
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency>
|
测试输出
从LoggerFactory
获取一个Logger
对象,就能使用了。
1 2 3 4 5 6
| public static void main(String[] args) { Logger log = LoggerFactory.getLogger(T1.class); log.info("info"); log.debug("debug"); log.warn("warn"); }
|
代码解析
委托给ILoggerFactory
获取过程中采用懒加载模式,为避免多线程堵塞,使用状态位标识初始化时的多种状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| public static Logger getLogger(Class<?> clazz) { Logger logger = getLogger(clazz.getName()); return logger; }
public static Logger getLogger(String name) { ILoggerFactory iLoggerFactory = getILoggerFactory(); return iLoggerFactory.getLogger(name); }
public static ILoggerFactory getILoggerFactory() { if (INITIALIZATION_STATE == UNINITIALIZED) { synchronized (LoggerFactory.class) { if (INITIALIZATION_STATE == UNINITIALIZED) { INITIALIZATION_STATE = ONGOING_INITIALIZATION; performInitialization(); } } } switch (INITIALIZATION_STATE) { case SUCCESSFUL_INITIALIZATION: return StaticLoggerBinder.getSingleton().getLoggerFactory(); case NOP_FALLBACK_INITIALIZATION: return NOP_FALLBACK_FACTORY; case FAILED_INITIALIZATION: throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG); case ONGOING_INITIALIZATION: return SUBST_FACTORY; } throw new IllegalStateException("Unreachable code"); }
static volatile int INITIALIZATION_STATE = UNINITIALIZED;
|
五种状态
- 1.如果第一次访问,为未初始化状态,
UNINITIALIZED
,则让第一个获得锁的进行初始化,且不会堵塞后来的线程,将状态切换成ONGOING_INITIALIZATION
。
- 2.如果初始化成功,
SUCCESSFUL_INITIALIZATION
,后来的线程可以直接从StaticLoggerBinder
中获取loggerFactory
。
- 3.如果没有底层实现类,
NOP_FALLBACK_INITIALIZATION
,则返回NOP_FALLBACK_FACTORY
。
- 4.如果初始化报错了,
FAILED_INITIALIZATION
,则直接抛出异常,阻止程序继续执行。
- 5.如果正在初始化中
ONGOING_INITIALIZATION
,则返回一个代理loggerFactory
,他会把所有的logger
记录在队列中,等待真正初始化完成后,用真实的logger
把队列中的日志输出出来。
其中比较有意思的就是,slf4j
为了在初始化Factory
期间不堵塞程序,设置了ONGOING_INITIALIZATION
状态,在此状态下(初始化中)获取Factory
会返回SubstituteLoggerFactory
。
1 2 3
| static final SubstituteLoggerFactory SUBST_FACTORY = new SubstituteLoggerFactory();
|
SubstituteLoggerFactory
实现细节
下面代码是SubstituteLoggerFactory
的getLogger()
方法。
第二行的loggers
是一个Map
,getLogger
方法会返回SubstituteLogger
对象,并缓存到loggers
里面。
1 2 3 4 5 6 7 8 9
| synchronized public Logger getLogger(String name) { SubstituteLogger logger = loggers.get(name); if (logger == null) { logger = new SubstituteLogger(name, eventQueue, postInitialization); loggers.put(name, logger); } return logger; }
|
SubstituteLogger
实现细节
这个logger是一个代理logger,它包含一个_delegate
对象,当_delegate
为空时,会根据createdPostInitialization
标志位返回NOP_LOGGER
或者EventRecodingLogger
。
EventRecodingLogger
会将日志输出到到eventQueue
里。
createdPostInitialization
默认是false,Logger初始化后该值被设置为true。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
private volatile Logger _delegate; private final boolean createdPostInitialization;
public void info(String msg) { delegate().info(msg); }
Logger delegate() { if(_delegate != null) { return _delegate; } if(createdPostInitialization) { return NOPLogger.NOP_LOGGER; } else { return getEventRecordingLogger(); } }
private Logger getEventRecordingLogger() { if (eventRecodingLogger == null) { eventRecodingLogger = new EventRecodingLogger(this, eventQueue); } return eventRecodingLogger; }
|
delegate()
方法会有三种返回可能,
- 如果存在被代理的
_delegate
,则返回_delegate
- 如果
createdPostInitialization
为true
,则返回一个nopLogger
,否则返回一个EventRecodingLogger
。
nopLogger
会扔掉所有的日志
EventRecodingLogger
会将所有日志放入queue
中存储起来。
所以slf4j在初始化阶段的日志也不会丢失,而是存储在queue中,等到初始化完成,会取出来输出。
loggerFactory
初始化过程
核心在bind方法里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| private final static void bind() { try { Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet(); StaticLoggerBinder.getSingleton(); INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION; fixSubstituteLoggers(); replayEvents(); SUBST_FACTORY.clear(); } catch (NoClassDefFoundError ncde) { String msg = ncde.getMessage(); if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) { INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION; } else { failedBinding(ncde); throw ncde; } } catch (java.lang.NoSuchMethodError nsme) { String msg = nsme.getMessage(); if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) { INITIALIZATION_STATE = FAILED_INITIALIZATION; } throw nsme; } catch (Exception e) { failedBinding(e); throw new IllegalStateException("Unexpected initialization failure", e); } }
|
总结
`slf4j`会反射`org/slf4j/impl/StaticLoggerBinder.class`类,如果发现多个实现类,会输出日志提醒用户存在多个实现,并使用第一个实现类。
他会直接调用StaticLoggerBinder.getSingleton()
,如果不存在org/slf4j/impl/StaticLoggerBinder.class
会抛ClassNotFound
异常,这就表明底层实现不存在。
获取StaticLoggerBinder
实例过后,会调用fixSubstituteLoggers()
修复初始话中创建的临时logger
,会将存储在queue
中的日志重新输出。
一个bug
上面的实现其实存在一个BUG
,核心方法bind()
里面的fixSubstituteLoggers()
方法,replayEvents()
方法只会在存在底层log实现时才会执行,不存在底层实现会抛异常进入cache
代码块。如果这种情况下将导致日志一直输出到Queue
里面,导致OOM
。
1 2 3 4
| fixSubstituteLoggers(); replayEvents(); SUBST_FACTORY.clear();
|
2020-04 更新
上面提到的bug已经在1.7.30
版本修复,新版本在finally
代码块里面处理的这些逻辑,无论是否初始化成功都会释放Queue
里面的日志,所以不用担心日志不释放的问题。
但极端情况下还会出现问题
1. 线程A执行下面代码,因为第一次执行会触发初始化逻辑:
1 2 3 4
| public static Logger getLogger(String name) { ILoggerFactory iLoggerFactory = getILoggerFactory(); return iLoggerFactory.getLogger(name); }
|
2. 线程B在执行下面代码,获取到的ILoggerFactory
是SubstituteLoggerFactory
实例
1 2 3 4 5
| public static Logger getLogger(String name) { ILoggerFactory iLoggerFactory = getILoggerFactory(); sleep(10000); return iLoggerFactory.getLogger(name); }
|
3. 线程A接着执行,直到LoggerFactory
初始化完成,在方法fixSubstituteLoggers()
里面会将SubstituteLoggerFactory
里面的状态位postInitialization
设为true,并修复创建LoggerFactory
期间生成的的代理Logger
。
4. 接着线程B继续执行,他会拿到ILoggerFactory
对象获取到一个SubstituteLogger
,这个SubstituteLogger
不能得到修复,会恒返回NOP_LOGGER
实例。从而使日志失效。
新版本
1.8
版本以后slf4j
加载底层实现改为java
的SPI
技术做了。请看这个类java.util.ServiceLoader
。