首页 > 编程语言 >从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的

从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的

时间:2024-08-22 08:56:14浏览次数:7  
标签:SpringBoot Spring Boot SLF4J 源码 LoggingSystem 日志 logback

开心一刻

今天心情不好,想约哥们喝点

我:心情不好,给你女朋友说一声,来我家,过来喝点

哥们:行!我给她说一声

我:你想吃啥?我点外卖

哥们:你俩定吧,我已经让她过去了

我:???我踏马让你过来!和她说一声

哥们:哈哈哈,我踏马寻思让她过去呢

成功给我逗笑了

前情回顾

SpringBoot2.7 霸王硬上弓 Logback1.3 → 不甜但解渴 实现了 spring-boot 2.x.xlogback 1.3.x 的集成,分两步

  1. 关闭 Spring Boot 的 LoggingSystem
  2. 配置文件用 logback.xml

从示例看,集成是成功的;但有些问题是没有分析的,比如

  1. System.setProperty("org.springframework.boot.logging.LoggingSystem", "none") 是如何生效的
  2. Spring Boot 的 LoggingSystem 是如何与日志组件绑定的
  3. Spring Boot 默认依赖 3 个日志组件:logback、log4j、jul,为什么默认启用的是 logback,而非其它两个?

基于如上 3 个问题,我们一起去翻一翻 Spring Boot 的源码;在看源码之前,我先带大家回顾一些内容,方便下文的源码分析

  1. 设计模式之观察者模式 → 事件机制的底层原理

    讲了观察者模式的实现,以及在 JDK 中的应用(JDK 事件模型)、Spring 中的应用(事件机制);大家可以重点看下 Spring 的那个案例,使用非常简单,总结一句就是

    SpringBoot 启动过程中发送的事件,所有 ApplicationListener 都会收到(即 onApplicationEvent 方法会被调用)

  2. spring-boot-2.0.3启动源码篇一 - SpringApplication构造方法

    大家不要通篇去读,重点看 getSpringFactoriesInstances,与本文息息相关的归纳成一句

    查找类路径下全部的 META-INF/spring.factories 的文件路径,并加载所有 spring.factories 中的内容到 SpringFactoriesLoader 的 cache 中,然后从缓存中获取 ApplicationListener 类型的类并进行实例化

下文是基于 Spring Boot 默认情况下的源码分析,而非集成 logback 1.3.x 的源码分析,大家注意下

集成 logback 1.3.x 需要关闭 Spring Boot 的 LoggingSystem,那还分析个毛

源码分析

问题来了,从哪开始跟?我就不绕圈子了,从 LoggingApplicationListener 开始跟,首先它在 META-INF/spring.factories

spring.factories_LoggingApplicationListener

其次它实现了 ApplicationListener

LoggingApplicationListener类图

那么 Spring Boot 在启动过程中会实例化 LoggingApplicationListener,Spring Boot 启动过程中发送的事件都会来到 LoggingApplicationListeneronApplicationEvent 方法

@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) {
		onContextClosedEvent((ContextClosedEvent) event);
	}
	else if (event instanceof ApplicationFailedEvent) {
		onApplicationFailedEvent();
	}
}

Spring Boot 启动过程分不同的阶段,在每个阶段都会发送对应阶段的事件,LoggingApplicationListener 针对这些事件会有不同的处理,我们暂且只需要关注以下事件

ApplicationStartingEvent,对应的处理方法:onApplicationStartingEvent

ApplicationEnvironmentPreparedEvent,对应的处理方法:onApplicationEnvironmentPreparedEvent

ApplicationPreparedEvent,对应的处理方法:onApplicationPreparedEvent

onApplicationStartingEvent

private void onApplicationStartingEvent(ApplicationStartingEvent event) {
	this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
	this.loggingSystem.beforeInitialize();
}

方法很简单,获取日志系统,然后调用其 beforeInitialize 方法,我们跟进 LoggingSystem.get

public static LoggingSystem get(ClassLoader classLoader) {
	String loggingSystemClassName = System.getProperty(SYSTEM_PROPERTY);
	if (StringUtils.hasLength(loggingSystemClassName)) {
		if (NONE.equals(loggingSystemClassName)) {
			return new NoOpLoggingSystem();
		}
		return get(classLoader, loggingSystemClassName);
	}
	LoggingSystem loggingSystem = SYSTEM_FACTORY.getLoggingSystem(classLoader);
	Assert.state(loggingSystem != null, "No suitable logging system located");
	return loggingSystem;
}

