【解决方案】SpringCloud框架下Logback.xml配置springProfile失效

一个会者不难,难者不会的问题。

1. 前言

就一个实际工作中遇到的问题的解决方法,没啥可前言的,直接开整吧。重点是最后面的源码解析部分。

2. 问题复现

问题复现只需要两步:

  1. bootstrap.yml(作为SpringCloud框架下会有一个优先级高于application.yml的默认配置文件)

    spring:
      profiles:
        active: ${
          
          PROFILE:dev}
    
  2. logback-spring.xml配置如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    
      ...... 省略
    
      <springProfile name="dev">
        <!-- 日志输出级别 -->
        <root level="INFO">
          <appender-ref ref="CONSOLE"/>
          <appender-ref ref="ASYNC_FILE_INFO"/>
          <appender-ref ref="ASYNC_FILE_ERROR"/>
        </root>
    
        <!--单独针对每个包或者类分别设置日志级别-->
        <logger name="cn.com.kanq" level="DEBUG"/>
      </springProfile>
    
      ...... 省略
    
    </configuration>
    
    

现在的问题表现就是:

  1. 如果你将profile的设置放在bootstrap.yml中,那么在上述logback-spring.xml配置下,你是看不到任何控制台日志输出的 —— 这逻辑不对啊,但这控制台杂一点反应都没有呢?
  2. 当然你要说我去除掉logback-spring.xml中的<springProfile>配置,那确实是可以的。

3. 解决方案

将以上bootstrap.yml中关于profile的配置,挪到application.yml文件中(如果没有该文件就新建一个)。

4. 原因分析

这才是本文存在的意义。我们需要涉及以下三方面的知识:

  1. SpringCloud的基本启动逻辑。
  2. SpringBoot对于logback框架的扩展。也就是对<springProfile>标签的支持。
  3. logback自身提供的logback.xml配置文件解析扩展。

4.1 SC相关启动逻辑

经过如下图的堆栈追踪,我们找到SpringCloud启动阶段的一个关键类BootstrapApplicationListener
在这里插入图片描述

这个BootstrapApplicationListener类有如下特点:

  1. 该类实现了ApplicationListener<ApplicationEnvironmentPreparedEvent>接口,监听了SpringBoot启动阶段的ApplicationEnvironmentPreparedEvent事件。我们会在接下来的部分对这一实现进行更详细的解读。
  2. 该类注册在spring-cloud-context-x.y.z.RELEASE.jar的配置文件META-INF/spring.factories中。关于这一配置文件,在 SpringBoot源码解析之AutoConfiguration 中有过详细的说明。

接下来让我们看看BootstrapApplicationListener类中的主要逻辑,也就是对ApplicationListener<ApplicationEnvironmentPreparedEvent>接口的实现:

// BootstrapApplicationListener.java
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
    
    
	// 读取配置, 判断是否显式设置不需要启动
	// 注意: 这个 environment  所指向的是初始启动的SpringContext对应的Environment
	ConfigurableEnvironment environment = event.getEnvironment();
	if (!environment.getProperty("spring.cloud.bootstrap.enabled", Boolean.class,
			true)) {
    
    
		return;
	}
	// don't listen to events in a bootstrap context
	if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
    
    
		return;
	}

	ConfigurableApplicationContext context = null;
	// configName默认情况下就是bootstrap, 这也就是我们熟悉的 bootstrap.yaml的由来
	String configName = environment
			.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
	for (ApplicationContextInitializer<?> initializer : event.getSpringApplication()
			.getInitializers()) {
    
    
		if (initializer instanceof ParentContextApplicationContextInitializer) {
    
    
			context = findBootstrapContext(
					(ParentContextApplicationContextInitializer) initializer,
					configName);
		}
	}
	if (context == null) {
    
    
		// 这里注意:
		// bootstrapServiceContext()方法里做了非常重要的两件事:
		// 	1. 正如上面堆栈所显示的, 其内部会再进行一次SpringApplicationBuilder.run(), 最终创建出一个新的ApplicationContext.
		// 	2. 它会将初始启动时创建的SpringContext, 与上一步创建的SpringContext之间建立一个父子关系: 初始化创建的SpringContext为子, 而由bootstrapServiceContext()方法内部创建的SpringContext为父, 这一点一定要注意甄别, 不要搞反. 
		// 3. 两个SpringContext父子关系的建立是在BootstrapApplicationListener.AncestorInitializer中完成的。另外关于这一点的验证你可以随便在自己的SpringCloud项目下创建一个EventListener, 输出`contextRefreshedEvent.getApplicationContext().getParent().getId()` 看返回值是不是`bootstrap`. 
		// 4. 作为返回值的context, 正是bootstrapServiceContext()所创建的 parent SpringContext.
		context = bootstrapServiceContext(environment, event.getSpringApplication(),
				configName);
		event.getSpringApplication()
				.addListeners(new CloseContextOnFailureApplicationListener(context));
	}
	
	// 将bootstrapServiceContext()中启动的SpringContext父容器里的ApplicationContextInitializer实现类, 复制到SpringContext子容器(也就是咱们熟悉的@SpringBootApplication启动的容器)中.
	apply(context, event.getSpringApplication(), environment);
}

