spring boot:日誌系統源碼深度解析

總所周知,spring boot對各類日誌組件進行了集成,使用起來非常便捷,讓我們需要定義對應日誌框架的配置文件,比如LogBack、Log4j2等,代碼內部便可以直接使用。話不多說,接下來讓我們來領略spring這塊的奧祕吧。

目錄

spring如何集成日誌組件

LoggingSystem

LogFile是什麼

LoggingSystem的實例化

logback.xml的加載優先級

logback-spring.xml的spring環境及變量


spring如何集成日誌組件

猜想肯定是spring應用啓動前已完成log組件的初始化工作?沒錯,spring boot中通過事件驅動,主要是藉助了ApplicationStartingEvent(啓動)以及ApplicationEnvironmentPreparedEvent(配置環境準備)來完成的。

入口在spring的SPI文件,spring-boot-2.1.3.RELEASE.jar/META-INF/spring.factories文件內容:

# 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,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

重點關注LoggingApplicationListener,而ApplicationListener想必就不陌生了,它的的初始化及觸發點在於spring boot的start-class SpringApplication#run

//源於package org.springframework.boot.context.logging;
public class LoggingApplicationListener implements GenericApplicationListener {
	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationStartingEvent) {//先執行
			onApplicationStartingEvent((ApplicationStartingEvent) event);
		}
		else if (event instanceof ApplicationEnvironmentPreparedEvent) {//後執行
			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 onApplicationStartingEvent(ApplicationStartingEvent event) {
		this.loggingSystem = LoggingSystem
				.get(event.getSpringApplication().getClassLoader());//實例化loggingSystem
		this.loggingSystem.beforeInitialize();//loggingSystem初始化操作的前置處理
	}

	private void onApplicationEnvironmentPreparedEvent(
			ApplicationEnvironmentPreparedEvent event) {
		if (this.loggingSystem == null) {
			this.loggingSystem = LoggingSystem
					.get(event.getSpringApplication().getClassLoader());
		}
		initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());//loggingSystem初始化操作
	}
	private void onContextClosedEvent() {
		if (this.loggingSystem != null) {
			this.loggingSystem.cleanUp();
		}
	}

	private void onApplicationFailedEvent() {
		if (this.loggingSystem != null) {
			this.loggingSystem.cleanUp();
		}
	}
	protected void initialize(ConfigurableEnvironment environment,
			ClassLoader classLoader) {
		new LoggingSystemProperties(environment).apply();
		LogFile logFile = LogFile.get(environment);//這個後面講解
		if (logFile != null) {
			logFile.applyToSystemProperties();
		}
		initializeEarlyLoggingLevel(environment);
		initializeSystem(environment, this.loggingSystem, logFile);
		initializeFinalLoggingLevels(environment, this.loggingSystem);
		registerShutdownHookIfNecessary(environment, this.loggingSystem);
	}
}
//源於package org.springframework.boot;
public class SpringApplication {
	public ConfigurableApplicationContext run(String... args) {//這個不陌生吧,spring boot的入口
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();//觸發ApplicationStartingEvent事件
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);//觸發ApplicationPreparedEvent事件
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(
					SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass)
						.logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}    
}    

到這來你基本上已經可以一覽它的原貌,而這裏面的細節(功能特性)會一一展開。

LoggingSystem

LoggingSystem是spring通用日誌組件的抽象,它支持4種類型的日誌:

  • Log:JavaLoggingSystem

  • Log4j:Log4JLoggingSystem

  • Log4j2:Log4J2LoggingSystem

  • Logback:LogbackLoggingSystem

LoggingSystem是個抽象類,內部有這幾個方法:

  • beforeInitialize,日誌系統初始化之前需要處理的事情。抽象方法,不同的日誌架構進行不同的處理

  • initialize,初始化日誌系統

  • cleanUp,日誌系統的清除工作

  • getShutdownHandler,返回一個Runnable用於當jvm退出的時候處理日誌系統關閉後需要進行的操作,默認返回null,也就是什麼都不做

  • setLogLevel,抽象方法,用於設置對應logger的級別

//源於package org.springframework.boot.logging;
public abstract class AbstractLoggingSystem extends LoggingSystem {
	@Override
	public void initialize(LoggingInitializationContext initializationContext,
			String configLocation, LogFile logFile) {
		if (StringUtils.hasLength(configLocation)) {// 如果傳遞了日誌配置文件
			initializeWithSpecificConfig(initializationContext, configLocation, logFile);
			return;
		}
        //加載各種默認配置文件
		initializeWithConventions(initializationContext, logFile);
	}

