开篇:日志这玩意儿,用好了是神器,用不好...呵呵
日志,程序员的“老朋友”? 谁还没事儿打几行日志啊!但你真的懂它吗? 别逗了。 大部分时候,我们只是机械地 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.properties
或 application.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: 抢占先机
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();
}
LoggingSystem
是 SpringBoot 操作日志的关键对象, 它贯穿 SpringBoot 的整个生命周期。 监听到 ApplicationStartingEvent
之后, 第一件事就是读取 org.springframework.boot.logging.LoggingSystem
这个系统属性, 得到要加载的 LoggingSystem
的全限定名, 然后加载它。
如果你用的是 Log4j2, 对应的 LoggingSystem
是 Log4J2LoggingSystem
。 如果你用的是 Logback, 对应的 LoggingSystem
是 LogbackLoggingSystem
。 当然, 你也可以自己实现 LoggingSystem
, 然后通过设置 org.springframework.boot.logging.LoggingSystem
来替换 SpringBoot 默认的实现。
拿到 LoggingSystem
之后, 就会调用它的 beforeInitialize()
方法, 完成日志框架初始化前的准备工作。 看看 Log4J2LoggingSystem
的 beforeInitialize()
方法:
@Override
public void beforeInitialize() {
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
super.beforeInitialize();
// 添加一个过滤器, 阻止所有日志打印
loggerContext.getConfiguration().addFilter(FILTER);
}
最关键的就是添加了一个过滤器! 虽然叫做过滤器, 但它实际上是个拦截器, 会阻止所有日志打印。 SpringBoot 这么做, 是为了防止日志系统在完全初始化之前, 打印出一些不可控的日志。
所以, 监听到 ApplicationStartingEvent
之后, LoggingApplicationListener
主要干了两件事:
- 加载
LoggingSystem
- 添加一个过滤器, 阻止日志打印
2. 监听到 ApplicationEnvironmentPreparedEvent: 加载配置, 初始化框架
LoggingApplicationListener
的 onApplicationEnvironmentPreparedEvent()
方法:
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());
}
继续跟进 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 配置, 设置 springBootLogging 级别
initializeEarlyLoggingLevel(environment);
// 调用 LoggingSystem 初始化日志框架
initializeSystem(environment, this.loggingSystem, this.logFile);
// 设置日志打印器组和日志打印器的级别
initializeFinalLoggingLevels(environment, this.loggingSystem);
registerShutdownHookIfNecessary(environment, this.loggingSystem);
}
这个方法主要做了三件事:
-
把日志相关配置设置到系统属性中。 比如, 可以通过
logging.pattern.console
配置控制台日志格式。 但 XML 文件里面没法直接读取logging.pattern.console
的值, 这时候就需要设置一个系统属性, 属性名是CONSOLE_LOG_PATTERN
, 属性值是logging.pattern.console
的值。 然后在 XML 文件里面就可以通过${sys:CONSOLE_LOG_PATTERN}
读取到logging.pattern.console
的值。下面是 SpringBoot 日志配置和系统属性名的对应关系:
-
调用
LoggingSystem
的initialize()
方法, 初始化日志框架。 这一步是真正初始化 Log4j2 或 Logback 的地方。 -
初始化完成后, 基于
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, 名字分别是
web
和sql
: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": "" }
web
和sql
这两个 LoggerGroup 的级别, 可以通过两种方式指定:- 配置
debug=true
, 将web
和sql
的级别设置为 DEBUG。 - 通过
logging.level.web
和logging.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 级别
- name: LoggerGroup 的名字, 通过 name 唯一确定一个 LoggerGroup。 比如, 有一个 LoggerGroup 名字叫
3. 监听到 ApplicationPreparedEvent: 注册 Bean, 大功告成
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)) {
// 把 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 搜索到并加载。
这些行为都发生在 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
: SpringBoot 说了算
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() 方法
// configLocation 为 null
super.initialize(initializationContext, configLocation, logFile);
// 设置 LoggerContext 的 ExternalContext, 标记为已初始化
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 寻找配置文件:
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) 作为日志配置文件。
拿到配置文件路径后, 最终会调用 Log4J2LoggingSystem
的 loadConfiguration()
方法加载配置:
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
之后, 就会调用 LoggerContext
的 start()
方法完成 Log4j2 框架的初始化。 这一步主要做三件事:
- 初始化 Configuration: 创建配置文件中定义的 Appender 和 LoggerConfig。
- 设置 Configuration 给 LoggerContext: 替换掉 LoggerContext 里面的旧的 Configuration。
- 更新 Logger: 基于新的 Configuration 替换掉 Logger 持有的 LoggerConfig。
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() 方法
// 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
的 loadConfiguration()
方法。
所以, 配置了 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 简单使用: 暴露接口, 方便操作
LoggersEndpoint
由 spring-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 的
黑客/网络安全学习包
资料目录
-
成长路线图&学习规划
-
配套视频教程
-
SRC&黑客文籍
-
护网行动资料
-
黑客必读书单
-
面试题合集
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*************************************
1.成长路线图&学习规划
要学习一门新的技术,作为新手一定要先学习成长路线图,方向不对,努力白费。
对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图&学习规划。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*************************************
2.视频教程
很多朋友都不喜欢晦涩的文字,我也为大家准备了视频教程,其中一共有21个章节,每个章节都是当前板块的精华浓缩。
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*************************************
3.SRC&黑客文籍
大家最喜欢也是最关心的SRC技术文籍&黑客技术也有收录
SRC技术文籍:
黑客资料由于是敏感资源,这里不能直接展示哦!
4.护网行动资料
其中关于HW护网行动,也准备了对应的资料,这些内容可相当于比赛的金手指!
5.黑客必读书单
**
**
6.面试题合集
当你自学到这里,你就要开始思考找工作的事情了,而工作绕不开的就是真题和面试题。
更多内容为防止和谐,可以扫描获取~
因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取
*************************************CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享*********************************