一. 前言
1. 相关日志文档
2. 说明
- 本文基于使用logback-spring.xml配置文件进行源码解析
- Spring项目可以使用的配置文件
- logback-test.groovy
- logback-test.xml
- logback.groovy
- logback.xml
- logback-test-spring.groovy
- logback-test-spring.xml
- logback-spring.groovy
- logback-spring.xml (本文基于该配置进行讲述)
- Spring项目可以使用的配置文件
- 本文只讲核心代码, 不是主线的代码不展示出来
- 本文的spring-boot版本为2.7.11-SNAPSHOT
- 本文涉及到的logback-classic依赖版本为1.2.12
- 如有疑问可留意或查看官方文档
3. 代码准备
3.1 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.11-SNAPSHOT</version>
</dependency>
依赖说明:
- spring-boot-starter-web会自动引入spring-boot 和 logback所需的依赖
- 依赖引用关系见下图 (不使用@slf4j注解的话请忽略lombok依赖)
3.2 添加logback-spring.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!-- 配置详情: https://blog.csdn.net/weixin_41377777/article/details/120962037 -->
<!-- 拷贝即可使用: 额外需要关注的就三个点, 1.配置spring.application.name可指定文件前缀名 2.修改dev的springProfile中自定义logger的name指定当前项目 3.修改rollingPolicy的MaxHistory确定日志保留数 -->
<configuration>
<property name="LOG_PATH" value="/apps/logs" />
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level [%C{36}.%M:%line] tid=%X{traceId}, %msg%n" />
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="defaultAppName"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!--本地环境 -->
<springProfile name="dev">
<logger name="com.chenlongji" level="DEBUG" />
<!--<logger name="org.mybatis" level="DEBUG" />-->
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
<!--测试环境 -->
<springProfile name="test">
<!-- 备注: 若springProfile仅激活一个, 那appender的name重复也不影响, 因为没激活的profile里面的appender不会进行初始化 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- layout使用encoder配置应该也行, 如CONSOLE的配置 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${LOG_PATTERN}</pattern>
</layout>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.log</FileNamePattern>
<MaxHistory>1</MaxHistory>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="FILE" />
</root>
</springProfile>
<!--生产环境 -->
<springProfile name="prd">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${LOG_PATTERN}</pattern>
</layout>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.log</FileNamePattern>
<MaxHistory>7</MaxHistory>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="FILE" />
</root>
</springProfile>
</configuration>
说明:
- 日志输出格式中的%X为mdc日志链路追踪使用, 这里不展开讲
- 确保没有logback.xml等日志配置文件, 否则logback-spring.xml可能不生效
- 若想了解配置详情, 点击查看此文章
- 若想了解配置对应的源码信息, 请看logback源码浅析
3.3 添加main测试方法
@SpringBootApplication
public class LogbackStudyApplication {
public static void main(String[] args) {
// 初始化入口
ConfigurableApplicationContext context = SpringApplication.run(LogbackStudyApplication.class, args);
Logger logger = LoggerFactory.getLogger(Test1.class);
logger.info("我是info级别的日志");
context.close();
}
}
4. 最终初始化使用的配置文件说明
- logback默认的配置文件
- logback-test.groovy
- logback-test.xml
- logback.groovy
- logback.xml
- springboot默认的logback配置文件
- logback-test-spring.groovy
- logback-test-spring.xml
- logback-spring.groovy
- logback-spring.xml
- 最终初始化使用的配置文件说明 (可以使用-Dlogging.config={xxx}指定配置文件)
- logging.config指定配置文件, 则使用该文件完成logger最终初始化
- logging.config未指定配置文件且有logback默认的配置文件, 则使用logback默认的配置文件完成logger最终初始化
- logging.config未指定配置文件且无logback任意的配置文件, 则使用DefaultLogbackConfiguration完成logger最终初始化
- logging.config未指定配置文件且无logback默认的配置文件且有springboot默认的logback配置文件, 则使用springboot默认的logback配置文件完成logger最终初始化
二. 执行原生logback初始化
前置说明:
springboot项目中是先执行原生的logback初始化, 再执行springboot下logback初始化的
入口
@SpringBootApplication
public class LogbackStudyApplication {
public static void main(String[] args) {
// 入口
ConfigurableApplicationContext context = SpringApplication.run(LogbackStudyApplication.class, args);
}
}
JVM加载SpringApplication类时, 完成其静态属性logger初始化
public class SpringApplication {
// 这里的LogFactory全限定名为org.apache.commons.logging.LogFactory, 是spring-jcl.jar下的类
private static final Log logger = LogFactory.getLog(SpringApplication.class);
}
进入LogFactory的getLog(Class)方法
public static Log getLog(Class<?> clazz) {
return getLog(clazz.getName());
}
进入LogFactory的getLog(String)方法
public static Log getLog(String name) {
return LogAdapter.createLog(name);
}
进入LogAdapter的createLog(String)方法
public static Log createLog(String name) {
// logApi枚举值来源于LogAdapter初始化是根据 是否可以类加载到某些类来确定值的. 不展开
// 添加spring-boot-starter-web依赖后, logApi = SLF4J_LAL
switch (logApi) {
case LOG4J:
return Log4jAdapter.createLog(name);
case SLF4J_LAL:
// 创建log对象
return Slf4jAdapter.createLocationAwareLog(name);
case SLF4J:
return Slf4jAdapter.createLog(name);
default:
return JavaUtilAdapter.createLog(name);
}
}
进入Slf4jAdapter的createLocationAwareLog(String)方法
public static Log createLocationAwareLog(String name) {
// 此次的LoggerFactory全限定类名为org.slf4j.LoggerFactory, 是slf4j-api下的类
// 因为spring-boot-starter-web依赖导入了logback-classic包, 故此次会进入logback原生的初始化流程
Logger logger = LoggerFactory.getLogger(name);
return (logger instanceof LocationAwareLogger
// 属于logback类型的logger, 包装为Slf4jLocationAwareLog返回
? new Slf4jLocationAwareLog((LocationAwareLogger) logger)
: new Slf4jLog<>(logger));
}
说明:
- 项目添加的spring-boot-starter-web依赖导入了logback-classic包, 后续会进入logback原生的初始化流程
- 原生logback初始化会使用logback.xml等不带spring字眼的配置文件, 若无则使用默认配置BasicConfigurator
- logback原生的初始化流程入口org.slf4j.LoggerFactory.getLogger(name). 请看logback源码浅析
三. 执行SpringBoot下logback的再次初始化
前置说明:
- 完成原生logback初始化后进入SpringApplication的run方法
- 然后一直执行到LoggingApplicationListener的initialize(ConfigurableEnvironment, ClassLoader)方法, 我们从这里开始看源码了解springboot下logback又如何再次初始化
- springboot代码量较多, 后面注释仅为logback相关的标注
进入LoggingApplicationListener的initialize(ConfigurableEnvironment, ClassLoader)方法
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
getLoggingSystemProperties(environment).apply();
this.logFile = LogFile.get(environment);
if (this.logFile != null) {
this.logFile.applyToSystemProperties();
}
this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
initializeEarlyLoggingLevel(environment);
// 初始化日志系统
initializeSystem(environment, this.loggingSystem, this.logFile);
initializeFinalLoggingLevels(environment, this.loggingSystem);
registerShutdownHookIfNecessary(environment, this.loggingSystem);
}
进入LoggingApplicationListener的initializeSystem(ConfigurableEnvironment, LoggingSystem, LogFile)方法
private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
// 获取环境变量中的logging.config指定的配置文件路径
// 注: logback中使用logback.configurationFile指定配置文件路径(springboot下该值最终是无效的)
String logConfig = StringUtils.trimWhitespace(environment.getProperty(CONFIG_PROPERTY));
try {
LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
if (ignoreLogConfig(logConfig)) {
// 核心代码: logging.config没有指定配置文件, 则后续再获取符合要求的配置文件
system.initialize(initializationContext, null, logFile);
}
else {
// 核心代码: logging.config指定了配置文件, 则使用指定的
system.initialize(initializationContext, logConfig, logFile);
}
}
catch (Exception ex) {
//...
}
}
进入LogbackLoggingSystem的initialize(LoggingInitializationContext, String, LogFile)方法
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
LoggerContext loggerContext = getLoggerContext();
// 判断logger上下文中, 是否已经执行了springboot下的logback初始化. 这里是使用LoggingSystem的全限定类名占位标志的
if (isAlreadyInitialized(loggerContext)) {
return;
}
// 核心代码: 初始化日志系统
super.initialize(initializationContext, configLocation, logFile);
loggerContext.getTurboFilterList().remove(FILTER);
// 完成springboot下的logback初始化后, 设置logger上下文springboot的logback已初始化标志(LoggingSystem的全限定类名占位)
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.");
}
}
进入AbstractLoggingSystem的initialize(LoggingInitializationContext, String, LogFile)方法
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
// logging.config指定了配置文件, 则执行initializeWithSpecificConfig方法. 两个方法都在loadConfiguration(LoggingInitializationContext,String,LogFile)中汇合
if (StringUtils.hasLength(configLocation)) {
initializeWithSpecificConfig(initializationContext, configLocation, logFile);
return;
}
// 未指定配置文件, 则执行initializeWithConventions方法. 两个方法都在loadConfiguration(LoggingInitializationContext,String,LogFile)中汇合
initializeWithConventions(initializationContext, logFile);
}
说明:
- initializeWithConventions方法读取默认的配置文件后, 会和initializeWithSpecificConfig在loadConfiguration(LoggingInitializationContext,String,LogFile)中汇合
- 这里我们加上没有指定配置文件, 我们继续从initializeWithConventions方法看源码
进入AbstractLoggingSystem的initializeWithConventions(LoggingInitializationContext, 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) {
// 使用原有的logback默认配置文件再次初始化(使用同一个配置文件再次执行了原生的logback初始化一次). 发生属性更改的属性会被重新初始化. 这里不展开
reinitialize(initializationContext);
return;
}
if (config == null) {
// 获取springboot特有的logback配置文件
config = getSpringInitializationConfig();
}
if (config != null) {
// 核心代码: 再次执行初始化
loadConfiguration(initializationContext, config, logFile);
return;
}
// 整个系统都没找到一个适合的配置文件, 则使用springboot默认的DefaultLogbackConfiguration配置类完成初始化. 不展开
loadDefaults(initializationContext, logFile);
}
说明: springboot初始logback过程汇总
logback默认的配置文件[logback-test.groovy, logback-test.xml, logback.groovy, logback.xml]
springboot默认的logback配置文件[logback-test-spring.groovy, logback-test-spring.xml, logback-spring.groovy, logback-spring.xml]
初始化过程汇总
- 系统无logback相关的任意配置文件. 则先使用logback默认配置类BasicConfigurator初始化一次, 再使用springboot的默认配置类DefaultLogbackConfiguration初始化一次
- 系统指定了配置文件但无logback默认配置文件. 则先使用logback默认配置类BasicConfigurator初始化一次, 再使用指定的配置文件初始化一次
- 系统指定了配置文件但有logback默认配置文件. 则先使用logback默认配置文件初始化一次, 再使用指定的配置文件初始化一次
- 系统未指定配置文件且存在logback默认配置文件. 则使用logback默认配置文件初始化两次
- 系统未指定配置文件且不存在logback默认配置文件且存在springboot的logback配置文件. 则先使用logback默认配置类BasicConfigurator初始化一次, 再使用springboot的logback配置文件初始化一次
最终汇总
- logging.config指定配置文件, 则使用该文件完成logger最终初始化
- logging.config未指定配置文件且有logback默认的配置文件, 则使用logback默认的配置文件完成logger最终初始化
- logging.config未指定配置文件且无logback任意的配置文件, 则使用DefaultLogbackConfiguration完成logger最终初始化
- logging.config未指定配置文件且无logback默认的配置文件且有springboot默认的logback配置文件, 则使用springboot默认的logback配置文件完成logger最终初始化
进入AbstractLoggingSystem的getSpringInitializationConfig()方法
protected String getSpringInitializationConfig() {
// getSpringConfigLocations()找出springboot默认的logback配置文件列表
// findConfig()找到系统中存在第一个 springboot默认的logback配置文件列表内 的配置文件. 该方法不展开
return findConfig(getSpringConfigLocations());
}
进入AbstractLoggingSystem的getSpringConfigLocations()方法
protected String[] getSpringConfigLocations() {
// 获取当前日志系统默认的配置文件. 当前为logback, 默认的配置文件有logback-test.groovy, logback-test.xml, logback.groovy, logback.xml
String[] locations = getStandardConfigLocations();
// 遍历拼接上"-spring.", springboot默认的logback配置文件列表如下
// [logback-test-spring.groovy, logback-test-spring.xml, logback-spring.groovy, logback-spring.xml]
for (int i = 0; i < locations.length; i++) {
String extension = StringUtils.getFilenameExtension(locations[i]);
locations[i] = locations[i].substring(0, locations[i].length() - extension.length() - 1) + "-spring."
+ extension;
}
return locations;
}
进入LogbackLoggingSystem的loadConfiguration(LoggingInitializationContext, String,LogFile)方法
protected void loadConfiguration(LoggingInitializationContext initializationContext, String location,
LogFile logFile) {
super.loadConfiguration(initializationContext, location, logFile);
LoggerContext loggerContext = getLoggerContext();
stopAndReset(loggerContext);
try {
// 通过配置文件重新初始化
configureByResourceUrl(initializationContext, loggerContext, ResourceUtils.getURL(location));
}
catch (Exception ex) {
throw new IllegalStateException("Could not initialize Logback logging from " + location, ex);
}
// ...
}
进入LogbackLoggingSystem的configureByResourceUrl(LoggingInitializationContext, LoggerContext,URL)方法
private void configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext,
URL url) throws JoranException {
if (XML_ENABLED && url.toString().endsWith("xml")) {
// 使用SpringBootJoranConfigurator完成初始化
JoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext);
configurator.setContext(loggerContext);
configurator.doConfigure(url);
}
else {
new ContextInitializer(loggerContext).configureByResource(url);
}
}
说明:
- 后面执行的就是SpringBootJoranConfigurator.doConfigure()方法, 与logback初始化流程几乎一致了.
- logback初始化流程请看logback源码浅析
- 下面的[SpringBootJoranConfigurator与JoranConfigurator初始化的差异] 继续将springboot中的一些和logback初始化不同的东西
四. SpringBootJoranConfigurator与JoranConfigurator初始化的差异
1. 继承关系
说明:
SpringBootJoranConfigurator为JoranConfigurator的子类
2. 了解SpringBootJoranConfigurator类
class SpringBootJoranConfigurator extends JoranConfigurator {
// springboot的logger初始化上下文对象, 存储了environment环境信息
private LoggingInitializationContext initializationContext;
SpringBootJoranConfigurator(LoggingInitializationContext initializationContext) {
this.initializationContext = initializationContext;
}
/**
* 添加实例规则
*/
@Override
public void addInstanceRules(RuleStore rs) {
super.addInstanceRules(rs);
Environment environment = this.initializationContext.getEnvironment();
// 添加解析<configuration>下<springProperty>的Action
rs.addRule(new ElementSelector("configuration/springProperty"), new SpringPropertyAction(environment));
// 添加解析* <springProfile>的Action
rs.addRule(new ElementSelector("*/springProfile"), new SpringProfileAction(environment));
// 添加* <springProfile>下的Action. 该Action为无操作Action, 由SpringProfileAction去处理<springProfile>下的标签内容
rs.addRule(new ElementSelector("*/springProfile/*"), new NOPAction());
}
}
3. SpringBoot特有的SpringPropertyAction
class SpringPropertyAction extends Action {
private static final String SOURCE_ATTRIBUTE = "source";
private static final String DEFAULT_VALUE_ATTRIBUTE = "defaultValue";
private final Environment environment;
SpringPropertyAction(Environment environment) {
this.environment = environment;
}
@Override
public void begin(InterpretationContext context, String elementName, Attributes attributes) throws ActionException {
String name = attributes.getValue(NAME_ATTRIBUTE);
String source = attributes.getValue(SOURCE_ATTRIBUTE);
Scope scope = ActionUtil.stringToScope(attributes.getValue(SCOPE_ATTRIBUTE));
String defaultValue = attributes.getValue(DEFAULT_VALUE_ATTRIBUTE);
if (OptionHelper.isEmpty(name) || OptionHelper.isEmpty(source)) {
addError("The \"name\" and \"source\" attributes of <springProperty> must be set");
}
// 读取<springProperty>的属性值, 缓存到指定的作用域中待用. 不展开
ActionUtil.setProperty(context, name, getValue(source, defaultValue), scope);
}
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);
}
@Override
public void end(InterpretationContext context, String name) throws ActionException {
}
}
4. SpringBoot特有的SpringProfileAction
class SpringProfileAction1 extends Action implements InPlayListener {
// 环境信息
private final Environment environment;
// 深度. 用于拦截嵌套的<springProfile>错误标签
private int depth = 0;
// 记录当前的<springProfile>是否满足运行的环境(即springProfile的name是否包含 项目的spring.profiles.active)
private boolean acceptsProfile;
// 事件暂存区. 寄存<springProfile>及内嵌标签事件的 缓存事件列表
private List<SaxEvent> events;
SpringProfileAction1(Environment environment) {
this.environment = environment;
}
@Override
public void begin(InterpretationContext ic, String name, Attributes attributes) throws ActionException {
// 深度加一, 标志开始解析<springProfile>
this.depth++;
// 只能处理外层的<springProfile>, 嵌套直接结束(示例: <springProfile><springProfile>...<springProfile/><springProfile/>)
if (this.depth != 1) {
return;
}
// 当前action入栈, 待后续使用
ic.pushObject(this);
// 记录 当前的<springProfile>是否满足运行的环境情况
this.acceptsProfile = acceptsProfiles(ic, attributes);
this.events = new ArrayList<>();
// 将当前的SpringProfileAction加入InterpretationContext的监听器列表中listenerList. 该监听器用于添加寄存的内嵌标签事件. 不展开
ic.addInPlayListener(this);
}
private boolean acceptsProfiles(InterpretationContext ic, Attributes attributes) {
if (this.environment == null) {
return false;
}
// 获取<springProfile>属性name, 按","分隔. 不展开
String[] profileNames = StringUtils
.trimArrayElements(StringUtils.commaDelimitedListToStringArray(attributes.getValue(NAME_ATTRIBUTE)));
if (profileNames.length == 0) {
return false;
}
// 获取profileNames的真实值. 不展开
for (int i = 0; i < profileNames.length; i++) {
profileNames[i] = OptionHelper.substVars(profileNames[i], ic, this.context);
}
// 判断当前的<springProfile>是否满足运行的环境. 不展开
return this.environment.acceptsProfiles(Profiles.of(profileNames));
}
@Override
public void end(InterpretationContext ic, String name) throws ActionException {
// 深度减一, 标志结束解析<springProfile>
this.depth--;
// 嵌套<springProfile>标签, 直接结束
if (this.depth != 0) {
return;
}
// 移除掉SpringProfileAction这个监听器
ic.removeInPlayListener(this);
// 校验当前action是否为栈顶对象, 是则弹出
verifyAndPop(ic);
// 重要代码: 当前<springProfile>有效, 将<springProfile>的内嵌标签事件 丢回正在处理eventList列表中. 插入的位置为下一个执行事件前面
if (this.acceptsProfile) {
addEventsToPlayer(ic);
}
}
private void verifyAndPop(InterpretationContext ic) {
// 校验当前action是否为栈顶对象, 是则弹出. 不展开
Object o = ic.peekObject();
Assert.state(o != null, "Unexpected null object on stack");
Assert.isInstanceOf(SpringProfileAction1.class, o, "logback stack error");
Assert.state(o == this, "ProfileAction different than current one on stack");
ic.popObject();
}
private void addEventsToPlayer(InterpretationContext ic) {
// 移除<springProfile>和<springProfile/>事件
Interpreter interpreter = ic.getJoranInterpreter();
this.events.remove(0);
this.events.remove(this.events.size() - 1);
// 将<springProfile>内嵌事件丢回正在处理eventList列表中. 插入的位置为下一个执行事件前面
interpreter.getEventPlayer().addEventsDynamically(this.events, 1);
}
@Override
public void inPlay(SaxEvent event) {
// 使用SpringProfileAction该监听器添加事件到事件寄存器events中.
// 调用处: 在EventPlayer的play方法中调用, 将当前的事件添加到各个监听器中去. 不展开
this.events.add(event);
}
}
说明:
- 正常配置下, 最多只有一个springProfile标签有效(配置的springProfile的name包含了spring.profiles.active)
- springProfile内的标签事件不是立即解析的, 是待springProfile标签结束后再处理内嵌的标签事件(springProfile有效的情况下)
- 假设当前spring.profiles.active=dev, 则解析标签顺序示例如下图