开心一刻
现实中,我有一个异性游戏好友,昨天我心情不好,找她聊天
我:我们两个都好久没有坐下来好好聊天了
她:你不是有女朋友吗
我:人家不需要我这种穷人啊
她:难道我需要吗

现实中,我有一个异性游戏好友,昨天我心情不好,找她聊天
我:我们两个都好久没有坐下来好好聊天了
她:你不是有女朋友吗
我:人家不需要我这种穷人啊
她:难道我需要吗

从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的 从源码的角度讲述了 Spring Boot 的 LoggingSystem 与日志组件的绑定,默认情况下绑定的是 Logback;但当我们具体去看 Spring Boot 的日志打印,却发现用的是 spring-jcl ,通过它适配了 slf4j,真正的日志打印还得依赖具体的日志组件,默认情况下使用的是 logback;那这么说来,Spring Boot 的日志打印与 Spring Boot 的 LoggingSystem 貌似没关系呀?
到底有没有关系,有何关系,我们慢慢往下看;先声明下
后面的分析都是基于 Spring Boot 默认的 Logback,其他日志组件可能有所不同,大家别带入错了
不管是我们用的 slf4j 方式
private static final Logger LOGGER = LoggerFactory.getLogger(TestWeb.class);
还是 Spring Boot 用的 spring-jcl 方式
private static final Log logger = LogFactory.getLog(SpringApplication.class);
都会通过 slf4j 的 org.slf4j.LoggerFactory#getLogger(java.lang.String) 方法来获取 Logger
public static Logger getLogger(String name) { ILoggerFactory iLoggerFactory = getILoggerFactory(); return iLoggerFactory.getLogger(name); }
LoggerFactory 被 final 修饰,且其构造方法是 private,不能被继承,也不能在其他地方 new,纯纯就是一个工具类;它 import 了 StaticLoggerBinder
import org.slf4j.impl.StaticLoggerBinder;
但大家去看下 slf4j-api 的包结构

根本就没有 StaticLoggerBinder 呀?这也可以?这里其实涉及到一个细节
编译后的 class,可以选择性的打包进 jar,运行的时候只要保证依赖的 class 被正常加载了就行,至于是否在同个 jar 包下并没有关系
slf4j 1.7 源码中其实是有 StaticLoggerBinder 的

只是打包的时候剔除了

所以,如果使用 1.7.x 及以下的 slf4j ,必须还得结合有 org.slf4j.impl.StaticLoggerBinder 的日志组件,比如 logback

这是不是又是个细节,你们是不是又学到了?

我们对它进行提炼下
/** * The unique instance of this class. */ private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder(); static { SINGLETON.init(); } private StaticLoggerBinder() { defaultLoggerContext.setName(CoreConstants.DEFAULT_CONTEXT_NAME); } private LoggerContext defaultLoggerContext = new LoggerContext(); public static StaticLoggerBinder getSingleton() { return SINGLETON; }
这是不是 饿汉式单例 的实现?那么 StaticLoggerBinder 的 LoggerContext defaultLoggerContext 是不是也可以当做单例来看待?
同样,我们对它进行精炼,重点关注 root、size、loggerCache、LoggerContext()、getLogger(final String name)
public class LoggerContext extends ContextBase implements ILoggerFactory, LifeCycle { final Logger root; private int size; private Map<String, Logger> loggerCache; 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>(); } public final Logger getLogger(final Class<?> clazz) { return getLogger(clazz.getName()); } @Override public final Logger getLogger(final String name) { if (name == null) { throw new IllegalArgumentException("name argument cannot be null"); } // if we are asking for the root logger, then let us return it without // wasting time if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) { return root; } int i = 0; Logger logger = root; // check if the desired logger exists, if it does, return it // without further ado. Logger childLogger = (Logger) loggerCache.get(name); // if we have the child, then let us return it without wasting time 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; } } } private void incSize() { size++; } int size() { return size; } }
root
Logger root 定义了最顶层的日志记录规则,可以被视为所有其他Logger对象的父级,并且它的配置会应用于所有的日志记录,除非被特定的Logger配置所覆盖
size
Logger 数量,也就是 loggerCache 的 size
loggerCache
Map<String, Logger> loggerCache 缓存了应用中所有的 Logger 实例;Logger 实例之间存在父子关系,涉及到日志规则的继承与覆盖
LoggerContext()
初始化 loggerCache,实例化 Logger root,并将 root 放到 loggerCache 中
getLogger(final String name)
先判断是否是 root,是则直接返回,不是则从 loggerCache 获取,获取到则直接返回;若还是没获取到,则说明当前 Logger 还没被创建,则通过 while(true) 按产品包逐层创建 Logger,绑定好 Logger 之间的父子关系,都 put 进 loggerCache 中

当应用启动完成后,所有的 Logger 实例都被创建并缓存到 LoggerContext 的 loggerCache 中

private static final Logger LOGGER = LoggerFactory.getLogger(TestWeb.class); @GetMapping("hello") public String hello(@RequestParam("name") String name) { LOGGER.info("hello接口入参:{}", name); return "hello, " + name; }
直接 debug 跟进 LOGGER.info,几次跟进后会来到 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); }
这里涉及到事件机制,不细讲,大家可以去看:设计模式之观察者模式 → 事件机制的底层原理,我们把重点放到 callAppenders 上,直译就是调用 appender,appender 在哪?是不是在配置文件中

