文章目录一个会者不难,难者不会的问题。
1. 前言2. 问题复现3. 解决方案4. 原因分析
4.1 SC相关启动逻辑4.2 SpringBoot对logback的扩展4.3 logback的机制 4. 总结5. 参考
1. 前言就一个实际工作中遇到的问题的解决方法,没啥可前言的,直接开整吧。重点是最后面的源码解析部分。
2. 问题复现问题复现只需要两步:
bootstrap.yml(作为SpringCloud框架下会有一个优先级高于application.yml的默认配置文件)
spring:
profiles:
active: ${PROFILE:dev}
logback-spring.xml配置如下:
...... 省略 ...... 省略
现在的问题表现就是:
- 如果你将profile的设置放在bootstrap.yml中,那么在上述logback-spring.xml配置下,你是看不到任何控制台日志输出的 —— 这逻辑不对啊,但这控制台杂一点反应都没有呢?当然你要说我去除掉logback-spring.xml中的
将以上bootstrap.yml中关于profile的配置,挪到application.yml文件中(如果没有该文件就新建一个)。
4. 原因分析这才是本文存在的意义。我们需要涉及以下三方面的知识:
- SpringCloud的基本启动逻辑。SpringBoot对于logback框架的扩展。也就是对
经过如下图的堆栈追踪,我们找到SpringCloud启动阶段的一个关键类BootstrapApplicationListener。
这个BootstrapApplicationListener类有如下特点:
- 该类实现了ApplicationListener接口,监听了SpringBoot启动阶段的ApplicationEnvironmentPreparedEvent事件。我们会在接下来的部分对这一实现进行更详细的解读。该类注册在spring-cloud-context-x.y.z.RELEASE.jar的配置文件meta-INF/spring.factories中。关于这一配置文件,在 SpringBoot源码解析之AutoConfiguration 中有过详细的说明。
接下来让我们看看BootstrapApplicationListener类中的主要逻辑,也就是对ApplicationListener接口的实现:
// 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);
}
以上启动逻辑之下,与咱们本文里的问题有啥关系呢?
- 正是因为SpringCloud之下的两次容器初始化启动逻辑,所以其实logback是初始化了两次的。而在BootstrapApplicationListener里启动的那次是可以读取到bootstrap.yml文件里的配置,所以logback-spring.xml里的配置是生效了的,这就解释了为什么启动阶段并不是所有的日志都没打印。但作为我们使用@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框架的初始化过程中。
关于SpringBoot对于logback的扩展,这里咱们就不作全面的展开,只列举与本文相关的一些逻辑。
首先是相关的类型:
- SpringBootJoranConfiguratorSpringProfileAction
相关的总结如下:
- 以上两个类均位于spring-boot-2.x.x.RELEASE.jar中,且均为内部类。所以想要使用IDE智能提示找到这两个类的,建议搜LogbackLoggingSystem。SpringBootJoranConfigurator通过扩展logback解析xml的配置类JoranConfigurator,添加了对于xml节点
@Override
public void addInstanceRules(RuleStore rs) {
super.addInstanceRules(rs);
Environment environment = this.initializationContext.getEnvironment();
rs.addRule(new ElementSelector("configuration/springProperty"), new SpringPropertyAction(environment));
// 解析节点
rs.addRule(new ElementSelector("*/springProfile"), new SpringProfileAction(environment));
// 不解析下的子元素
rs.addRule(new ElementSelector("*/springProfilespringProfile/*"), 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解析的原理解读了。
- logback中,对于XML解析,其和 commons-digester 非常类似。这一点可以在其源码中的Interpreter类上的注释可见一斑。logback中对于logback.xml配置的解析,各个节点所对应的解析类,它们之间的关系维护是在 JoranConfigurator类中完成的。
必须得来一个的话,大概就是:老老实实跟着官方最佳实践来,别图省事踩一堆坑又回到官方最佳实践上来,那不叫"摸石头过河"。
放到本例中就是这个项目一直以来只有一个bootstrap.yml这一个文件,图省事压根没有创建过诸如application.yml配置文件。
5. 参考- SpringBoot源码分析之LOG



