专栏名称: Java基基
一个苦练基本功的 Java 公众号,所以取名 Java 基基
目录
相关文章推荐
龙船风电网  ·  2艘风电运维船!这家船厂拟中标 ·  6 小时前  
龙船风电网  ·  2艘风电运维船!这家船厂拟中标 ·  6 小时前  
36氪Pro  ·  海外短剧平台StoReel完成数百万美元战略 ... ·  8 小时前  
36氪Pro  ·  海外短剧平台StoReel完成数百万美元战略 ... ·  8 小时前  
凤凰网  ·  2025年最猛的相亲方式:直播间群面 ·  3 天前  
凤凰网  ·  2025年最猛的相亲方式:直播间群面 ·  3 天前  
51好读  ›  专栏  ›  Java基基

史上最全,全方位阐述 SpringBoot 中的日志是怎么工作(珍藏版)

Java基基  · 公众号  ·  · 2024-05-23 11:55

正文

👉 这是一个或许对你有用 的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入 芋道快速开发平台 知识星球。 下面是星球提供的部分资料:

👉 这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本

来源:juejin.cn/post/
7348309454700183561


前言

日志,是开发中熟悉又陌生的伙伴,熟悉是因为我们经常会在各种场合打印日志,陌生是因为大部分时候我们都不太关心日志是怎么打印出来的,因为打印一条日志,在我们看来是一件太平常不过的事情了,特别是在宇宙第一框架Springboot的加持下,日志打印是怎么工作的就更没人关注了。

但是了解日志框架怎么工作,以及学会Springboot怎么和Log4j2或Logback等日志框架集成,对我们扩展日志功能以及优雅打印日志大有好处,甚至在有些场景,还能通过调整日志的打印策略来提升我们的系统吞吐量。

所以本文将以Springboot集成Log4j2为例,详细说明Springboot框架下Log4j2是如何工作的,你可能会担心,如果是使用Logback日志框架该怎么办呢,其实Log4j2和Logback极其相似,Springboot在启动时处理Log4j2和处理Logback也几乎是一样的套路,所以学会Springboot框架下Log4j2如何工作,切换成Logback也是轻轻松松的。

本文遵循一个该深则深,该浅则浅的整体指导方针,全方位的阐述Springboot中日志怎么工作,思维导图如下所示。

  • Springboot版本:2.7.2
  • Log4j2版本:2.17.2

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

一. Log4j2简单工作原理分析

使用Log4j2打印日志时,我们自己接触最多的就是Logger对象了,Logger对象叫做日志打印器,负责打印日志,一个Logger对象,结构简单示意如下。

实际打印日志的是Logger对象使用的Appender对象,至于Appender对象怎么打印日志,不在我们本文的关注范围内。特别注意,在Log4j2中,Logger对象实际只是一个壳子,灵魂是其持有的LoggerConfig对象,LoggerConfig决定打印时使用哪些Appender对象,以及Logger的级别。

LoggerConfig和Appender通常是在Log4j2的配置文件中定义出来的,配置文件通常命名为Log4j2.xml,Log4j2框架在初始化时,会去加载这个配置文件并解析成一个配置对象Configuration,示意如下。

我们每在配置文件的 标签下增加一项,解析得到的Configuration的appenders中就多一个Appender,每在 标签下增加一项,解析得到的Configuration的loggerConfigs中就多一个LoggerConfig,并且LoggerConfig解析出来时,其和Appender的关系也就确认了。

在Log4j2中,还有一个LoggerContext对象,这个对象持有上述的Configuration对象,我们使用的每一个Logger,一开始都会先去LoggerContext的loggerRegistry中获取,如果没有,则会创建一个Logger出来再缓存到LoggerContext的loggerRegistry中,同时我们在创建Logger时其实核心就是要为这个创建的Logger找到它对应的LoggerConfig,那么去哪里找LoggerConfig呢,当然就是去Configuration中找,所以Logger,LoggerContext和Configuration的关系可以描述成下面这样子。

所以Log4j2在这种结构下,要修改日志打印器是十分方便的,我们通过LoggerContext就可以拿到Configuration,拿到Configuration之后,我们就可以方便的操作LoggerConfig了,例如最常用的日志打印器级别热更新就是这么完成的。

