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项目中,
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
  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的相关知识,也是收获不少

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351