描述
Spring Boot在所有内部日志中使用Apache Commons Logging,但是默认配置也提供了对常用日志的支持,如:Java Util Logging,Log4J, Log4J2和Logback。每种Logger都可以通过配置使用控制台或者文件输出日志内容
SpringBoot默认使用的是SLF4J(日志门面)+logback日志实现框架。jul-to-slf4j表示使用slf4j替换掉java.util.logging,log4j-over-slf4j表示使用slf4j替换掉log4j,jcl-over-slf4j表示使用slf4j替换掉java.commongs.logging。这里使用“替换”或许比较难理解,这是slf4j提供的一种桥接模式,对外统一使用slf4j门面。
下面通过代码跟踪spring日志初始化的流程查看spring对日志配置的初始化以及加载
日志初始化入口
springboot 初始化加载日志通过ApplicationListener完成工作的,springboot通过读取spring.factories扩展配置文件,加载定义好的ApplicationListener,默认的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,\
#日志初始化加载,销毁监听器,负责完成日志初始化以及销毁工作
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
在配置中我们看到springboot默认配置了LoggingApplicationListener,springboot就是通过此监听完成日志的加载,初始化,以及销毁等工作的。
- LoggingApplicationListener
public class LoggingApplicationListener implements GenericApplicationListener {
/**
* 日志配置文件在配置环境中的key
* springboot 通过 environment根据此key 查找我们自定义的日志配置根据
*/
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 对不同日志统一封装接口,用于初始化日志接口
*
*/
private LoggingSystem loggingSystem;
private LogLevel springBootLogging = null;
/**
* 监听事件触发执行方法,用于根据不同事件完成不同的操作,此处包含日志初始化的前置操作,日志的初始化操作,日志的销毁清除操作
*/
@Override
public void onApplicationEvent(ApplicationEvent event) {
//系统开始启动事件,LoggingSystem初始化前置操作
if (event instanceof ApplicationStartingEvent) {
onApplicationStartingEvent((ApplicationStartingEvent) event);
}
//环境资源加载完成事件,初始化LoggingSystem
else if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
//Application启动完成事件,将LoggingSystem注册到容器管理维护
else if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent((ApplicationPreparedEvent) event);
}
//容器关闭事件,销毁日志
else if (event instanceof ContextClosedEvent && ((ContextClosedEvent) event)
.getApplicationContext().getParent() == null) {
onContextClosedEvent();
}
//Application启动失败事件,销毁日志
else if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent();
}
}
/**
* 执行LoggingSystem初始化的前置操作
*/
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
//获取LoggingSystem的真实实现,
// 此处会根据不同的日志框架获取不同的实现,
// logback : LogbackLoggingSystem
// log4j2: Log4J2LoggingSystem
// javalog: JavaLoggingSystem
this.loggingSystem = LoggingSystem
.get(event.getSpringApplication().getClassLoader());
//执行beforeInitialize方法完成初始化前置操作
this.loggingSystem.beforeInitialize();
}
/**
* 执行LoggingSystem初始化操作
*/
private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
if (this.loggingSystem == null) {
this.loggingSystem = LoggingSystem
.get(event.getSpringApplication().getClassLoader());
}
//调用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的值创建LogFile对象,当这两个值任何一个不存在时LogFile对象为null
LogFile logFile = LogFile.get(environment);
if (logFile != null) {
//logFile存在时将配置信息设置到System中
logFile.applyToSystemProperties();
}
//初始化早期的日志等级
initializeEarlyLoggingLevel(environment);
//初始化LoggingSystem对象
initializeSystem(environment, this.loggingSystem, logFile);
//注册最终的日志等级
initializeFinalLoggingLevels(environment, this.loggingSystem);
//注册关闭钩子用于系统关闭是销毁
registerShutdownHookIfNecessary(environment, this.loggingSystem);
}
/**
* 初始化LoggingSystem对象
*/
private void initializeSystem(ConfigurableEnvironment environment,
LoggingSystem system, LogFile logFile) {
//构建日志初始化(LoggingSystem)的上下问环境
LoggingInitializationContext initializationContext = new LoggingInitializationContext(
environment);
//获取自定义配置的日志配置文件,在key(logging.config)中获取
String logConfig = environment.getProperty(CONFIG_PROPERTY);
//判断自定义配置是否有效
if (ignoreLogConfig(logConfig)) {
//无效时LoggingSystem的初始化,不根据logging.config初始化日志
system.initialize(initializationContext, null, logFile);
}
else {
try {
//校验logging.config的配置文件是否可读
ResourceUtils.getURL(logConfig).openStream().close();
//根据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 监听不同的事件,根据不同事件对日志做不同的操作:
- ApplicationStartingEvents事件
- 完成LoggingSystem的创建,根据不同的日志框架获取不同的实现:(logback :LogbackLoggingSystem;log4j2: Log4J2LoggingSystem;javalog:JavaLoggingSystem)
- 执行LoggingSystem的beforeInitialize方法完成初始化前置操作
- ApplicationEnvironmentPreparedEvent事件
- 获取LogFile对象: 根据配置logging.file和logging.path的值创建LogFile对象,当这两个值任何一个不存在时LogFile对象为null
- 初始化早期的日志等级:initializeEarlyLoggingLevel
- 初始化LoggingSystem对象: initializeSystem
- 获取自定义日志配置:在key(logging.config)中获取
- 根据义日志配置与LogFile对象调用LoggingSystem的initialize方法初始化日志
6.初始化最终的日志等级给LoggingSystem对象- 注册销毁钩子,添加loggingSystem的销毁
- ApplicationPreparedEvent事件
将LoggingSystem对象注册到IOC容器托管
- ContextClosedEvent与ApplicationFailedEvent事件
清除销毁LoggingSystem对象
通过代码以及上面分析可以看到LoggingApplicationListener主要是完成了LoggingSystem的初始化以及销毁等工作,根据不同的事件。那么日志的初始化主要流程是通过LoggingSystem完成的,下面我们通过源码查看LoggingSystem的初始化工作。
日志系统的核心类初始化
spring实现了多个日志框架的的LoggingSystem默认使用的是LogbackLoggingSystem,下面我们通过分析LogbackLoggingSystem源码来看logbak在初始化前以及初始化是如何加载配置的。
LogbackLoggingSystem继承Slf4JLoggingSystem
Slf4JLoggingSystem 继承AbstractLoggingSystem
初始化前置操作 beforeInitialize
// LogbackLoggingSystem类
@Override
public void beforeInitialize() {
// 创建日志上下文环境
LoggerContext loggerContext = getLoggerContext();
//判断日志是否初始化过,初始化过直接返回
if (isAlreadyInitialized(loggerContext)) {
return;
}
//调用父类的beforeInitialize
super.beforeInitialize();
//上下文环境中添加过滤器
loggerContext.getTurboFilterList().add(FILTER);
}
//父类 Slf4JLoggingSystem
@Override
public void beforeInitialize() {
//继续调用父类的beforeInitialize
super.beforeInitialize();
//配置日志的桥接处理器
configureJdkLoggingBridgeHandler();
}
// 父类 AbstractLoggingSystem
@Override
public void beforeInitialize() {
//空实现
}
beforeInitialize 为日志的初始化前置操作,通过LoggingApplicationListener看到是事件ApplicationStartingEvents触发
beforeInitialize主要完成以下功能:
- 创建log系统的上下文件环境LoggerContext
- 配置日志桥接处理器configureJdkLoggingBridgeHandler
- 上下文件环境LoggerContext添加过滤器 filter
初始化操作 initialize
// LogbackLoggingSystem类
@Override
public void initialize(LoggingInitializationContext initializationContext,
String configLocation, LogFile logFile) {
//获取日志上下文环境
LoggerContext loggerContext = getLoggerContext();
//判断当前上下文环境是否已经初始化过
if (isAlreadyInitialized(loggerContext)) {
return;
}
//调用父类的initialize执行初始化
super.initialize(initializationContext, configLocation, logFile);
// 移除上下文中的过滤器FILTER
loggerContext.getTurboFilterList().remove(FILTER);
//设置当前上下文环境的初始化标识
markAsInitialized(loggerContext);
}
// 父类 AbstractLoggingSystem
@Override
public void initialize(LoggingInitializationContext initializationContext,
String configLocation, LogFile logFile) {
//判断是否存在自定义的日志配置文件路径
if (StringUtils.hasLength(configLocation)) {
//根据自定义的日志配置文件初始化日志
//此处是根据key(logging.config)的值初始化日志
initializeWithSpecificConfig(initializationContext, configLocation, logFile);
return;
}
//根据约定俗成的规则初始化日志
initializeWithConventions(initializationContext, logFile);
}
initialize 为日志的初始化操作,通过LoggingApplicationListener看到是事件ApplicationEnvironmentPreparedEvent触发
initialize主要完成以下功能:
- 获取日志上下文环境
- KEY(logging.config)值存在时根据此值初始化日志:initializeWithSpecificConfig
- KEY(logging.config)值不存在时根据约定俗成规则初始化日志:initializeWithConventions
- 移除上下文环境中的FILTER过滤器
- 设置当前上下文环境的初始化标识markAsInitialized
++备注:++ 初始化操作在initialize方法中出现了两个分支,是根据配置logging.config判断的,当前springboot的environment存在logging.config值是按照配置的日志配置初始化日志系统,否则按照约定俗成的规则初始化日志。
根据自定义配置初始化日志initializeWithSpecificConfig
// AbstractLoggingSystem
private void initializeWithSpecificConfig(
LoggingInitializationContext initializationContext, String configLocation,
LogFile logFile) {
//转换配置自定义配置文件资源路径
configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);
//调用实现类的loadConfiguration完成配置加载
loadConfiguration(initializationContext, configLocation, logFile);
}
// LogbackLoggingSystem 实现类
protected void loadConfiguration(LoggingInitializationContext initializationContext,
String location, LogFile logFile) {
//获取日志上下文件
LoggerContext loggerContext = getLoggerContext();
//设置停止和重置上下文件环境
stopAndReset(loggerContext);
try {
//调用configureByResourceUrl方法加载配置
configureByResourceUrl(initializationContext, loggerContext,
ResourceUtils.getURL(location));
}
catch (Exception ex) {
}
}
/**
* 根据配置资源的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 主要是根据配置logging.config加载日志配置信息,logging.config为系统启动时我们执行的配置信息如:
java -jar xxx.jar --logging.config=/opt/config/log4j2-spring.xml
按照约定俗成的规则初始化日志initializeWithConventions
// AbstractLoggingSystem
private void initializeWithConventions(
LoggingInitializationContext initializationContext, LogFile logFile) {
//获取约定俗成的配置资源路径
String config = getSelfInitializationConfig();
if (config != null && logFile == null) {
//存在时加载资源,并返回
reinitialize(initializationContext);
return;
}
if (config == null) {
//获取spring规定的配置文件资源路径
config = getSpringInitializationConfig();
}
if (config != null) {
//存在时加载并返回
loadConfiguration(initializationContext, config, logFile);
return;
}
//加载spring默认的配置信息
loadDefaults(initializationContext, logFile);
}
/**
* 获取约定俗成的日志配置资源路径
*/
protected String getSelfInitializationConfig() {
return findConfig(getStandardConfigLocations());
}
/**
* 获取spring规定的配置文件资源路径
*/
protected String getSpringInitializationConfig() {
return findConfig(getSpringConfigLocations());
}
/**
* 返回spring规定的配置文件名集合
* 约定俗成规则如下: 在标准的配置文件名中+ “-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;
}
/**
* 在系统classpath下查找是否存在给定的配置名,当存在时直接返回,永远使用先查到的
*/
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标准的配置文件名称
*/
protected String[] getStandardConfigLocations() {
//标准的配置文件名称集合
return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy",
"logback.xml" };
}
/**
* 加载默认的配置信息
*/
@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按照约定俗成的规则初始化日志,在initializeWithConventions方法中我们可以看到以下几步:
- 获取标准的配置资源路径,如果存在任何一个,通过此资源配置加载,标准的配置文件名集合如下:
{ "logback-test.groovy", "logback-test.xml", "logback.groovy",
"logback.xml" }
- 获取spring规定的配置资源路径,如果存在任何一个,通过此资源配置加载,spring的规定规则如下: 在标准的配置文件名中+ “-spring”,如下:
{ "logback-test-spring.groovy", "logback-test-spring.xml", "logback-spring.groovy",
"logback-spring.xml" }
3.加载spring默认的配置信息,具体可看loadDefaults
备注:
- initializeWithConventions 加载配置是分优先级的,优先级如上1,2,3一致,1的优先级最高,当存在多个条件并存的话,默认使用优先级最高的。
- 同时initializeWithConventions 查找配置是否存在都是通过findConfig查找的,此方法只在classpath中查找,不在classpath中的是不会查找的
至此,springboot初始化日志组件的所有流程已根据源码跟踪完毕,在跟踪源码时默认使用的是logback的实现,Log4J2的实现与logback的流程近视一致。
顺便描述下今天定位的一个测试问题,在测试环境启动脚本中配置--logging.config ,而测试环境是好几个服务同时启动的,使用的是相同的打包流程,相同的脚本,有几个服务是正常的,有几个服务日志文件是不生成的。搞了好长时间,也因此查看了springboot的日志加载流程,最后发现原来是启动主类的启动方法调用的不同导致的正常生成日志的启动类如下:
//将args参数传入到SpringApplication,springboot负责解析成PropertySource存放到environment中供其它地方使用
SpringApplication.run(GatewayServerApplication.class, args);
不能生成日志文件的启动类写法如下:
//未将参数传入到SpringApplication处理,导致 在脚本中 --logging.config没有被解析,日志的初始化,不是预想的配置初始化的,所以也看不到定义的目录存在日志。
SpringApplication.run(GatewayServerApplication.class);
不同服务不同人开发着,启动类太简单,导致问题出现从未想过是这里的问题,排查了好半天,最后发现竟然这么的尴尬,哎。