全方位阐述 SpringBoot 中的日志是怎么工作,从零基础到精通,收藏这篇就够了!

开篇:日志这玩意儿,用好了是神器,用不好...呵呵

日志,程序员的“老朋友”? 谁还没事儿打几行日志啊!但你真的懂它吗? 别逗了。 大部分时候,我们只是机械地 System.out.println 或者用个什么框架糊弄一下。 尤其是 SpringBoot,号称宇宙第一,把啥都给你安排明白了,日志?那更是小菜一碟。

但!是! 真正的大佬, 都是抠细节的。 日志打得好,关键时刻能救命。 打不好?等着背锅吧! 性能瓶颈?安全漏洞? 搞不好都藏在你的日志里。

所以,今天咱就来扒一扒 SpringBoot 里面 Log4j2 这套东西, 看看它到底是怎么运作的。 别怕,就算你用的是 Logback,也差不太多。 掌握了核心思想,换个框架还不是分分钟的事儿?

当然,我这篇文章也不是什么“保姆级教程”。 重点是带你思考, 让你对日志这玩意儿, 有更深刻的理解。

先上个导图,免得你一会儿跟不上我的思路:

版本信息? 看看得了。 重要的是思想, 思想!

  • SpringBoot版本:2.7.2
  • Log4j2版本:2.17.2

什么? 你还不知道 Ruoyi-Vue-Pro? 自己去看!

一、Log4j2:表面上的 Logger,背地里的 Config

平时写代码, 我们直接操作的就是 Logger 对象, 咔咔一顿 logger.info()logger.error()。 Logger 就像个日志打印机, 你给它啥,它就吐啥。

但!是! Log4j2 里面, Logger 只是个傀儡! 真正干活的是 Appender。 至于 Appender 怎么把日志写到文件、数据库, 那是另一回事儿, 今天不细说。

重点是, Logger 背后藏着一个 LoggerConfig! 这玩意儿才是灵魂! LoggerConfig 决定了:

  • 用哪些 Appender 打印日志
  • 日志的级别

LoggerConfig 和 Appender 都是在 Log4j2 的配置文件(Log4j2.xml)里定义的。 Log4j2 启动的时候,会把这个 XML 文件解析成一个 Configuration 对象。

<Appenders> 里面有多少个 <Appender>, Configuration 里面的 appenders 集合就有多少个 Appender 对象。 <Loggers> 里面有多少个 <Logger>, Configuration 里面的 loggerConfigs 集合就有多少个 LoggerConfig 对象。 而且, LoggerConfig 和 Appender 的关系,也在解析的时候就确定了。

Log4j2 还有一个 LoggerContext 对象, 它持有 Configuration 对象。 每次我们要用 Logger 的时候, 都会先从 LoggerContext 的 loggerRegistry 里面找。 找不到? 那就创建一个, 然后放到 loggerRegistry 里面缓存起来。

创建 Logger 的时候, 最关键的就是找到它对应的 LoggerConfig。 去哪儿找? 当然是去 Configuration 里面找!

所以, Logger、LoggerContext 和 Configuration 的关系是这样的:

有了这套结构, 修改日志打印器就方便多了。 通过 LoggerContext 拿到 Configuration, 就能随意操作 LoggerConfig。 比如, 日志级别热更新, 就是这么搞的。

注意! SpringBoot 操作 Logger 的时候, 操作的是 Logger 对象。 比如, 把 com.honey.Login 这个 Logger 的级别设置为 DEBUG。 但 Log4j2 框架底层, 实际上是在设置 com.honey.Login 这个 LoggerConfig 的级别。 Logback 框架也类似, 只是直接设置 Logger 对象的级别。 别搞混了!

二、SpringBoot 日志配置: 别只知道 application.properties

SpringBoot 简化了日志配置, 但也别啥都不知道。 虽然我们大部分时候都会提供一个 Log4j2.xml 文件, 但 SpringBoot 还是提供了一些配置项, 可以在 application.propertiesapplication.yml 里面设置。 搞清楚这些配置, 才能更好地理解 SpringBoot 的日志启动机制。

1. logging.file.name: 日志文件, 丢哪儿?
logging:
  file:
    name: test.log

这样配置, SpringBoot 就会把日志输出到项目根目录下的 test.log 文件里面。 注意,是根目录! 不是 src/main/resources

2. logging.file.path: 日志目录, 你说了算
logging:
  file:
    path: /var/log/myapp

这样配置, SpringBoot 就会把日志输出到 /var/log/myapp/spring.log 文件里面。 注意, 如果目录不存在, SpringBoot 不会帮你创建!

3. logging.level: 谁的日志, 你说了算
logging:
  level:
    com.pww.App: warn