打个断点调试下,你们就会发现 SYSTEM_PROPERTY 的值是 org.springframework.boot.logging.LoggingSystem

system_property

从系统属性中获取 org.springframework.boot.logging.LoggingSystem ,是不是和

System.setProperty("org.springframework.boot.logging.LoggingSystem", "none") 是如何生效的

对应上了?如果获取的值是 none,直接返回 NoOpLoggingSystem 实例

/**
 * {@link LoggingSystem} that does nothing.
 */
static class NoOpLoggingSystem extends LoggingSystem {

	@Override
	public void beforeInitialize() {

	}

	@Override
	public void setLogLevel(String loggerName, LogLevel level) {

	}

	@Override
	public List<LoggerConfiguration> getLoggerConfigurations() {
		return Collections.emptyList();
	}

	@Override
	public LoggerConfiguration getLoggerConfiguration(String loggerName) {
		return null;
	}

}

全是空实现,相当于关闭了 Spring Boot 的 LoggingSystem;org.springframework.boot.logging.LoggingSystem 还可以设置成其他值,但需要有对应的实现。默认情况下 loggingSystemClassName 的值是 null ,会跳过 if 来到 SYSTEM_FACTORY.getLoggingSystem(classLoader);

@Override
public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
	List<LoggingSystemFactory> delegates = (this.delegates != null) ? this.delegates.apply(classLoader) : null;
	if (delegates != null) {
		for (LoggingSystemFactory delegate : delegates) {
			LoggingSystem loggingSystem = delegate.getLoggingSystem(classLoader);
			if (loggingSystem != null) {
				return loggingSystem;
			}
		}
	}
	return null;
}

这里推荐用断点调试去跟源码,按 F7 之后会来到 LoggingSystemFactory#fromSpringFactories

/**
 * Return a {@link LoggingSystemFactory} backed by {@code spring.factories}.
 * @return a {@link LoggingSystemFactory} instance
 */
static LoggingSystemFactory fromSpringFactories() {
	return new DelegatingLoggingSystemFactory(
			(classLoader) -> SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader));
}

SpringFactoriesLoader.loadFactories 是不是很眼熟?(不眼熟的去看:spring-boot-2.0.3启动源码篇一 - SpringApplication构造方法)此时它会做三件事

  1. 从 SpringFactoriesLoader#cache 中获取 LoggingSystemFactory 类型的工厂类的类名列表

    spring.factories_LoggingSystemFactory

    之前已经加载到 SpringFactoriesLoader#cache 中,所以此时从缓存中获取;注意看三个实现类的顺序,LogbackLoggingSystem.Factory 在最前面

  2. 实例化这些工厂类

  3. 对这些工厂类实例按 @Order 升序排序

    这三个工厂类的 @Order 值是一样的,都是 @Order(Ordered.LOWEST_PRECEDENCE),所以顺序不变,LogbackLoggingSystem.Factory 仍在最前面

    LoggingSystemFactory列表

回到 DelegatingLoggingSystemFactory#getLoggingSystem,对这些工厂类实例逐个遍历,得到 LoggingSystem 立即返回,不再遍历后面的工厂实例;第一个遍历的的是 LogbackLoggingSystem.Factory,调用其 getLoggingSystem 方法

private static final boolean PRESENT = ClassUtils.isPresent("ch.qos.logback.classic.LoggerContext",
		Factory.class.getClassLoader());

@Override
public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
	if (PRESENT) {
		return new LogbackLoggingSystem(classLoader);
	}
	return null;
}

ch.qos.logback.classic.LoggerContext 存在(即存在logback依赖),直接创建 LogbackLoggingSystem 实例并返回;至此 Spring Boot 的 LoggingSystem 确定将基于 logback,而非 log4j,也非 jul,问题

Spring Boot 的 LoggingSystem 是如何与日志组件绑定的

Spring Boot 默认依赖 3 个日志组件:logback、log4j、jul,为什么默认启用的是 logback,而非其它两个?

是不是清楚了?LoggingSystem 确定为 LogbackLoggingSystem 后回到 LoggingApplicationListener#onApplicationStartingEvent 方法的第二行,即调用 LogbackLoggingSystem#beforeInitialize 方法

@Override
public void beforeInitialize() {
	LoggerContext loggerContext = getLoggerContext();
	if (isAlreadyInitialized(loggerContext)) {
		return;
	}
	super.beforeInitialize();
	loggerContext.getTurboFilterList().add(FILTER);
}

主要初始化 LoggerContext,跟进 getLoggerContext()