以上启动逻辑之下,与咱们本文里的问题有啥关系呢?

  1. 正是因为SpringCloud之下的两次容器初始化启动逻辑,所以其实logback是初始化了两次的。而在BootstrapApplicationListener里启动的那次是可以读取到bootstrap.yml文件里的配置,所以logback-spring.xml里的配置是生效了的,这就解释了为什么启动阶段并不是所有的日志都没打印。
  2. 但作为我们使用@SpringBootApplication启动的容器,在logback框架初始化阶段,因为无法读取到bootstrap.yml里的配置,所以springProfile失效(因为在Environment中没有读取到spring.profiles.active的值)。这里要补充两点:
    a. 所谓"在logback框架初始化阶段",更准确的说应该是SpringBoot对logback的扩展SpringProfileAction类中。更具体的我们放在下面的小节中。
    b. 至于说到的"无法读取到bootstrap.yml里的配置,所以springProfile失效"原因,这个应该是和SpringBoot中生命周期阶段处理有关,笔者并未再作进一步的研究。不过需要专门提醒的是:在@SpringBootApplication启动时,bootstrap.yml中的配置是可以读取到的。因此这里面所谓的"无法读取到bootstrap.yml里的配置"当下只适用于logback框架的初始化过程中

4.2 SpringBoot对logback的扩展

关于SpringBoot对于logback的扩展,这里咱们就不作全面的展开,只列举与本文相关的一些逻辑。

首先是相关的类型:

  1. SpringBootJoranConfigurator
  2. SpringProfileAction

相关的总结如下:

  1. 以上两个类均位于spring-boot-2.x.x.RELEASE.jar中,且均为内部类。所以想要使用IDE智能提示找到这两个类的,建议搜LogbackLoggingSystem
  2. SpringBootJoranConfigurator通过扩展logback解析xml的配置类JoranConfigurator,添加了对于xml节点<springProperty><springProfile>的解析逻辑。其中核心关键是在<springProfile>
	@Override
	public void addInstanceRules(RuleStore rs) {
    
    
		super.addInstanceRules(rs);
		Environment environment = this.initializationContext.getEnvironment();
		rs.addRule(new ElementSelector("configuration/springProperty"), new SpringPropertyAction(environment));
		// 解析<springProfile>节点
		rs.addRule(new ElementSelector("*/springProfile"), new SpringProfileAction(environment));
		// 不解析<springProfile>下的子元素
		rs.addRule(new ElementSelector("*/springProfile/*"), new NOPAction());
	}
  1. SpringProfileAction正是由SpringBootJoranConfigurator来注册,负责解析<springProfile>节点。

    // SpringProfileAction.java
    @Override
    public void begin(InterpretationContext ic, String name, Attributes attributes) throws ActionException {
          
          
    	......
    	
    	//判断当前profile
    	this.acceptsProfile = acceptsProfiles(ic, attributes);
    	
    	......
    }
    
    
    @Override
    public void end(InterpretationContext ic, String name) throws ActionException {
          
          
    	......
    	if (this.acceptsProfile) {
          
          
    		// 
    		addEventsToPlayer(ic);
    	}
    }
    
    private void addEventsToPlayer(InterpretationContext ic) {
          
          
    	Interpreter interpreter = ic.getJoranInterpreter();
    	// 关键就是这两句了, 
    	// 上面logback-spring.xml配置下, <springProfile>最终形成的堆栈结构为: 
    	//		[StartEvent(springProfilename="dev")  [97,29], StartEvent(rootlevel="INFO")  [99,24], StartEvent(appender-refref="CONSOLE")  [100,36],   EndEvent(appender-ref)  [100,36], StartEvent(appender-refref="ASYNC_FILE_INFO")  [101,44],   EndEvent(appender-ref)  [101,44], StartEvent(appender-refref="ASYNC_FILE_ERROR")  [102,45],   EndEvent(appender-ref)  [102,45],   EndEvent(root)  [103,12], StartEvent(loggername="cn.com.kanq" level="DEBUG")  [106,47],   EndEvent(logger)  [106,47],   EndEvent(springProfile)  [107,19]]
    	// 在上述堆栈结构下, 最终负责解析这段xml的将是SpringBootJoranConfigurator中注册的 rs.addRule(new ElementSelector("*/springProfile/*"), new NOPAction());  也就是不解析, 自然也就不会生效了
    	// 但有了下面这两行, 解析工作将转而由 RootLoggerAction 负责.
    	this.events.remove(0);
    	this.events.remove(this.events.size() - 1);
    	interpreter.getEventPlayer().addEventsDynamically(this.events, 1);
    }
    

4.3 logback的机制

对于本文而言,关于logback的应该就是对于其如何实现XML解析的原理解读了。

  1. logback中,对于XML解析,其和 commons-digester 非常类似。这一点可以在其源码中的Interpreter类上的注释可见一斑。
  2. logback中对于logback.xml配置的解析,各个节点所对应的解析类,它们之间的关系维护是在 JoranConfigurator类中完成的。

4. 总结

必须得来一个的话,大概就是:老老实实跟着官方最佳实践来,别图省事踩一堆坑又回到官方最佳实践上来,那不叫"摸石头过河"。

放到本例中就是这个项目一直以来只有一个bootstrap.yml这一个文件,图省事压根没有创建过诸如application.yml配置文件。

补充:这里给出一个明确的建议:除了 spring.cloud开头的配置项,其它的都写在application.yml里。除非你明确知道自己需要这个特性,并且知道SpringCloud两层容器的实现原理。

5. 参考

  1. SpringBoot源码分析之LOG

猜你喜欢

转载自blog.csdn.net/lqzkcx3/article/details/123685250