总所周知,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中日志系统何时被加载,何时被卸载。