问题复现

在项目中引入一个二方包后在后台日志诡异的不见了,于是使用

mvn dependency:tree -l tree.txt

输出依赖关系树,并定位到新引入的二方包部分,二方包引入了spring-boot-starter-logging其依赖了logback-classic与项目中的log4j产生了冲突,所以将前者排掉就项目就可以正常启动了,这个排包的过程不算难,那slf4j是如何实现绑定的呢?

slf4j源码浅析

这里使用到的slf4j-api版本是1.7.25,在老版本中是存在一些线程安全问题的,通常我们打日志的时候都会写一段这样的代码

1
private static final Logger logger = LoggerFactory.getLogger(XXX.class)
  1. 我们从LoggerFactory这个类开始分析源码,首先看到getLogger方法

    1
    2
    3
    4
    5
    6
    7
    8
    public static Logger getLogger(Class<?> clazz) {
    Logger logger = getLogger(clazz.getName());
    .....
    }
    public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
    }

    从上面源码可以看到会调用getLogger(String name)这个方法,通过LoggerFactory最终获取Logger对象,
    所以重点就是获取LoggerFactory.

  2. 我们重点看下 getILoggerFactory这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 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:
// support re-entrant behavior.
// See also http://jira.qos.ch/browse/SLF4J-97
return SUBST_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}

从源码中可以看到INITIALIZATION_STATE是一个静态的volatile变量,在之前的版本中没有volatile修饰,在这个方法中主要就是调用了performInitialization方法完成初始化,在该方法中主要完成绑定工作并进行检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private final static void performInitialization() {
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
versionSanityCheck();
}
}

private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
fixSubstituteLoggers();
replayEvents();
// release all resources in SUBST_FACTORY
SUBST_FACTORY.clear();
}
...

其中有个重要的方法findPossibleStaticLoggerBinderPathSet 顾名思义就是找可能存在的StaticLoggerBinder路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

static Set<URL> findPossibleStaticLoggerBinderPathSet() {
// use Set instead of list in order to deal with bug #138
// LinkedHashSet appropriate here because it preserves insertion order
// during iteration
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
while (paths.hasMoreElements()) {
URL path = paths.nextElement();
staticLoggerBinderPathSet.add(path);
}
}
...
return staticLoggerBinderPathSet;
}

其实就是找日志实现包中的的StaticLoggerBinder,如slf4j-log4j12中的,将这些类的类路径添加到上面的set中,接着通过reportMultipleBindingAmbiguity方法检查是不是存在多个日志实现绑定产生冲突, 即看一下binderPathSet中元素个数是不是大于1,很简单,因此当项目中logback和slf4j-log4j同时存在时会打印出多个”Found binding in …”

1
2
3
4
5
6
7
8
9
10
11
12
13
private static boolean isAmbiguousStaticLoggerBinderPathSet(Set<URL> binderPathSet) {
return binderPathSet.size() > 1;
}

private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
Util.report("Class path contains multiple SLF4J bindings.");
for (URL path : binderPathSet) {
Util.report("Found binding in [" + path + "]");
}
Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
}
}

回到bind方法,看到StaticLoggerBinder.getSingleton();,其实就是创建一个单例的StaticLoggerBinder对象,而这个对象中含有一个LoggerFactory,针对不同的日志框架有不同的实现

  • log4j
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private final ILoggerFactory loggerFactory;

    private StaticLoggerBinder() {
    loggerFactory = new Log4jLoggerFactory();
    try {
    Level level = Level.TRACE;
    } catch (NoSuchFieldError nsfe) {
    Util
    .report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
    }
    }
  • logback-classic
    1
    2
    3
    4
    5
    6
    7
    private boolean initialized = false;
    private LoggerContext defaultLoggerContext = new LoggerContext();
    private final ContextSelectorStaticBinder contextSelectorBinder = ContextSelectorStaticBinder.getSingleton();

    private StaticLoggerBinder() {
    defaultLoggerContext.setName(CoreConstants.DEFAULT_CONTEXT_NAME);
    }
    通过这种绑定的方式就可以实现LoggerFactory的获取,如引入了log4j,就会利用log4j实现的StaticLoggerBinder类来获取log4j的LoggerFactory,而LoggerFactory可以简单地理解为一个Map,key为loggerName,value为Logger对象