private LoggerContext getLoggerContext() {
	ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();
	Assert.isInstanceOf(LoggerContext.class, factory,
			() -> String.format(
					"LoggerFactory is not a Logback LoggerContext but Logback is on "
							+ "the classpath. Either remove Logback or the competing "
							+ "implementation (%s loaded from %s). If you are using "
							+ "WebLogic you will need to add 'org.slf4j' to "
							+ "prefer-application-packages in WEB-INF/weblogic.xml",
					factory.getClass(), getLocation(factory)));
	return (LoggerContext) factory;
}

StaticLoggerBinder 有没有很熟悉?看下它的全类名:org.slf4j.impl.StaticLoggerBinder,在 logback-classic-1.2.12.jar 下 ,而 logback 1.3.x 没有这个类

所以 spring-boot 2.x.x 默认不支持 logback 1.3.x

总结下,onApplicationStartingEvent 方法确定了日志系统是 LogbackLoggingSystem

onApplicationEnvironmentPreparedEvent

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
	SpringApplication springApplication = event.getSpringApplication();
	if (this.loggingSystem == null) {
		this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());
	}
	initialize(event.getEnvironment(), springApplication.getClassLoader());
}

很显然 loggingSystem 不为 null,我们直接跟 initialize 方法

protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
	getLoggingSystemProperties(environment).apply();
	this.logFile = LogFile.get(environment);
	if (this.logFile != null) {
		this.logFile.applyToSystemProperties();
	}
	// 日志分组,暂不关注
	this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
	// 设置早期日志级别,主要debug和trace之间的抉择
	initializeEarlyLoggingLevel(environment);
	// 初始化日志系统
	initializeSystem(environment, this.loggingSystem, this.logFile);
	// 设置最终日志级别
	initializeFinalLoggingLevels(environment, this.loggingSystem);
	registerShutdownHookIfNecessary(environment, this.loggingSystem);
}

我们暂时只关注 initializeSystem 方法

initializeSystempng

继续往下跟,来到 LogbackLoggingSystem#initialize

LogbackLoggingSystem

继续往下跟,来到 AbstractLoggingSystem#initialize

AbstractLoggingSystem

继续往下跟,来到 AbstractLoggingSystem#initializeWithConventions

private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
	String config = getSelfInitializationConfig();
	if (config != null && logFile == null) {
		// self initialization has occurred, reinitialize in case of property changes
		reinitialize(initializationContext);
		return;
	}
	if (config == null) {
		config = getSpringInitializationConfig();
	}
	if (config != null) {
		loadConfiguration(initializationContext, config, logFile);
		return;
	}
	loadDefaults(initializationContext, logFile);
}

其中 getSelfInitializationConfig() 就是从 classpath 下逐个寻找

logback-test.groovy, logback-test.xml, logback.groovy, logback.xml

这四个文件,一旦找到则直接返回;因为找到了 logback.xml,所以来到第一个 if

initializeWithConventions-reinitialize

继续跟进,来到 LogbackLoggingSystem#reinitialize

LogbackLoggingSystem

logback.xml 中的配置进行加载;至此,Spring Boot 的 LoggingSystem 与 Logback 的绑定就完成了,你们清楚了吗?

我们重新回到 AbstractLoggingSystem#initializeWithConventions ,如果 classpath

logback-test.groovy, logback-test.xml, logback.groovy, logback.xml

这四个文件都没有,会来到 config = getSpringInitializationConfig();,逐步跟下去会来到 AbstractLoggingSystem#getSpringConfigLocations

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;
}

这个方法大家都能看懂吧,locations 的值

logback-test.groovy, logback-test.xml, logback.groovy, logback.xml

逐个遍历,然后进行拼接,最终得到

logback-test-spring.groovy, logback-test-spring.xml, logback-spring.groovy, logback-spring.xml

getSpringConfigLocations

同样从 classpath 下逐个寻找,一旦找到直接返回;这也是为什么我们的日志配置文件是 logback-spring.xml 也能生效的原因。我们可以给 Spring Boot 的日志配置文件排个优先级

logback-test.groovy > logback-test.xml > logback.groovy > logback.xml > logback-test-spring.groovy > logback-test-spring.xml > logback-spring.groovy > logback-spring.xml

总结下,onApplicationEnvironmentPreparedEvent 完成了日志系统的初始化(日志配置文件的加载)

onApplicationPreparedEvent