在继续阅读后文之前,有一个很重要的概念需要阐述清楚,那就是对于Springboot来说,Springboot在操作Logger时,操作的对象就是一个Logger,比如要给一个名字为 com.honey.Login 的Logger设置级别为DEBUG,那么在Springboot看来,它就是在设置名字为 com.honey.Login 的Logger的级别为DEBUG,但是具体到Log4j2框架,其实底层是在设置名字为 com.honey.Login 的LoggerConfig的级别为DEBUG,而具体到Logback框架,就是在设置名字为 com.honey.Login 的Logger的级别为DEBUG。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

二. Springboot日志简单配置说明

我们在Springboot中使用Log4j2时,虽然大部分时候我们还是会提供一个Log4j2.xml文件来供Log4j2框架读取,但是Springboot也提供了一些配置来供我们使用,在分析Springboot日志启动机制前,先学习一下里面的若干配置项可以方便我们后续的机制理解。

1. logging.file.name

假如我们像下面这样配置。

logging:
  file:
    name: test.log

那么Springboot会把日志内容输出一份到当前项目根路径下的test.log文件中。

2. logging.file.path

假如我们像下面这样配置。

logging:
  file:
    path: /

那么Springboot会把日志内容输出一份到指定目录下的spring.log文件中。

3. logging.level

假如我们像下面这样配置。

logging:
  level:
    com.pww.App: warn

那么我们可以指定名称为 com.pww.App 的日志打印器的级别为warn级别。

三. Springboot日志启动机制分析

通常我们使用Springboot时,就算不提供Log4j2.xml配置文件,Springboot也能输出很漂亮的日志,那么Springboot肯定在背后有帮我们完成Log4j2或Logback等框架的初始化,那么本节就刨析一下Springboot中的日志启动机制。

Springboot中的日志启动主要依赖于 LoggingApplicationListener ,这个监听器在Springboot启动流程中主要会监听如下三个事件。

  • ApplicationStartingEvent: 在启动 SpringApplication 之后就发布该事件,先于Environmen和 ApplicationContext 可用之前发布;
  • ApplicationEnvironmentPreparedEvent: 在Environmen准备好之后立即发布;
  • ApplicationPreparedEvent: ApplicationContext 完全准备好之后但刷新容器之前发布。

下面依次分析下监听到这些事件后, LoggingApplicationListener 会完成一些什么事情来帮助初始化日志框架。

1. 监听到ApplicationStartingEvent

LoggingApplicationListener的 onApplicationStartingEvent() 方法如下所示。

private void onApplicationStartingEvent(ApplicationStartingEvent event) {
    // 读取org.springframework.boot.logging.LoggingSystem系统属性来加载得到LoggingSystem
    this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
    // 调用LoggingSystem的beforeInitialize()方法提前做一些初始化准备工作
    this.loggingSystem.beforeInitialize();
}

Springboot中操作日志的最关键的一个对象就是LoggingSystem,这个对象会在Springboot的整个生命周期中掌控着日志,在 LoggingApplicationListener 监听到 ApplicationStartingEvent 事件后,第一件事情就是先读取 org.springframework.boot.logging.LoggingSystem 系统属性,得到要加载的LoggingSystem的全限定名,然后完成加载。

如果是使用Log4j2框架,对应的LoggingSystem是 Log4J2LoggingSystem ,如果是使用Logback框架,对应的LoggingSystem是 LogbackLoggingSystem ,当然我们也可以在 LoggingApplicationListener 监听到 ApplicationStartingEvent 事件之前,提前把 org.springframework.boot.logging.LoggingSystem 设置为我们自己提供的LoggingSystem的全限定名,这样我们就可以对Springboot中的日志初始化做一些定制修改。

拿到LoggingSystem后,就会调用其 beforeInitialize() 方法来完成日志框架初始化前的一些准备,这里看一下 Log4J2LoggingSystem beforeInitialize() 方法实现,如下所示。

@Override
public void beforeInitialize() {
    LoggerContext loggerContext = getLoggerContext();
    if (isAlreadyInitialized(loggerContext)) {
        return;
    }
    super.beforeInitialize();
    // 添加一个过滤器
    // 这个过滤器会阻止所有日志的打印
    loggerContext.getConfiguration().addFilter(FILTER);
}

上述方法最关键的就是添加了一个过滤器,虽然叫做过滤器,但是实则为阻断器,因为这个 FILTER 会阻止所有日志打印,Springboot这样设计是为了防止日志系统在完全完成初始化前打印出不可控的日志。

