描述
Spring Boot在所有內(nèi)部日志中使用Apache Commons Logging,但是默認(rèn)配置也提供了對常用日志的支持,如:Java Util Logging,Log4J, Log4J2和Logback。每種Logger都可以通過配置使用控制臺或者文件輸出日志內(nèi)容
SpringBoot默認(rèn)使用的是SLF4J(日志門面)+logback日志實(shí)現(xiàn)框架。jul-to-slf4j表示使用slf4j替換掉java.util.logging,log4j-over-slf4j表示使用slf4j替換掉log4j,jcl-over-slf4j表示使用slf4j替換掉java.commongs.logging。這里使用“替換”或許比較難理解,這是slf4j提供的一種橋接模式,對外統(tǒng)一使用slf4j門面。
下面通過代碼跟蹤spring日志初始化的流程查看spring對日志配置的初始化以及加載
日志初始化入口
springboot 初始化加載日志通過ApplicationListener完成工作的,springboot通過讀取spring.factories擴(kuò)展配置文件,加載定義好的ApplicationListener,默認(rèn)的springboot包下配置如下:
# 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.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
#日志初始化加載,銷毀監(jiān)聽器,負(fù)責(zé)完成日志初始化以及銷毀工作
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
在配置中我們看到springboot默認(rèn)配置了LoggingApplicationListener,springboot就是通過此監(jiān)聽完成日志的加載,初始化,以及銷毀等工作的。
- LoggingApplicationListener
public class LoggingApplicationListener implements GenericApplicationListener {
/**
* 日志配置文件在配置環(huán)境中的key
* springboot 通過 environment根據(jù)此key 查找我們自定義的日志配置根據(jù)
*/
public static final String CONFIG_PROPERTY = "logging.config";
/**
* The name of the {@link LoggingSystem} bean.
*/
public static final String LOGGING_SYSTEM_BEAN_NAME = "springBootLoggingSystem";
/**
* springboot 對不同日志統(tǒng)一封裝接口,用于初始化日志接口
*
*/
private LoggingSystem loggingSystem;
private LogLevel springBootLogging = null;
/**
* 監(jiān)聽事件觸發(fā)執(zhí)行方法,用于根據(jù)不同事件完成不同的操作,此處包含日志初始化的前置操作,日志的初始化操作,日志的銷毀清除操作
*/
@Override
public void onApplicationEvent(ApplicationEvent event) {
//系統(tǒng)開始啟動事件,LoggingSystem初始化前置操作
if (event instanceof ApplicationStartingEvent) {
onApplicationStartingEvent((ApplicationStartingEvent) event);
}
//環(huán)境資源加載完成事件,初始化LoggingSystem
else if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
//Application啟動完成事件,將LoggingSystem注冊到容器管理維護(hù)
else if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent((ApplicationPreparedEvent) event);
}
//容器關(guān)閉事件,銷毀日志
else if (event instanceof ContextClosedEvent && ((ContextClosedEvent) event)
.getApplicationContext().getParent() == null) {
onContextClosedEvent();
}
//Application啟動失敗事件,銷毀日志
else if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent();
}
}
/**
* 執(zhí)行LoggingSystem初始化的前置操作
*/
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
//獲取LoggingSystem的真實(shí)實(shí)現(xiàn),
// 此處會根據(jù)不同的日志框架獲取不同的實(shí)現(xiàn),
// logback : LogbackLoggingSystem
// log4j2: Log4J2LoggingSystem
// javalog: JavaLoggingSystem
this.loggingSystem = LoggingSystem
.get(event.getSpringApplication().getClassLoader());
//執(zhí)行beforeInitialize方法完成初始化前置操作
this.loggingSystem.beforeInitialize();
}
/**
* 執(zhí)行LoggingSystem初始化操作
*/
private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
if (this.loggingSystem == null) {
this.loggingSystem = LoggingSystem
.get(event.getSpringApplication().getClassLoader());
}
//調(diào)用initialize方法完成LoggingSystem初始化
initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
}
/**
* LoggingSystem 注冊到IOC容器托管
*/
private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
ConfigurableListableBeanFactory beanFactory = event.getApplicationContext()
.getBeanFactory();
if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
}
}
/**
* LoggingSystem 銷毀操作
*/
private void onContextClosedEvent() {
if (this.loggingSystem != null) {
this.loggingSystem.cleanUp();
}
}
/**
* LoggingSystem 銷毀操作
*/
private void onApplicationFailedEvent() {
if (this.loggingSystem != null) {
this.loggingSystem.cleanUp();
}
}
/**
* 初始化 LoggingSystem
*/
protected void initialize(ConfigurableEnvironment environment,
ClassLoader classLoader) {
new LoggingSystemProperties(environment).apply();
//獲取logFile信息
// logFile 分別在 environment中獲取key為:logging.file和logging.path的值創(chuàng)建LogFile對象,當(dāng)這兩個值任何一個不存在時LogFile對象為null
LogFile logFile = LogFile.get(environment);
if (logFile != null) {
//logFile存在時將配置信息設(shè)置到System中
logFile.applyToSystemProperties();
}
//初始化早期的日志等級
initializeEarlyLoggingLevel(environment);
//初始化LoggingSystem對象
initializeSystem(environment, this.loggingSystem, logFile);
//注冊最終的日志等級
initializeFinalLoggingLevels(environment, this.loggingSystem);
//注冊關(guān)閉鉤子用于系統(tǒng)關(guān)閉是銷毀
registerShutdownHookIfNecessary(environment, this.loggingSystem);
}
/**
* 初始化LoggingSystem對象
*/
private void initializeSystem(ConfigurableEnvironment environment,
LoggingSystem system, LogFile logFile) {
//構(gòu)建日志初始化(LoggingSystem)的上下問環(huán)境
LoggingInitializationContext initializationContext = new LoggingInitializationContext(
environment);
//獲取自定義配置的日志配置文件,在key(logging.config)中獲取
String logConfig = environment.getProperty(CONFIG_PROPERTY);
//判斷自定義配置是否有效
if (ignoreLogConfig(logConfig)) {
//無效時LoggingSystem的初始化,不根據(jù)logging.config初始化日志
system.initialize(initializationContext, null, logFile);
}
else {
try {
//校驗logging.config的配置文件是否可讀
ResourceUtils.getURL(logConfig).openStream().close();
//根據(jù)logging.config的配置初始化日志
system.initialize(initializationContext, logConfig, logFile);
}
catch (Exception ex) {
}
}
}
/**
* 校驗logging.config獲取到的日志配置是否可以忽視
*/
private boolean ignoreLogConfig(String logConfig) {
return !StringUtils.hasLength(logConfig) || logConfig.startsWith("-D");
}
}
LoggingApplicationListener 監(jiān)聽不同的事件,根據(jù)不同事件對日志做不同的操作:
- ApplicationStartingEvents事件
- 完成LoggingSystem的創(chuàng)建,根據(jù)不同的日志框架獲取不同的實(shí)現(xiàn):(logback :LogbackLoggingSystem;log4j2: Log4J2LoggingSystem;javalog:JavaLoggingSystem)
- 執(zhí)行LoggingSystem的beforeInitialize方法完成初始化前置操作
- ApplicationEnvironmentPreparedEvent事件
- 獲取LogFile對象: 根據(jù)配置logging.file和logging.path的值創(chuàng)建LogFile對象,當(dāng)這兩個值任何一個不存在時LogFile對象為null
- 初始化早期的日志等級:initializeEarlyLoggingLevel
- 初始化LoggingSystem對象: initializeSystem
- 獲取自定義日志配置:在key(logging.config)中獲取
- 根據(jù)義日志配置與LogFile對象調(diào)用LoggingSystem的initialize方法初始化日志
6.初始化最終的日志等級給LoggingSystem對象- 注冊銷毀鉤子,添加loggingSystem的銷毀
- ApplicationPreparedEvent事件
將LoggingSystem對象注冊到IOC容器托管
- ContextClosedEvent與ApplicationFailedEvent事件
清除銷毀LoggingSystem對象
通過代碼以及上面分析可以看到LoggingApplicationListener主要是完成了LoggingSystem的初始化以及銷毀等工作,根據(jù)不同的事件。那么日志的初始化主要流程是通過LoggingSystem完成的,下面我們通過源碼查看LoggingSystem的初始化工作。
日志系統(tǒng)的核心類初始化
spring實(shí)現(xiàn)了多個日志框架的的LoggingSystem默認(rèn)使用的是LogbackLoggingSystem,下面我們通過分析LogbackLoggingSystem源碼來看logbak在初始化前以及初始化是如何加載配置的。
LogbackLoggingSystem繼承Slf4JLoggingSystem
Slf4JLoggingSystem 繼承AbstractLoggingSystem
初始化前置操作 beforeInitialize
// LogbackLoggingSystem類
@Override
public void beforeInitialize() {
// 創(chuàng)建日志上下文環(huán)境
LoggerContext loggerContext = getLoggerContext();
//判斷日志是否初始化過,初始化過直接返回
if (isAlreadyInitialized(loggerContext)) {
return;
}
//調(diào)用父類的beforeInitialize
super.beforeInitialize();
//上下文環(huán)境中添加過濾器
loggerContext.getTurboFilterList().add(FILTER);
}
//父類 Slf4JLoggingSystem
@Override
public void beforeInitialize() {
//繼續(xù)調(diào)用父類的beforeInitialize
super.beforeInitialize();
//配置日志的橋接處理器
configureJdkLoggingBridgeHandler();
}
// 父類 AbstractLoggingSystem
@Override
public void beforeInitialize() {
//空實(shí)現(xiàn)
}
beforeInitialize 為日志的初始化前置操作,通過LoggingApplicationListener看到是事件ApplicationStartingEvents觸發(fā)
beforeInitialize主要完成以下功能:
- 創(chuàng)建log系統(tǒng)的上下文件環(huán)境LoggerContext
- 配置日志橋接處理器configureJdkLoggingBridgeHandler
- 上下文件環(huán)境LoggerContext添加過濾器 filter
初始化操作 initialize
// LogbackLoggingSystem類
@Override
public void initialize(LoggingInitializationContext initializationContext,
String configLocation, LogFile logFile) {
//獲取日志上下文環(huán)境
LoggerContext loggerContext = getLoggerContext();
//判斷當(dāng)前上下文環(huán)境是否已經(jīng)初始化過
if (isAlreadyInitialized(loggerContext)) {
return;
}
//調(diào)用父類的initialize執(zhí)行初始化
super.initialize(initializationContext, configLocation, logFile);
// 移除上下文中的過濾器FILTER
loggerContext.getTurboFilterList().remove(FILTER);
//設(shè)置當(dāng)前上下文環(huán)境的初始化標(biāo)識
markAsInitialized(loggerContext);
}
// 父類 AbstractLoggingSystem
@Override
public void initialize(LoggingInitializationContext initializationContext,
String configLocation, LogFile logFile) {
//判斷是否存在自定義的日志配置文件路徑
if (StringUtils.hasLength(configLocation)) {
//根據(jù)自定義的日志配置文件初始化日志
//此處是根據(jù)key(logging.config)的值初始化日志
initializeWithSpecificConfig(initializationContext, configLocation, logFile);
return;
}
//根據(jù)約定俗成的規(guī)則初始化日志
initializeWithConventions(initializationContext, logFile);
}
initialize 為日志的初始化操作,通過LoggingApplicationListener看到是事件ApplicationEnvironmentPreparedEvent觸發(fā)
initialize主要完成以下功能:
- 獲取日志上下文環(huán)境
- KEY(logging.config)值存在時根據(jù)此值初始化日志:initializeWithSpecificConfig
- KEY(logging.config)值不存在時根據(jù)約定俗成規(guī)則初始化日志:initializeWithConventions
- 移除上下文環(huán)境中的FILTER過濾器
- 設(shè)置當(dāng)前上下文環(huán)境的初始化標(biāo)識markAsInitialized
++備注:++ 初始化操作在initialize方法中出現(xiàn)了兩個分支,是根據(jù)配置logging.config判斷的,當(dāng)前springboot的environment存在logging.config值是按照配置的日志配置初始化日志系統(tǒng),否則按照約定俗成的規(guī)則初始化日志。
根據(jù)自定義配置初始化日志initializeWithSpecificConfig
// AbstractLoggingSystem
private void initializeWithSpecificConfig(
LoggingInitializationContext initializationContext, String configLocation,
LogFile logFile) {
//轉(zhuǎn)換配置自定義配置文件資源路徑
configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);
//調(diào)用實(shí)現(xiàn)類的loadConfiguration完成配置加載
loadConfiguration(initializationContext, configLocation, logFile);
}
// LogbackLoggingSystem 實(shí)現(xiàn)類
protected void loadConfiguration(LoggingInitializationContext initializationContext,
String location, LogFile logFile) {
//獲取日志上下文件
LoggerContext loggerContext = getLoggerContext();
//設(shè)置停止和重置上下文件環(huán)境
stopAndReset(loggerContext);
try {
//調(diào)用configureByResourceUrl方法加載配置
configureByResourceUrl(initializationContext, loggerContext,
ResourceUtils.getURL(location));
}
catch (Exception ex) {
}
}
/**
* 根據(jù)配置資源的URL加載日志配置
*/
private void configureByResourceUrl(
LoggingInitializationContext initializationContext,
LoggerContext loggerContext, URL url) throws JoranException {
//加載xml類型的日志配置文件
if (url.toString().endsWith("xml")) {
JoranConfigurator configurator = new SpringBootJoranConfigurator(
initializationContext);
configurator.setContext(loggerContext);
configurator.doConfigure(url);
}
else {
//加載非xml類型的日志配置文件
new ContextInitializer(loggerContext).configureByResource(url);
}
}
initializeWithSpecificConfig 主要是根據(jù)配置logging.config加載日志配置信息,logging.config為系統(tǒng)啟動時我們執(zhí)行的配置信息如:
java -jar xxx.jar --logging.config=/opt/config/log4j2-spring.xml
按照約定俗成的規(guī)則初始化日志initializeWithConventions
// AbstractLoggingSystem
private void initializeWithConventions(
LoggingInitializationContext initializationContext, LogFile logFile) {
//獲取約定俗成的配置資源路徑
String config = getSelfInitializationConfig();
if (config != null && logFile == null) {
//存在時加載資源,并返回
reinitialize(initializationContext);
return;
}
if (config == null) {
//獲取spring規(guī)定的配置文件資源路徑
config = getSpringInitializationConfig();
}
if (config != null) {
//存在時加載并返回
loadConfiguration(initializationContext, config, logFile);
return;
}
//加載spring默認(rèn)的配置信息
loadDefaults(initializationContext, logFile);
}
/**
* 獲取約定俗成的日志配置資源路徑
*/
protected String getSelfInitializationConfig() {
return findConfig(getStandardConfigLocations());
}
/**
* 獲取spring規(guī)定的配置文件資源路徑
*/
protected String getSpringInitializationConfig() {
return findConfig(getSpringConfigLocations());
}
/**
* 返回spring規(guī)定的配置文件名集合
* 約定俗成規(guī)則如下: 在標(biāo)準(zhǔn)的配置文件名中+ “-spring”
* logback.xml spring約定如下:logback-spring.xml
*/
protected String[] getSpringConfigLocations() {
String[] locations = getStandardConfigLocations();
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;
}
/**
* 在系統(tǒng)classpath下查找是否存在給定的配置名,當(dāng)存在時直接返回,永遠(yuǎn)使用先查到的
*/
private String findConfig(String[] locations) {
for (String location : locations) {
ClassPathResource resource = new ClassPathResource(location,
this.classLoader);
if (resource.exists()) {
return "classpath:" + location;
}
}
return null;
}
// LogbackLoggingSystem
/**
* 返回logback標(biāo)準(zhǔn)的配置文件名稱
*/
protected String[] getStandardConfigLocations() {
//標(biāo)準(zhǔn)的配置文件名稱集合
return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy",
"logback.xml" };
}
/**
* 加載默認(rèn)的配置信息
*/
@Override
protected void loadDefaults(LoggingInitializationContext initializationContext,
LogFile logFile) {
LoggerContext context = getLoggerContext();
stopAndReset(context);
//
LogbackConfigurator configurator = new LogbackConfigurator(context);
Environment environment = initializationContext.getEnvironment();
context.putProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN,
environment.resolvePlaceholders(
"${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}"));
context.putProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN,
environment.resolvePlaceholders(
"${logging.pattern.dateformat:${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}"));
new DefaultLogbackConfiguration(initializationContext, logFile)
.apply(configurator);
context.setPackagingDataEnabled(true);
}
initializeWithConventions按照約定俗成的規(guī)則初始化日志,在initializeWithConventions方法中我們可以看到以下幾步:
- 獲取標(biāo)準(zhǔn)的配置資源路徑,如果存在任何一個,通過此資源配置加載,標(biāo)準(zhǔn)的配置文件名集合如下:
{ "logback-test.groovy", "logback-test.xml", "logback.groovy",
"logback.xml" }
- 獲取spring規(guī)定的配置資源路徑,如果存在任何一個,通過此資源配置加載,spring的規(guī)定規(guī)則如下: 在標(biāo)準(zhǔn)的配置文件名中+ “-spring”,如下:
{ "logback-test-spring.groovy", "logback-test-spring.xml", "logback-spring.groovy",
"logback-spring.xml" }
3.加載spring默認(rèn)的配置信息,具體可看loadDefaults
備注:
- initializeWithConventions 加載配置是分優(yōu)先級的,優(yōu)先級如上1,2,3一致,1的優(yōu)先級最高,當(dāng)存在多個條件并存的話,默認(rèn)使用優(yōu)先級最高的。
- 同時initializeWithConventions 查找配置是否存在都是通過findConfig查找的,此方法只在classpath中查找,不在classpath中的是不會查找的
至此,springboot初始化日志組件的所有流程已根據(jù)源碼跟蹤完畢,在跟蹤源碼時默認(rèn)使用的是logback的實(shí)現(xiàn),Log4J2的實(shí)現(xiàn)與logback的流程近視一致。
順便描述下今天定位的一個測試問題,在測試環(huán)境啟動腳本中配置--logging.config ,而測試環(huán)境是好幾個服務(wù)同時啟動的,使用的是相同的打包流程,相同的腳本,有幾個服務(wù)是正常的,有幾個服務(wù)日志文件是不生成的。搞了好長時間,也因此查看了springboot的日志加載流程,最后發(fā)現(xiàn)原來是啟動主類的啟動方法調(diào)用的不同導(dǎo)致的正常生成日志的啟動類如下:
//將args參數(shù)傳入到SpringApplication,springboot負(fù)責(zé)解析成PropertySource存放到environment中供其它地方使用
SpringApplication.run(GatewayServerApplication.class, args);
不能生成日志文件的啟動類寫法如下:
//未將參數(shù)傳入到SpringApplication處理,導(dǎo)致 在腳本中 --logging.config沒有被解析,日志的初始化,不是預(yù)想的配置初始化的,所以也看不到定義的目錄存在日志。
SpringApplication.run(GatewayServerApplication.class);
不同服務(wù)不同人開發(fā)著,啟動類太簡單,導(dǎo)致問題出現(xiàn)從未想過是這里的問題,排查了好半天,最后發(fā)現(xiàn)竟然這么的尷尬,哎。