SpringBoot源码分析之LOG

x33g5p2x  于2021-12-25 转载在 其他  
字(8.6k)|赞(0)|评价(0)|浏览(429)

突然对SpringBoot如何集成LOG的感兴趣,于是抽空研究了下。这里以 spring-boot-1.5.13.RELEASE.jar 版本为例。

1. 概述

SpringBoot默认使用Logback来作为底层日志支撑,但整个引入和配置过程对于使用者而言,尤其是SpringBoot默认配置已经满足了需求的使用者而言,将是完全透明的;这就导致在遇到相关问题时候,对于那些平时缺少这方面思考的同学来说就只剩下拼浏览器速度这一条路了,毕竟客户可没那么好的耐心等着你现场开始学。SpringBoot在简化了大量配置,减轻了记忆负担的同时,也将问题埋得更深了。

2. 分析

如下的Maven依赖将导致以依赖的形式引入 spring-boot-starter-logging-xxx.jar, 该JAR里面就一个 META-INF/spring.provides文件,内容是provides: logback-classic,jcl-over-slf4j,jul-to-slf4j——用以确保这些JAR的引入。

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
  </dependency>

现在让我们将目光集中到 spring-boot-xxx.jar 的 package - org.springframework.boot.logging中,SpringBoot的包设计不得不表扬一下,真的是相当完美,SpringBoot中与LOG相关的类都在这个package中了。正如笔者的偶像梁飞所描述的“包结构的稳定性,需求的变更应该是导致包中的类一同被修改,而不是这个包里改一点,那个包里也需要改一点”。

以上截图中我们可以看到,默认情况下,SpringBoot是提供了三种实现, JDK,logback,log4j2。因为笔者比较习惯log4j2,所以本次我们关注的中心主要在于SpringBoot默认的logback和log4j2上。

不过在此之前让我们先看看这张图中比较显眼的类:

2.1 ClasspathLoggingApplicationListener

首先,在spring-boot-xxx.jarMETA-INF/spring.factories文件中有如下一行:

这正是ClasspathLoggingApplicationListener 介入到SpringBoot生命周期的关键。

接下来让我们来看其对GenericApplicationListener接口的实现——其实没啥逻辑,就是个报告的功能。

2.2 LoggingApplicationListener

然后就是 LoggingApplicationListener,上面的截图中正好一起截取到了,正好省却了篇幅。

观察这个类里对GenericApplicationListener接口的实现,基本就是本package中核心逻辑所在了。其它类都是为这个类里的逻辑服务的。

// LoggingApplicationListener类
@Override
public void onApplicationEvent(ApplicationEvent event) {
	if (event instanceof ApplicationStartingEvent) {
		onApplicationStartingEvent((ApplicationStartingEvent) event);
	}
	else if (event instanceof ApplicationEnvironmentPreparedEvent) {
		// 我们平时在配置文件中配置的 debug=true 就是在这里生效的
		// logging.config=config/log4j2.xml 也是在这里生效的
		// logging.level.xx=yy 也是这里, 例如logging.level.root=DEBUG
		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();
	}
}

以上监听的五个事件类型中,我们比较关心的应该是ApplicationStartingEventApplicationEnvironmentPreparedEventApplicationPreparedEvent。这三个事件的触发顺序也正是这里列出的顺序——即: 先ApplicationStartingEvent,再ApplicationEnvironmentPreparedEvent,最后是ApplicationPreparedEvent

ApplicationStartingEvent
该事件发生时,LoggingApplicationListener中相应的处理如下:

private void onApplicationStartingEvent(ApplicationStartingEvent event) {
	// 猜测当前最适宜的日志系统
	// 注意这里的LoggingSystem为SpringBoot定义的, 可以在上面的截图中得到确认。
	// 具体的核心逻辑还得去LoggingSystem找寻找,请看下方专门的LoggingSystem小节。
	// 基本逻辑是: 按照logback, log4j2, jdk log 的顺序在classpath下搜寻, 先找到的将胜出。
	this.loggingSystem = LoggingSystem
			.get(event.getSpringApplication().getClassLoader());
	this.loggingSystem.beforeInitialize();
}