所以小结一下, LoggingApplicationListener 监听到 ApplicationStartingEvent 之后,主要完成两件事情。

  • 从系统属性中拿到LoggingSystem的全限定名并完成加载;
  • 调用LoggingSystem的 beforeInitialize() 方法来添加会拒绝打印任何日志的过滤器以阻止日志打印。

2. 监听到ApplicationEnvironmentPreparedEvent

LoggingApplicationListener onApplicationEnvironmentPreparedEvent() 方法如下所示。

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    SpringApplication springApplication = event.getSpringApplication();
    if (this.loggingSystem == null) {
        this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());
    }
    // 因为此时Environment已经完成了加载
    // 获取到Environment并继续调用initialize()方法
    initialize(event.getEnvironment(), springApplication.getClassLoader());
}

继续跟进 LoggingApplicationListener initialize() 方法。

protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
    // 把通过logging.xxx配置的值设置到系统属性中
    getLoggingSystemProperties(environment).apply();
    this.logFile = LogFile.get(environment);
    if (this.logFile != null) {
        // 把logging.file.name和logging.file.path的值设置到系统属性中
        this.logFile.applyToSystemProperties();
    }
    // 基于预置的web和sql日志打印器初始化LoggerGroups
    this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
    // 读取配置中的debug和trace是否设置为true
    // 哪个为true就把springBootLogging级别设置为什么
    // 同时设置为true则trace优先级更高
    initializeEarlyLoggingLevel(environment);
    // 调用到具体的LoggingSystem实际初始化日志框架
    initializeSystem(environment, this.loggingSystem, this.logFile);
    // 完成日志打印器组和日志打印器的级别的设置
    initializeFinalLoggingLevels(environment, this.loggingSystem);
    registerShutdownHookIfNecessary(environment, this.loggingSystem);
}

上述方法概括下来就是做了三部分的事情。

1、把日志相关配置设置到系统属性中。例如我们可以通过 logging.pattern.console 来配置标准输出日志格式,但是在XML文件里面没办法读取到 logging.pattern.console 配置的值,此时就需要设置一个系统属性,属性名是 CONSOLE_LOG_PATTERN ,属性值是 logging.pattern.console 配置的值,后续在XML文件中就可以通过 ${sys:CONSOLE_LOG_PATTERN} 读取到 logging.pattern.console 配置的值。

下表是Springboot中日志配置和系统属性名的对应关系:

2、调用LoggingSystem的 initialize() 方法来完成日志框架初始化。这里就是实际完成Log4j2或Logback等框架的初始化;

3、在日志框架完成初始化后基于 logging.level 的配置来设置日志打印器组和日志打印器的级别。

上述第2点是Springboot如何完成具体的日志框架的初始化,这个在后面章节中会详细分析。上述第3点是日志框架初始化完毕后,Springboot如何帮助我们完成日志打印器组或日志打印器的级别的设置,这里就扯出来一个概念:日志打印器组,也就是LoggerGroup。

我们如果要操作一个Logger,那么实际就是要拿着这个Logger的名称,去找到Logger,然后再进行操作,这在Logger不多的时候是没问题的,但是假如我有几十上百个Logger呢,一个一个去找到Logger再操作无疑是很不现实的,一个实际的场景就是修改Logger的级别,如果是通过Logger的名字去找到Logger再修改级别,那么是很痛苦的一件事情,但是如果能够把所有Logger按照功能进行分组,我们一组一组的去修改,一下子就优雅起来了,LoggerGroup就是干这个事情的。

一个LoggerGroup,有三个字段,说明如下。

  • name: 表示LoggerGroup的名字,要操作LoggerGroup时,就通过name来唯一确定一个LoggerGroup,假如有一个LoggerGroup名字为login,那么我们可以通过 logging.level.loggin=debug ,将这个LoggerGroup下所有的Logger的级别设置为debug;
  • members: 是当前LoggerGroup里所有Logger的名字的集合;
  • configuredLevel: 表示最近一次给LoggerGroup设置的级别。

在Springboot中,通过 logging.group 可以配置LoggerGroup,示例如下。

logging:
  group:
    login:
      - com.lee.controller.LoginController
      - com.lee.service.LoginService
      - com.lee.dao.LoginDao
    common:
      - com.lee.util
      - com.lee.config