private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
	ConfigurableApplicationContext applicationContext = event.getApplicationContext();
	ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
	if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
		beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
	}
	if (this.logFile != null && !beanFactory.containsBean(LOG_FILE_BEAN_NAME)) {
		beanFactory.registerSingleton(LOG_FILE_BEAN_NAME, this.logFile);
	}
	if (this.loggerGroups != null && !beanFactory.containsBean(LOGGER_GROUPS_BEAN_NAME)) {
		beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, this.loggerGroups);
	}
	if (!beanFactory.containsBean(LOGGING_LIFECYCLE_BEAN_NAME) && applicationContext.getParent() == null) {
		beanFactory.registerSingleton(LOGGING_LIFECYCLE_BEAN_NAME, new Lifecycle());
	}
}

代码不复杂,就是注册了几个 Bean 到 Spring 容器,其中的 loggingSystem 是我们暂时比较关注的,默认情况下其类型是:LogbackLoggingSystem

日志打印

Spring Boot 的 LoggingSystem 完成与 Logback 的绑定后,它是如何使用然后打印日志的呢?是不是也像

业务日志打印样例

这样来使用的?那绝对不可能的!

绝对不可能

这么使用的话,跟 Spring Boot 的 LoggingSystem 有鸡毛的关系?我们来看下 Spring Boot 中日志的使用,SpringApplication 179 行就用到了

SpringApplicaton179行

我们会发现 LogLogFactoryspring-jcl-5.3.31.jar 包下

spring-jcl_LogFactory

spring-jcl 类似 slf4j,也是一个日志门面,本文不展开

跟进 LogFactory.getLog ,一路跟下去会来到 LogAdapter#createLog

public static Log createLog(String name) {
	switch (logApi) {
		case LOG4J:
			return Log4jAdapter.createLog(name);
		case SLF4J_LAL:
			return Slf4jAdapter.createLocationAwareLog(name);
		case SLF4J:
			return Slf4jAdapter.createLog(name);
		default:
			// Defensively use lazy-initializing adapter class here as well since the
			// java.logging module is not present by default on JDK 9. We are requiring
			// its presence if neither Log4j nor SLF4J is available; however, in the
			// case of Log4j or SLF4J, we are trying to prevent early initialization
			// of the JavaUtilLog adapter - e.g. by a JVM in debug mode - when eagerly
			// trying to parse the bytecode for all the cases of this switch clause.
			return JavaUtilAdapter.createLog(name);
	}
}

logApi 的值获取如下

private static final String LOG4J_SPI = "org.apache.logging.log4j.spi.ExtendedLogger";

private static final String LOG4J_SLF4J_PROVIDER = "org.apache.logging.slf4j.SLF4JProvider";

private static final String SLF4J_SPI = "org.slf4j.spi.LocationAwareLogger";

private static final String SLF4J_API = "org.slf4j.Logger";

private static final LogApi logApi;

static {
	if (isPresent(LOG4J_SPI)) {
		if (isPresent(LOG4J_SLF4J_PROVIDER) && isPresent(SLF4J_SPI)) {
			// log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI;
			// however, we still prefer Log4j over the plain SLF4J API since
			// the latter does not have location awareness support.
			logApi = LogApi.SLF4J_LAL;
		}
		else {
			// Use Log4j 2.x directly, including location awareness support
			logApi = LogApi.LOG4J;
		}
	}
	else if (isPresent(SLF4J_SPI)) {
		// Full SLF4J SPI including location awareness support
		logApi = LogApi.SLF4J_LAL;
	}
	else if (isPresent(SLF4J_API)) {
		// Minimal SLF4J API without location awareness support
		logApi = LogApi.SLF4J;
	}
	else {
		// java.util.logging as default
		logApi = LogApi.JUL;
	}
}

根据优先级逐个去类路径下寻找类,找到了直接返回;Spring Boot 默认情况下用的是 SLF4J + Logback,所以 logApi 的值是 SLF4J_SPI,那么 LogAdapter#createLog 的返回值的类型是 LogAdapter$Slf4jLocationAwareLog

LogAdapter$Slf4jLocationAwareLog

相当于完成了 spring-jclslf4j 的适配;这么说来,Spring Boot 日志还是走的 SLF4J + Logback ?跟 Spring Boot 的 LoggingSystem 有什么关系呢?敬请期待下篇

未完待续

总结

  1. onApplicationStartingEvent

    确定日志系统类型并创建对应的 LoggingSystem,默认情况下是 LogbackLoggingSystem

  2. onApplicationEnvironmentPreparedEvent

    完成日志配置文件的加载以及 LoggingSystem 的初始化

  3. Spring Boot 的日志打印貌似与 LoggingSystem 没有关系?下篇分析