ApplicationEnvironmentPreparedEvent
该事件发生时,LoggingApplicationListener中相应的处理如下:

private void onApplicationEnvironmentPreparedEvent(
		ApplicationEnvironmentPreparedEvent event) {
	// 确保loggingSystem初始化完毕
	if (this.loggingSystem == null) {
		this.loggingSystem = LoggingSystem
				.get(event.getSpringApplication().getClassLoader());
	}
	// 实现在下面贴出来
	initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
}

protected void initialize(ConfigurableEnvironment environment,
		ClassLoader classLoader) {
	// 下面这一步完美体现了封装和职责单一的原则——设置Logging相关的系统属性
	// LoggingSystemProperties的访问级别为package, 主要功用是读取用户自定义配置的logging.exception-conversion-word, logging.pattern.console,logging.pattern.file,logging.pattern.level属性值, 并推入到System Property中,以便在相应的配置文件中使用,例如log4j2的`spring-boot-1.5.7.RELEASE.jar!/org/springframework/boot/logging/log4j2/log4j2.xml`中的`${sys:LOG_EXCEPTION_CONVERSION_WORD}`。
	new LoggingSystemProperties(environment).apply();
	// 如果用户设置了logging.file 和 logging.path 配置属性,则进行日志文本化输出。
	LogFile logFile = LogFile.get(environment);
	if (logFile != null) {
		logFile.applyToSystemProperties();
	}
	// 读取用户是否配置了 debug=true或trace=true来查看更详尽的日志, 其实细节略有偏差, 但笔者认为这里只知道成这样反而更好。
	// 这个方法名已经非常清晰地表明了自己所做的事情。 
	initializeEarlyLoggingLevel(environment);
	// 这里会使用用户配置的 logging.config=classpath:config/log4j2.xml 去初始化 日志系统
	initializeSystem(environment, this.loggingSystem, logFile);
	// 依然是方法名解释自身做的事情,设置各个日志Log的级别, 例如 用户自定义配置的 logging.level.root=debug就是在这里生效的
	initializeFinalLoggingLevels(environment, this.loggingSystem);
	// 方法名
	registerShutdownHookIfNecessary(environment, this.loggingSystem);
}

ApplicationPreparedEvent
该事件发生时,LoggingApplicationListener中相应的处理如下:

private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
	// 逻辑很简单, 就是将前面初始化,配置完毕的日志系统注册到Spring容器中, 相应的bean名称为springBootLoggingSystem
	// 好奇心旺盛的可以尝试着从容器里取出来看看
	ConfigurableListableBeanFactory beanFactory = event.getApplicationContext()
			.getBeanFactory();
	if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
		beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
	}
}

3. LoggingSystem

LoggingSystem——顶级基类,名字就能看出是日志系统,SpringBoot用来隔离外界变化用的抽象层,值得念叨下。

首先我们来看看其内部定义的静态构造块:

static {
	// 有序的Map
	Map<String, String> systems = new LinkedHashMap<String, String>();
       // key为各个日志框架的核心类, value则是与之对应的SpringBoot中的配置类
       // 可以看到现在支持java, logback, log4j2
	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, 其是private static final 修饰符, 所以可以放心大胆得只在本类中找其使用范围
       // 最终是在 LoggingSystem.get() 中找到使用, 也只有这一处
	SYSTEMS = Collections.unmodifiableMap(systems);
}

接下来让我们来看看继承链:

可以看到SpringBoot采用了基于SLF4J的方式来统一log4j2和logback。默认提供了JDK,logback和log4j2三种日志实现方式。

最后因为笔者偏好的问题,这里我们来看看Log4j2的实现。

3.1 Log4J2LoggingSystem
  1. 首先此类也是有自己的静态构造块的,逻辑如下:
// 很好的思路,将logj42中的日志级别和SpringBoot自定义的日志级别作了映射,这样就能隔离掉外界日志级别或名称不同对SpringBoot自身造成的影响,又是一次中间层的胜利。
// 不难猜出在LogbackLoggingSystem, JavaLoggingSystem也有着相似的静态代码块。
static {
	// 这里的 LEVELS 为 AbstractLoggingSystem.LogLevels<T>类型, 有相似需求的时候可以借鉴下。
	LEVELS.map(LogLevel.TRACE, Level.TRACE);
	LEVELS.map(LogLevel.DEBUG, Level.DEBUG);
	LEVELS.map(LogLevel.INFO, Level.INFO);
	LEVELS.map(LogLevel.WARN, Level.WARN);
	LEVELS.map(LogLevel.ERROR, Level.ERROR);
	LEVELS.map(LogLevel.FATAL, Level.FATAL);
	LEVELS.map(LogLevel.OFF, Level.OFF);
}
  1. 然后是其实现的getStandardConfigLocations ()方法,
// 这里说明了如果你将自定义的log4j2.xml放到classpath下,SpringBoot将自动读取,不需要你专门去配置以告诉SpringBoot配置文件的位置。
// 当然如果不是classpath下,则需要使用上面的`logging.config=config/log4j2.xml`

@Override
protected String[] getStandardConfigLocations() {
	return getCurrentlySupportedConfigLocations();
}

private String[] getCurrentlySupportedConfigLocations() {
	List<String> supportedConfigLocations = new ArrayList<String>();
	// 经典的判断方式
	if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) {
		Collections.addAll(supportedConfigLocations, "log4j2.yaml", "log4j2.yml");
	}
	if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) {
		Collections.addAll(supportedConfigLocations, "log4j2.json", "log4j2.jsn");
	}
	supportedConfigLocations.add("log4j2.xml");
	return supportedConfigLocations
			.toArray(new String[supportedConfigLocations.size()]);
}

protected boolean isClassAvailable(String className) {
	return ClassUtils.isPresent(className, getClassLoader());
}

4. 常用配置

配置文件 application.properties 中:

# LOGGING
# Location of the logging configuration file. For instance, `classpath:logback.xml` for Logback.
logging.config= 
# Conversion word used when logging exceptions.
logging.exception-conversion-word=%wEx 
# Log file name (for instance, `myapp.log`). Names can be an exact location or relative to the current directory.
logging.file= 
# Maximum of archive log files to keep. Only supported with the default logback setup.
logging.file.max-history=0 
# Maximum log file size. Only supported with the default logback setup.
logging.file.max-size=10MB 
# Log levels severity mapping. For instance, `logging.level.org.springframework=DEBUG`.
logging.level.*= 
# Location of the log file. For instance, `/var/log`.
logging.path= 
# Appender pattern for output to the console. Supported only with the default Logback setup.
logging.pattern.console=
# Appender pattern for log date format. Supported only with the default Logback setup. 
logging.pattern.dateformat=yyyy-MM-dd HH:mm:ss.SSS 
# Appender pattern for output to a file. Supported only with the default Logback setup.
logging.pattern.file= 
# Appender pattern for log level. Supported only with the default Logback setup.
logging.pattern.level=%5p 
# Register a shutdown hook for the logging system when it is initialized.
logging.register-shutdown-hook=false 

# 日志级别
logging.level.root=DEBUG

# 输出到日志文件
logging.file=d:/logs/javastack.log

# 控制框架中的日志级别
logging.level.org.springframework=INFO
logging.level.sun=WARN

5. 启用log4j2

直接看下面给出的链接吧,笔者就不赘述了。

  1. Office Site - Logging
  2. Spring Boot干货系列:(七)默认日志框架配置
  3. Spring Boot自定义log4j2日志文件
  4. Spring Boot 日志详解

相关文章