结合logging.level可以直接给一组Logger设置级别,示例如下。

logging:
  level:
    login: info
    common: debug
  group:
    login:
      - com.lee.controller.LoginController
      - com.lee.service.LoginService
      - com.lee.dao.LoginDao
    common:
      - com.lee.util
      - com.lee.config

那么此时名称为login的LoggerGroup表示如下。

{
    "name""login",
    "members": [
        "com.lee.controller.LoginController",
        "com.lee.service.LoginService",
        "com.lee.dao.LoginDao"
    ],
    "configuredLevel""INFO"
}

名称为common的LoggerGroup表示如下。

{
    "name""common",
    "members": [
        "com.lee.util",
        "com.lee.config"
    ],
    "configuredLevel""DEBUG"
}

最后再看一下Springboot中预置的LoggerGroup,有两个,名字分别为web和sql,如下所示。

{
    "name""web",
    "members": [
        "org.springframework.core.codec",
        "org.springframework.http",
        "org.springframework.web",
        "org.springframework.boot.actuate.endpoint.web",
        "org.springframework.boot.web.servlet.ServletContextInitializerBeans"
    ],
    "configuredLevel"""
}
{
    "name""sql",
    "members": [
        "org.springframework.jdbc.core",
        "org.hibernate.SQL",
        "org.jooq.tools.LoggerListener"
    ],
    "configuredLevel"""
}

至于web和sql这两个LoggerGroup的级别是什么,有两种手段来指定,第一种是通过配置 debug=true 来将web和sql这两个LoggerGroup的级别指定为DEBUG,第二种是通过 logging.level.web logging.level.sql 来指定web和sql这两个LoggerGroup的级别,其中第二种优先级高于第一种。

上面最后讲的这一点,其实就是告诉我们怎么来控制Springboot自己的相关的日志的打印级别,如果配置 debug=true ,那么如下的Springboot自己的LoggerGroup和Logger级别会设置为debug。

sql
web
org.springframework.boot

如果配置 trace=true ,那么如下的Springboot自己的Logger级别会设置为trace。

org.springframework
org.apache.tomcat
org.apache.catalina
org.eclipse.jetty
org.hibernate.tool.hbm2ddl

现在小结一下,监听到 ApplicationEnvironmentPreparedEvent 事件后,Springboot主要完成三件事情。

  • 把通过配置文件配置的日志相关属性设置为系统属性;
  • 实际完成日志框架的初始化;
  • 设置Springboot和用户自定义的LoggerGroup与Logger级别。

3. 监听到ApplicationPreparedEvent

LoggingApplicationListener onApplicationPreparedEvent() 方法如下所示。

private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
    ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory();
    if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
        // 把实际加载的LoggingSystem注册到容器中
        beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
    }
    if (this.logFile != null && !beanFactory.containsBean(LOG_FILE_BEAN_NAME)) {
        // 把实际使用的LogFile注册到容器中
        beanFactory.registerSingleton(LOG_FILE_BEAN_NAME, this.logFile);
    }
    if (this.loggerGroups != null && !beanFactory.containsBean(LOGGER_GROUPS_BEAN_NAME)) {
        // 把保存着所有LoggerGroup的LoggerGroups注册到容器中
        beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, this.loggerGroups);
    }
}

主要就是把之前加载的LoggingSystem,LogFile和LoggerGroups添加到Spring容器中,进行到这里,其实整个日志框架已经完成初始化了,这里只是把一些和日志密切相关的一些对象注册为容器中的bean。

最后,本节以下图对Springboot日志启动流程做一个总结。

四. Springboot集成Log4j2原理说明

在Springboot中使用Log4j2时,我们不提供Log4j2的配置文件也能打印日志,而我们提供了Log4j2的配置文件后日志打印行为又会以我们提供的配置文件为准,这里面其实Springboot为我们做了很多事情,当我们不提供Log4j2配置文件时,Springboot会加载其预置的配置文件,并且会根据我们是否配置了 logging.file.xxx 自动决定是加载预置的log4j2.xml还是log4j2-file.xml,而与此同时Springboot也会尽可能的去搜索我们提供的配置文件,无论我们在classpath下提供的配置文件名字是Log4j2.xml还是Log4j2-spring.xml,都是能够被Springboot搜索到并加载的。

