Java 日志 Slf4j

概要

slf4j只是为日志的输出提供了统一接口,并没有具体的实现,就好像JDBC一样。那么,大家会不会好奇slf4j是怎么绑定/适配/桥接到log4j或者logback其他日志实现组件的呢?

适配过程原理

统计API接口,说明slf4j使用的是门面模式(Facade),然后就很容易猜测到大致的调用过程是,slf4j是通过自己的api去调用实现组件的api,这样来完成适配的。重点看看是怎么做到适配的。
源码基于slf4j-api.1.7.25

slf4j通用门面的实现

调用slf4j时都是使用它的api,首先需要获取它的logger
一般使用slf4j都是这样子的

  1. import org.slf4j.Logger;
  2. import org.slf4j.LoggerFactory;
  3. private Logger logger = LoggerFactory.getLogger(LogTest.class);

getLogger

getLogger()方法源码跟踪下去

  1. public static Logger getLogger(Class<?> clazz) {
  2. Logger logger = getLogger(clazz.getName());
  3. if (DETECT_LOGGER_NAME_MISMATCH) {
  4. Class<?> autoComputedCallingClass = Util.getCallingClass();
  5. if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
  6. Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
  7. autoComputedCallingClass.getName()));
  8. Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
  9. }
  10. }
  11. return logger;
  12. }
  13. public static Logger getLogger(String name) {
  14. //获取logger的工厂来生成logger
  15. ILoggerFactory iLoggerFactory = getILoggerFactory();
  16. return iLoggerFactory.getLogger(name);
  17. }

ILoggerFactory的名字上来看,这是一个接口,而它又可以生成到具体实际的logger,那应该猜测到这个ILoggerFactory会跟其他日志实现相关,但是例如log4j,自己的实现肯定不会关心slf4j的呀,所以应该由适配jar包,即slf4j-log4j12.jar来实现。
继续看代码

  1. public static ILoggerFactory getILoggerFactory() {
  2. //从ILoggerFactory的状态可以看出,ILoggerFactory只会一次初始化
  3. if (INITIALIZATION_STATE == UNINITIALIZED) {
  4. synchronized (LoggerFactory.class) {
  5. //同步语句 + 双重判断,防止多次初始化
  6. //如果还没初始化,则进行初始化
  7. if (INITIALIZATION_STATE == UNINITIALIZED) {
  8. INITIALIZATION_STATE = ONGOING_INITIALIZATION;
  9. performInitialization();
  10. }
  11. }
  12. }
  13. switch (INITIALIZATION_STATE) {
  14. //初始化成功,即绑定成功,则从StaticLoggerBinder获取ILoggerFactory并返回
  15. case SUCCESSFUL_INITIALIZATION:
  16. return StaticLoggerBinder.getSingleton().getLoggerFactory();
  17. case NOP_FALLBACK_INITIALIZATION:
  18. return NOP_FALLBACK_FACTORY;
  19. case FAILED_INITIALIZATION:
  20. throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
  21. case ONGOING_INITIALIZATION:
  22. return SUBST_FACTORY;
  23. }
  24. throw new IllegalStateException("Unreachable code");
  25. }
  26. //对ILoggerFactory的状态做说明
  27. static final int UNINITIALIZED = 0; //没初始化
  28. static final int ONGOING_INITIALIZATION = 1; //正在初始化
  29. static final int FAILED_INITIALIZATION = 2; //初始化失败
  30. static final int SUCCESSFUL_INITIALIZATION = 3; //初始化成功
  31. static final int NOP_FALLBACK_INITIALIZATION = 4; //无日志实现

bind

performInitialization()方法看来是重点

  1. private final static void performInitialization() {
  2. bind();
  3. if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
  4. versionSanityCheck();
  5. }
  6. }

