Spring Boot -日志配置加载流程

描述

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事件
  1. 完成LoggingSystem的创建,根据不同的日志框架获取不同的实现:(logback :LogbackLoggingSystem;log4j2: Log4J2LoggingSystem;javalog:JavaLoggingSystem)
  2. 执行LoggingSystem的beforeInitialize方法完成初始化前置操作
  • ApplicationEnvironmentPreparedEvent事件
  1. 获取LogFile对象: 根据配置logging.file和logging.path的值创建LogFile对象,当这两个值任何一个不存在时LogFile对象为null
  2. 初始化早期的日志等级:initializeEarlyLoggingLevel
  3. 初始化LoggingSystem对象: initializeSystem
  4. 获取自定义日志配置:在key(logging.config)中获取
  5. 根据义日志配置与LogFile对象调用LoggingSystem的initialize方法初始化日志
    6.初始化最终的日志等级给LoggingSystem对象
  6. 注册销毁钩子,添加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主要完成以下功能:

  1. 创建log系统的上下文件环境LoggerContext
  2. 配置日志桥接处理器configureJdkLoggingBridgeHandler
  3. 上下文件环境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主要完成以下功能:

  1. 获取日志上下文环境
  2. KEY(logging.config)值存在时根据此值初始化日志:initializeWithSpecificConfig
  3. KEY(logging.config)值不存在时根据约定俗成规则初始化日志:initializeWithConventions
  4. 移除上下文环境中的FILTER过滤器
  5. 设置当前上下文环境的初始化标识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方法中我们可以看到以下几步:

  1. 获取标准的配置资源路径,如果存在任何一个,通过此资源配置加载,标准的配置文件名集合如下:
 { "logback-test.groovy", "logback-test.xml", "logback.groovy",
                "logback.xml" }
  1. 获取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);

不同服务不同人开发着,启动类太简单,导致问题出现从未想过是这里的问题,排查了好半天,最后发现竟然这么的尴尬,哎。

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

推荐阅读更多精彩内容