上述的Springboot集成Log4j2的行为,全部发生在 Log4J2LoggingSystem 中,本节将对这里面的流程和原理进行说明。

在第三节中已经知道,Springboot启动时,当 LoggingApplicationListener 监听到 ApplicationEnvironmentPreparedEvent 事件后,最终会调用到 LoggingApplicationListener initializeSystem() 方法来完成日志框架的初始化,所以我们先看一下这里的逻辑是什么,源码实现如下。

private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
    // 读取环境变量中的logging.config作为用户提供的配置文件路径
    String logConfig = StringUtils.trimWhitespace(environment.getProperty(CONFIG_PROPERTY));
    try {
        // 创建LoggingInitializationContext用于传递Environment对象
        LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
        if (ignoreLogConfig(logConfig)) {
            // 1. 没有配置logging.config
            system.initialize(initializationContext, null, logFile);
        } else {
            // 2. 配置了logging.config
            system.initialize(initializationContext, logConfig, logFile);
        }
    } catch (Exception ex) {
        // 省略异常处理
    }
}

LoggingApplicationListener initializeSystem() 方法会读取 logging.config 环境变量得到用户提供的配置文件路径,然后带着配置文件路径,调用到 Log4J2LoggingSystem initialize() 方法,所以后续分两种情况讨论,即没配置 logging.config 和有配置 logging.config

1. 没配置logging.config

Log4J2LoggingSystem initialize() 方法如下所示。

@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile)  {
    LoggerContext loggerContext = getLoggerContext();
    // 判断LoggerContext的ExternalContext是不是当前LoggingSystem的全限定名
    // 如果是则表明当前LoggingSystem已经执行过初始化逻辑
    if (isAlreadyInitialized(loggerContext)) {
        return;
    }
    // 移除之前添加的防噪过滤器
    loggerContext.getConfiguration().removeFilter(FILTER);
    // 调用到父类AbstractLoggingSystem的initialize()方法
    // 注意因为没有配置logging.config所以这里configLocation为null
    super.initialize(initializationContext, configLocation, logFile);
    // 将当前LoggingSystem的全限定名设置给LoggerContext的ExternalContext
    // 表明当前LoggingSystem已经对LoggerContext执行过初始化逻辑
    markAsInitialized(loggerContext);
}

上述方法会继续调用到 AbstractLoggingSystem initialize() 方法,并且因为没有配置 logging.config ,所以传递过去的 configLocation 参数为null,下面看一下 AbstractLoggingSystem initialize() 方法的实现,如下所示。

@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    if (StringUtils.hasLength(configLocation)) {
        initializeWithSpecificConfig(initializationContext, configLocation, logFile);
        return;
    }
    // 基于约定寻找配置文件并完成初始化
    initializeWithConventions(initializationContext, logFile);
}

因为configLocation为null,所以会继续调用到 initializeWithConventions() 方法完成初始化,并且初始化使用到的配置文件,Springboot会按照约定的名字去classpath寻找,下面看一下 initializeWithConventions() 方法的实现。

private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
    // 搜索标准日志配置文件路径
    String config = getSelfInitializationConfig();
    if (config != null && logFile == null) {
        reinitialize(initializationContext);
        return;
    }
    if (config == null) {
        // 搜索Spring日志配置文件路径
        config = getSpringInitializationConfig();
    }
    if (config != null) {
        // 如果搜索到约定的配置文件则进行配置文件加载
        loadConfiguration(initializationContext, config, logFile);
        return;
    }
    // 如果搜索不到则使用LoggingSystem同目录下的配置文件
    loadDefaults(initializationContext, logFile);
}

上述方法中,首先会去搜索标准日志配置文件路径,其实就是判断classpath下是否存在如下名字的配置文件。

log4j2-test.properties
log4j2-test.json
log4j2-test.jsn
log4j2-test.xml
log4j2.properties
log4j2.json
log4j2.jsn
log4j2.xml

如果不存在,则再去搜索Spring日志配置文件路径,也就是判断classpath下是否存在如下名字的配置文件。

log4j2-test-spring.properties
log4j2-test-spring.json
log4j2-test-spring.jsn
log4j2-test-spring.xml
log4j2-spring.properties
log4j2-spring.json
log4j2-spring.jsn
log4j2-spring.xml

