写好 Spring Starter : 控制好Bean的加载顺序与原理

一 .前言

想写好一个 Starter , 控制配置的加载和Bean的加载是其中至关重要的一步.

这一篇把如何做好Bean管理做了一个总结 , 来好好看看Bean如何控制顺序.

二. 基础篇 - Bean 的控制

Bean 名称控制

  • 同一个包里面 Bean 名称根据字母优先级排序 ,是可以控制Bean的加载流程
  • 不同包里面 按照包名层级有序加载
  • 不同依赖包 和包的加载顺序有关

注解控制

对Bean加载顺序起作用的注解主要有 : @DependsOn , @Bean

  • @Bean 主要还是和方法的加载有关 , 由上到下自然加载
  • @DependsOn 用于控制互相依赖 , 先加载依赖的包
  • @SpringBootApplicationscanBasePackages 可以控制包有序

特别注意

  • @Resouce 和 @Autowired 并不能控制依赖关系
  • 构造器中 @Autowired 可以影响到加载顺序

2.1 Bean 加载流程简化版

之前出过一个详细的 SpringIOC 处理流程图 , 这一篇再来看一个简图 :

写好 Spring Starter : 控制好Bean的加载顺序与原理_第1张图片

三. 特别要点原理分析

3.1 包路径有序的控制

在上一篇文章 juejin.cn/post/715769… 中已经看了 BeanDefinition 的加载流程 , 我们可以知道这里有两个阶段 :

  • 阶段一 : 通过 registerBeanDefinition 往 Map 中放 BeanDefinition , 同时往一个 List 中放入名称
  • 阶段二 : 取出 BeanDefinition 后逐条加载Bean

这里面其实涉及到几个点 :

S1 : ClassPathBeanDefinitionScanner 中 doScan 时是按照 @SpringBootApplication 配置的扫描路径进行扫描

写好 Spring Starter : 控制好Bean的加载顺序与原理_第2张图片

S2 : DefaultListableBeanFactory # registerBeanDefinition 时是放入有序的 list

写好 Spring Starter : 控制好Bean的加载顺序与原理_第3张图片

总结 : 这里可以看到 , 从package 扫描到后续加载都是 list 控制的有序 , 所以 @Bean 的加载是有序的 , 并且我们可以通过 scanBasePackages 注解控制相对有序!!!

写好 Spring Starter : 控制好Bean的加载顺序与原理_第4张图片

写好 Spring Starter : 控制好Bean的加载顺序与原理_第5张图片

3.2 @Bean 的加载

  • S1 : 首先是3.1中说到的 , 按照包名去加载 Bean
  • S2 : 在 ConfigurationClassBeanDefinitionReader 处理 Configuration 类时会讲方法作为BeanNames

写好 Spring Starter : 控制好Bean的加载顺序与原理_第6张图片

这里其实很多文章里面说的是 @Bean 并不是写在前面的会先加载 ,但是在我个人读取源码的过程中 , 发现@Bean的书写顺序确实是能控制顺序的 , 从源码的角度来分析一下 :

S1 : Spring 发现该@Bean是一个配置类 , 后续触发ConfigurationClassBeanDefinitionReader对配置类进行读取
S2 : doProcessConfigurationClass 中 retrieveBeanMethodMetadata 获取到所有的 Set
S3 : 循环所有的 Set , 将 BeanName 放入 List 中 
复制代码

整个过程中最重要的就是 S2 , 虽然它是个 Set ,但是看看源码就知道 , 这是一个 LinkedHashSet , 所以它其实是有序的

private Set retrieveBeanMethodMetadata(SourceClass sourceClass) {
   AnnotationMetadata original = sourceClass.getMetadata();
   // 通过 @Bean 获取到所有的 MethodMetadata , 这里的 Set 实际上是 LinkdedHashSet
   Set beanMethods = original.getAnnotatedMethods(Bean.class.getName());
   if (beanMethods.size() > 1 && original instanceof StandardAnnotationMetadata) {
         AnnotationMetadata asm =
               this.metadataReaderFactory.getMetadataReader(original.getClassName()).getAnnotationMetadata();
         Set asmMethods = asm.getAnnotatedMethods(Bean.class.getName());
         // .............. 省略一些其他场景
      }
   }
   return beanMethods;
}
复制代码

实际上在我更换 @Configuration 中 @Bean 的书写顺序时 , 得到的效果也和源码中看的一致.

3.3 @DependsOn 的生效原理

DependsOn 的处理应该算是在 Spring Bean 加载流程的第二阶段处理的.