这样配置, 就可以把 com.pww.App 这个 Logger 的级别设置为 WARN。 级别低于 WARN 的日志, 就不会被打印出来。

三、SpringBoot 日志启动: 一场精心策划的 “阴谋”

就算你不提供 Log4j2.xml 配置文件, SpringBoot 也能输出漂亮的日志。 这背后肯定有猫腻! SpringBoot 肯定在偷偷地帮你初始化 Log4j2 或 Logback。 这一节, 咱就来扒一扒 SpringBoot 的日志启动机制。

SpringBoot 的日志启动, 主要依赖于 LoggingApplicationListener 这个监听器。 它会监听 SpringBoot 启动过程中的三个事件:

  • ApplicationStartingEvent: SpringApplication 启动之后发布, Environment 和 ApplicationContext 还没准备好。
  • ApplicationEnvironmentPreparedEvent: Environment 准备好之后立即发布。
  • ApplicationPreparedEvent: ApplicationContext 完全准备好之后, 刷新容器之前发布。

接下来, 咱就看看监听到这些事件之后, LoggingApplicationListener 都会干些啥。

1. 监听到 ApplicationStartingEvent: 抢占先机

LoggingApplicationListeneronApplicationStartingEvent() 方法:

private void onApplicationStartingEvent(ApplicationStartingEvent event) {
    // 读取 org.springframework.boot.logging.LoggingSystem 系统属性,加载 LoggingSystem
    this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
    // 调用 LoggingSystem 的 beforeInitialize() 方法,提前准备
    this.loggingSystem.beforeInitialize();
}

LoggingSystem 是 SpringBoot 操作日志的关键对象, 它贯穿 SpringBoot 的整个生命周期。 监听到 ApplicationStartingEvent 之后, 第一件事就是读取 org.springframework.boot.logging.LoggingSystem 这个系统属性, 得到要加载的 LoggingSystem 的全限定名, 然后加载它。

如果你用的是 Log4j2, 对应的 LoggingSystemLog4J2LoggingSystem。 如果你用的是 Logback, 对应的 LoggingSystemLogbackLoggingSystem。 当然, 你也可以自己实现 LoggingSystem, 然后通过设置 org.springframework.boot.logging.LoggingSystem 来替换 SpringBoot 默认的实现。

拿到 LoggingSystem 之后, 就会调用它的 beforeInitialize() 方法, 完成日志框架初始化前的准备工作。 看看 Log4J2LoggingSystembeforeInitialize() 方法:

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

最关键的就是添加了一个过滤器! 虽然叫做过滤器, 但它实际上是个拦截器, 会阻止所有日志打印。 SpringBoot 这么做, 是为了防止日志系统在完全初始化之前, 打印出一些不可控的日志。

所以, 监听到 ApplicationStartingEvent 之后, LoggingApplicationListener 主要干了两件事:

  • 加载 LoggingSystem
  • 添加一个过滤器, 阻止日志打印
2. 监听到 ApplicationEnvironmentPreparedEvent: 加载配置, 初始化框架

LoggingApplicationListeneronApplicationEnvironmentPreparedEvent() 方法:

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    SpringApplication springApplication = event.getSpringApplication();
    if (this.loggingSystem == null) {
        this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());
    }
    // Environment 已经加载完毕, 调用 initialize() 方法
    initialize(event.getEnvironment(), springApplication.getClassLoader());
}

继续跟进 LoggingApplicationListenerinitialize() 方法:

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 配置, 设置 springBootLogging 级别
    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. 调用 LoggingSysteminitialize() 方法, 初始化日志框架。 这一步是真正初始化 Log4j2 或 Logback 的地方。

  3. 初始化完成后, 基于 logging.level 的配置, 设置日志打印器组和日志打印器的级别。 这里引入了一个概念: 日志打印器组(LoggerGroup)。

    如果要操作一个 Logger, 就需要知道它的名字, 然后找到它, 再进行操作。 Logger 少的时候还好, Logger 多了就麻烦了。 比如, 要修改 Logger 的级别, 一个一个找太痛苦了。 如果能把 Logger 按照功能分组, 一组一组地修改, 就方便多了。 LoggerGroup 就是干这个的。

    一个 LoggerGroup 有三个字段:

    • name: LoggerGroup 的名字, 通过 name 唯一确定一个 LoggerGroup。 比如, 有一个 LoggerGroup 名字叫 login, 就可以通过 logging.level.login=debug 把这个 LoggerGroup 下所有 Logger 的级别设置为 DEBUG。
    • members: LoggerGroup 里面所有 Logger 的名字的集合。
    • configuredLevel: 最近一次给 LoggerGroup 设置的级别。

    通过 logging.group 可以配置 LoggerGroup:

    yaml 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 设置级别:

    yaml 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 表示如下:

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

    名字为 common 的 LoggerGroup 表示如下:

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

    SpringBoot 预置了两个 LoggerGroup, 名字分别是 websql

    json { "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": "" }

    websql 这两个 LoggerGroup 的级别, 可以通过两种方式指定:

    • 配置 debug=true, 将 websql 的级别设置为 DEBUG。
    • 通过 logging.level.weblogging.level.sql 指定级别。

    第二种方式优先级更高。

    所以, 配置 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: 注册 Bean, 大功告成