如果都找不到,此时Springboot就会将 Log4J2LoggingSystem 同目录下的log4j2.xml(无LogFile)或log4j2-file.xml(有LogFile)作为日志配置文件,所以不用担心找不到配置文件,有Springboot为我们进行兜底。

在获取到配置文件路径后,最终会调用到 Log4J2LoggingSystem 如下的加载配置的方法。

protected void loadConfiguration(String location, LogFile logFile, List overrides) {
    Assert.notNull(location, "Location must not be null");
    try {
        List configurations = new ArrayList<>();
        LoggerContext context = getLoggerContext();
        // 根据配置文件路径加载得到Configuration并添加到集合中
        configurations.add(load(location, context));
        // 加载logging.log4j2.config.override配置的配置文件为Configuration
        // 所有加载的Configuration都要添加到configurations集合中
        for (String override : overrides) {
            configurations.add(load(override, context));
        }
        // 如果得到了大于1个的Configuration则基于所有Configuration创建CompositeConfiguration
        Configuration configuration = (configurations.size() > 1) ? createComposite(configurations)
                : configurations.iterator().next();
        // 将加载得到的Configuration启动并设置给LoggerContext
        // 这里会将加载得到的Configuration覆盖LoggerContext持有的老的Configuration
        context.start(configuration);
    } catch (Exception ex) {
        throw new IllegalStateException("Could not initialize Log4J2 logging from " + location, ex);
    }
}

上述方法中实际就会拿着配置文件的路径去加载得到Configuration,与此同时还会拿到所有通过 logging.log4j2.config.override 配置的路径,去加载得到Configuration,最终如果得到大于1个的Configuration,则将这些Configuration创建为 CompositeConfiguration

这里可能会有疑问, logging.log4j2.config.override 到底是一个什么东西,其实不难发现,无论是通过 logging.config 指定了配置文件路径,还是按照Springboot约定提供了配置文件,亦或者使用了Springboot预置的配置文件,其实最终都只能得到一个配置文件路径然后得到一个Configuration,那么怎么才能加载多份配置文件呢,那就要通过 logging.log4j2.config.override 来指定多个配置文件路径,使用示例如下。

logging:
  config: classpath:Log4j2.xml
  log4j2:
    config:
      override:
        - classpath:Log4j2-custom1.xml
        - classpath:Log4j2-custom2.xml

如果按照上面这样配置,那么最终就会加载得到三个Configuration,然后再基于这三个Configuration创建得到一个 CompositeConfiguration

在加载得到Configuration之后,就会调用到 LoggerContext start() 方法完成Log4j2框架的初始化,那么这里其实会做如下三件事情。

  • 调用Configuration的start() 方法完成配置对象的初始化。 这里其实就是将我们在配置文件中定义的各种Appedner和LoggerConfig等都创建出来并完成启动;
  • 将启动完毕的Configuration设置给LoggerContext。 这里会把LoggerContext持有的老的Configuration覆盖掉,所以如果LoggerContext之前持有其它的Configuration,那么其实在Springboot日志初始化完毕后老的Configuration会被丢弃掉;
  • 更新Logger。 如果之前有已经创建好的Logger,那么就基于新的Configuration替换掉这些Logger持有的LoggerConfig。

至此,没配置 logging.config 时的初始化逻辑就分析完毕。

2. 有配置logging.config

有配置logging.config时,情况就变得简单了。还是从 Log4J2LoggingSystem initialize() 方法出发,跟一下源码。

@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    LoggerContext loggerContext = getLoggerContext();
    if (isAlreadyInitialized(loggerContext)) {
        return;
    }
    loggerContext.getConfiguration().removeFilter(FILTER);
    // 调用到父类AbstractLoggingSystem的initialize()方法
    // 注意因为配置了logging.config所以这里configLocation不为null
    super.initialize(initializationContext, configLocation, logFile);
    markAsInitialized(loggerContext);
}

继续跟进 AbstractLoggingSystem initialize() 方法,如下所示。

@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    if (StringUtils.hasLength(configLocation)) {
        // 基于指定的配置文件完成初始化
        initializeWithSpecificConfig(initializationContext, configLocation, logFile);
        return;
    }
    initializeWithConventions(initializationContext, logFile);
}

由于指定了配置文件,所以会调用到 AbstractLoggingSystem initializeWithSpecificConfig() 方法,该方法没有什么额外逻辑,最终会执行到和没配置 logging.config 时一样的 Log4J2LoggingSystem 的加载配置的方法,如下所示。