在这个阶段中SpringIOC获取Bean的BeanDefinition , 并且对Bean类型进行分析同时判断其关联关系.

这个流程主要在 AbstractBeanFactory 中实现.

C- AbstractBeanFactory # doGetBean
// 这里获取的就是@DependsOn 注解中的方法名
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
   for (String dep : dependsOn) {
      // S1 : 这里需要判断这个依赖是否已经注册, 会从 S2 里面的Map 中获取
      if (isDependent(beanName, dep)) {
           throw new BeanCreationException(.....); 
      }     
      // S2 : 这里把DependsOn 的Bean注册到Map中 ,避免重复
      registerDependentBean(dep, beanName);
      
      // S3 : 递归获取 , 这里就是按照Bean全流程递归处理
      getBean(dep);
   }
}
复制代码

3.4 不是说循环依赖会优先加载吗 , 为什么引用@Resource 并没有控制顺序

在以往很多场景中 ,我都是通过@Resource 引入某个对象 , 来实现优先加载 . 但是事实证明这种方式是不生效的.

这里面涉及到Spring循环依赖的相关特点 ,即 Spring 依赖时创建的对象 , 还没有被初始化了.

但是如果在构造器中引入依赖 , 是可以影响到Bean的加载顺序的 :

@Bean
public BeanThree getBeanThree(BeanTwo beanTwo) {
    return new BeanThree(beanTwo);
}
复制代码

这是简单说一下原理 , 在构造器载入的场景下 , Spring 需要先将依赖的对象初始化后 , 再作为 argsToUse 传入被构建的对象中 :

// argsToUse 即为构造器中的对象
bw.setBeanInstance(instantiate(beanName, mbd, factoryBean, factoryMethodToUse, argsToUse));
复制代码

流程跟踪 :

// S1 : Spring 容器初始化 BeanThree
C- AbstractAutowireCapableBeanFactory # doCreateBean
    - instanceWrapper = createBeanInstance("getBeanThree", mbd, args);


// S2 : 通过构造器进行初始化
C- AbstractAutowireCapableBeanFactory # instantiateUsingFactoryMethod
    - return new ConstructorResolver(this).instantiateUsingFactoryMethod(beanName, mbd, explicitArgs);
    
// S3 :  初始化 BeanThree 前先初始化 BeanTwo
public BeanWrapper instantiateUsingFactoryMethod(
      String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) {

   // 3-1 省略获取工厂的相关逻辑 
   
   // 3-2 获取到构造器方法
   - public .....ConfigutationA.getBeanThree(.....BeanTwo)
   Method factoryMethodToUse = null;
   ArgumentsHolder argsHolderToUse = null;
   Object[] argsToUse = null;

   //... 省略   
   if (factoryMethodToUse == null || argsToUse == null) {
      List candidates = null;
      if (mbd.isFactoryMethodUnique) {
         if (factoryMethodToUse != null) {
            // 此处获取所有的构造器方法
            candidates = Collections.singletonList(factoryMethodToUse);
         }
      }
      //... 省略额外的构造器处理
        minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw, cargs, resolvedValues);

      LinkedList causes = null;

      for (Method candidate : candidates) {
         // 3-3 此处就开始处理构造器中的所有参数 , 以及需要autowired处理的参数
         // - 在这个环节中 , 会优先调用依赖的对象的初始化操作
        argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw,
                        paramTypes, paramNames, candidate, autowiring, candidates.size() == 1);
      }
   }

   // S3-4 继续调用 BeanThree 初始化逻辑
   bw.setBeanInstance(instantiate(beanName, mbd, factoryBean, factoryMethodToUse, argsToUse));
   return bw;
}


// S4 : 初始化 BeanTwo
// S5 : 继续初始化 BeanThree
    
复制代码

总结 : 可以看到 , 如果构造器中存在了需要初始化后才能注入的对象 ,是会触发提前加载. 但是如果单纯的类中的属性 , 实际上是在后面 populationBean 中进行的处理

总结

Bean 控制有序的方式还是有很多种的 , 以上的加载方式只能说以Idea启动时能确定是存在影响的 , 但是会不会由于某种场景例如JVM的加载等等破坏这种操作 , 暂时是不清楚的.

同时是否会由于一些代理等操作影响到这种加载 , 也存在不确定.

不过如果遇到需要控制Bean加载的场景 , 不妨试试上面的办法 , 说不定能满足需求.

下一篇来看一下配置文件如何进行有序的控制.

你可能感兴趣的:(spring,java,后端)