	private void initializeWithSpecificConfig(
			LoggingInitializationContext initializationContext, String configLocation,
			LogFile logFile) {
        // 處理日誌配置文件中的佔位符
		configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);
		loadConfiguration(initializationContext, configLocation, logFile);
	}   
	private void initializeWithConventions(
			LoggingInitializationContext initializationContext, LogFile logFile) {
        //加載classpath下的默認的配置文件
		String config = getSelfInitializationConfig();
		if (config != null && logFile == null) {
			// self initialization has occurred, reinitialize in case of property changes
			reinitialize(initializationContext);
			return;
		}
		if (config == null) {//加載classpath下的默認的配置文件(僅包含spring)
			config = getSpringInitializationConfig();
		}
		if (config != null) {
			loadConfiguration(initializationContext, config, logFile);
			return;
		}
        // 還是沒找到日誌配置文件的話,調用loadDefaults抽象方法加載,讓子類實現
		loadDefaults(initializationContext, logFile);
	}
	protected String getSelfInitializationConfig() {
		return findConfig(getStandardConfigLocations());
	}
	protected String getSpringInitializationConfig() {
		return findConfig(getSpringConfigLocations());
	}
	//findConfig有個特點就是找到第一個存在的立即返回
	private String findConfig(String[] locations) {
		for (String location : locations) {
			ClassPathResource resource = new ClassPathResource(location,
					this.classLoader);
			if (resource.exists()) {
				return "classpath:" + location;
			}
		}
		return null;
	}
    //將默認的配置文件中替換成-spring的配置文件
	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;
	}
	protected abstract String[] getStandardConfigLocations();    
}
//源自package org.springframework.boot.logging;
public abstract class Slf4JLoggingSystem extends AbstractLoggingSystem {
}
public class LogbackLoggingSystem extends Slf4JLoggingSystem {
	@Override
	protected String[] getStandardConfigLocations() {
		return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy",
				"logback.xml" };
	}
}    

LogFile是什麼

在spring yml配置中提供了logging.file和logging.path的配置,而它正是作用於LogFile。

//源自package org.springframework.boot.logging;
public class LogFile {
	LogFile(String file, String path) {
		Assert.isTrue(StringUtils.hasLength(file) || StringUtils.hasLength(path),
				"File or Path must not be empty");
		this.file = file;
		this.path = path;
	}
	@Override
	public String toString() {
		if (StringUtils.hasLength(this.file)) {
			return this.file;
		}
		String path = this.path;
		if (!path.endsWith("/")) {
			path = path + "/";
		}
		return StringUtils.applyRelativePath(path, "spring.log");
	}
	public static LogFile get(PropertyResolver propertyResolver) {
		String file = propertyResolver.getProperty(FILE_PROPERTY);
		String path = propertyResolver.getProperty(PATH_PROPERTY);
		if (StringUtils.hasLength(file) || StringUtils.hasLength(path)) {
			return new LogFile(file, path);
		}
		return null;
	}
}    

這個配置導致了調用initialize方法的時候logFile存在,這樣不止有ConsoleAppender,還有一個FileAppender,這個FileAppender對應的文件就是LogFile文件,也就是logging.file配置的日誌文件。

從上面代碼實現可以看出,我們如果配置了logging.path和logging.file,那麼生效的只有logging.file配置。

其實個人覺得LogFile沒啥用,你會脫落開源日誌組件的控制。

LoggingSystem的實例化

LoggingSystem被實例化那個,這個很多人講的不太對,默認它取得順序是LogbackLoggingSystem>Log4J2LoggingSystem>JavaLoggingSystem,如果類存在就選擇。

//源自package org.springframework.boot.logging;
public abstract class LoggingSystem {
	static {
		Map<String, String> systems = new LinkedHashMap<>();
		systems.put("ch.qos.logback.core.Appender",
				"org.springframework.boot.logging.logback.LogbackLoggingSystem");
		systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
				"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
		systems.put("java.util.logging.LogManager",
				"org.springframework.boot.logging.java.JavaLoggingSystem");
		SYSTEMS = Collections.unmodifiableMap(systems);
	}
	public static LoggingSystem get(ClassLoader classLoader) {
		String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
		if (StringUtils.hasLength(loggingSystem)) {//你也通過org.springframework.boot.logging.LoggingSystem來特殊指定
			if (NONE.equals(loggingSystem)) {
				return new NoOpLoggingSystem();
			}
			return get(classLoader, loggingSystem);
		}
		return SYSTEMS.entrySet().stream()
				.filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
				.map((entry) -> get(classLoader, entry.getValue())).findFirst()
				.orElseThrow(() -> new IllegalStateException(
						"No suitable logging system located"));//取第一個
	}    
}    

logback.xml的加載優先級