protected void loadConfiguration(String location, LogFile logFile, List overrides) {
    Assert.notNull(location, "Location must not be null");
    try {
        List configurations = new ArrayList<>();
        LoggerContext context = getLoggerContext();
        // 根据配置文件路径加载得到Configuration并添加到集合中
        configurations.add(load(location, context));
        // 加载logging.log4j2.config.override配置的配置文件为Configuration
        // 所有加载的Configuration都要添加到configurations集合中
        for (String override : overrides) {
            configurations.add(load(override, context));
        }
        // 如果得到了大于1个的Configuration则基于所有Configuration创建CompositeConfiguration
        Configuration configuration = (configurations.size() > 1) ? createComposite(configurations)
                : configurations.iterator().next();
        // 将加载得到的Configuration启动并设置给LoggerContext
        // 这里会将加载得到的Configuration覆盖LoggerContext持有的老的Configuration
        context.start(configuration);
    } catch (Exception ex) {
        throw new IllegalStateException("Could not initialize Log4J2 logging from " + location, ex);
    }
}

所以配置了 logging.config 时,就会以 logging.config 指定的配置文件作为最终使用的配置文件,而不会去基于约定搜索配置文件,同时也不会去使用LoggingSystem同目录下预置的配置文件。

小结一下,Springboot集成Log4j2日志框架时,主要分为两种情况:

  • 没配置logging.config。 这种情况下,Springboot会基于约定努力去寻找符合的配置文件,如果找不到则会使用预置的配置文件且预置的配置文件需要在 LoggingSystem 的同目录下,拿到配置文件后就会加载为 Configuration 然后替换掉 LoggerContext 里的旧的 Configuration ,此时就完成日志框架初始化;
  • 有配置logging.config。 这种情况下,会将 logging.config 指定的配置文件加载为 Configuration ,然后替换掉 LoggerContext 里的旧的 Configuration ,此时就完成日志框架初始化。

无论有没有配置 logging.config ,都只能加载一个配置文件为Configuration,如果想加载多个Configuration,那么需要通过 logging.log4j2.config.override 配置多个配置文件路径,此时就能加载多个Configuration来初始化Log4j2日志框架了。

Springboot集成Log4j2日志框架的流程图如下所示。

五. Springboot日志打印器级别热更新

在日志打印中,一条日志在发起打印时,会根据我们的指定携带一个日志级别,同时打印日志的日志打印器,也有一个级别,日志打印器只能打印级别高于或等于自身的日志。

由于日志打印时,日志级别是由代码决定的,所以日志级别除非改代码,否则无法改变,但是日志打印器的级别是可以随时更改的,最简单的方式就是通过配置环境变量来更改 logging.level ,此时我们的应用进程所处的容器就会重启,就可以读取到我们更改后的 logging.level ,最终完成日志打印器级别的修改。

但是这种方式会使应用重启,导致流量受损,我们更希望的是通过一种热更新的方式来修改日志打印器的级别,spring-boot-actuator包中提供了LoggersEndpoint来完成日志打印器级别热更新,所以本节将结合LoggersEndpoint的简单使用和实现原理,说明一下Springboot中,如何热更新日志打印器级别。

1. LoggersEndpoint简单使用

LoggersEndpoint由 spring-boot-actuator 提供,可以暴露一些端点用于获取Springboot应用中的所有日志打印器信息及其级别信息以及热更新日志打印器级别,由于默认情况下,LoggersEndpoint暴露的端点只能通过JMX的方式访问,所以想要通过HTTP请求的方式访问到LoggersEndpoint,需要做如下配置。

management:
  server:
    address: 127.0.0.1
    port: 10999
  endpoints:
    web:
      base-path: /actuator
      exposure:
        include: loggers    # 设置LoggersEndpoint可以通过HTTP方式访问
  endpoint:
    loggers:
      enabled: true     # 打开LoggersEndpoint

按照上述这么配置,我们可以通过GET调用如下接口拿到当前所有的日志打印器的相关数据。

http://localhost:10999/actuator/loggers

获取数据如下所示。

