日志是一个系统必不可缺少的东西,记录了系统运行时的点点滴滴,便于我们了解自己系统的运行状态,在我们使用 Spring Boot时,默认就已经提供了日志功能,使用Logback作为默认的日志框架。那么,接下来我们依赖来看看 Spring Boot是如何初始化好日志系统的。关于Spring的知识点总结了一个图谱,分享给大家:
为什么Spring Boot默认的日志框架是Logbasck呢?
因为在spring-boot-starter模块中引入spring-boot-starter-logging模块,该Starter引入了logback-classic依赖。
在我们的日常工作中,可能看到项目中依赖的跟日志相关的jar包有很多,例如commons-logging、log4j、log4j2、sl4j和logback等等,眼花缭乱。经常会碰到各种依赖冲入的问题,非常烦恼,例如这几个问题:
1.Failed to load class org.slf4j.impl.StaticLoggerBinder,没找到日志实现,如果你觉得你已经添加了对应的日志实现依赖了,那应该检查一下版本是否兼容
2.Multiple bindings,找到了多个日志实现,也可能是版本问题,slf4j会找其中一个作为日志实现 如果想要正确地使用它们,有必要先理清它们之间的关系,我们可以来看看Log的发展史,首先从 Java Log 的发展历程开始说起:
1.1.log4j(作者Ceki Gülcü)出来后,被们广泛的应用(注意,这里是直接使用),当初是 Java 日志事实上的标准,并成为了 Apache 的项目
2.2.Apache 要求把 log4j 并入到 jdk,SUN 表示拒绝,并在 jdk1.4 版本后增加了 JUL(java.util.logging);
3.3.毕竟是 JDK 自带的,JUL 也被很多人使用。同时还有其他的日志组件,如 SimpleLog 等。这个时候如果有人想换成其他日志组件,如 log4j 换成 JUL,因为 API 完全不同,就需要改动代码,当然很多人不愿意呀;
4.4.Apache 见此,开发了 JCL(Jakarta Commons Logging),即 commons-logging-xx.jar。它只提供一套通用的日志接口 API,并不提供日志的实现。很好的设计原则嘛,依赖抽象而非实现。这样一来,我们的应用程序可以在运行时选择自己想要的日志实现组件;
5.5.这样看上去也挺美好的,但是log4j的作者觉得JCL不好用,自己开发出一套slf4j,它跟JCL类似,本身不替供日志的具体实现,只对外提供接口或门面。目的就是为了替代JCL。同时,还开发出logback,一个比log4j 拥有更高性能的组件,目的是为了替代log4j;
6.6.Apache 参考了 logback,并做了一系列优化,推出了一套 log4j2 日志框架。 对于性能没什么特别高要求的使用 Spring Boot 中默认的 logback 就可以了,如果想要使用 log4j2 可以参考我的 《MyBatis 使用手册》 这篇,有提到过。
回到前面的 《SpringApplication 启动类的启动过程》 这篇,Spring Boot 启动应用的入口和主流程都是在 SpringApplication#run(String.. args) 方法中。
在启动 Spring 应用的整个过程中,到了不同的阶段会发布不同类型的事件,例如最开始会发布一个 应用正在启动 的事件,对于不同类型的事件都是通过 EventPublishingRunListener 事件发布器来发布,里面有一个事件广播器,封装了几个 ApplicationListener 事件监听器,如下:
其中有一个 LoggingApplicationListener 对象,监听到不同事件后,会对日志系统进行一些相关的初始化工作
提示:Spring Boot 的 LoggingSystem 日志系统的初始化过程有点绕,嵌套的方法有点多,可参考序号耐心查看
LoggingApplicationListener
org.springframework.boot.context.logging.LoggingApplicationListener,Spring Boot 事件监听器,用于初始化日志系统
onApplicationEvent方法
onApplicationEvent(ApplicationEvent 方法,处理监听到的事件
对于不同的事件调用不同的方法,事件的发布顺序也就是上面从上往下的顺序
处理应用正在启动的事件
过程如下:
1.1.创建LoggingSystem对象,指定了类型则使用指定的,没有则尝试创建对应的对象,ClassLoader中有对应的Class对象则创建(logback > log4j2 > java logging)
2.2.调用LoggingSystem 的 beforeInitialize()方法,初始化前置处理
处理环境已准备事件
过程如下:
1.1.如果还未明确LoggingSystem类型,那么这里继续创建LoggingSystem对象
2.2.调用initialize(..) 方法,初始化LoggingSystem对象,创建日志文件,设置日志级别
初始过程如下:
1.1.根据Environment环境通过LoggingSystemProperties往System进行一些日志配置
2.2.根据Environment环境配置的日志名称和路径创建一个日志文件,默认情况没有配置,这个对象也为null,而是在打印个日志的时候会创建(如果不存在的话)
3.3.往System添加日志文件的名称和路径
4.4.创建一个日志分组对象
5.5.初始化早期的 Spring Boot 日志级别(Debug 或者 Trace)
初始化LoggingSystem对象
初始化最终的Spring Boot日志级别,逐个设置Environment配置的日志级别
向JVM注册一个钩子,用于在JVM关闭时关闭日志系统
可以看到需要通过LoggingSystem日志系统对象来初始化,后面会讲到
处理应用已准备事件
LoggingSystem
org.springframework.boot.logging.LoggingSystem 抽象类,Spring Boot 的日志系统对象,每个日志框架,都会对应一个实现类。如下图所示:
1.1 get 方法
创建一个 LoggingSystem 日志系统对象,如下:
过程如下:
1.1.从系统参数org.springframework.boot.logging.LoggingSystem获得LoggingSystem类型
2.2.如果非空,说明配置了,那么创建一个该类型的LoggingSystem实例对象
3.3.否则,没有配置,则通过顺序依次尝试创建对应类型的LoggingSystem实例对象,也就是在static代码块中初始化好的集合,logback > log4j2 > java logging
初始化的前置操作,抽象方法,交由子类实现
初始化操作,空方法,由子类来重写
AbstractLoggingSystem
org.springframework.boot.logging.AbstractLoggingSystem 抽象类,继承 LoggingSystem 抽象类,作为一个基类
重写父类的 initialize(..) 方法,提供模板化的初始化逻辑,如下:
有指定的配置文件,则调用 initializeWithSpecificConfig(..) 方法, 使用指定配置文件进行初始化
没有自定义的配置文件,则调用 initializeWithConventions(..) 方法,使用约定配置文件进行初始化
initializeWithSpecificConfig(LoggingInitializationContext, String, LogFile) 方法,使用指定配置文件进行初始化
先获取配置文件的路径(可能有占位符),然后调用 loadConfiguration(..) 抽象方法,加载配置文件到日志系统中
initializeWithConventions(LoggingInitializationContext, LogFile) 方法,使用约定配置文件进行初始化
调用 getSelfInitializationConfig() 方法,尝试获得约定配置文件,例如 log4j2 约定的是 log4j2.xml
如果找到了约定的配置文件,则调用 reinitialize(..) 抽象方法,自定义初始化,子类实现
protected void reinitialize(LoggingInitializationContext initializationContext) { }
调用 getSpringInitializationConfig(..) 方法,尝试获取约定的配置文件(带有 -spring ),例如 log4j2 对应是 log4j2-spring.xml
获取到了 -spring 配置文件,则调用 loadConfiguration(..) 抽象方法,加载到日志系统中,子类实现
protected abstract void loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile); 还没有找到到指定的配置文件,那么调用 loadDefaults(..) 抽象方法,加载默认配置,子类实现
protected abstract void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile); 整个过程就是尝试获取到各个日志框架约定好的配置文件名称,如果存在这个配置文件,则加载到日志系统中,否则使用默认的配置
Slf4JLoggingSystem org.springframework.boot.logging.Slf4JLoggingSystem,继承 AbstractLoggingSystem 抽象类,基于 Slf4J 的 LoggingSystem 的抽象基类
初始化的前置操作
@Overridepublic void beforeInitialize() { super.beforeInitialize(); // <1> 配置 JUL 的桥接处理器,桥接到 slf4j configureJdkLoggingBridgeHandler();}
先调用父类的 beforeInitialize() 方法,然后调用 configureJdkLoggingBridgeHandler() 方法,配置 JUL 的桥接处理器,桥接到 slf4j
private void configureJdkLoggingBridgeHandler() { try { // <1> 判断 JUL 是否桥接到 SLF4J 了 if (isBridgeJulIntoSlf4j()) { // <2> 移除 JUL 桥接处理器 removeJdkLoggingBridgeHandler(); // <3> 重新安装 SLF4JBridgeHandler SLF4JBridgeHandler.install(); } } catch (Throwable ex) { // Ignore. No java.util.logging bridge is installed. }}
过程如下:
1.判断JUL是否桥接到slf4j了
protected final boolean isBridgeJulIntoSlf4j() { // 存在 SLF4JBridgeHandler 类,且 JUL 只有 ConsoleHandler 处理器被创建 return isBridgeHandlerAvailable() && isJulUsingASingleConsoleHandlerAtMost();}
3.移除JUL桥接处理器
private void removeJdkLoggingBridgeHandler() { try { // 移除 JUL 的 ConsoleHandler removeDefaultRootHandler(); // 卸载 SLF4JBridgeHandler SLF4JBridgeHandler.uninstall(); } catch (Throwable ex) { // Ignore and continue }}private void removeDefaultRootHandler() { try { Logger rootLogger = LogManager.getLogManager().getLogger(""); Handler[] handlers = rootLogger.getHandlers(); if (handlers.length == 1 && handlers[0] instanceof ConsoleHandler) { rootLogger.removeHandler(handlers[0]); } } catch (Throwable ex) { // Ignore and continue }}
3.重新安装 SLF4JBridgeHandler
重写AbstractLoggingSystem父类的方法,加载指定的日志配置文件到日志系统中
@Overrideprotected void loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile) { Assert.notNull(location, "Location must not be null"); if (initializationContext != null) { // 将 Environment 中的日志配置往 System 中配置 applySystemProperties(initializationContext.getEnvironment(), logFile); }}
实际上就是将Environment中的日志配置往System中配置
LogbackLoggingSystem
org.springframework.boot.logging.logback.LogbackLoggingSystem,继承Slf4JLoggingSystem抽象类,基于 logback的LoggingSystem实现类
重写LoggingSystem的方法,初始化前置操作
@Overridepublic void beforeInitialize() { // <1> 获得 LoggerContext 日志上下文 LoggerContext loggerContext = getLoggerContext(); // <2> 如果 LoggerContext 已有 LoggingSystem,表示已经初始化,则直接返回 if (isAlreadyInitialized(loggerContext)) { return; } // <3> 调用父方法 super.beforeInitialize(); // <4> 添加 FILTER 到其中,因为还未初始化,不打印日志 loggerContext.getTurboFilterList().add(FILTER);}
过程如下:
1.调用getLoggerContext() 方法,获得LoggerContext日志上下文
private LoggerContext getLoggerContext() { ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory(); // 这里会校验 factory 是否为 LoggerContext 类型 return (LoggerContext) factory;}
2.如果LoggerContext已有LoggingSystem,表示已经初始化,则直接返回
private boolean isAlreadyInitialized(LoggerContext loggerContext) { return loggerContext.getObject(LoggingSystem.class.getName()) != null;}
3.调用父方法
4.添加FILTER到其中,因为还未初始化,不打印日志
private static final TurboFilter FILTER = new TurboFilter() { @Override public FilterReply decide(Marker marker, ch.qos.logback.classic.Logger logger, Level level, String format, Object[] params, Throwable t) { // 一律拒绝 return FilterReply.DENY; }};
getStandardConfigLocations方法
重写AbstractLoggingSystem的方法,获取logback标准的配置文件名称
@Overrideprotected String[] getStandardConfigLocations() { return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" };}
重写LoggingSystem的方法,初始化操作
@Overridepublic void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) { // <1> 获得 LoggerContext 日志上下文 LoggerContext loggerContext = getLoggerContext(); // <2> 如果 LoggerContext 已有 LoggingSystem,表示已经初始化,则直接返回 if (isAlreadyInitialized(loggerContext)) { return; } // <3> 调用父方法 super.initialize(initializationContext, configLocation, logFile); // <4> 移除之前添加的 FILTER,可以开始打印日志了 loggerContext.getTurboFilterList().remove(FILTER); // <5> 标记为已初始化,往 LoggerContext 中添加一个 LoggingSystem 对象 markAsInitialized(loggerContext); if (StringUtils.hasText(System.getProperty(CONFIGURATION_FILE_PROPERTY))) { getLogger(LogbackLoggingSystem.class.getName()).warn("Ignoring '" + CONFIGURATION_FILE_PROPERTY + "' system property. Please use 'logging.config' instead."); }}
过程如下:
1.调用getLoggerContext() 方法,获得LoggerContext日志上下文
private LoggerContext getLoggerContext() { ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory(); // 这里会校验 factory 是否为 LoggerContext 类型 return (LoggerContext) factory;}
2.如果LoggerContext已有LoggingSystem,表示已经初始化,则直接返回
private boolean isAlreadyInitialized(LoggerContext loggerContext) { return loggerContext.getObject(LoggingSystem.class.getName()) != null;}
3.调用父方法
4.移除之前添加的FILTER,可以开始打印日志了
5.调用markAsInitialized(..) 方法,标记为已初始化,往LoggerContext中添加一个LoggingSystem对象
重写AbstractLoggingSystem的方法,加载指定的日志配置文件到日志系统中
@Overrideprotected void loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile) { // <1> 调用父方法 super.loadConfiguration(initializationContext, location, logFile); LoggerContext loggerContext = getLoggerContext(); // <2> 重置 LoggerContext 对象 // 这里会添加一个 LevelChangePropagator 监听器,当日志级别被修改时会立即生效,而不用重启应用 stopAndReset(loggerContext); try { // <3> 读取配置文件并解析,配置到 LoggerContext 中 configureByResourceUrl(initializationContext, loggerContext, ResourceUtils.getURL(location)); } catch (Exception ex) { throw new IllegalStateException("Could not initialize Logback logging from " + location, ex); } // <4> 判断是否发生错误,有的话抛出 IllegalStateException 异常 List
过程如下:
1.调用父方法
2.重置LoggerContext对象,这里会添加一个LevelChangePropagator监听器,当日志级别被修改时会立即生效,而不用重启应用 private void stopAndReset(LoggerContext loggerContext) { // 停止 loggerContext.stop(); // 重置 loggerContext.reset(); // 如果有桥接器 if (isBridgeHandlerInstalled()) { // 添加一个日志级别的监听器,能够及时更新日志级别 addLevelChangePropagator(loggerContext); }}private void addLevelChangePropagator(LoggerContext loggerContext) { LevelChangePropagator levelChangePropagator = new LevelChangePropagator(); levelChangePropagator.setResetJUL(true); levelChangePropagator.setContext(loggerContext); loggerContext.addListener(levelChangePropagator);}
3.读取配置文件并解析,配置到LoggerContext中
private void configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext, URL url) throws JoranException { if (url.toString().endsWith("xml")) { JoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext); configurator.setContext(loggerContext); configurator.doConfigure(url); } else { new ContextInitializer(loggerContext).configureByResource(url); }}
4.判断是否发生错误,有的话抛出IllegalStateException异常
reinitialize方法
实现类AbstractLoggingSystem的方法,重新初始化
@Overrideprotected void reinitialize(LoggingInitializationContext initializationContext) { // 重置 getLoggerContext().reset(); // 清空资源 getLoggerContext().getStatusManager().clear(); // 加载指定的配置文件,此时使用约定的配置文件 loadConfiguration(initializationContext, getSelfInitializationConfig(), null);}
loadDefaults方法
实现类AbstractLoggingSystem的方法,没有指定的配置文件,也没有约定的配置文件,那么加载默认的配置到日志系统
@Overrideprotected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) { LoggerContext context = getLoggerContext(); // <1> 重置 LoggerContext 对象 // 这里会添加一个 LevelChangePropagator 监听器,当日志级别被修改时会立即生效,而不用重启应用 stopAndReset(context); // <2> 如果开启 debug 模式则添加一个 OnConsoleStatusListener 监听器 boolean debug = Boolean.getBoolean("logback.debug"); if (debug) { StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener()); } // <3> 往 LoggerContext 中添加默认的日志配置 LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context) : new LogbackConfigurator(context); Environment environment = initializationContext.getEnvironment(); context.putProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN, environment.resolvePlaceholders("${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}")); context.putProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN, environment.resolvePlaceholders( "${logging.pattern.dateformat:${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}")); context.putProperty(LoggingSystemProperties.ROLLING_FILE_NAME_PATTERN, environment .resolvePlaceholders("${logging.pattern.rolling-file-name:${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}")); // <4> 创建 DefaultLogbackConfiguration 对象,设置到 configurator 中 // 设置转换规则,例如颜色转换,空格转换 new DefaultLogbackConfiguration(initializationContext, logFile).apply(configurator); // <5> 设置日志文件,按天切割 context.setPackagingDataEnabled(true);}
过程如下:
1.重置LoggerContext对象,这里会添加一个LevelChangePropagator监听器,当日志级别被修改时会立即生效,而不用重启应用
private void stopAndReset(LoggerContext loggerContext) { // 停止 loggerContext.stop(); // 重置 loggerContext.reset(); // 如果有桥接器 if (isBridgeHandlerInstalled()) { // 添加一个日志级别的监听器,能够及时更新日志级别 addLevelChangePropagator(loggerContext); }}private void addLevelChangePropagator(LoggerContext loggerContext) { LevelChangePropagator levelChangePropagator = new LevelChangePropagator(); levelChangePropagator.setResetJUL(true); levelChangePropagator.setContext(loggerContext); loggerContext.addListener(levelChangePropagator);}
2.如果开启debug模式则添加一个OnConsoleStatusListener监听器
3.往LoggerContext中添加默认的日志配置
4.创建DefaultLogbackConfiguration对象,设置到configurator中,设置转换规则,例如颜色转换,空格转换
5.设置日志文件,按天切割
Log4J2LoggingSystem
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem,继承Slf4JLoggingSystem抽象类,基于log4j2的LoggingSystem实现类
和LogbackLoggingSystem基本类似,感兴趣的小伙伴可以自己去瞧一瞧
JavaLoggingSystem
org.springframework.boot.logging.java.JavaLoggingSystem,继承AbstractLoggingSystem抽象类,基于jul的LoggingSystem实现类
逻辑比较简单,感兴趣的小伙伴可以自己去瞧一瞧
本文分析了Sping Boot初始化不同LoggingSystem日志系统的一个过程,同样是借助于Spring的ApplicationListener事件监听器机制,在启动Spring应用的过程中,例如会广播应用正在启动的事件和应用环境已准备好,然后LoggingApplicationListener监听到不同的事件会进行不同的初始化操作。
LoggingSystem日志系统主要分为logback、log4j2和JUL三种,本文主要对logback的初始化过程进行了分析,因为它是Spring Boot的默认日志框架嘛。整个的初始化过程稍微有点绕,嵌套的方法有点多,主要的小节都标注了序号。
大致流程就是先配置JUL到slf4j的桥接器,然后尝试找到指定的配置文件对日志系统进行配置,可通过 logging.config设置;没有指定则获取约定好的配置文件,例如logback.xml、log4j2.xml;还没有获取到则 Spring约定好的配置文件,例如logback-spring.xml、log4j2-spring.xml;要是还没有找到配置文件,那只能尝试加载默认的配置了。
另外还整理成了40多套PDF文档:全套的Java面试宝典手册,“性能调优+微服务架构+并发编程+开源框架+分布式”等七大面试专栏,包含Tomcat、JVM、MySQL、SpringCloud、SpringBoot、Dubbo、并发、Spring、SpringMVC、MyBatis、Zookeeper、Ngnix、Kafka、MQ、Redis、MongoDB、memcached等等。如果你对这个感兴趣,小编可以免费分享。
资料获取方式:关注小编+转发+私信【面试题】获取上述资料~
重要的事情说三遍,转发+转发+转发,一定要记得转发哦!!