logback自定义配置文件遇到的问题和解决方案

需求

项目中多个子项目,每个子项目都有一个logback.xml配置文件,文件内容基本相同,每次修改配置,都要修改多个配置文件,非常麻烦。 所以现在要把该配置文件统一化。

实现

项目中spring boot, 但我们把spring boot项目和业务项目分开了。
在spring boot项目中,我们写好通用的Configuration,并打成jar包,供业务项目使用。
如mybatis, 如果spring boot项目检测到配置文件中定义的dao文件路径,就会创建SqlSessionTemplate,SqlSessionFactory,MapperScannerConfigurer,DataSourceTransactionManager等一系列的配置类。

对于logback,要做到的效果是,如果项目配置了日志配置,使用项目配置,如果没有,就使用默认的配置文件。

在logback配置官方文档中说道,logback会按如下步骤查找配置文件:

  1. 在classpath下查找 logback-test.xml文件
  2. 第一步没找到,则在classpath下查找logback.groovy文件
  3. 第二步没找到,则在classpath下查找logback.xml文件
  4. 第三步没找到,通过spi机制,查找META-INF\services\ch.qos.logback.classic.spi.Configurator 配置的 com.qos.logback.classic.spi.Configurator的实现类,调用他的configure方法进行配置。
  5. 第四步没找到,使用默认的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

解决问题

  1. 如果不需要spring-boot-starter-logging, 可以在maven配置移除,他默认引入到spring-boot-starter项目中,
        
            org.springframework.boot
            spring-boot-starter
            
                
                    org.springframework.boot
                    spring-boot-starter-logging
                
            
        
  1. 回顾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的相关知识,也是收获不少

你可能感兴趣的:(logback自定义配置文件遇到的问题和解决方案)