LoggingApplicationListeneronApplicationPreparedEvent() 方法:

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)) {
        // 把 LoggerGroups 注册到容器中
        beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, this.loggerGroups);
    }
}

这一步主要是把之前加载的 LoggingSystemLogFileLoggerGroups 添加到 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 搜索到并加载。

这些行为都发生在 Log4J2LoggingSystem 中。 接下来, 咱就来分析一下这里的流程和原理。

在第三节中已经知道, SpringBoot 启动时, 当 LoggingApplicationListener 监听到 ApplicationEnvironmentPreparedEvent 事件后, 最终会调用到 LoggingApplicationListenerinitializeSystem() 方法来初始化日志框架。 先看看这里的逻辑:

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) {
        // 省略异常处理
    }
}

LoggingApplicationListenerinitializeSystem() 方法会读取 logging.config 环境变量, 得到用户提供的配置文件路径。 然后带着配置文件路径, 调用 Log4J2LoggingSysteminitialize() 方法。 接下来分两种情况讨论: 没配置 logging.config 和配置了 logging.config

1. 没配置 logging.config: SpringBoot 说了算

Log4J2LoggingSysteminitialize() 方法:

@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() 方法
    // configLocation 为 null
    super.initialize(initializationContext, configLocation, logFile);
    // 设置 LoggerContext 的 ExternalContext, 标记为已初始化
    markAsInitialized(loggerContext);
}

这个方法会继续调用 AbstractLoggingSysteminitialize() 方法, 并且因为没有配置 logging.config, 所以 configLocation 参数为 null。 看看 AbstractLoggingSysteminitialize() 方法:

@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 寻找配置文件:

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) 作为日志配置文件。

拿到配置文件路径后, 最终会调用 Log4J2LoggingSystemloadConfiguration() 方法加载配置:

protected void loadConfiguration(String location, LogFile logFile, List<String> overrides) {
    Assert.notNull(location, "Location must not be null");
    try {
        List<Configuration> configurations = new ArrayList<>();
        LoggerContext context = getLoggerContext();
        // 加载配置文件, 得到 Configuration
        configurations.add(load(location, context));
        // 加载 logging.log4j2.config.override 配置的配置文件
        for (String override : overrides) {
            configurations.add(load(override, context));
        }
        // 如果有多个 Configuration, 创建 CompositeConfiguration
        Configuration configuration = (configurations.size() > 1) ? createComposite(configurations)
                : configurations.iterator().next();
        // 启动 Configuration, 设置给 LoggerContext
        context.start(configuration);
    } catch (Exception ex) {
        throw new IllegalStateException("Could not initialize Log4J2 logging from " + location, ex);
    }
}

这个方法会拿着配置文件路径, 加载得到 Configuration。 同时, 还会拿到所有通过 logging.log4j2.config.override 配置的路径, 加载得到 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 之后, 就会调用 LoggerContextstart() 方法完成 Log4j2 框架的初始化。 这一步主要做三件事:

  • 初始化 Configuration: 创建配置文件中定义的 Appender 和 LoggerConfig。
  • 设置 Configuration 给 LoggerContext: 替换掉 LoggerContext 里面的旧的 Configuration。
  • 更新 Logger: 基于新的 Configuration 替换掉 Logger 持有的 LoggerConfig。
2. 配置了 logging.config: 你说了算

配置了 logging.config 之后, 情况就简单多了。 还是从 Log4J2LoggingSysteminitialize() 方法出发:

@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    LoggerContext loggerContext = getLoggerContext();
    if (isAlreadyInitialized(loggerContext)) {
        return;
    }
    loggerContext.getConfiguration().removeFilter(FILTER);
    // 调用父类 AbstractLoggingSystem 的 initialize() 方法
    // configLocation 不为 null
    super.initialize(initializationContext, configLocation, logFile);
    markAsInitialized(loggerContext);
}

继续跟进 AbstractLoggingSysteminitialize() 方法:

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

