背景
最近在群里看到了这样的两个问题:
- 想通过继承 ZuulProxyAutoConfiguration 的方式实现一些自定义处理,但是项目却启动失败,子类代码和报错信息如下:
@Configuration
public class ZuulConfiguration extends ZuulProxyAutoConfiguration {
public ZuulConfiguration(MeterRegistry meterRegistry) {
// ......
}
}
No qualifying bean of type 'com.netflix.zuul.monitoring.CounterFactory' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
ZuulServerAutoConfiguration.ZuulCounterFactoryConfiguration#counterFactory:
Did not match:
- @ConditionalOnBean (types: io.micrometer.core.instrument.MeterRegistry; SearchStrategy: all) did not find any beans of tyio.micrometer.core.instrument.MeterRegistry (OnBeanCondition)
ZuulServerAutoConfiguration.ZuulMetricsConfiguration#counterFactory:
Did not match:
- @ConditionalOnMissingClass found unwanted class 'io.micrometer.core.instrument.MeterRegistry' (OnClassCondition)
- 项目中的 RedissonLock 加锁失败,通过打印日志发现切面 RedissonLockAspect 没有成功加载到 Spring 容器中,切面代码和日志信息如下:
@Aspect
@Component
@ConditionalOnBean(RedissonClient.class)
class RedissonLockAspect implements Ordered {
private final RedissonClient redissonClient;
public RedissonLockAspect(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
@Around("execution(* ai.advance..*(..)) && @annotation(ai.advance.lock.RedissonLock)")
Object lock(ProceedingJoinPoint point) throws Throwable {
// ......
}
}
RedissonLockAspect:
Did not match:
- @ConditionalOnBean (types: org.redisson.api.RedissonClient; SearchStrategy: all) did not find any beans of type org.redisson.api.RedissonClient (OnBeanCondition)
通过日志可以很明显的发现 ZuulServerAutoConfiguration.ZuulCounterFactoryConfiguration 和 RedissonLockAspect 因为不满足注册成 bean 的前置条件,即容器中没有类型为 MeterRegistry 和 RedissonClient 的 bean, 从而导致其本身未能成功加载到 Spring 容器中。但是通过 debug 却发现 registry.beanDefinitionMap 里面是存在 simpleMeterRegistry 和 redissonClient 两个 bean 的,所以猜测是因为 Spring 容器加载 bean 的前后顺序导致的该问题,通过进一步的 debug 容器启动过程也验证了这一猜想。
写这篇文章的目的,是通过分析 Spring 源码总结一下容器加载 bean 的逻辑究竟是什么样的,因为 Spring 启动过程中所做的事情很多,为了避免喧宾夺主,此文章仅着重描述与 bean 加载有关的源码。依赖版本如下:
- org.springframework.boot:spring-boot-starter-web:2.3.10.RELEASE
- org.springframework.cloud:spring-cloud-starter-netflix-zuul:2.2.8.RELEASE
源码分析
1. 项目启动类
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
我相信大家对上面这段代码再熟悉不过了,这是我们启动 Spring Boot 项目最常见的方式,我们先来大致看一下这段代码背后究竟发生了什么。
- SpringApplication 的构造方法
public SpringApplication(ResourceLoader resourceLoader, Class>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 通过分析字节码确定应用类型:REACTIVE/SERVLET/NONE
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// ApplicationContextInitializer 是 Spring 容器刷新之前的回调接口,允许我们在 refresh ApplicationContext 之前做很多的处理
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// ApplicationListener 是 Spring 事件机制中的监听器,加载监听器以处理启动过程中的各个事件
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 通过调用栈信息找出 main 方法所在的类,从而确定启动类
this.mainApplicationClass = deduceMainApplicationClass();
}
- 再看一下 run 方法所做的事情
public ConfigurableApplicationContext run(String... args) {
// ......
// 通过 spring.factories 机制加载 SpringApplicationRunListener 的实现类,用于启动过程中的事件发送
SpringApplicationRunListeners listeners = getRunListeners(args);
// 发送事件 ApplicationStartingEvent
listeners.starting();
try {
// 处理启动参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 构建运行环境
// 在该过程中通过监听器完成了 bootstrap.properties, application.yml 等配置文件的加载
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 创建容器
// 会向容器注册 ConfigurationClassPostProcessor 这个极其重要的 bean,它将会承担注册 bean 和完成自动配置的职责,也是这篇文章的重点
context = createApplicationContext();
// 准备容器
// 进行容器刷新之前的准备工作,包括将启动类封装成 bean 注册到容器中
// 之前我们提到的 ApplicationContextInitializer 回调接口的处理也是在该过程中完成的
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 刷新容器,完成了其余的 bean 注册和自动配置过程
refreshContext(context);
// 刷新容器之后的扩展接口,供开发者实现自定义处理,默认为空
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 发送事件 ApplicationStartedEvent
listeners.started(context);
callRunners(context, applicationArguments);
}
// ......
}
getSpringFactoriesInstances
上面的代码中有一个很重要的方法 getSpringFactoriesInstances, 在 Spring 源码中存在着很多类似的方法,他们最终会调用 SpringFactoriesLoader.loadFactoryNames, 其作用是通过 classLoader 读取 META-INF/spring.factories 文件来查找指定接口的实现类。
META-INF/spring.factories 文件以 key-value 的形式定义了一系列接口与实现类的对应关系。上面的代码正是利用 spring.factories 机制加载了 ApplicationContextInitializer 和 ApplicationListener 的实现类,从而实现在启动过程中的更多功能。getRunListener
getRunListener 方法再次利用 spring.factories 机制加载了 SpringApplicationRunListener 的实现类 EventPublishingRunListener。该类是 Spring 中第一个监听器,其内部封装了 ApplicationEventMulticaster 事件广播器。
EventPublishingRunListener 在收到事件后通过其内部的广播器将事件传递下去,交给 ApplicationEventMulticaster 内部封装的各个 ApplicationListener 去执行各自的逻辑。比如 BootstrapApplicationListener 在监听到 ApplicationEnvironmentPreparedEvent 事件后会初始化 parent 上下文并加载 bootstrap.properties 文件;ConfigFileApplicationListener 在监听到 ApplicationEnvironmentPreparedEvent 事件后,会读取 application.yml/application.properties 等文件以加载配置信息。createApplicationContext
createApplicationContext 方法会通过 webApplicationType=SERVLET 确定 ApplicationContext 的具体类型为 AnnotationConfigServletWebServerApplicationContext,然后再实例化该类。调用的构造方法如下:
public AnnotationConfigServletWebServerApplicationContext() {
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
跟进 AnnotatedBeanDefinitionReader 的构造方法最终会调用 AnnotationConfigUtils.registerAnnotationConfigProcessors 方法,这个方法会将一些 processor 注册到 Spring 容器中, 部分代码如下:
public static Set registerAnnotationConfigProcessors(BeanDefinitionRegistry registry, @Nullable Object source) {
// ......
Set beanDefs = new LinkedHashSet<>(8);
if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
}
// ......
}
可以看到 ConfigurationClassPostProcessor 便是在 createApplicationContext 过程中注册到 Spring 容器中的,该类也是 Spring 完成自动配置的核心,后面会着重介绍。
-
refreshContext
先来看下 refreshContext 方法后面的调用栈:
这里最终将所有的 BeanFactoryPostProcessor 委托给了 PostProcessorRegistrationDelegate 类,由其调用 BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry 和 postProcessBeanFactory 方法,以及 BeanFactoryPostProcessor 的 postProcessBeanFactory 方法,完成所有的后置处理逻辑,其中就包括了完成 bean 注册和自动配置的 ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry 方法。
2. ConfigurationClassPostProcessor
-
postProcessBeanDefinitionRegistry
我总结了一下 postProcessBeanDefinitionRegistry 后面的方法调用示意图,看下整个流程经过是什么样的,也便于后面的代码介绍:
processConfigBeanDefinitions
先来看一下 processConfigBeanDefinitions 的主要逻辑:
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
// 找出已经注册到容器的 configuration, 也就是启动类
List configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}
// ......
// 创建 ConfigurationClassParser
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
do {
// parse configuration 过程
// 扫面项目中定义的以及引入的依赖包中的 configuration class, 将其保存到 parser.configurationClasses 中
parser.parse(candidates);
// ......
// register bean 过程
// 遍历 parser.configurationClasses 完成 bean 的注册,即添加到 registry.beanDefinitionMap 中
this.reader.loadBeanDefinitions(configClasses);
// ......
}while (!candidates.isEmpty());
// ......
}
- 在上面的代码注释中反复提到了一个数据结构 parser.configurationClasses, 这是在 PARSE_CONFIGURATION 过程中用于存储 configuration class 的,其类型是有序的 LinkedHashMap, 所以在后面的 REGISTER_BEAN 过程中遍历是按照插入顺序一一处理 configuration class 的,这也就近似代表了 bean 注册的顺序,下面我们会重点关注 parser.configurationClasses 的插入顺序。
- 提到的另外一个数据结构是 registry.beanDefinitionMap, 这是我们常说的容器,bean 注册其实就是将 bean 存储到该 Map 中。
- 上面的代码被简单的分为 PARSE_CONFIGURATION 和 REGISTER_BEAN 两个阶段,后面会解释在不同阶段 @ConditionalOnBean/@ConditionOnClass 等注解的不同处理。
3. ConfigurationClassParser
- 跟进 ConfigurationClassParser.parse 方法会到 ConfigurationClassParser.processConfigurationClass 方法,主要逻辑如下:
protected void processConfigurationClass(ConfigurationClass configClass, Predicate filter) throws IOException {
// 校验是否需要跳过该类
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}
// ......
// 此处循环是为了处理该类及其父类
SourceClass sourceClass = asSourceClass(configClass, filter);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);
// configurationClasses 的定义 Map configurationClasses = new LinkedHashMap<>()
this.configurationClasses.put(configClass, configClass);
}
- ConditionEvaluator.shouldSkip 负责判断是否需要跳过该类,其中逻辑如下:
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
// 先会判断该类是否被 Conditional 注解,如果没有此注解那肯定不需要跳过
if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
return false;
}
// ......
// 如果有该注解则会对注解条件进行校验,校验包括两部分:
// 一是注解所需的 ConfigurationPhase 与目前正在进行的阶段是否一致
// 二是所需条件是否满足
for (Condition condition : conditions) {
ConfigurationPhase requiredPhase = null;
if (condition instanceof ConfigurationCondition) {
requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
}
if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
return true;
}
}
return false;
}
我们先来看一下 ConditionalOnClass 的源码
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
// ......
}
再跟进 OnClassCondition 类发现并没有定义 ConfigurationPhase,根据 shouldSkip 方法的逻辑,如果 requiredPhase == null 是要做条件检验的,所以该注解在任何阶段都是生效的。再来看一下 ConditionalOnBean 的源码
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {
// ......
}
再跟进 OnBeanCondition 类
@Order(Ordered.LOWEST_PRECEDENCE)
class OnBeanCondition extends FilteringSpringBootCondition implements ConfigurationCondition {
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
// ......
}
可以看到 OnBeanCondition 的 ConfigurationPhase 是 REGISTER_BEAN,也就是说在注册 bean 的阶段该注解才生效。而 ConfigurationClassParser.parse 所处的是 PARSE_CONFIGURATION 阶段,因为阶段不同所以可以忽略该条件。既然 ConditionalOnBean 到目前为止是不生效的,那文章开头的两个问题的答案显然不在这里了,我们先往后看。
- doProcessConfigurationClass
接下来是 ConfigurationClassParser 的核心方法 doProcessConfigurationClass,该方法的代码逻辑涉及到递归调用,为了更好的解释,我们借助一下实际场景来模拟下该方法的执行过程。假设项目中存在以下的类文件:
@Configuration
public class MyOuter {
@Configuration
class MyInnerComponent {
}
@Configuration
static class MyInnerStaticComponent {
}
}
@Configuration
public class UserConfiguration {
@Bean
public User initUser() {
}
}
@Configuration
public class ZuulConfiguration extends ZuulProxyAutoConfiguration {
}
@EnableZuulProxy
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
可以结合着上面的方法调用示意图来理解下面的步骤:
- 在第一次调用 configurationClassParser.doProcessConfigurationClass 方法时 configClass 是项目启动类 MainApplication, 因为 @SpringBootApplication 注解的存在,启动类上是存在 @ComponentScan 注解的,所以会触发 processComponentScan 逻辑。
- 在处理 @ComponentScan 的逻辑中,先会调用 componentScanParser.parse 方法,该方法通过 classPathBeanDefinitionScanner.doScan 方法扫描所有 basePackage 下的 class 文件。在该过程中会扫描每一级目录及其子目录,将同一级目录下的文件和文件夹按照字母升序排列,然后通过 excludeFilter 和 includeFilter 对 class 文件进行过滤,仅保留 independency configuration class, 即 top-class 和 static-inner-class. 在该模拟场景下,会返回 {MyOuter$MyInnerStaticComponent.class, MyOuter.class, UserConfiguration.class, ZuulConfiguration.class} 这样的列表。
- componentScanParser.parse 方法会将得到的 independency configuration class 注册到 Spring 容器中,即添加到 registry.beanDefinitionMap 中,然后按顺序返回 bean 列表。
- configurationClassParser.doProcessConfigurationClass 方法会遍历 componentScanParser.parse 返回的 bean 列表并递归调用 configurationClassParser.processConfigurationClass 方法,递归调用过程是下面的步骤 5-8。
- 第一个要处理的是 MyOuter$MyInnerStaticComponent.class, 因为该类极其简单,不再赘述过程,最后会直接将其保存到 parser.configurationClasses 中。
- 第二个是 MyOuter.class, 因为该类含有内部类,所以会触发 processMemberClass 逻辑,在 processMemberClass 中会先拿到未处理过的内部类,即 MyOuter$MyInnerComponent.class, 递归调用 configurationClassParser.processConfigurationClass 方法,最终将内部类保存到 parser.configurationClasses 中。在递归处理完 MyOuter 的内部类之后,最后处理 MyOuter 将其保存到 parser.configurationClasses 中。
- 第三个是 UserConfiguration.class, 该类存在 @Bean method, 所以会先将 initUser method 保存在 UserConfiguration 的 beanMethods 属性中,最后将 UserConfiguration 保存到 parser.configurationClasses 中。
- 第四个是 ZuulConfiguration.class, 因为该类存在非 Object 的父类,所以会通过 configurationClassParser.processConfigurationClass 方法中的 do-while 循环处理其父类 ZuulProxyAutoConfiguration 和 ZuulServerAutoConfiguration, 以每一个父类作为 sourceClass 重复 configurationClassParser.doProcessConfigurationClass 的处理逻辑(configClass 始终不变,仍然为子类 ZuulConfiguration),然后将父类的内部类添加到 parser.configurationClasses 中,将父类的 @Bean method 保存到子类 ZuulConfiguration 的 beanMethods 属性中,最后将 ZuulConfiguration 保存到 parser.configurationClasses 中。
- 到目前为止,configurationClasses 里面的数据为
[
"MyOuter$MyInnerStaticComponent.class",
"MyOuter$MyInnerComponent.class",
"MyOuter.class",
"UserConfiguration.class(包含 beanMethods)",
"ZuulProxyAutoConfiguration$EndpointConfiguration.class",
"RibbonCommandFactoryConfiguration$HttpClientRibbonConfiguration.class",
"HttpClientConfiguration$ApacheHttpClientConfiguration.class",
"HttpClientConfiguration.class",
"ZuulServerAutoConfiguration$ZuulMetricsConfiguration.class",
"ZuulServerAutoConfiguration$ZuulCounterFactoryConfiguration.class",
"ZuulServerAutoConfiguration$ZuulFilterConfiguration.class",
"ZuulConfiguration.class(包含 beanMethods)"
]
- 回到步骤 4, 在处理完 @ComponentScan 扫描到的类文件之后,继续处理 MainApplication.class, 因为 @EnableZuulProxy 和 @SpringBootApplication 注解的存在会触发 @Import 的处理逻辑。processImports 方法处理逻辑如下:
- 如果导入的是 ImportSelector 子类, 则将其保存到 parser.deferredImportSelectorHandler.deferredImportSelectors 中,供后面的自动配置过程使用,下一小节会介绍如何使用该数据。然后递归调用 processImports 方法继续以同样的逻辑处理该 ImportSelector 子类, 看看该 ImportSelector 子类上是否还有新的 ImportSelector. 比如在启动类上添加 @EnableZuulProxy 注解会将 EnableCircuitBreakerImportSelector 引入;
- 如果导入的是 ImportBeanDefinitionRegistrar, 则会将其保存在 configClass 的 importBeanDefinitionRegistrars 属性中,比如在启动类上添加 @MapperScan 注解,会将 MapperScannerRegistrar 引入到启动类;
- 如果导入的是其他类,则调用 processConfigurationClass 方法当作配置类递归处理,还是以 @EnableZuulProxy 注解为例,该注解会引入 ZuulProxyMarkerConfiguration 并且交给 configurationClassParser 处理,重复上面的步骤。
- 完成 MainApplication 的处理之后,parser.configurationClasses 数据如下
[
"MyOuter$MyInnerStaticComponent.class",
"MyOuter$MyInnerComponent.class",
"MyOuter.class",
"UserConfiguration.class(包含 beanMethods)",
"ZuulProxyAutoConfiguration$EndpointConfiguration.class",
"RibbonCommandFactoryConfiguration$HttpClientRibbonConfiguration.class",
"HttpClientConfiguration$ApacheHttpClientConfiguration.class",
"HttpClientConfiguration.class",
"ZuulServerAutoConfiguration$ZuulMetricsConfiguration.class",
"ZuulServerAutoConfiguration$ZuulCounterFactoryConfiguration.class",
"ZuulServerAutoConfiguration$ZuulFilterConfiguration.class",
"ZuulConfiguration.class(包含 beanMethods)",
"ZuulProxyMarkerConfiguration(包含 beanMethods)",
"MainApplication"
]
以 MainApplication 为 configClass 参数调用的 ConfigurationClassParser.processConfigurationClass 方法完全结束后,Spring 就扫描完了 basePackages 指定的配置类及其父类,并且保存到了 parser.configurationClasses 中,对于 top-class 和 static-inner-class 则注册到了 registry.beanDefinitionMap 中,这也就完成了方法调用示意图中的“第一步”。回到 ConfigurationClassParser.parse 方法,该方法的第二步是调用DeferredImportSelectorHandler.process() 方法完成自动配置过程。
4. DeferredImportSelectorHandler
DeferredImportSelectorHandler 是 ConfigurationClassParser 的内部类,负责处理 DeferredImportSelector. 还记得上面提到的 @EnableZuulProxy 注解引入的 EnableCircuitBreakerImportSelector 和 @SpringBootApplication 注解引入的 AutoConfigurationImportSelector 吗,它们都是 DeferredImportSelector 的子类,都被保存在了 DeferredImportSelectorHandler 的 deferredImportSelectors 列表里,然后通过其 process 方法对 selectors 进行处理。
- process
public void process() {
List deferredImports = this.deferredImportSelectors;
this.deferredImportSelectors = null;
try {
if (deferredImports != null) {
DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
// 对 deferredImportSelectors 按照优先级排序 (比较各个 selector 类上的 @Order)
deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
// 对 deferredImportSelectors 分组并注册到 DeferredImportSelectorGroupingHandler
deferredImports.forEach(handler::register);
// 由 DeferredImportSelectorGroupingHandler 完成真正的 import 过程
handler.processGroupImports();
}
}
finally {
this.deferredImportSelectors = new ArrayList<>();
}
}
上面代码的注释提到了对 deferredImportSelectors 进行分组,组是以 DeferredImportSelector.getImportGroup() 的不同返回值来划分的,DeferredImportSelector 的不同子类的该方法会返回不同值。
对于子类 AutoConfigurationImportSelector 该方法会返回 AutoConfigurationGroup.class, 那这个组的名字就是 AutoConfigurationGroup.class。
而 EnableCircuitBreakerImportSelector.getImportGroup() 会返回 NULL,那也就意味着它不知道自己是哪个组的,所以会默认分配到 DefaultDeferredImportSelectorGroup.class 小组内。
每个小组的名字和成员列表用 DeferredImportSelectorGrouping 类来保存,该类的数据结构如下:
private static class DeferredImportSelectorGrouping {
// 组名
private final DeferredImportSelector.Group group;
// 组内的 selector 列表
private final List deferredImports = new ArrayList<>();
}
于是就有了两个小组:
{AutoConfigurationGroup.class, [AutoConfigurationImportSelector]}
{DefaultDeferredImportSelectorGroup.class, [EnableCircuitBreakerImportSelector]}
- processGroupImports
看方法名可以猜到该方法是负责导入每个 selector 选中的类的,那么 selector 是如何实现自己的目的,将自己选中的类导入呢?来看下代码:
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
Predicate exclusionFilter = grouping.getCandidateFilter();
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata());
try {
processImports(configurationClass, asSourceClass(configurationClass, exclusionFilter),
Collections.singleton(asSourceClass(entry.getImportClassName(), exclusionFilter)),
exclusionFilter, false);
}
// ......
});
}
}
上面的代码会循环逐一处理每个 group, 处理每个 group 的过程可以分为两个阶段:getImports 和 processImports. processImports 方法的逻辑在前文中的模拟场景步骤 10 已经介绍过了,主要目的是将 configuration class 存到 parser.configurationClasses 中。这里说下 DeferredImportSelectorGrouping.getImports 方法:
public Iterable getImports() {
for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
this.group.process(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getImportSelector());
}
return this.group.selectImports();
}
这里会调用每个 group 的 process 方法来导入组内的 selector 选中的类,我们以最常见也是 Spring 体系中最重要的 AutoConfigurationGroup 为例,来看下这个导入过程是怎么样的。
- AutoConfigurationGroup.process
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
// ......
// 获取 autoConfigurations
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
.getAutoConfigurationEntry(annotationMetadata);
this.autoConfigurationEntries.add(autoConfigurationEntry);
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}
- AutoConfigurationImportSelector.getAutoConfigurationEntry
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 获取 configurations
List configurations = getCandidateConfigurations(annotationMetadata, attributes);
// ......
}
- AutoConfigurationImportSelector.getCandidateConfigurations
protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// 利用 spring.factories 机制查找 EnableAutoConfiguration.class 的实现类
List configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
- AutoConfigurationImportSelector.getSpringFactoriesLoaderFactoryClass
protected Class> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
这里调用栈虽然有点深,但是逻辑很简单,最终又是通过 spring.factories 机制来导入某些接口的实现类,对于 AutoConfigurationImportSelector 则是导入 EnableAutoConfiguration.class 的实现类。
导入完成之后最后通过 AutoConfigurationGroup.selectImports 方法对 autoConfigurations 进行排序,排序逻辑是通过 AutoConfigurationSorter.getInPriorityOrder 方法来完成的。
List getInPriorityOrder(Collection classNames) {
AutoConfigurationClasses classes = new AutoConfigurationClasses(this.metadataReaderFactory,
this.autoConfigurationMetadata, classNames);
List orderedClassNames = new ArrayList<>(classNames);
// Initially sort alphabetically
Collections.sort(orderedClassNames);
// Then sort by order
orderedClassNames.sort((o1, o2) -> {
int i1 = classes.get(o1).getOrder();
int i2 = classes.get(o2).getOrder();
return Integer.compare(i1, i2);
});
// Then respect @AutoConfigureBefore @AutoConfigureAfter
orderedClassNames = sortByAnnotation(classes, orderedClassNames);
return orderedClassNames;
}
- 排序规则有三个,最优规则是 @AutoConfigureBefore @AutoConfigureAfter 的限制条件,其次是 @Order 优先级,最低级规则是字母排序。最后将排好序的 configurations 存储到 parser.configurationClasses 中。这时候我们模拟场景的 parser.configurationClasses 应该是这样子的:
[
"MyOuter$MyInnerStaticComponent.class",
"MyOuter$MyInnerComponent.class",
"MyOuter.class",
"UserConfiguration.class(包含 beanMethods)",
"ZuulProxyAutoConfiguration$EndpointConfiguration.class",
"RibbonCommandFactoryConfiguration$HttpClientRibbonConfiguration.class",
"HttpClientConfiguration$ApacheHttpClientConfiguration.class",
"HttpClientConfiguration.class",
"ZuulServerAutoConfiguration$ZuulMetricsConfiguration.class",
"ZuulServerAutoConfiguration$ZuulCounterFactoryConfiguration.class",
"ZuulServerAutoConfiguration$ZuulFilterConfiguration.class",
"ZuulConfiguration.class(包含 beanMethods)",
"ZuulProxyMarkerConfiguration(包含 beanMethods)",
"MainApplication",
sortedAutoConfigurations......
]
5. ConfigurationClassBeanDefinitionReader
回到 ConfigurationClassPostProcessor.processConfigBeanDefinitions 方法,在完成ConfigurationClassParser.parse 方法的调用后,接下来将进入 ConfigurationClassBeanDefinitionReader.loadBeanDefinitions 方法
- loadBeanDefinitions
public void loadBeanDefinitions(Set configurationModel) {
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
// 顺序遍历所有的 configurationClasses, 逐一处理
for (ConfigurationClass configClass : configurationModel) {
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
}
}
还记得我之前说 parser.configurationClasses 的类型是 LinkedHashMap 吗,这里的 configurationModel 正是由 parser.configurationClasses 转化生成的 LinkedHashSet, 所以这里的循环是按照之前的 parser.configurationClasses 的插入顺序处理的。
- loadBeanDefinitionsForConfigurationClass
private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
// 调用内部类的 TrackedConditionEvaluator.shouldSkip 方法来判断是否需要跳过该 configuration
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
// 如果需要跳过该类,但是该类又已经注册到 Spring 容器中,则从容器中移除
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}
// 正常处理该类以及 @Bean method, 加载到 Spring 容器中
}
- shouldSkip
public boolean shouldSkip(ConfigurationClass configClass) {
Boolean skip = this.skipped.get(configClass);
if (skip == null) {
if (configClass.isImported()) {
boolean allSkipped = true;
for (ConfigurationClass importedBy : configClass.getImportedBy()) {
if (!shouldSkip(importedBy)) {
allSkipped = false;
break;
}
}
if (allSkipped) {
// The config classes that imported this one were all skipped, therefore we are skipped...
skip = true;
}
}
if (skip == null) {
// 注意这里是 ConfigurationPhase.REGISTER_BEAN
skip = conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN);
}
this.skipped.put(configClass, skip);
}
return skip;
}
注意这里在调用 conditionEvaluator.shouldSkip 方法时 phase = ConfigurationPhase.REGISTER_BEAN, 也就是说 @ConditionalOnBean 等注解在该阶段生效了。那么文章开头的两个问题的答案也在这里了。
- 问题一
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(MeterRegistry.class)
protected static class ZuulCounterFactoryConfiguration {
@Bean
@ConditionalOnBean(MeterRegistry.class)
@ConditionalOnMissingBean(CounterFactory.class)
public CounterFactory counterFactory(MeterRegistry meterRegistry) {
return new DefaultCounterFactory(meterRegistry);
}
}
- 因为 autoConfigurations 在待处理的配置类列表 configurationModel 里面的位置比较靠后,所以在处理 ZuulServerAutoConfiguration$ZuulCounterFactoryConfiguration.class 时通过 autoConfiguration 装配的 SimpleMeterRegistry bean 还不存在。那么其内部的 @Bean method counterFactory 因为不满足条件 @ConditionalOnBean(MeterRegistry.class) 所以被跳过了,未能注册到 Spring 容器中。即使后来 SimpleMeterRegistry 被加载进来了,也于事无补了,失去的就是失去了。
- 但是如果我们没有增加 ZuulConfiguration 这个类,那么 ZuulProxyAutoConfiguration 会在 DeferredImportSelectorHandler.process() 过程中加载进来,根据字母排序规则其位置一定在 SimpleMetricsExportAutoConfiguration 之后,所以其父类 ZuulServerAutoConfiguration 的 @Bean method counterFactory 可以成功注册到 Spring 容器中。
- 这里补充一下,ZuulProxyMarkerConfiguration 定义了一个空的内部类 Marker, 一开始完全不知道这个类是如何发光发热的。在 debug 过程中发现该类的作用是作为 ZuulProxyAutoConfiguration 装配的前置条件。因为在启动类加上 @EnableZuulProxy 注解之后,那么会通过 parser.processImports 逻辑提前将 ZuulProxyMarkerConfiguration 以及内部的 @Bean method 注册到 Spring 容器中,也就满足了 ZuulProxyAutoConfiguration 的前置条件 @ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)。相反,如果没有加 @EnableZuulProxy 注解,那么 ZuulProxyAutoConfiguration 会因为没有 ZuulProxyMarkerConfiguration.Marker bean 而不会加载到 Spring 容器中。
- 问题二
问题二中的 RedissonLockAspect 是 basePackages 下的 top-class, 所以在 componentScanParser.parse 过程中会被成功注册到 Spring 容器中,即添加到 registry.beanDefinitionMap 中。但是在 loadBeanDefinitionsForConfigurationClass 方法中,因为未能找到 autoConfiguration 导入的 RedissonClient.class 从而导致 shouldSkip = true, 所以又从容器移除了。到后来 RedissonClient.class 加载到容器中之后,一切又是追悔莫及,还是失去了 RedissonLockAspect.
总结
- Spring 注册 bean 的过程可以分为两个阶段 PARSE_CONFIGURATION 和 REGISTER_BEAN,条件注解在各个阶段是否生效取决于 CONFIGURATION_PHASE.
- 两个重要的数据结构 LinkedHashMap configurationClasses 和 ConcurrentHashMap beanDefinitionMap. 在 PARSE_CONFIGURATION 阶段 ConfigurationClassParser 将配置类存储到 configurationClasses 中,在 REGISTER_BEAN 阶段 ConfigurationClassPostProcessor 顺序遍历 configurationClasses 将其及相关配置类注册到 beanDefinitionMap 中。
- basePackages 下的配置类、内部类、beanMethod 及其父类的内部类、父类的 beanMethod 会优先于通过 autoConfiguration 机制引入的配置类被处理。
- 文章开头提到的两个问题都是因为 @ComponentScan 指定的 basePackages 下的 configurationClass、内部类、beanMethod、父类的内部类、父类的 beanMethod 依赖了通过 autoConfiguration 装配的 bean, 从而导致在 REGISTER_BEAN 过程中因为不满足条件而未能注册到 Spring 容器中。
Spring 启动过程中所做的事情极其繁多,除了文章中提到的,还有 bean 的实例化、beanPostProcessor 的应用等等,因为篇幅有限所以有些内容只是一带而过。本文目的在于通过两个小问题给大家梳理下 bean 的加载顺序,如果有不准确的地方欢迎指正。