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