因为指定了配置文件, 所以会调用 AbstractLoggingSysteminitializeWithSpecificConfig() 方法。 这个方法没有额外的逻辑, 最终会执行到和没配置 logging.config 时一样的 Log4J2LoggingSystemloadConfiguration() 方法。

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

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

  • 没配置 logging.config: SpringBoot 会基于约定努力去寻找符合的配置文件, 如果找不到则会使用预置的配置文件。 拿到配置文件后就会加载为 Configuration, 然后替换掉 LoggerContext 里的旧的 Configuration
  • 配置了 logging.config: 将 logging.config 指定的配置文件加载为 Configuration, 然后替换掉 LoggerContext 里的旧的 Configuration

无论有没有配置 logging.config, 都只能加载一个配置文件为 Configuration。 如果想加载多个 Configuration, 那么需要通过 logging.log4j2.config.override 配置多个配置文件路径。

SpringBoot 集成 Log4j2 日志框架的流程图如下:

五、SpringBoot 日志级别热更新: 不重启, 也能改日志

在日志打印中, 一条日志在发起打印时, 会携带一个日志级别。 同时, 打印日志的 Logger 也有一个级别。 Logger 只能打印级别高于或等于自身的日志。

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

但是这种方式会使应用重启, 导致流量受损。 我们更希望的是通过一种热更新的方式来修改 Logger 的级别。 spring-boot-actuator 包提供了 LoggersEndpoint 来完成 Logger 级别热更新。 这一节, 咱就结合 LoggersEndpoint 的简单使用和实现原理, 看看 SpringBoot 是如何热更新 Logger 级别的。

1. LoggersEndpoint 简单使用: 暴露接口, 方便操作

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

management:
  server:
    address: 127.0.0.1
    port: 10999
  endpoints:
    web:
      base-path: /actuator
      exposure:
        include: loggers # 允许通过 HTTP 方式访问 LoggersEndpoint
  endpoint:
    loggers:
      enabled: true # 启用 LoggersEndpoint

配置完成后, 就可以通过 GET 请求访问如下接口, 拿到当前所有 Logger 的相关数据:

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 表示当前所有 Logger 的级别信息。 groups 表示当前所有 LoggerGroup 的级别信息。

configuredLevel 表示当前 Logger 或 LoggerGroup 被设置过的级别。 只要通过 LoggersEndpoint 给某个 Logger 或 LoggerGroup 设置过级别, 对应的 configuredLevel 字段就有值。 effectiveLevel 表示当前 Logger 正在生效的级别。

如果只想看某个 Logger 或 LoggerGroup 的级别信息, 可以调用如下的 GET 接口:

http://localhost:10999/actuator/loggers/{Logger 名或 LoggerGroup 名}

如果 pathVariable 是 Logger 名, 得到的结果如下:

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

如果 pathVariable 是 LoggerGroup 名, 得到的结果如下:

{
    "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"
    ]
}

除了查询 Logger 或 LoggerGroup 的级别信息, LoggersEndpoint 更重要的功能是设置级别。 可以通过如下 POST 接口来设置级别:

http://localhost:10999/actuator/loggers/{Logger 名或 LoggerGroup 名}

{
    "configuredLevel": "DEBUG"
}

设置完成后, 对应的 Logger 或 LoggerGroup 的级别就会更新为设置的级别, 并且 configuredLevel 也会更新为设置的级别。

2. LoggersEndpoint 原理分析: 偷梁换柱,瞒天过海

这里主要关注 LoggersEndpoint 如何实现 Logger 级别的热更新。 对应的端点方法如下:

@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 的

黑客/网络安全学习包

资料目录

  1. 成长路线图&学习规划

  2. 配套视频教程

  3. SRC&黑客文籍

  4. 护网行动资料

  5. 黑客必读书单

  6. 面试题合集

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*************************************

1.成长路线图&学习规划

要学习一门新的技术,作为新手一定要先学习成长路线图方向不对,努力白费

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图&学习规划。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。


因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*************************************

2.视频教程

很多朋友都不喜欢晦涩的文字,我也为大家准备了视频教程,其中一共有21个章节,每个章节都是当前板块的精华浓缩


因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*************************************

3.SRC&黑客文籍

大家最喜欢也是最关心的SRC技术文籍&黑客技术也有收录

SRC技术文籍:

黑客资料由于是敏感资源,这里不能直接展示哦!

4.护网行动资料

其中关于HW护网行动,也准备了对应的资料,这些内容可相当于比赛的金手指!

5.黑客必读书单

**

**

6.面试题合集

当你自学到这里,你就要开始思考找工作的事情了,而工作绕不开的就是真题和面试题。

更多内容为防止和谐,可以扫描获取~

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*********************************

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值