其實從上面的代碼AbstractLoggingSystem#initializeWithConventions中已經可以看出,它默認的加載順序爲:

  • logback-test.groovy

  • logback-test.xml

  • logback.groovy

  • logback.xml

  • logback-test-spring.groovy

  • logback-test-spring.xml

  • logback-spring.groovy

  • logback-spring.xml

logback-spring.xml的spring環境及變量

很多使用logback-spring.xml的同學大多比較在意可以使用<springProperty/>和<springProfile/>

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <property name="LOG_HOME" value="/log"/>
    <conversionRule conversionWord="ipandhostname" converterClass="com.yonghui.logback.IpConvert"/>

    <!--
        1. 文件的命名和加載順序
           logback.xml早於application.yml加載,logback-spring.xml晚於application.yml加載
           如果logback配置需要使用application.yml中的屬性,需要命名爲logback-spring.xml
        2. logback使用application.yml中的屬性
           使用springProperty纔可使用application.yml中的值 可以設置默認值
    -->
    <springProperty scope="context" name="projectName" source="spring.project.name"/>
    <springProperty scope="context" name="appName" source="spring.application.name"/>
    <springProperty scope="context" name="appDev" source="spring.profiles.active"/>
    <springProperty scope="context" name="kafkaTopic" source="logback.kafka.topic"/>
    <springProperty scope="context" name="kafkaServers" source="logback.kafka.servers"/>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                <!-- 時間|環境 |項目名稱 |應用名稱|錯誤級別|ip|hostname|[%thread]| %logger{50}| %msg%n -->
                <pattern>%d{yyyy-MM-dd HH:mm:ss SSS}|${appDev}|${projectName}|${appName}|%-5level|%ipandhostname|[%thread]|%logger{50}|%tid|%msg%n
                </pattern>
            </layout>
        </encoder>
    </appender>
   <logger name="org.apache.kafka.clients.NetworkClient" level="error"/>
   <logger name="c.c.f.apollo.internals.RemoteConfigLongPollService" level="error"/>

    <springProfile name="SIT">
        <root level="info">
            <appender-ref ref="STDOUT"/>
            <appender-ref ref="ASYNC"/>
        </root>
    </springProfile>

    <springProfile name="UAT">
        <root level="info">
            <appender-ref ref="STDOUT"/>
            <appender-ref ref="ASYNC"/>
        </root>
    </springProfile>

    <springProfile name="PRO">
        <root level="info">
            <appender-ref ref="ASYNC"/>
        </root>
    </springProfile>
</configuration>    

它的底層實現其實也不復雜, 如果配置文件是xml,解析時藉助日誌組件的攔截(解析spring環境信息)

//源自package org.springframework.boot.logging.logback;
public class LogbackLoggingSystem extends Slf4JLoggingSystem {
	//AbstractLoggingSystem#initializeWithConventions中被調用(config找到第一件事就是幹這個) 
	@Override
	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);
		}
		List<Status> statuses = loggerContext.getStatusManager().getCopyOfStatusList();
		StringBuilder errors = new StringBuilder();
		for (Status status : statuses) {
			if (status.getLevel() == Status.ERROR) {
				errors.append((errors.length() > 0) ? String.format("%n") : "");
				errors.append(status.toString());
			}
		}
		if (errors.length() > 0) {
			throw new IllegalStateException(
					String.format("Logback configuration error detected: %n%s", errors));
		}
	}

	private void configureByResourceUrl(
			LoggingInitializationContext initializationContext,
			LoggerContext loggerContext, URL url) throws JoranException {
		if (url.toString().endsWith("xml")) {//如果是xml,加載spring環境
			JoranConfigurator configurator = new SpringBootJoranConfigurator(
					initializationContext);
			configurator.setContext(loggerContext);
			configurator.doConfigure(url);//觸發的GenericConfigurator.doConfigure(通過攔截器解析xml部分spring配置)
		}
		else {
			new ContextInitializer(loggerContext).configureByResource(url);
		}
	}    
}
//源自package org.springframework.boot.logging.logback;
class SpringBootJoranConfigurator extends JoranConfigurator {

	private LoggingInitializationContext initializationContext;

	SpringBootJoranConfigurator(LoggingInitializationContext initializationContext) {
		this.initializationContext = initializationContext;
	}

	@Override
	public void addInstanceRules(RuleStore rs) {
		super.addInstanceRules(rs);
		Environment environment = this.initializationContext.getEnvironment();
		rs.addRule(new ElementSelector("configuration/springProperty"),
				new SpringPropertyAction(environment));
		rs.addRule(new ElementSelector("*/springProfile"),
				new SpringProfileAction(environment));
		rs.addRule(new ElementSelector("*/springProfile/*"), new NOPAction());
	}

}

至此,你可能再也不會糾結於spring中日誌系統何時被加載,何時被卸載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章