bind()方法

  1. private final static void bind() {
  2. try {
  3. Set<URL> staticLoggerBinderPathSet = null;
  4. if (!isAndroid()) {
  5. //找出可能绑定的日志的path,其实即StaticLoggerBinder.class文件
  6. staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
  7. //如果找出多个的话则打印错误信息。(等下会演示)
  8. reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
  9. }
  10. //通过获取单例来做初始化
  11. StaticLoggerBinder.getSingleton();
  12. INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
  13. //打印实际绑定的那个日志实现。(等下会演示)
  14. reportActualBinding(staticLoggerBinderPathSet);
  15. fixSubstituteLoggers();
  16. replayEvents();
  17. // release all resources in SUBST_FACTORY
  18. SUBST_FACTORY.clear();
  19. } catch (NoClassDefFoundError ncde) {
  20. String msg = ncde.getMessage();
  21. if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
  22. INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
  23. Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
  24. Util.report("Defaulting to no-operation (NOP) logger implementation");
  25. Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
  26. } else {
  27. failedBinding(ncde);
  28. throw ncde;
  29. }
  30. } catch (java.lang.NoSuchMethodError nsme) {
  31. String msg = nsme.getMessage();
  32. if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
  33. INITIALIZATION_STATE = FAILED_INITIALIZATION;
  34. Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
  35. Util.report("Your binding is version 1.5.5 or earlier.");
  36. Util.report("Upgrade your binding to version 1.6.x.");
  37. }
  38. throw nsme;
  39. } catch (Exception e) {
  40. failedBinding(e);
  41. throw new IllegalStateException("Unexpected initialization failure", e);
  42. }
  43. }

StaticLoggerBinder

findPossibleStaticLoggerBinderPathSet()方法
从hard code看重要性,org/slf4j/impl/StaticLoggerBinder.class就是slf4j日志适配的关键

  1. //hard code
  2. private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
  3. static Set<URL> findPossibleStaticLoggerBinderPathSet() {
  4. Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
  5. try {
  6. //获取LoggerFactory,即slf4j-apoi的类加载器
  7. ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
  8. Enumeration<URL> paths;
  9. //为null说明是由Bootstrap Classloader加载的,则转为App Classloader去加载
  10. if (loggerFactoryClassLoader == null) {
  11. paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
  12. } else {
  13. //用跟slf4j一样的Classloader去加载
  14. paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
  15. }
  16. while (paths.hasMoreElements()) {
  17. URL path = paths.nextElement();
  18. staticLoggerBinderPathSet.add(path);
  19. }
  20. } catch (IOException ioe) {
  21. Util.report("Error getting resources from path", ioe);
  22. }
  23. return staticLoggerBinderPathSet;
  24. }

从类加载器的用法说明,org/slf4j/impl/StaticLoggerBinder.class要跟slf4j-api.jar包在同一个类加载器中,一般来说即要求放在同一路径下比较稳妥,当然也可以通过-classpath来指定。
前面已经猜测org/slf4j/impl/StaticLoggerBinder应该是由各种适配器来实现的,来看看
Slf4j 日志框架适配原理 - 图1
在IDE的类搜索,可以找到两个StaticLoggerBinder
Slf4j 日志框架适配原理 - 图2
调试刚刚的源码,可以看到找到了两个StaticLoggerBinder.class文件
那是因为这里依赖了

  1. <dependency>
  2. <groupId>ch.qos.logback</groupId>
  3. <artifactId>logback-classic</artifactId>
  4. <version>${logback.version}</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.slf4j</groupId>
  8. <artifactId>slf4j-log4j12</artifactId>
  9. <version>${slf4j.version}</version>
  10. </dependency>

所以只是看到logback和log4j的适配器包。slf4j是对每一种日志实现都有对应的一个适配实现。适配器包的具体内容等下再看。(PS:这不是一个好的依赖配置,等下会说)
到这里已经找到了StaticLoggerBinder类了,StaticLoggerBinder是由各自的slf4j适配器包提供的。
这里有个trick,既然StaticLoggerBinder在slf4j-api有,也在其他logback-classic或slf4j-log4j12有,那么怎么确保JVM只加载到适配器包中的StaticLoggerBinder?其实看看slf4j代码的pom.xml就发现,答案是打包时是没有StaticLoggerBinder打进去的,这样slf4j-api.jar包是没有StaticLoggerBinder类的,JVM在找类时只会找到其他jar包的StaticLoggerBinder
刚刚的源码到bind()方法的这一句

  1. StaticLoggerBinder.getSingleton();

这一句其实已经是调用适配包的代码,将会看到logback和log4j对应StaticLoggerBinder类的代码。

对logback适配实现

从上面的依赖可以看出,为什么slf4j对logback的适配是在logback-classic.jar包呢?logback-classic应该是logback的核心包才对,不应该关心slf4j的。那是因为slf4j和logback是同一个作者,所以才说logback是天然集成slf4j的。
来看看logback-classic.jar中的StaticLoggerBinder

  1. static {
  2. SINGLETON.init();
  3. }
  4. public static StaticLoggerBinder getSingleton() {
  5. return SINGLETON;
  6. }
  7. void init() {
  8. try {
  9. try {
  10. new ContextInitializer(defaultLoggerContext).autoConfig();
  11. } catch (JoranException je) {
  12. Util.report("Failed to auto configure default logger context", je);
  13. }
  14. // logback-292
  15. if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
  16. StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
  17. }
  18. contextSelectorBinder.init(defaultLoggerContext, KEY);
  19. initialized = true;
  20. } catch (Exception t) { // see LOGBACK-1159
  21. Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
  22. }
  23. }

