1 概述
日志這里采用logback,其為springboot默認的日志工具。其整體已經被springboot封裝得比較好了,扔個配置文件到classpath里就能夠使用。
但在實際使用中,日志配置文件有可能需要進行改動,比如日志的打印級別,平時可能定的是WARN或者ERROR級別,如果出點問題,可能希望臨時能夠調整為INFO或者DEBUG,方便產生更加豐富的日志以定位問題。如果配置文件是放到classpath里,也就會被打包到jar包里,修改配置文件就需要重新打包、部署、啟動等,很可能做不到只修改配置文件并生效。要想改變配置文件的位置,就有必要了解一下這個配置文件是如何加載的。
2 原理
2.1 logback是如何被依賴的
前面看對spring-boot-starter的依賴的時候,有個不起眼的依賴:spring-boot-starter-logging
https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter/2.7.18/spring-boot-starter-2.7.18.pom
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot</artifactId><version>2.7.18</version><scope>compile</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId><version>2.7.18</version><scope>compile</scope></dependency><!-- 省略其它依賴 -->
<dependencies>
?查看spring-boot-starter-logging的依賴:https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-logging/2.7.18/spring-boot-starter-logging-2.7.18.pom
<dependencies><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.12</version><scope>compile</scope></dependency><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-to-slf4j</artifactId><version>2.17.2</version><scope>compile</scope></dependency><dependency><groupId>org.slf4j</groupId><artifactId>jul-to-slf4j</artifactId><version>1.7.36</version><scope>compile</scope></dependency>
</dependencies>
?logback-classic包提供了logback打印日志功能。
2.2 查找logback.xml配置文件
2.2.1 觸發查找的Listener
spring-boot-2.7.18.jar包里提供了META-INF/spring.factories配置文件,里面配置了LoggingApplicationListener:
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener
2.2.2 查找過程
Springboot提供了一個LoggingApplicationListener,其繼實現了GenericApplicationListener接口,該接口最終繼承了ApplicationListener。
按Springboot的規則,實現了ApplicationListener接口的都會被Springboot統一調用。這個類就是響應springboot的準備環境對象事件來初始化日志對象LogbackLoggingSystem的:
// 源碼位置:org.springframework.boot.context.logging.LoggingApplicationListener
public void onApplicationEvent(ApplicationEvent event) {if (event instanceof ApplicationStartingEvent) {onApplicationStartingEvent((ApplicationStartingEvent) event);}else if (event instanceof ApplicationEnvironmentPreparedEvent) {// 1. 在PreparedEvent的時候加載日志配置文件onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);}else if (event instanceof ApplicationPreparedEvent) {onApplicationPreparedEvent((ApplicationPreparedEvent) event);}else if (event instanceof ContextClosedEvent && ((ContextClosedEvent) event).getApplicationContext().getParent() == null) {onContextClosedEvent();}else if (event instanceof ApplicationFailedEvent) {onApplicationFailedEvent();}
}
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {SpringApplication springApplication = event.getSpringApplication();if (this.loggingSystem == null) {this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());}// 2. 調用日志初始化接口initialize(event.getEnvironment(), springApplication.getClassLoader());
}
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {getLoggingSystemProperties(environment).apply();// 3. 獲取環境變量里配置的日志文件,在環境變量里配置了才有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);
}// 源碼位置:org.springframework.boot.logging.LogFile
public static LogFile get(PropertyResolver propertyResolver) {// 4. 如果配置了logging.file.name、logging.file.path環境變量,則它們組成一個log文件的路徑,用此路徑初始化一個日志文件對象// FILE_NAME_PROPERTY = "logging.file.name"// FILE_PATH_PROPERTY = "logging.file.path"String file = propertyResolver.getProperty(FILE_NAME_PROPERTY);String path = propertyResolver.getProperty(FILE_PATH_PROPERTY);if (StringUtils.hasLength(file) || StringUtils.hasLength(path)) {return new LogFile(file, path);}return null;
}// 回到LoggingApplicationListener繼續處理環境變量里配置的日志文件
// 源碼位置:org.springframework.boot.context.logging.LoggingApplicationListener
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {getLoggingSystemProperties(environment).apply();// 3. 獲取環境變量里配置的日志文件,在環境變量里配置了才有this.logFile = LogFile.get(environment);if (this.logFile != null) {// 5. 把環境變量里的配置日志文件路徑和文件名設置到系統屬性里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);
}// 源碼位置:org.springframework.boot.logging.LogFile
public void applyToSystemProperties() {// 6. 提供系統屬性為參數applyTo(System.getProperties());
}
public void applyTo(Properties properties) {// 7. 把配置文件的路徑和文件名設置到系統屬性里,可以在logback.xml里作為變量引用,如${LOG_PATH}// LoggingSystemProperties.LOG_PATH = "LOG_PATH"// LoggingSystemProperties.LOG_FILE = "LOG_FILE"put(properties, LoggingSystemProperties.LOG_PATH, this.path);put(properties, LoggingSystemProperties.LOG_FILE, toString());
}
private void put(Properties properties, String key, String value) {if (StringUtils.hasLength(value)) {properties.put(key, value);}
}// 回到LoggingApplicationListener繼續處理環境變量里配置的日志文件
// 源碼位置:org.springframework.boot.context.logging.LoggingApplicationListener
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {getLoggingSystemProperties(environment).apply();// 3. 獲取環境變量里配置的日志文件,在環境變量里配置了才有this.logFile = LogFile.get(environment);if (this.logFile != null) {// 5. 把環境變量里的配置日志文件路徑和文件名設置到系統屬性里this.logFile.applyToSystemProperties();}this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);initializeEarlyLoggingLevel(environment);// 8. 初始化LogbackLoggingSystem對象initializeSystem(environment, this.loggingSystem, this.logFile);initializeFinalLoggingLevels(environment, this.loggingSystem);registerShutdownHookIfNecessary(environment, this.loggingSystem);
}
private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {// 9. 讀取logging.config配置(在命令行配置),CONFIG_PROPERTY = "logging.config"String logConfig = StringUtils.trimWhitespace(environment.getProperty(CONFIG_PROPERTY));try {LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);// 10. 調用LogbackLoggingSystem對象的初始化方法,// 如果配置了logging.config,則把配置的文件作為logConfig參數傳入,否則logConfig參數為nullif (ignoreLogConfig(logConfig)) {system.initialize(initializationContext, null, logFile);}else {system.initialize(initializationContext, logConfig, logFile);}}// 省略其它代碼
}// 源碼位置:org.springframework.boot.logging.logback.LogbackLoggingSystem
// 注意:configLocation有null和非null兩種情況
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {LoggerContext loggerContext = getLoggerContext();if (isAlreadyInitialized(loggerContext)) {return;}// 11. 調用父類的初始化方法,父類為AbstractLoggingSystemsuper.initialize(initializationContext, configLocation, logFile);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.");}
}// 源碼位置:org.springframework.boot.logging.AbstractLoggingSystem
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {// 12. 如果logging.config配置的值不為空,加載配置指定的日志配置文件if (StringUtils.hasLength(configLocation)) {initializeWithSpecificConfig(initializationContext, configLocation, logFile);return;}// 13. 當沒有配置logging.config時,則加載默認的配置文件,這里重點關注默認的initializeWithConventions(initializationContext, logFile);
}
private void initializeWithSpecificConfig(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {// 配置了logging.config的時候,主要是先把里面可能出現${}占位符替換為實際值,// 然后得到一個正常的日志配置文件路徑,按正常流程處理,參考下面對loadConfiguration()的說明configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);loadConfiguration(initializationContext, configLocation, logFile);
}
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {// 14. 獲取可能的默認的日志配置文件路徑String config = getSelfInitializationConfig();if (config != null && logFile == null) {reinitialize(initializationContext);return;}if (config == null) {config = getSpringInitializationConfig();}if (config != null) {loadConfiguration(initializationContext, config, logFile);return;}loadDefaults(initializationContext, logFile);
}
protected String getSelfInitializationConfig() {// 15. getStandardConfigLocations()獲取默認的日志配置文件return findConfig(getStandardConfigLocations());
}// 源碼位置:org.springframework.boot.logging.logback.LogbackLoggingSystem
protected String[] getStandardConfigLocations() {// 16. 默認支持四種配置文件名稱,注意其順序,帶test的在前面,logback.xml是最后一種// 如果開發環境了放logback-test.xml和logback.xml,生產環境只放logback.xml,// 則可以開發環境用的是帶test的,不影響生產文件的修改,會比較便利return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" };
}// 回到AbstractLoggingSystem的getSelfInitializationConfig(),調用findConfig()
// 源碼位置:org.springframework.boot.logging.AbstractLoggingSystem
protected String getSelfInitializationConfig() {// 17. 調findConfig()查找配置文件的路徑return findConfig(getStandardConfigLocations());
}
private String findConfig(String[] locations) {// 18. 遍歷每個可能的配置文件名,調用Spring提供的ClassPathResource,從classpath中檢查文件是否存在,即上面指定的4中文件需要放到classpath中// 如果存在,則在文件名的前面加上classpath:路徑前綴,注意這里體現順序,只要找到第一個就返回for (String location : locations) {ClassPathResource resource = new ClassPathResource(location, this.classLoader);if (resource.exists()) {return "classpath:" + location;}}return null;
}// 回到AbstractLoggingSystem的initializeWithConventions(),繼續處理獲取到的日志文件路徑
// 源碼位置:org.springframework.boot.logging.AbstractLoggingSystem
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {// 14. 獲取可能的默認的日志配置文件路徑String config = getSelfInitializationConfig();if (config != null && logFile == null) {// 15. 重新初始化,這里并沒有傳入找到的文件,因為里面還會再找一次reinitialize(initializationContext);return;}if (config == null) {config = getSpringInitializationConfig();}if (config != null) {loadConfiguration(initializationContext, config, logFile);return;}loadDefaults(initializationContext, logFile);
}// 源碼位置:org.springframework.boot.logging.logback.LogbackLoggingSystem
protected void reinitialize(LoggingInitializationContext initializationContext) {getLoggerContext().reset();getLoggerContext().getStatusManager().clear();// 16. 加載配置文件,這里重新調了getSelfInitializationConfig(),從classpath找配置文件路徑,參考上面步驟17loadConfiguration(initializationContext, getSelfInitializationConfig(), null);
}
protected void loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile) {// 如果logFile有值,則調父類的方法加載配置文件,沒有用到location,這里先忽略// logFile是前面從環境變量里獲取的日志配置文件路徑和文件名super.loadConfiguration(initializationContext, location, logFile);LoggerContext loggerContext = getLoggerContext();stopAndReset(loggerContext);try {// 17. 用url的方式加載文件,ResourceUtils.getURL(location)把以classpath前綴的路徑轉為文件絕對路徑,比如classpath:logback.xmlconfigureByResourceUrl(initializationContext, loggerContext, ResourceUtils.getURL(location));}catch (Exception ex) {throw new IllegalStateException("Could not initialize Logback logging from " + location, ex);}reportConfigurationErrorsIfNecessary(loggerContext);
}// 源碼位置:org.springframework.boot.logging.logback.LogbackLoggingSystem
private void configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext, URL url) throws JoranException {if (XML_ENABLED && url.toString().endsWith("xml")) {JoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext);configurator.setContext(loggerContext);// 18. SpringBootJoranConfigurator繼承了logback-classic包的類GenericConfigurator,從而轉到logback-classic包進行日志文件處理了// GenericConfigurator提供了doConfigure()方法實際加載配置文件,由logback包完成日志對象的初始化configurator.doConfigure(url);}else {new ContextInitializer(loggerContext).configureByResource(url);}
}// 上面看的是如果配置了日志文件(如logback.xml的情況),回到AbstractLoggingSystem.initialize()看看沒有配置日志文件的情況
// 源碼位置:org.springframework.boot.logging.AbstractLoggingSystem
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {// 14. 獲取可能的默認的日志配置文件路徑String config = getSelfInitializationConfig();if (config != null && logFile == null) {reinitialize(initializationContext);return;}// 19. 如果沒有配置日志文件,可找spring提供的日志配置文件if (config == null) {config = getSpringInitializationConfig();}if (config != null) {loadConfiguration(initializationContext, config, logFile);return;}loadDefaults(initializationContext, logFile);
}// 源碼位置:org.springframework.boot.logging.AbstractLoggingSystem
protected String getSpringInitializationConfig() {// 20. 找配置文件return findConfig(getSpringConfigLocations());
}
protected String[] getSpringConfigLocations() {// 21. 先找標準文件,參考步驟16可得logback-test.groovy、logback-test.xml、logback.groovy、logback.xmlString[] locations = getStandardConfigLocations();// 22. 給每個文件名加上-spring后綴,作為新的文件名,如logback.xml轉為logback-spring.xmlfor (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;
}// 回到AbstractLoggingSystem.initialize()繼續加載帶-spring后綴的日志配置文件
// 源碼位置:org.springframework.boot.logging.AbstractLoggingSystem
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {// 14. 獲取可能的默認的日志配置文件路徑String config = getSelfInitializationConfig();if (config != null && logFile == null) {reinitialize(initializationContext);return;}// 19. 如果沒有配置日志文件,可找spring提供的日志配置文件if (config == null) {config = getSpringInitializationConfig();}// 23. 加載日志配置文件,方式同步驟17if (config != null) {loadConfiguration(initializationContext, config, logFile);return;}// 24. 如果spring的配置文件也沒有,則加載默認的,保證一定可以打印日志loadDefaults(initializationContext, logFile);
}// 默認的日志是把日志格式等硬編碼在代碼里的,一般也不使用,大概參考一下即可
// 源碼位置:org.springframework.boot.logging.logback.DefaultLogbackConfiguration
private void defaults(LogbackConfigurator config) {config.conversionRule("clr", ColorConverter.class);config.conversionRule("wex", WhitespaceThrowableProxyConverter.class);config.conversionRule("wEx", ExtendedWhitespaceThrowableProxyConverter.class);config.getContext().putProperty("CONSOLE_LOG_PATTERN", resolve(config, "${CONSOLE_LOG_PATTERN:-"+ "%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) "+ "%clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} "+ "%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"));String defaultCharset = Charset.defaultCharset().name();config.getContext().putProperty("CONSOLE_LOG_CHARSET", resolve(config, "${CONSOLE_LOG_CHARSET:-" + defaultCharset + "}"));config.getContext().putProperty("FILE_LOG_PATTERN", resolve(config, "${FILE_LOG_PATTERN:-"+ "%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] "+ "%-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"));config.getContext().putProperty("FILE_LOG_CHARSET", resolve(config, "${FILE_LOG_CHARSET:-" + defaultCharset + "}"));config.logger("org.apache.catalina.startup.DigesterFactory", Level.ERROR);config.logger("org.apache.catalina.util.LifecycleBase", Level.ERROR);config.logger("org.apache.coyote.http11.Http11NioProtocol", Level.WARN);config.logger("org.apache.sshd.common.util.SecurityUtils", Level.WARN);config.logger("org.apache.tomcat.util.net.NioSelectorPool", Level.WARN);config.logger("org.eclipse.jetty.util.component.AbstractLifeCycle", Level.ERROR);config.logger("org.hibernate.validator.internal.util.Version", Level.WARN);config.logger("org.springframework.boot.actuate.endpoint.jmx", Level.WARN);
}
2.2.3 小結
- 如果在啟動參數里通過-Dlogging.config指定了日志配置文件,則直接加載此日志配置文件;這種方法指定的配置文件,可以使用${}占位符引用系統屬性或者系統環境變量。
- 如果沒有手工指定,則從classpath目錄下按順序加載四種日志配置文件(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,只要加載到一個就返回。
- 如何還沒有加載到日志配置文件,則加載默認的,默認的日志配置是硬編碼到代碼里的。
3 使用
3.1 不需要修改配置文件的場景
3.2 需要修改配置文件的場景
- 在生產環境中,一般日志級別只會開到WARN甚至ERROR級別,如果想看INFO甚至DEBUG級別的日志就有可能看不到,在定位棘手問題時可能需要更詳細的日志信息。此時如果想修改一下日志級別,那么就希望配置文件能夠改動一下。
- 日志配置的一些參數配置不理想,想調整一下。比如日志文件過大或者過小,不利于日志文件維護。
- 增加一些場景的日志打印,比如原來沒有加spring相關的日志控制,比較影響問題定位,希望加上spring相關日志只有ERROR才打印的控制等。
3.3 測試和生產分開的場景
4 架構一小步
- 開發測試環境,在代碼src/main/resources目錄下放一個帶test的配置文件(如logback-test.xml),springboot優先加載帶test是日志配置文件;一般也放一個不帶test的配置文件,作為代碼版本管理的一部分。
- 在部署環境的時候,不把logback.xml文件打包到jar中,而是放到和jar包同級的config/logback.xml中,使用啟動參數-Dlogging.config手工指定配置文件。