配置文件什么时候加载的,在 StaticLoggerBinder 加载的时候就完成了
private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder(); static { SINGLETON.init(); } /** * Package access for testing purposes. */ void init() { try { try { new ContextInitializer(defaultLoggerContext).autoConfig(); } catch (JoranException je) { Util.report("Failed to auto configure default logger context", je); } // logback-292 if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) { StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext); } contextSelectorBinder.init(defaultLoggerContext, KEY); initialized = true; } catch (Exception t) { // see LOGBACK-1159 Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t); } }
autoConfig() 就不细跟了(感兴趣的可以去看:从源码来理解slf4j的绑定,以及logback对配置文件的加载),执行完之后,我们看下 LoggerContext 的 objectMap

简单来说,就是将日志配置文件 (logback.xml)加载到了 LoggerContext 的 objectMap 中;我们再回到 Spring Boot 的 LoggingSystem,以 LoggingApplicationListener#onApplicationEnvironmentPreparedEvent 方法作为起点(细节就不跟了,大家直接去看:从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的),我们直接来看 LogbackLoggingSystem#reinitialize
@Override protected void reinitialize(LoggingInitializationContext initializationContext) { getLoggerContext().reset(); getLoggerContext().getStatusManager().clear(); loadConfiguration(initializationContext, getSelfInitializationConfig(), null); }
getLoggerContext() 就不用多说了吧,就是获取全局唯一的 LoggerContext 实例,重点看它的 reset()
@Override public void reset() { resetCount++; super.reset(); initEvaluatorMap(); initCollisionMaps(); root.recursiveReset(); resetTurboFilterList(); cancelScheduledTasks(); fireOnReset(); resetListenersExceptResetResistant(); resetStatusListeners(); }
super.reset()
public void reset() { removeShutdownHook(); getLifeCycleManager().reset(); propertyMap.clear(); objectMap.clear(); }
reset 执行完之后,LoggerContext 的 objectMap 被置空了

说白了就是 Spring Boot 把 Logback 加载的日志配置给清空了,接下来就是 Spring Boot 加载日志配置信息到 LoggerContext 中,也就是如下代码完成的事
loadConfiguration(initializationContext, getSelfInitializationConfig(), null);
不继续跟了,感兴趣的自行去跟;该方法执行完之后,LoggerContext 的 objectMap 又有内容了

总结下
- StaticLoggerBinder 类加载的时候,会加载日志配置文件内容到 LoggerContext 的 objectMap 中
- Spring Boot 启动过程中会重置 LoggerContext,其中包括 LoggerContext 的 objectMap,然后重新加载日志配置文件内容到 LoggerContext 的 objectMap中
所以甭管是使用 spring-jcl ,还是使用 slf4j 进行的日志打印,用到的 Appenders 都是 Spring Boot 启动过程中从日志配置文件中加载的,那么 spring-jcl 与 LoggingSystem 有什么关系,大家清楚了吗?
补充个问题
将 logback.xml 重命名成 logback-spring.xml,为什么 Spring Boot 的日志以及我们的业务日志都能正常打印,并且与使用 logback.xml 时一样?
这个问题要是答不上来,那你们肯定是没仔细看 从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的,里面详细介绍了 Spring Boot 对日志配置文件的加载
StaticLoggerBinder 类加载的时候,会加载日志配置文件内容到 LoggerContext
Logback 1.2.12 默认日志配置文件的优先级
logback.configurationFile > logback-test.xml > logback.xml
Spring Boot 启动过程中会重置 LoggerContext,然后重新加载日志配置文件内容到 LoggerContext
Spring Boot 2.7.18 先按优先级
logback-test.groovy > logback-test.xml > logback.groovy > logback.xml
如果如上四个都不存在,则继续按优先级
logback-test-spring.groovy > logback-test-spring.xml > logback-spring.groovy > logback-spring.xml
寻找日志配置文件
正因为 Spring Boot 启动过程中会重新加载日志配置文件内容到 LoggerContext,所以不管是 spring-jcl 还是 slf4j 打印,日志格式是一致的
Spring Boot 拓展了日志配置文件的文件名
评论已关闭。