文章目录
- 一、入口
- 二、源码解析
- LoggingApplicationListener
- 三、其它支持
- 四、总结
本节以logback为背景介绍的
一、入口
gav: org.springframework.boot:spring-boot:3.3.4
spring.factories文件中有如下两个配置
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory
org.springframework.context.ApplicationListener=\
org.springframework.boot.context.logging.LoggingApplicationListener,\
// 省略其它的...
这里定义了一个ApplicationListener的监听器, 以及三种不同日志实现的工厂
说明: 本节只分析使用logback作为slf4j实现的场景
二、源码解析
LoggingApplicationListener
继承链: GenericApplicationListener -> GenericApplicationListener -> SmartApplicationListener -> ApplicationListener
监听的事件为ApplicationEvent
public class LoggingApplicationListener implements GenericApplicationListener {
// 触发事件
public void onApplicationEvent(ApplicationEvent event) {
// 启动初期触发
if (event instanceof ApplicationStartingEvent startingEvent) {
onApplicationStartingEvent(startingEvent);
}
// 环境准备之后触发
else if (event instanceof ApplicationEnvironmentPreparedEvent environmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(environmentPreparedEvent);
}
// 容器启动完成触发
else if (event instanceof ApplicationPreparedEvent preparedEvent) {
onApplicationPreparedEvent(preparedEvent);
}
// 容器关闭触发
else if (event instanceof ContextClosedEvent contextClosedEvent) {
onContextClosedEvent(contextClosedEvent);
}
// 容器启动失败触发
else if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent();
}
}
}
容器启动事件
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
// 实例化LoggingSystem对象
this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
// 初始化前置处理
this.loggingSystem.beforeInitialize();
}
// LoggingSystem.get
public static LoggingSystem get(ClassLoader classLoader) {
// 系统配置的LoggingSystem; key:org.springframework.boot.logging.LoggingSystem
String loggingSystemClassName = System.getProperty(SYSTEM_PROPERTY);
if (StringUtils.hasLength(loggingSystemClassName)) {
if (NONE.equals(loggingSystemClassName)) {
return new NoOpLoggingSystem();
}
return get(classLoader, loggingSystemClassName);
}
// SPI获取LoggingSystem, 顺序是LogbackLoggingSystem->Log4J2LoggingSystem->JavaLoggingSystem
LoggingSystem loggingSystem = SYSTEM_FACTORY.getLoggingSystem(classLoader);
Assert.state(loggingSystem != null, "No suitable logging system located");
return loggingSystem;
}
// LogbackLoggingSystem#beforeInitialize
@Override
public void beforeInitialize() {
// 获取logContext日志上下文
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
super.beforeInitialize();
configureJdkLoggingBridgeHandler();
loggerContext.getTurboFilterList().add(FILTER);
}
// 获取logContext日志上下文
private LoggerContext getLoggerContext() {
ILoggerFactory factory = getLoggerFactory();
// ....
return (LoggerContext) factory;
}
// 获取logContext日志上下文
private ILoggerFactory getLoggerFactory() {
// slf4j获取LoggerContext
ILoggerFactory factory = LoggerFactory.getILoggerFactory();
while (factory instanceof SubstituteLoggerFactory) {
try {
Thread.sleep(50);
}
catch (InterruptedException ex) {
// 设置当前线程的中断标志位,表示该线程已被请求中断,但并不会立即停止线程的执行。
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for non-substitute logger factory", ex);
}
factory = LoggerFactory.getILoggerFactory();
}
return factory;
}
方法小结
- 容器在启动时通过ApplicationStartingEvent事件创建日志上下文
- 可以通过系统属性配置LoggingSystem对象, key为org.springframework.boot.logging.LoggingSystem
- 如果没有指定使用的LoggingSystem, 那么通过SPI获取, 由于在
spring.factories
中配置的LoggingSystemFactory
里面LogbackLoggingSystem.Factory
在第一个, 所以默认使用的LogbackLoggingSystem.Factory
(如果有logback相关包的话) - 执行
LogbackLoggingSystem
的beforeInitialize进行前置初始化 - beforeInitialize中使用SLF4J创建日志上下文; 这里就是SL4FJ和logback的内容了, 通过前面文章的介绍, 大家应该很熟悉了
在容器启动时创建了LoggingSystem, 一般是LogbackLoggingSystem
, 同时创建了日志上下文LogContext
环境准备事件
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
SpringApplication springApplication = event.getSpringApplication();
// 容器启动事件中创建过了, 一般是LogbackLoggingSystem
if (this.loggingSystem == null) {
this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());
}
// 进行初始化
initialize(event.getEnvironment(), springApplication.getClassLoader());
}
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
// 这里创建LogbackLoggingSystemProperties, 用于给日志上下文添加必要的属性
getLoggingSystemProperties(environment).apply();
// 从环境变量中获取logging.file.name和logging.file.path, 然后构建LogFile
this.logFile = LogFile.get(environment);
if (this.logFile != null) {
// 将logging.file.path的值添加到系统属性中, key为LOG_PATH
// 将logging.file.path目录下spring.log文件的路径添加到系统属性中, key为LOG_FILE
this.logFile.applyToSystemProperties();
}
this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
// 设置spring启动时的日志等级, 如果环境变量中有debug, 那么是debug登记,
// 如果环境变量中有trace, 那么是trace等级
initializeEarlyLoggingLevel(environment);
// 初始化LogbackLoggingSystem
initializeSystem(environment, this.loggingSystem, this.logFile);
// 环境变量中获取logging.group的内容添加到loggerGroups中, 并设置spring启动时相关包的日志等级
initializeFinalLoggingLevels(environment, this.loggingSystem);
// 添加shutdown的回调
registerShutdownHookIfNecessary(environment, this.loggingSystem);
}
private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
// 环境变量中的logging.config, 指定的日志配置文件路径
String logConfig = environment.getProperty(CONFIG_PROPERTY);
if (StringUtils.hasLength(logConfig)) {
// 去掉字符串两端的空格
logConfig = logConfig.strip();
}
try {
// 就封装了一个environment的getter方法
LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
// 没有配置logging.config或者以-D开头
if (ignoreLogConfig(logConfig)) {
// LogbackLoggingSystem初始化
system.initialize(initializationContext, null, logFile);
}
else {
// LogbackLoggingSystem初始化
system.initialize(initializationContext, logConfig, logFile);
}
}
catch (Throwable ex) {
// ...
}
}
小结
- springboot在环境准备完成后发出
ApplicationEnvironmentPreparedEvent
事件, 然后开始对LogbackLoggingSystemProperties
进行初始化 - 创建
LogbackLoggingSystemProperties
对象, 并添加系统变量值, 下面是添加的内容
- LOGGED_APPLICATION_NAME:spring.application.name的值
- PID: pid的值
- CONSOLE_LOG_CHARSET: 环境变量中
logging.charset.console
的值, 默认是U8 - FILE_LOG_CHARSET: 环境变量中
logging.charset.file
的值, 默认是U8 - CONSOLE_LOG_THRESHOLD: 环境变量中
logging.threshold.console
的值, 可选true/false - LOG_EXCEPTION_CONVERSION_WORD: 环境变量中logging.exception-conversion-word的值
- CONSOLE_LOG_PATTERN: 环境变量中
logging.pattern.console
的值 - FILE_LOG_PATTERN: 环境变量中
logging.pattern.file
的值 - LOG_LEVEL_PATTERN:环境变量中
logging.pattern.level
的值 - LOG_DATEFORMAT_PATTERN: 环境变量中
logging.pattern.dateformat
的值 - LOG_CORRELATION_PATTERN: 环境变量中
logging.pattern.correlation
的值 - 如果环境变量中logging.file.name存在, 添加
LOG_FILE: file的值
到系统变量中 - 如果环境变量中logging.file.path存在, 添加
LOG_PATH: path的值
到系统变量中
- 设置springboot的日志等级, 如果系统变量中有debug值, 设置为debug等级, 如果有trace值, 设置为trace等级
- 可以在系统变量中使用
logging.config
指定日志文件路径, 也可以不指定使用默认的logback.xml, 然后进行LogbackLoggingSystem的初始化 - 设置一些包/类的日志等级, 该等级由第3步即系统变量中有debug值或者trace值来设置, 可以配置的内置模块日志等级的有如下几个
- 如果环境变量中有debug, 那么设置包sql相关的包
org.springframework.jdbc.core
,org.hibernate.SQL
,org.jooq.tools.LoggerListener
和web相关的包org.springframework.core.codec
,org.springframework.http
,org.springframework.web
,org.springframework.boot.actuate.endpoint.web
,org.springframework.boot.web.servlet.ServletContextInitializerBeans
的日志级别为debug - 如果环境变量中有trace, 那么设置包
org.springframework
、org.apache.tomcat
,org.apache.catalina
,org.eclipse.jetty
,org.hibernate.tool.hbm2ddl
的日志级别为trace - 如果环境变量中有
logging.level
, 那么设置指定web或者sql的日志等级为配置的日志等级,logging.level
可以这么配置
logging.level.web=info
logging.level.org.springframework.boot=info
## 等等与上面环境变量中可配置的包相同
当然处理默认的web和sql两种类型的包之外, 还可以使用环境变量logging.group
来自定义springboot中包或者类的日志级别
这里环境变量中logging.level
的优先级要高于debug
的配置
LogbackLoggingSystem的初始化
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
// 容器启动事件中创建的日志上下文
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
// 非aot环境下直接返回false, 那么这里就是true, 这里对aot环境下不考虑
if (!initializeFromAotGeneratedArtifactsIfPossible(initializationContext, logFile)) {
// 初始化的核心
super.initialize(initializationContext, configLocation, logFile);
}
// 环境上下文添加到日志上下文中
loggerContext.putObject(Environment.class.getName(), initializationContext.getEnvironment());
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.");
}
}
如果没有开启aot, 那么这个方法没有什么内容, 直接看AbstractLoggingSystem#initialize方法即可
AbstractLoggingSystem
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
// 环境变量中没有使用logging.config指定日志文件路径的话走这里
if (StringUtils.hasLength(configLocation)) {
initializeWithSpecificConfig(initializationContext, configLocation, logFile);
return;
}
// 指定日志文件路径的话走这里
initializeWithConventions(initializationContext, logFile);
}
// 使用logging.config指定日志文件路径的场景
private void initializeWithSpecificConfig(LoggingInitializationContext initializationContext, String configLocation,
LogFile logFile) {
// 使用系统属性中的值替换configLocation中的占位符
configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);
loadConfiguration(initializationContext, configLocation, logFile);
}
// 没有指定日志文件路径的场景
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
// logback支持的文件名,只取一个, 顺序为:"logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml"
String config = getSelfInitializationConfig();
// 存在上面这几种文件的话
if (config != null && logFile == null) {
// 重置容器状态, 并调用loadConfiguration方法开始解析配置
reinitialize(initializationContext);
return;
}
// 项目中没有配置默认的四个文件
if (config == null) {
// 这里获取spring扩展的四个文件名, 顺序为: "logback-test-spring.groovy", "logback-test-spring.xml", "logback-spring.groovy", "logback-spring.xml"
config = getSpringInitializationConfig();
}
// 存在配置文件的话
if (config != null) {
// 解析配置文件
loadConfiguration(initializationContext, config, logFile);
return;
}
// 使用一套默认的配置, appender仅为ConsoleAppender, 这里不做解释
loadDefaults(initializationContext, logFile);
}
方法小结
- 如果使用
logging.config
指定了日志文件的路径(路径支持使用占位符, 将从系统变量中获取变量值), 使用loadConfiguration方法进行日志文件解析 - 如果没有指定日志文件的路径, 那么先获取默认配置文件(“logback-test.groovy”, “logback-test.xml”, “logback.groovy”, “logback.xml” ), 如果没有默认的配置文件, 取带有spring后缀的日志文件(“logback-test-spring.groovy”, “logback-test-spring.xml”, “logback-spring.groovy”, “logback-spring.xml” )
- 如果配置文件存在, 使用loadConfiguration方法进行日志文件解析
- 如果没有配置文件, 那么使用logback默认的容器, 以及一个ConsoleAppender
解析配置
protected void loadConfiguration(LoggingInitializationContext initializationContext, String location,
LogFile logFile) {
// 日志上下文
LoggerContext loggerContext = getLoggerContext();
// 停止并重启; 如果你的springBoot启动类中有静态属性Logger使用LoggerFactory.getLogger获取的话,它会在spring启动之前执行, 这里就会存在一个loggerContext, 需要关闭
stopAndReset(loggerContext);
withLoggingSuppressed(() -> {
// initializationContext对象仅仅是环境上下文的载体, 提供getEnvironment方法
if (initializationContext != null) {
// 创建LogbackLoggingSystemProperties对象, 并将一堆环境变量添加到系统变量中, 上面的环境准备事件中有介绍
applySystemProperties(initializationContext.getEnvironment(), logFile);
}
try {
// 配置的日志文件资源
Resource resource = new ApplicationResourceLoader().getResource(location);
// 解析日志配置文件
configureByResourceUrl(initializationContext, loggerContext, resource.getURL());
}
catch (Exception ex) {
throw new IllegalStateException("Could not initialize Logback logging from " + location, ex);
}
// 标识日志容器被启动
loggerContext.start();
});
// 打印解析异常信息, 略过
reportConfigurationErrorsIfNecessary(loggerContext);
}
private void configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext,
URL url) throws JoranException {
// 只允许xml为后缀的文件
if (url.getPath().endsWith(".xml")) {
// 使用springboot视线的SpringBootJoranConfigurator来解析配置
JoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext);
configurator.setContext(loggerContext);
// 开始解析配置
configurator.doConfigure(url);
}
else {
throw new IllegalArgumentException("Unsupported file extension in '" + url + "'. Only .xml is supported");
}
}
方法小结
- 添加一些环境变量参数到系统变量中
- 配置文件仅支持xml结尾的文件, 然后使用SpringBootJoranConfigurator来解析日志配置文件, 这里是对JoranConfigurator的扩展
SpringBootJoranConfigurator
class SpringBootJoranConfigurator extends JoranConfigurator {
private final LoggingInitializationContext initializationContext;
SpringBootJoranConfigurator(LoggingInitializationContext initializationContext) {
this.initializationContext = initializationContext;
}
@Override
protected void addModelHandlerAssociations(DefaultProcessor defaultProcessor) {
// 添加处理configuration/springProperty的handler
defaultProcessor.addHandler(SpringPropertyModel.class,
(handlerContext, handlerMic) -> new SpringPropertyModelHandler(this.context,
this.initializationContext.getEnvironment()));
// 添加处理*/springProfile的handler
defaultProcessor.addHandler(SpringProfileModel.class,
(handlerContext, handlerMic) -> new SpringProfileModelHandler(this.context,
this.initializationContext.getEnvironment()));
super.addModelHandlerAssociations(defaultProcessor);
}
@Override
public void addElementSelectorAndActionAssociations(RuleStore ruleStore) {
super.addElementSelectorAndActionAssociations(ruleStore);
// 添加允许的标签configuration/springProperty
ruleStore.addRule(new ElementSelector("configuration/springProperty"), SpringPropertyAction::new);
// 添加允许的标签*/springProfile
ruleStore.addRule(new ElementSelector("*/springProfile"), SpringProfileAction::new);
ruleStore.addTransparentPathPart("springProfile");
}
@Override
public void buildModelInterpretationContext() {
super.buildModelInterpretationContext();
// modelInterpretationContext中的JoranConfigurator替换成SpringBootJoranConfigurator
this.modelInterpretationContext.setConfiguratorSupplier(() -> {
SpringBootJoranConfigurator configurator = new SpringBootJoranConfigurator(this.initializationContext);
configurator.setContext(this.context);
return configurator;
});
}
// 省略一些代码...
}
SpringBootJoranConfigurator类在不考虑aot的情况下, 添加了对configuration/springProperty
和*/springProfile
标签的支持, 其中*/springProfile
是一种后缀标签的形式, 也就是说它可以放在任意标签的后面; 下面看看这两个handler
SpringPropertyModelHandler
@Override
public void handle(ModelInterpretationContext intercon, Model model) throws ModelHandlerException {
SpringPropertyModel propertyModel = (SpringPropertyModel) model;
// 作用域, 支持LOCAL(model上下文), CONTEXT(日志上下文), SYSTEM(系统级别); 默认是LOCAL, 在解析配置文件时有效
Scope scope = ActionUtil.stringToScope(propertyModel.getScope());
// 默认值
String defaultValue = propertyModel.getDefaultValue();
// source就是属性的名称
String source = propertyModel.getSource();
// name和source都不能为空
if (OptionHelper.isNullOrEmpty(propertyModel.getName()) || OptionHelper.isNullOrEmpty(source)) {
addError("The \"name\" and \"source\" attributes of <springProperty> must be set");
}
// 将属性添加到指定的作用域中
PropertyModelHandlerHelper.setProperty(intercon, propertyModel.getName(), getValue(source, defaultValue),
scope);
}
// 从环境变量中获取source属性对应的值
private String getValue(String source, String defaultValue) {
if (this.environment == null) {
addWarn("No Spring Environment available to resolve " + source);
return defaultValue;
}
return this.environment.getProperty(source, defaultValue);
}
方法小结
configuration/springProperty
标签支持name
,source
,scope
,defaultValue
四个属性
- name: 标签名称
- source: 属性名称; 从环境变量中获取值的那个key
- scope: 属性存放的位置, LOCAL:logback配置文件解析期间, CONTEXT:日志上线文范文内, SYSTEM: 系统属性
总的来说就是: configuration/springProperty
将从环境变量中获取的值添加到日志容器中, 供解析日志使用, 其中key为name属性的值, value为source属性在环境变量中对应的值
例如:
// application.properties
log.fileName=info.log
// logback.xml
<configuration>
<springProperty name="fileName" source="log.fileName" scope="LOCAL" defaultValue="temp.log"/>
</configuration>
SpringProfileModelHandler
class SpringProfileModelHandler extends ModelHandlerBase {
private final Environment environment;
@Override
public void handle(ModelInterpretationContext intercon, Model model) throws ModelHandlerException {
SpringProfileModel profileModel = (SpringProfileModel) model;
// 如果当前spring的环境(spring.profiles.active)不是springProfile指定下的, 那么被springProfile标签包裹的子标签将不会生效
if (!acceptsProfiles(intercon, profileModel)) {
model.deepMarkAsSkipped();
}
}
private boolean acceptsProfiles(ModelInterpretationContext ic, SpringProfileModel model) {
if (this.environment == null) {
return false;
}
// name根据逗号分割
String[] profileNames = StringUtils
.trimArrayElements(StringUtils.commaDelimitedListToStringArray(model.getName()));
if (profileNames.length == 0) {
return false;
}
for (int i = 0; i < profileNames.length; i++) {
try {
// 从LOCAL、CONTEXT、SYSTEM范围内获取值替换占位符; 没有占位符的话用原值
profileNames[i] = OptionHelper.substVars(profileNames[i], ic, this.context);
}
catch (ScanException ex) {
throw new RuntimeException(ex);
}
}
// 判断是不是环境变量中配置的spring.profiles.active的值
return this.environment.acceptsProfiles(Profiles.of(profileNames));
}
}
方法小结
springProfile标签可以放在任意子标签下, 其中name
属性用来指定当前的环境, 它可以指定什么环境下使用什么样的配置, 如果当前环境与springProfile配置的不同, 那么springProfile的子标签将不会生效; 例如
<root level="info">
<springProfile name="dev,test">
<appender-ref ref="CONSOLE" />
</springProfile>
<springProfile name="prod">
<appender-ref ref="ROLLER" />
</springProfile>
</root>
这种配置下dev或者test环境 CONSOLE的appender将会生效, ROLLER的appender不会生效
三、其它支持
spring还提供了ColorConverter
,ExtendedWhitespaceThrowableProxyConverter
,WhitespaceThrowableProxyConverter
转换器, 用来给控制台输出颜色日志的、异常等
四、总结
- 在springboot启动初期(ApplicationStartingEvent事件), 实例化了
LogbackLoggingSystem
对象 - 在环境准备完成后(ApplicationEnvironmentPreparedEvent事件), 对logback容器做了初始化并启动
-
springboot对日志slf4j的实现默认顺序为
LogbackLoggingSystem->Log4J2LoggingSystem->JavaLoggingSystem
, 确保其中有ch.qos.logback:logback-classic:版本号
的包 -
关于环境变量中配置的
logging.file.name
和logging.file.path
属性, 是用来给默认日志配置设置滚动文件的, 就像appender中的file属性一样, 但是如果你配置了日志文件(例如logback.xml), 它就没什么用了 -
可以在环境变量中配置
debug=true或者trace=true
来设置springboot内置包的日志等级; 同样也可以在环境变量中设置指定包的日志级别, 就不限于debug或者trace了, 例如logging.level.web=info; logging.level.org.springframework.boot=info
这种logging.level.web=info
的方式优先级高于debug=true
-
可以使用环境变量
logging.config
配置日志文件的位置, 支持classpath的配置, 即放在项目的resources
目录下即可;
- 如过没有使用
logging.config
指定日志配置, 那么会默认读取"logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml"
中的一个 - 如果这一步也没有指定, 那么读取springboot扩展的配置文件
"logback-test-spring.groovy", "logback-test-spring.xml", "logback-spring.groovy", "logback-spring.xml"
- 如果直接没有配置文件, 那么默认构建ConsoleAppender和root的logger对象, 如果配置了
logging.file.name
和logging.file.path
属性, 那么就会多创建一个info级别的RollingFileAppender
- springboot使用
SpringBootJoranConfigurator
扩展了JoranConfigurator
, 添加了如下的相关支持
configuration/springProperty
标签, 用来从环境变量中获取属性, 作用到解析日志文件中*/springProfile
标签, 该标签是后缀匹配型, 可以放在任意位置, 它用于指定哪些配置在不同的环境下生效
- springboot还提供了额外的转换器, 例如
ColorConverter
, 大家配置ConsoleAppender
的时候可以借用它 - 另外, 在遇到Thread.sleep的时候, 可以用
Thread.currentThread().interrupt();
设置当前线程的中断标志位,表示该线程已被请求中断,但并不会立即停止线程的执行。也曾看到一些地方什么都不处理, 这种应该是标准做法, 在几个源码中见过了
个人公众号