标签:SpringBoot,Spring,Boot,SLF4J,源码,LoggingSystem,日志,logback
From: https://www.cnblogs.com/youzhibing/p/18362863

相关文章

  • MySQL 源码|51 - 语法解析:高级表达式
    目录文档:MySQL源码|源码剖析文档目录源码位置(版本=MySQL8.0.37):sql/sql_yacc.yy前置文档:MySQL源码|50-语法解析:基础表达式(simple_expr)在梳理了基础表达式simple_expr之后,我们就可以梳理更高级的表达式了。高级表达式的相关关系如下图所示:其中绿色节点为本章节梳......
  • MySQL 源码|50 - 语法解析:基础表达式(simple_expr)
    目录文档:MySQL源码|源码剖析文档目录源码位置(版本=MySQL8.0.37):sql/sql_yacc.yy前置文档:MySQL源码|37-语法解析:聚集函数MySQL源码|38-语法解析:窗口函数MySQL源码|43-语法解析:关键字函数MySQL源码|44-语法解析:非关键字函数MySQL源码|45-语法解析:通用函数MySQL......
  • springboot+vue高校多媒体教室管理系统【程序+论文+开题】-计算机毕业设计
    系统程序文件列表开题报告内容研究背景随着信息技术的飞速发展和教育改革的不断深入,高校教学模式正逐步向信息化、智能化转型。多媒体教室作为现代化教学的重要载体,其管理效率与服务质量直接影响到教学活动的开展与教学效果的提升。然而,传统的高校多媒体教室管理方式多依赖......
  • springboot+vue高校多媒体教室管理系统【程序+论文+开题】-计算机毕业设计
    系统程序文件列表开题报告内容研究背景随着信息技术的飞速发展,高校教育日益依赖于多媒体教学手段来提升教学质量与效率。传统的高校教室管理模式面临着诸多挑战,如教室资源分配不均、使用效率低下、信息更新滞后等问题,严重制约了教育资源的优化配置与利用。因此,开发一套高效......
  • springboot+vue葛根庙镇乡村服务小程序【程序+论文+开题】-计算机毕业设计(1)
    系统程序文件列表开题报告内容研究背景随着数字乡村战略的深入实施,农村地区正逐步迈向信息化、智能化的发展道路。葛根庙镇,作为典型的乡村地区,其经济、社会、文化的全面发展离不开信息技术的有力支撑。然而,当前乡村服务信息散乱、获取渠道不畅、农牧技术推广效率低等问题日......
  • springboot+vue高校大学生党员活动管理平台【程序+论文+开题】-计算机毕业设计
    系统程序文件列表开题报告内容研究背景在信息化高速发展的今天,高校党建工作面临着新的机遇与挑战。随着大学生党员数量的不断增加,如何高效地组织、管理和展示党员活动,成为提升党建工作质量、增强党员凝聚力和战斗力的重要课题。传统的管理方式往往依赖于纸质材料和人工统计......
  • springboot+vue高校实习管理系统的设计与实现【程序+论文+开题】-计算机毕业设计
    系统程序文件列表开题报告内容研究背景随着高等教育的普及与深化,实习作为学生理论与实践相结合、提升职业技能的重要途径,其管理效率和质量直接关系到学生综合素质的培养与未来职业发展。然而,传统的高校实习管理模式往往依赖于纸质文档和人工操作,存在信息更新滞后、管理效率......
  • springboot+vue高校摄影网站【程序+论文+开题】-计算机毕业设计
    系统程序文件列表开题报告内容研究背景随着数字技术的飞速发展,摄影艺术已成为高校文化生活中不可或缺的一部分。高校师生对摄影的热爱不仅体现在专业课程的学习上,更渗透于日常记录、艺术创作及文化交流之中。然而,当前高校内缺乏一个集作品展示、学习交流、资讯分享于一体的......
  • 基于SpringBoot+Vue+uniapp的钢材销售管理系统的详细设计和实现(源码+lw+部署文档+讲
    文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我代码参考数据库参考源码获取前言......
  • 基于SpringBoot+Vue+uniapp的大学生二手闲置物品置换交易管理系统的详细设计和实现(源
    文章目录前言详细视频演示具体实现截图技术栈后端框架SpringBoot前端框架Vue持久层框架MyBaitsPlus系统测试系统测试目的系统功能测试系统测试结论为什么选择我代码参考数据库参考源码获取前言......