需求
项目中多个子项目,每个子项目都有一个logback.xml配置文件,文件内容基本相同,每次修改配置,都要修改多个配置文件,非常麻烦。 所以现在要把该配置文件统一化。
实现
项目中spring boot, 但我们把spring boot项目和业务项目分开了。
在spring boot项目中,我们写好通用的Configuration,并打成jar包,供业务项目使用。
如mybatis, 如果spring boot项目检测到配置文件中定义的dao文件路径,就会创建SqlSessionTemplate,SqlSessionFactory,MapperScannerConfigurer,DataSourceTransactionManager等一系列的配置类。
对于logback,要做到的效果是,如果项目配置了日志配置,使用项目配置,如果没有,就使用默认的配置文件。
在logback配置官方文档中说道,logback会按如下步骤查找配置文件:
- 在classpath下查找 logback-test.xml文件
- 第一步没找到,则在classpath下查找logback.groovy文件
- 第二步没找到,则在classpath下查找logback.xml文件
- 第三步没找到,通过spi机制,查找META-INF\services\ch.qos.logback.classic.spi.Configurator 配置的 com.qos.logback.classic.spi.Configurator的实现类,调用他的configure方法进行配置。
- 第四步没找到,使用默认的BasicConfigurator(实现了Configurator接口)进行配置。
既然这样,就在spring boot项目中,resources/META-INF/services/下创建ch.qos.logback.classic.spi.Configurator,内容为
com.spring.boot.config.log.LogDefaultConfigurator
这样spi机制就可以找到我们自定义的LogDefaultConfigurator了。
LogDefaultConfigurator中, 使用logback提供的JoranConfigurator读取文件
package com.spring.boot.config.log;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.classic.spi.Configurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.spi.ContextAwareBase;
import org.slf4j.helpers.Util;
import java.net.URL;
public class LogDefaultConfigurator extends ContextAwareBase implements Configurator {
public void configure(LoggerContext loggerContext) {
this.addInfo("Setting up retail default configuration.");
// 清除loggerContext已加载配置,重新加载
loggerContext.reset();
JoranConfigurator configurator = new JoranConfigurator();
try {
// 获取jar中默认配置文件路径
URL url = Configurator.class.getClassLoader().getResource("logback-default.xml");
// 设置loggerContext到JoranConfigurator
configurator.setContext(loggerContext);
// 加载默认配置
configurator.doConfigure(url);
} catch (JoranException e) {
Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", e);
}
}
}
logback-default.xml就放置在spring boot项目中的classpath中,最后会打到jar中,放到业务项目使用。
(ContextAwareBase是logback提供的,实现了Configurator的一些通用的方法)
这样如果项目中没有logback.xml等配置,logback就可以使用spring boot中的logback-default.xml了。
问题
这时问题来了,logback-default.xml没有生效
是出了什么问题呢
LogDefaultConfigurator配置出错吗
logback加载配置过程在ContextInitializer.autoConfig方法中
public void autoConfig() throws JoranException {
StatusListenerConfigHelper.installIfAsked(this.loggerContext);
// 查找classpath下的logback.groovy/logback-test.xml/logback.xml配置
URL url = this.findURLOfDefaultConfigurationFile(true);
if (url != null) {
// 加载配置
this.configureByResource(url);
} else {
// 通知spi机制,找到用户定义的Configurator类
Configurator c = (Configurator)EnvUtil.loadFromServiceLoader(Configurator.class);
if (c != null) {
try {
// 调用configure方法
c.setContext(this.loggerContext);
c.configure(this.loggerContext);
} catch (Exception var4) {
throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass().getCanonicalName() : "null"), var4);
}
} else {
// 使用默认的BasicConfigurator配置
BasicConfigurator basicConfigurator = new BasicConfigurator();
basicConfigurator.setContext(this.loggerContext);
basicConfigurator.configure(this.loggerContext);
}
}
}
通过debug,可以看到LogDefaultConfigurator是生效了的,Setting up spring boot default configuration.
也输出了
LoggerContext不一致吗
logback加载配置文件,就是把当中配置文件内容放到LoggerContext中, 以便后续使用。
logback的使用
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyApp1 {
final static Logger logger = LoggerFactory.getLogger(MyApp1.class);
}
这里使用了slf4j获取对应的Logger, 获取logger要通过ILoggerFactory
public static Logger getLogger(String name) {
iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
LoggerContext就是实现了ILoggerFactory的类, 会不会slf4j中的LoggerContext和LogDefaultConfigurator中的LoggerContext不同,导致开始加载的配置没有使用到?
slf4j的加载过程
这里来看看slf4j的加载过程
slf4j是典型的桥接模式,他不实现log操作,只是把log操作转发给具体的log框架。 通过slf4j可以使用不同的log实现。
[图片上传失败...(image-eb56ec-1514865832340)]
LoggerFactory.getILoggerFactory可以看到log环境的初始化过程
public static ILoggerFactory getILoggerFactory() {
// 未初始化
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
// 初始化操作
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
// 已成功初始化
case SUCCESSFUL_INITIALIZATION:
return StaticLoggerBinder.getSingleton().getLoggerFactory();
// 异常处理
...
}
有兴趣可以看看初始化过程,这里不深入。主要是获取StaticLoggerBinder类,StaticLoggerBinder类为每个具体的log框架做绑定操作。
这里看一下ch.qos.logback/logback-classic下的StaticLoggerBinder,他负责logback框架环境初始化和绑定
public class StaticLoggerBinder implements LoggerFactoryBinder {
// 创建LoggerContext
private LoggerContext defaultLoggerContext = new LoggerContext();
// 创建SINGLETON对象
private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
// 获取SINGLETON对象
public static StaticLoggerBinder getSingleton() {
return SINGLETON;
}
// 静态代码块中初始化
static {
SINGLETON.init();
}
void init() {
new ContextInitializer(defaultLoggerContext).autoConfig();
}
// 获取LoggerContext
public ILoggerFactory getLoggerFactory() {
return defaultLoggerContext;
}
}
这里可以看到了ContextInitializer.autoConfig方法的执行,也看到了LoggerContext的创建。
回看一下LoggerFactory.getILoggerFactory方法,会发现return StaticLoggerBinder.getSingleton().getLoggerFactory();
,这里可以看到slf4j中的LoggerContext和LogDefaultConfigurator中的LoggerContext是一致的,没有改变过这个对象引用,通过在LogDefaultConfigurator中修改LoggerFactory.name
loggerContext.setName("springbootDefaultConfigContext");
在业务项目中输出LoggerFactory.name
System.out.println(((LoggerContext)LoggerFactory.getILoggerFactory()).getName());
也可以确定这一点。
LoggerContext有没有被修改过
LoggerContext既然是一致的,那么有没有被修改过呢
通过给LoggerContext.reset打断点,终于发现问题所在了。
spring-boot-starter-logging中重新定义了log文件的加载逻辑。
spring-boot-starter-logging
spring boot启动时,会发送ApplicationEvent,由LoggingApplicationListener处理,看看LoggingApplicationListener.initialize
protected void initialize(ConfigurableEnvironment environment,
ClassLoader classLoader) {
new LoggingSystemProperties(environment).apply();
// 获取配置文件中配置的logging.file/logging.path
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);
}
跟踪initializeSysteminitializeSyste方法,会调用的到LogbackLoggingSystem的initialize方法
public void initialize(LoggingInitializationContext initializationContext,
String configLocation, LogFile logFile) {
// 获取LoggerContext
LoggerContext loggerContext = getLoggerContext();
// 如果已initialize,就直接返回
if (isAlreadyInitialized(loggerContext)) {
return;
}
loggerContext.getTurboFilterList().remove(FILTER);
super.initialize(initializationContext, configLocation, logFile);
markAsInitialized(loggerContext);
}
super.initialize
最终会调用到initializeSystem.stopAndReset方法
private void stopAndReset(LoggerContext loggerContext) {
loggerContext.stop();
loggerContext.reset();
if (isBridgeHandlerAvailable()) {
addLevelChangePropagator(loggerContext);
}
}
终于找到问题了,这里reset把LogDefaultConfigurator加载的配置清除了,并加载了默认的BasicConfigurator
解决问题
- 如果不需要spring-boot-starter-logging, 可以在maven配置移除,他默认引入到spring-boot-starter项目中,
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-logging
- 回顾LogbackLoggingSystem.initialize,会通过isAlreadyInitialized是否已初始化,如果是,就直接返回
private boolean isAlreadyInitialized(LoggerContext loggerContext) {
return loggerContext.getObject(LoggingSystem.class.getName()) != null;
}
这样的话,只要添加如下代码就可以了
loggerContext.putObject(LoggingSystem.class.getName(), 1);
目前使用该方案。
解决这个问题,花了不少时间,主要是对spring boot不够熟悉,但也借机了解了slf4j,logback的相关知识,也是收获不少