上面的就是logback的初始化了。

  1. public ILoggerFactory getLoggerFactory() {
  2. if (!initialized) {
  3. return defaultLoggerContext;
  4. }
  5. if (contextSelectorBinder.getContextSelector() == null) {
  6. throw new IllegalStateException("contextSelector cannot be null. See also " + NULL_CS_URL);
  7. }
  8. return contextSelectorBinder.getContextSelector().getLoggerContext();
  9. }

getLoggerFactory()方法会返回logbackLoggerContext,而LoggerContext是继承slf4j的ILoggerFactory的,这样就适配到slf4j。
Logger是从LoggerFactory取出的。
看看LoggerContextgetLogger()方法

  1. public final Logger getLogger(final Class<?> clazz) {
  2. return getLogger(clazz.getName());
  3. }
  4. @Override
  5. public final Logger getLogger(final String name) {
  6. if (name == null) {
  7. throw new IllegalArgumentException("name argument cannot be null");
  8. }
  9. // if we are asking for the root logger, then let us return it without
  10. // wasting time
  11. if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
  12. return root;
  13. }
  14. int i = 0;
  15. Logger logger = root;
  16. // check if the desired logger exists, if it does, return it
  17. // without further ado.
  18. Logger childLogger = (Logger) loggerCache.get(name);
  19. // if we have the child, then let us return it without wasting time
  20. if (childLogger != null) {
  21. return childLogger;
  22. }
  23. // if the desired logger does not exist, them create all the loggers
  24. // in between as well (if they don't already exist)
  25. String childName;
  26. while (true) {
  27. int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
  28. if (h == -1) {
  29. childName = name;
  30. } else {
  31. childName = name.substring(0, h);
  32. }
  33. // move i left of the last point
  34. i = h + 1;
  35. synchronized (logger) {
  36. childLogger = logger.getChildByName(childName);
  37. if (childLogger == null) {
  38. childLogger = logger.createChildByName(childName);
  39. loggerCache.put(childName, childLogger);
  40. incSize();
  41. }
  42. }
  43. logger = childLogger;
  44. if (h == -1) {
  45. return childLogger;
  46. }
  47. }
  48. }

这里涉及了logback很多逻辑,不太需要理会。这里主要看logback的Logger其实是继承了slf4j的Logger,这样就适配到slf4j。

对log4j配置实现

看了logback的适配,就猜到log4j的也差不多
slf4j-log4j12的StaticLoggerBinder

  1. private StaticLoggerBinder() {
  2. loggerFactory = new Log4jLoggerFactory();
  3. try {
  4. @SuppressWarnings("unused")
  5. Level level = Level.TRACE;
  6. } catch (NoSuchFieldError nsfe) {
  7. Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
  8. }
  9. }
  10. public ILoggerFactory getLoggerFactory() {
  11. return loggerFactory;
  12. }

Log4jLoggerFactory()是继承了slf4j的ILoggerFactory。继续看getLogger方法。

  1. public Logger getLogger(String name) {
  2. Logger slf4jLogger = loggerMap.get(name);
  3. if (slf4jLogger != null) {
  4. return slf4jLogger;
  5. } else {
  6. org.apache.log4j.Logger log4jLogger;
  7. if (name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME))
  8. log4jLogger = LogManager.getRootLogger();
  9. else
  10. log4jLogger = LogManager.getLogger(name);
  11. Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
  12. Logger oldInstance = loggerMap.putIfAbsent(name, newInstance);
  13. return oldInstance == null ? newInstance : oldInstance;
  14. }
  15. }

这里又是把log4j的Logger包装成slf4j的Logger,适配到slf4j。

图解

Slf4j 日志框架适配原理 - 图3

总结

slf4j的适配原理是通过适配包的org/slf4j/impl/StaticLoggerBinder来做转承,适配包通过继承和使用slf4j-api的ILoggerFactoryLogger来完成适配。
在最新的版本(1.8.0)已经改为使用Java的SPI机制来实现,StaticLoggerBinder类已经不用了,改为SLF4JServiceProvider,这样就真正的面向接口编程了,不用打包时忽略StaticLoggerBinder