{
    "levels": [
        "OFF",
        "FATAL",
        "ERROR",
        "WARN",
        "INFO",
        "DEBUG",
        "TRACE"
    ],
    "loggers": {
        "ROOT": {
            "configuredLevel"null,
            "effectiveLevel""INFO"
        },
        "org.springframework.boot.actuate.autoconfigure.web.server": {
            "configuredLevel"null,
            "effectiveLevel""DEBUG"
        },
        "org.springframework.http.converter.ResourceRegionHttpMessageConverter": {
            "configuredLevel"null,
            "effectiveLevel""ERROR"
        }
    },
    "groups": {
        "web": {
            "configuredLevel"null,
            "members": [
                "org.springframework.core.codec",
                "org.springframework.http",
                "org.springframework.web",
                "org.springframework.boot.actuate.endpoint.web",
                "org.springframework.boot.web.servlet.ServletContextInitializerBeans"
            ]
        },
        "login": {
            "configuredLevel""INFO",
            "members": [
                "com.lee.controller.LoginController",
                "com.lee.service.LoginService",
                "com.lee.dao.LoginDao"
            ]
        },
        "common": {
            "configuredLevel""DEBUG",
            "members": [
                "com.lee.util",
                "com.lee.config"
            ]
        },
        "sql": {
            "configuredLevel"null,
            "members": [
                "org.springframework.jdbc.core",
                "org.hibernate.SQL",
                "org.jooq.tools.LoggerListener"
            ]
        }
    }
}

上述内容中,返回的levels表示当前支持的日志级别,返回的loggers表示当前所有日志打印器的级别信息,返回的groups表示当前所有日志打印器组的级别信息,但是请注意,上述示例中的loggers其实做了大量的删减,实际调用接口时得到的loggers里面的内容会非常非常多,因为所有的日志打印器的信息都会被输出出来。

此外,上述内容中出现的configuredLevel字段表示当前日志打印器或日志打印器组被设置过的级别,也就是只要通过LoggersEndpoint给某个日志打印器或日志打印器组设置过级别,那么对应的configuredLevel字段就有值,最后上述内容中出现的effectiveLevel字段表示当前日志打印器正在生效的级别。

如果只想看某个日志打印器或日志打印器组的级别信息,可以调用如下的GET接口。

http://localhost:10999/actuator/loggers/{日志打印器名或日志打印器组名}

如果pathVariable是日志打印器名,那么会得到如下结果。

{
    "configuredLevel"null,
    "effectiveLevel""INFO"
}

如果pathVariable是日志打印器组名,那么会得到如下结果。

{
    "configuredLevel"null,
    "members": [
        "org.springframework.core.codec",
        "org.springframework.http",
        "org.springframework.web",
        "org.springframework.boot.actuate.endpoint.web",
        "org.springframework.boot.web.servlet.ServletContextInitializerBeans"
    ]
}

除了查询日志打印器或日志打印器组的级别信息,LoggersEndpoint更重要的功能是设置级别,比如可以通过如下POST接口来设置级别。

http://localhost:10999/actuator/loggers/{日志打印器名或日志打印器组名}

{
 "configuredLevel""DEBUG"
}

此时对应的日志打印器或日志打印器组的级别就会更新为设置的级别,并且其configuredLevel也会更新为设置的级别。

2. LoggersEndpoint原理分析

这里主要关注LoggersEndpoint如何实现日志打印器级别的热更新。LoggersEndpoint实现日志打印器级别的热更新对应的端点方法如下所示。

@WriteOperation
public void configureLogLevel(@Selector String name, @Nullable LogLevel configuredLevel) {
    Assert.notNull(name, "Name must not be empty");
    // 先尝试获取到LoggerGroup
    LoggerGroup group = this.loggerGroups.get(name);
    if (group != null && group.hasMembers()) {
        // 如果能获取到LoggerGroup则对组下每个Logger热更新级别
        group.configureLogLevel(configuredLevel, this.loggingSystem::setLogLevel);
        return;
    }
    // 获取不到LoggerGroup则按照Logger来处理
    this.loggingSystem.setLogLevel(name, configuredLevel);
}

上述方法的name即可以是Logger的名称,也可以是LoggerGroup的名称,如果是Logger的名称,那么就基于 LoggingSystem setLogLevel() 方法来设置这个Logger的级别,如果是LoggerGroup的名称,那么就遍历这个组下所有的Logger,每个遍历到的Logger都基于LoggingSystem的setLogLevel() 方法来设置级别。







请到「今天看啥」查看全文