文章目录
- 一、入口
- 二、源码解析
- 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.Factoryorg.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.LoggingSystemString loggingSystemClassName = System.getProperty(SYSTEM_PROPERTY);if (StringUtils.hasLength(loggingSystemClassName)) {if (NONE.equals(loggingSystemClassName)) {return new NoOpLoggingSystem();}return get(classLoader, loggingSystemClassName);}// SPI获取LoggingSystem, 顺序是LogbackLoggingSystem->Log4J2LoggingSystem->JavaLoggingSystemLoggingSystem 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获取LoggerContextILoggerFactory 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();// 容器启动事件中创建过了, 一般是LogbackLoggingSystemif (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, 然后构建LogFilethis.logFile = LogFile.get(environment);if (this.logFile != null) {// 将logging.file.path的值添加到系统属性中, key为LOG_PATH// 将logging.file.path目录下spring.log文件的路径添加到系统属性中, key为LOG_FILEthis.logFile.applyToSystemProperties();}this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);// 设置spring启动时的日志等级, 如果环境变量中有debug, 那么是debug登记, // 如果环境变量中有trace, 那么是trace等级initializeEarlyLoggingLevel(environment);// 初始化LogbackLoggingSysteminitializeSystem(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;}@Overrideprotected void addModelHandlerAssociations(DefaultProcessor defaultProcessor) {// 添加处理configuration/springProperty的handlerdefaultProcessor.addHandler(SpringPropertyModel.class,(handlerContext, handlerMic) -> new SpringPropertyModelHandler(this.context,this.initializationContext.getEnvironment()));// 添加处理*/springProfile的handlerdefaultProcessor.addHandler(SpringProfileModel.class,(handlerContext, handlerMic) -> new SpringProfileModelHandler(this.context,this.initializationContext.getEnvironment()));super.addModelHandlerAssociations(defaultProcessor);}@Overridepublic void addElementSelectorAndActionAssociations(RuleStore ruleStore) {super.addElementSelectorAndActionAssociations(ruleStore);// 添加允许的标签configuration/springPropertyruleStore.addRule(new ElementSelector("configuration/springProperty"), SpringPropertyAction::new);// 添加允许的标签*/springProfileruleStore.addRule(new ElementSelector("*/springProfile"), SpringProfileAction::new);ruleStore.addTransparentPathPart("springProfile");}@Overridepublic void buildModelInterpretationContext() {super.buildModelInterpretationContext();// modelInterpretationContext中的JoranConfigurator替换成SpringBootJoranConfiguratorthis.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;@Overridepublic 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
转换器, 用来给控制台输出颜色日志的、异常等
四、总结
- spring自动装配了
LoggingApplicationListener
监听器, 监听ApplicationEvent事件, 在springboot启动周期中对日志做了一些扩展
- 在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();
设置当前线程的中断标志位,表示该线程已被请求中断,但并不会立即停止线程的执行。也曾看到一些地方什么都不处理, 这种应该是标准做法, 在几个源码中见过了
个人公众号