ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot 日志系统源码深度解析:从日志级别、LoggingSystem 工厂到初始化链路

2026/9/13 6:53:18 拓冰建站 浏览量
Spring Boot 日志系统源码深度解析:从日志级别、LoggingSystem 工厂到初始化链路 Spring Boot 日志系统源码深度解析从日志级别、LoggingSystem 工厂到初始化链路【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter导读日志系统是每个 Spring Boot 应用启动时最先被初始化的基础组件之一。本文以org.springframework.boot.logging包为核心从日志级别LogLevel枚举、LoggingSystem抽象类与工厂方法get()到LoggingApplicationListener驱动的beforeInitialize/initialize完整调用链路逐层剖析 Spring Boot 如何自动识别并装配 Logback、Log4j2、JUL 等日志实现以及自定义配置文件如logback.xml是如何被定位和加载的。读完本文你将掌握 Spring Boot 日志自动配置的完整脉络并能结合源码定位logging.config、logging.pattern.level等配置项的底层作用为日常排障与二次定制打下基础。本文基于仓库 README.md 中 SpringBoot 系列源码笔记展开对应源码阅读笔记为 docs/SpringBoot/SpringBoot-LogSystem.md分析对象为 Spring Boot 的org.springframework.boot.logging包及其在应用启动阶段docs/SpringBoot/Spring-Boot-Run.md 中的SpringApplication.run流程中的调用时机。日志级别LogLevel 枚举Spring Boot 在org.springframework.boot.logging.LogLevel中定义了一套与具体日志框架无关的日志级别抽象作为所有日志实现的统一入口public enum LogLevel { TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF }这 7 个级别覆盖了从最细粒度的TRACE到完全关闭日志的OFF。它的意义在于Spring Boot 屏蔽了各日志框架各自定义的级别体系对外只暴露这一套统一枚举上层代码如setLogLevel、getLoggerConfigurations等 API只需面向LogLevel编程而具体如何映射到某个日志框架的原生级别由各LoggingSystem实现自行负责。Java 日志实现JavaLoggingSystem 与级别映射org.springframework.boot.logging.java.JavaLoggingSystem是 JULjava.util.logging的实现类其类继承关系见下图LoggingSystem抽象基类→AbstractLoggingSystem→JavaLoggingSystem。该类在静态代码块中完成了Spring Boot 日志级别 ↔ JDK 日志级别的映射static { // KEY : springBoot 定义的日志级别, value: jdk 定义的日志级别 LEVELS.map(LogLevel.TRACE, Level.FINEST); LEVELS.map(LogLevel.DEBUG, Level.FINE); LEVELS.map(LogLevel.INFO, Level.INFO); LEVELS.map(LogLevel.WARN, Level.WARNING); LEVELS.map(LogLevel.ERROR, Level.SEVERE); LEVELS.map(LogLevel.FATAL, Level.SEVERE); LEVELS.map(LogLevel.OFF, Level.OFF); }注意两个细节TRACE对应 JUL 的FINESTDEBUG对应FINE这是级别粒度上的等价映射FATAL与ERROR都映射到 JUL 的SEVERE因为 JUL 原生并没有独立于SEVERE的FATAL级别只能共用。映射关系被存放在内部类LogLevelsT中它维护了两张互为反向的 Mapprotected static class LogLevelsT { /** * key SpringBoot 中定义的日志级别, value: 其他日志框架的日志级别 */ private final MapLogLevel, T systemToNative; /** * key : 其他日志框架的日志级别 , value: springBoot 中定义中定义的日志级别 */ private final MapT, LogLevel nativeToSystem; }systemToNativeSpring Boot 级别 → 框架原生级别用于向具体日志框架下发级别设置nativeToSystem框架原生级别 → Spring Boot 级别用于把框架当前状态翻译回统一的LogLevel视图例如getLoggerConfigurations查询各 Logger 当前级别时使用。这种双 Map 互转设计是适配器模式的典型体现Spring Boot 通过它把不同日志框架的级别体系统一收敛到LogLevel枚举上。LoggingSystem 抽象类与日志实现工厂org.springframework.boot.logging.LoggingSystem是抽象类是所有日志系统实现的统一父类。它内部维护了一个静态 MapSYSTEMS用于声明检测到某个日志框架的类 → 使用哪个 Spring Boot 处理类/** * key: 第三方日志框架的类 value: springBoot 中的处理类 */ private static final MapString, String SYSTEMS; static { MapString, String systems new LinkedHashMap(); systems.put(ch.qos.logback.core.Appender, org.springframework.boot.logging.logback.LogbackLoggingSystem); systems.put(org.apache.logging.log4j.core.impl.Log4jContextFactory, org.springframework.boot.logging.log4j2.Log4J2LoggingSystem); systems.put(java.util.logging.LogManager, org.springframework.boot.logging.java.JavaLoggingSystem); SYSTEMS Collections.unmodifiableMap(systems); }可见 Spring Boot 内置支持三大主流日志实现检测标志类classpath 中存在即生效对应 Spring Boot 处理类ch.qos.logback.core.AppenderLogbackLoggingSystemorg.apache.logging.log4j.core.impl.Log4jContextFactoryLog4J2LoggingSystemjava.util.logging.LogManagerJavaLoggingSystemLinkedHashMap保证了检测顺序的确定性——Logback 优先于 Log4j2、Log4j2 优先于 JUL。由于spring-boot-starter-logging默认引入 Logback因此Spring Boot 的默认日志实现就是LogbackLoggingSystem。LoggingSystem对外暴露的抽象方法即各日志系统必须实现的能力方法名称作用beforeInitialize初始化之前调用目的是减少日志输出避免启动早期刷屏initialize初始化日志加载配置、建立 LoggerContext 等cleanUp清除日志释放资源、关闭上下文getShutdownHandler获取关闭时的处理回调注册到 JVM ShutdownHookgetSupportedLogLevels获取支持的日志级别集合setLogLevel设置指定 Logger 的日志级别getLoggerConfigurations获取日志配置各 Logger 当前级别快照工厂方法 get()日志系统是怎么选出来的LoggingSystem.get(ClassLoader)是日志系统装配的入口工厂方法其逻辑分为两条路径public static LoggingSystem get(ClassLoader classLoader) { // 获取系统属性 String loggingSystem System.getProperty(SYSTEM_PROPERTY); if (StringUtils.hasLength(loggingSystem)) { // 是不是NONE if (NONE.equals(loggingSystem)) { // 空的日志系统 return new NoOpLoggingSystem(); } return get(classLoader, loggingSystem); } // 循环所有日志, return SYSTEMS.entrySet().stream().filter((entry) - ClassUtils.isPresent(entry.getKey(), classLoader)) .map((entry) - // 实例化具体日志 get(classLoader, entry.getValue())).findFirst() .orElseThrow(() - new IllegalStateException(No suitable logging system located)); }路径一显式指定。如果设置了系统属性logging.system即SYSTEM_PROPERTY值为NONE时返回NoOpLoggingSystem空日志系统什么也不输出否则把属性值当作LoggingSystem实现类的全限定名直接走反射实例化。路径二按 classpath 自动探测。遍历SYSTEMS用ClassUtils.isPresent判断 classpath 上是否存在对应框架的标志类第一个命中项对应的处理类被选中。若全部未命中则抛出IllegalStateException(No suitable logging system located)。实例化过程通过反射完成需要目标类提供接收ClassLoader的构造器private static LoggingSystem get(ClassLoader classLoader, String loggingSystemClass) { try { Class? systemClass ClassUtils.forName(loggingSystemClass, classLoader); Constructor? constructor systemClass.getDeclaredConstructor(ClassLoader.class); constructor.setAccessible(true); return (LoggingSystem) constructor.newInstance(classLoader); } catch (Exception ex) { throw new IllegalStateException(ex); } }这里使用getDeclaredConstructorsetAccessible(true)说明各日志系统实现类的构造器并非 public 可见反射绕过了可见性限制。最终默认得到org.springframework.boot.logging.logback.LogbackLoggingSystem实例。初始化前beforeInitialize 的调用链路日志系统的初始化由应用上下文之外的监听器驱动。LoggingApplicationListenerorg.springframework.boot.context.logging.LoggingApplicationListener是 Spring Boot 启动时注册的SpringApplicationRunListener之一它通过事件回调串起整条日志初始化链路。beforeInitialize的调用链路为LoggingApplicationListener#onApplicationEvent收到ApplicationStartingEventLoggingApplicationListener#onApplicationStartingEventLoggingSystem#beforeInitialize这一调用时机对应 docs/SpringBoot/Spring-Boot-Run.md 中SpringApplication.run里listeners.starting()阶段——此时应用环境尚未完全准备因此只做最小化初始化目的是减少启动早期的日志输出。下图为调试器中的实际调用栈截图可以直观看到main→SpringApplication.run→SpringApplicationRunListeners.starting→LoggingApplicationListener.onApplicationEvent的链路。以默认的LogbackLoggingSystem为例其beforeInitialize实现如下Override public void beforeInitialize() { // 日志上下文 LoggerContext loggerContext getLoggerContext(); // 是否初始化 if (isAlreadyInitialized(loggerContext)) { return; } // 父类方法 super.beforeInitialize(); // 添加过滤器 loggerContext.getTurboFilterList().add(FILTER); }关键点getLoggerContext()获取 Logback 的LoggerContext单例通过isAlreadyInitialized做幂等保护避免重复初始化调用父类AbstractLoggingSystem.beforeInitialize向loggerContext的TurboFilterList添加一个过滤器FILTER。TurboFilter是 Logback 的全局快速过滤器它能在日志事件创建前就决定是否输出——这里的作用正是抑制启动早期的低级别日志如 Spring 的 INFO 级启动日志等真正初始化完成后再移除见下文initialize中的remove(FILTER)。初始化initialize 的完整链路beforeInitialize完成后真正加载配置、建立日志体系的工作在initialize中完成。触发事件是ApplicationEnvironmentPreparedEvent——此时ConfigurableEnvironment已经就绪可以读取logging.*配置了。事件入口与整体编排private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) { if (this.loggingSystem null) { this.loggingSystem LoggingSystem.get(event.getSpringApplication().getClassLoader()); } initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader()); }首次进入时通过LoggingSystem.get完成日志系统实例的获取与启动早期beforeInitialize使用的是同一实例随后调用initialize。整体编排方法如下protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) { new LoggingSystemProperties(environment).apply(); this.logFile LogFile.get(environment); if (this.logFile ! null) { this.logFile.applyToSystemProperties(); } this.loggerGroups new LoggerGroups(DEFAULT_GROUP_LOGGERS); // 早期 的日志级别 initializeEarlyLoggingLevel(environment); // 初始化日志系统 initializeSystem(environment, this.loggingSystem, this.logFile); // 初始化日志级别 initializeFinalLoggingLevels(environment, this.loggingSystem); registerShutdownHookIfNecessary(environment, this.loggingSystem); }各步骤的作用LoggingSystemProperties(environment).apply()把logging.*环境属性如logging.pattern.*落地到日志系统可感知的属性中LogFile.get(environment)解析logging.file.*/logging.path等日志文件配置若存在则applyToSystemProperties把日志文件信息写入系统属性供日志框架引用${LOG_FILE}等占位符new LoggerGroups(DEFAULT_GROUP_LOGGERS)构建默认 Logger 分组如 SQL、Web 分组用于批量设置日志级别initializeEarlyLoggingLevel设置早期日志级别控制初始化过程中框架自身的输出initializeSystem真正初始化日志系统加载配置文件或默认配置initializeFinalLoggingLevels初始化最终日志级别应用logging.level.*配置registerShutdownHookIfNecessary注册 JVM 关闭钩子应用退出时调用getShutdownHandler做清理。initializeSystem配置文件优先默认配置兜底private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) { LoggingInitializationContext initializationContext new LoggingInitializationContext(environment); String logConfig environment.getProperty(CONFIG_PROPERTY); if (ignoreLogConfig(logConfig)) { // 日志系统初始化 system.initialize(initializationContext, null, logFile); } else { try { ResourceUtils.getURL(logConfig).openStream().close(); system.initialize(initializationContext, logConfig, logFile); } catch (Exception ex) { // NOTE: We cant use the logger here to report the problem System.err.println(Logging system failed to initialize using configuration from logConfig ); ex.printStackTrace(System.err); throw new IllegalStateException(ex); } } }这里的CONFIG_PROPERTY即logging.config是用户显式指定日志配置文件的最直接入口若未设置logging.config则以null作为配置位置交给system.initialize随后走约定优先的默认配置文件查找若已设置先通过ResourceUtils.getURL(logConfig).openStream().close()校验配置资源可访问再交给system.initialize加载校验失败会在标准错误流打印错误并抛出IllegalStateException此时日志尚未就绪无法使用 Logger 记录。LogbackLoggingSystem#initialize初始化完成前的收尾Override public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) { LoggerContext loggerContext getLoggerContext(); if (isAlreadyInitialized(loggerContext)) { return; } // 日志初始化 super.initialize(initializationContext, configLocation, logFile); loggerContext.getTurboFilterList().remove(FILTER); 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.); } }与beforeInitialize形成闭环初始化完成后把临时加的FILTER从TurboFilterList中移除日志恢复按配置正常输出。markAsInitialized的实现是在LoggerContext中放入一个标记对象用于isAlreadyInitialized的幂等判断private void markAsInitialized(LoggerContext loggerContext) { loggerContext.putObject(LoggingSystem.class.getName(), new Object()); }另一个值得注意的细节如果开发者设置了 Logback 原生的logback.configurationFile系统属性Spring Boot 会给出明确警告——请使用logging.config而不是logback.configurationFile因为后者无法被 Spring Boot 的环境抽象接管。initializeWithConventions约定优先的配置解析AbstractLoggingSystem#initializeWithConventions是决定到底加载哪个配置文件的核心逻辑private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) { String config getSelfInitializationConfig(); if (config ! null logFile null) { // self initialization has occurred, reinitialize in case of property changes reinitialize(initializationContext); return; } if (config null) { config getSpringInitializationConfig(); } if (config ! null) { loadConfiguration(initializationContext, config, logFile); return; } // 加载默认配置 loadDefaults(initializationContext, logFile); }决策顺序如下getSelfInitializationConfig()在标准位置查找日志框架自带的配置文件如logback.xml若找到且未配置日志文件logFile null说明日志框架可能已被外部如容器或工具自初始化此时调用reinitialize重来一遍确保应用配置生效否则尝试getSpringInitializationConfig()Spring Boot 提供了logback-spring.xml这类带 Spring 扩展的配置位置支持springProfile等特性都没找到时loadDefaults加载 Spring Boot 内置的默认配置。loadDefaults内置默认配置与占位符注入以 Logback 为例默认配置的组装逻辑在LogbackLoggingSystem#loadDefaultsOverride protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) { LoggerContext context getLoggerContext(); stopAndReset(context); boolean debug Boolean.getBoolean(logback.debug); if (debug) { StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener()); } 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})); new DefaultLogbackConfiguration(initializationContext, logFile).apply(configurator); context.setPackagingDataEnabled(true); }这里同时揭示了几个常用logging.*配置项及其默认值采用嵌套占位符 默认值的解析方式${a:b}表示 a 缺失时用 b配置项默认值含义logging.pattern.level${LOG_LEVEL_PATTERN:%5p}即%5p日志输出中的级别格式logging.pattern.dateformat${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}日志时间戳格式logging.pattern.rolling-file-name${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz日志滚动文件命名规则logback.debug系统属性false是否打印 Logback 自身调试状态这些默认值通过context.putProperty写入 LoggerContext 属性供默认配置模板引用DefaultLogbackConfiguration则把控制台/文件 Appender、%d/%level等 pattern 组装进 Logback。默认配置文件Spring Boot 如何找到 logback.xmlSpring Boot 的约定优先体现在默认配置文件的查找上位置顺序由getStandardConfigLocations定义Override protected String[] getStandardConfigLocations() { return new String[] { logback-test.groovy, logback-test.xml, logback.groovy, logback.xml }; }而getSelfInitializationConfig→findConfig的实现说明了一切protected String getSelfInitializationConfig() { // 寻找配置文件 return findConfig(getStandardConfigLocations()); } private String findConfig(String[] locations) { for (String location : locations) { ClassPathResource resource new ClassPathResource(location, this.classLoader); if (resource.exists()) { return classpath: location; } } return null; }即按logback-test.groovy→logback-test.xml→logback.groovy→logback.xml的顺序在 classpath 根目录探测第一个存在的文件胜出返回classpath:前缀的定位串。因此只要把logback.xml放到src/main/resources下Spring Boot 就能自动发现并使用它。实测验证加依赖、加配置、观察查找结果要复现这条链路只需两步第一步确认依赖。引入spring-boot-starter-logging通常由spring-boot-starter-web等起步依赖传递引入dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-logging/artifactId version${revision}/version /dependency第二步添加配置文件。在src/main/resources下放置logback.xml示意图中为项目添加该文件后的目录结构包含Application主类与resources下的application.yml、logback.xml。此时在initializeWithConventions方法处打断点可以观察到config变量被解析为classpath:logback.xml说明自定义配置文件已被成功定位reinitialize 与 loadConfiguration配置变更后的重载当检测到日志框架已被自初始化、需要重来一遍或logFile ! null需要重新应用属性时会走reinitializeOverride protected void reinitialize(LoggingInitializationContext initializationContext) { // 日志上下文重新设置 getLoggerContext().reset(); getLoggerContext().getStatusManager().clear(); // 加载配置文件 loadConfiguration(initializationContext, getSelfInitializationConfig(), null); }loadConfiguration在 Logback 中的实现做了两件事先通过stopAndReset复位上下文再用configureByResourceUrl从配置文件 URL 重新装配并在加载后扫描 Status 列表中的 ERROR 级状态一旦发现配置错误立即抛出IllegalStateException并附上全部错误明细Override protected void loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile) { // 父类方法 super.loadConfiguration(initializationContext, location, logFile); // 获取上下文 LoggerContext loggerContext getLoggerContext(); // 停止并且重启 stopAndReset(loggerContext); try { // 配置文件加载 configureByResourceUrl(initializationContext, loggerContext, ResourceUtils.getURL(location)); } catch (Exception ex) { throw new IllegalStateException(Could not initialize Logback logging from location, ex); } ListStatus statuses loggerContext.getStatusManager().getCopyOfStatusList(); StringBuilder errors new StringBuilder(); for (Status status : statuses) { if (status.getLevel() Status.ERROR) { errors.append((errors.length() 0) ? String.format(%n) : ); errors.append(status.toString()); } } if (errors.length() 0) { throw new IllegalStateException(String.format(Logback configuration error detected: %n%s, errors)); } }configureByResourceUrl则根据配置文件扩展名选择不同的解析器——xml结尾使用SpringBootJoranConfiguratorSpring Boot 定制的 Joran 配置器负责执行doConfigure完成 Logback 的 XML 配置解析否则交给 Logback 原生的ContextInitializerprivate void configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext, URL url) throws JoranException { if (url.toString().endsWith(xml)) { // logback 日志操作 JoranConfigurator configurator new SpringBootJoranConfigurator(initializationContext); // 设置上下文 configurator.setContext(loggerContext); // 执行配置 configurator.doConfigure(url); } else { new ContextInitializer(loggerContext).configureByResource(url); } }SpringBootJoranConfigurator的意义在于在标准 Joran 解析基础上注入 Spring Boot 的环境上下文使得logback-spring.xml中可以使用springProperty等 Spring 扩展标签把application.yml里的属性动态接入 Logback 配置。XML 解析本身由 Logback 的 Joran 引擎完成这部分属于 Logback 框架内部逻辑Spring Boot 只是在其上做了环境注入。小结一次 Spring Boot 启动中的日志装配全景把整条链路串起来Spring Boot 应用的日志系统装配过程可以概括为级别抽象LogLevel枚举统一 7 级日志级别各实现类通过LogLevels双 Map 完成与原生级别的互转实现选择LoggingSystem.get依据logging.system系统属性显式或SYSTEMS映射 classpath 探测默认选出LogbackLoggingSystem事件驱动LoggingApplicationListener监听启动事件在ApplicationStartingEvent阶段调用beforeInitialize加 TurboFilter 压制早期日志在ApplicationEnvironmentPreparedEvent阶段调用initialize正式装配配置解析initializeSystem依据logging.config显式配置或由initializeWithConventions按logback-spring.xml/logback.xml等约定位置查找兜底使用loadDefaults的内置默认配置收尾与生命周期初始化完成后移除临时过滤器并markAsInitialized同时注册 JVM 关闭钩子以便cleanUp。理解这条链路后遇到日志配置不生效启动日志看不到配置文件加载失败等问题时就能从LoggingApplicationListener的事件时机、logging.config的校验逻辑、loadConfiguration的 Status 错误扫描这几个关键节点入手快速定位。进一步延伸阅读可参考同仓库的 docs/SpringBoot/Spring-Boot-Run.md日志监听器挂载的SpringApplication.run启动流程与 docs/SpringBoot/SpringBoot-application-load.md环境与配置加载日志初始化依赖的Environment来源。【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考