关于Spring的两三事:如何控制Bean的加载顺序

4,601 阅读6分钟

关于Spring的两三事:如何控制Bean的加载顺序.png

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

人生苦短,不如养狗

作者:Brucebat.Sun

公众号:Brucebat的伪技术鱼塘

一、为什么需要Bean的加载顺序控制

  作为一名面向Spring开发的开发人员,在日常的工作、学习或者面试中或多或少都会遇到这样一个问题:如何控制Bean的加载顺序?

  在开始回答这个问题之前我们需要先解答另一个问题:为什么需要进行Bean的加载顺序控制?

  首先尝试使用我们的第一感觉来回答这个问题,需要进行Bean的加载顺序控制意味着在加载Bean的过程中部分Bean和Bean之间存在依赖关系,也就是说Bean A的加载需要等待Bean B加载完成之后才能进行。但是想一想我们日常开发中对于存在依赖关系的Bean的组织方式 (一般为关联/聚合/组合其中之一,即将依赖的Bean作为成员变量引入) ,貌似上面所说的情况并不需要开发人员感知。在Spring创建Bean的过程中,如果发现Bean所依赖的成员变量中存在还没有创建完成的,就会先对这个尚未创建的成员变量Bean进行创建,保证Bean所依赖的成员变量一定会在Bean创建之前完成创建和加载操作。也正是这个原因,我们在进行Spring项目开发时一般情况下基本不会关注以成员变量方式引入的Bean的加载顺序问题,因为框架本身已经帮助我们处理好了这个问题。

  除了因为Bean本身存在依赖关系需要进行Bean的加载顺序控制以外,当需要确定一组实现了相同接口的Bean的执行顺序时也需要进行Bean的加载顺序控制。我们可以通过一个例子感受一下这种场景:

  从上面的例子中可以看到,我们将穿衣服 (如果使用代码表示则为ClothWearInterface接口) 的过程分解成了穿内裤、穿内衣、穿外裤以及穿外套四个步骤 (即ClothWearInterface接口的实现类) 。在实际步骤执行的过程中必须按照上图中指定的顺序进行,否则就有可能出现内裤外穿的情况(当然裤子和上衣的顺序可以随意调整)。下面我们用一段简单的代码来实现上述的场景:

/*
 * 穿衣服接口实现类集合
 */
private List<ClothWearInterface> clothWearInterfaces;
​
public void wearClothes(People people) {
  clothWearInterfaces.forEach(p -> p.doWear(people));
}

  从上面的代码中不难发现,ClothWearInterface接口实现类Bean的执行顺序实际上是通过集合中Bean的加载顺序来决定的。而Spring框架在没有明确指定加载顺序的情况下是无法按照业务逻辑预期的顺序进行Bean加载,所以需要Spring框架提供能让开发人员显示地指定Bean加载顺序的能力。

二、Bean的加载顺序控制

  在上一个小节中我们探讨了为什么需要进行Bean的加载顺序控制(或者说需要进行Bean加载顺序控制的场景)。结合上面探讨的内容,我们来看一下Spring提供的用于进行Bean加载顺序控制的能力。

  结合Spring官网提供的文档以及源码可以发现,在Spring中提供了如下的方法来进行Bean加载顺序的控制:

  • 实现Ordered/PriorityOrdered接口;
  • 使用@Order/@Priority注解,@Order注解可以用于方法级别,而@Priority注解则不行;

  针对自定义的Bean而言,上述的方式都可以实现Bean加载顺序的控制。无论是实现接口的方式还是使用注解的方式,值设置的越小则优先级越高,而通过实现PriorityOrdered接口或者使用@Priority注解的Bean时其加载优先级会高于实现Ordered接口或者使用@Order注解的Bean。需要注意的是,使用上述方式只会改变实现同一接口Bean加载到集合 (比如List、Set等) 中的顺序(或者说优先级),但是这种方式并不会影响到Spring应用上下文启动时不同Bean的初始化顺序(startup order)。下面是官方文档中的解释:

官方文档:@Order values may influence priorities at injection points, but be aware that they do not influence singleton startup order, which is an orthogonal concern determined by dependency relationships and @DependsOn declarations.

  除了自定义Bean以外,Spring还将这一功能提供给了BeanPostProcessor/BeanFactoryProcessor、过滤器Filter、拦截器Interceptor等开发人员可以感知的扩展点。而这一功能的实现主要是依赖于AnnotationAwareOrderComparator及其父类OrderComparator。下面我们以自定义Bean中注入集合Bean为例来了解一下Spring是如何进行Bean的加载顺序控制的。 (以下的流程是基于SpringBoot 2.4.4版本进行绘制)

  这里我们省略了无关的代码,只保留了基本的调用链路。从上面的调用链路可以看到,集合Bean加载顺序的处理是在Bean创建实例阶段 (即调用createBeanInstance) 完成的。需要注意,只有使用了@Autowired注解或者构造器方式注入作为成员变量的集合Bean才能通过上面的链路进行集合中Bean加载顺序的控制,使用@Resource注解是无法做到这一点。在调用链路的最后,DefaultListableBeanFactory#resolveMultipleBeans方法中进行了实际的Bean加载顺序的调整,这里我们摘取部分关键代码进行展示:

// DefaultListableBeanFactory#resolveMultipleBeans部分代码
    else if (type.isArray()) {
      ......
      if (result instanceof Object[]) {
        // 获取到比较器
        Comparator<Object> comparator = adaptDependencyComparator(matchingBeans);
        if (comparator != null) {
          Arrays.sort((Object[]) result, comparator);
        }
      }
      return result;
    }
    else if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
      ......
      if (result instanceof List) {
        if (((List<?>) result).size() > 1) {
          // 获取到比较器
          Comparator<Object> comparator = adaptDependencyComparator(matchingBeans);
          if (comparator != null) {
            ((List<?>) result).sort(comparator);
          }
        }
      }
      return result;
    }

  从上面的代码中可以看到,可以进行顺序控制的Bean类型只有数组类型Object[]List类型,其他类型的成员Bean是无法进行加载顺序控制的。有兴趣的同学可以尝试着使用不同类型的Bean,看一看是否能够按照预期目标进行Bean的加载。

  除了上述较为常见的自定义Bean,我们再来看一下平日不是经常使用到的BeanPostProcessor。对于BeanPostProcessor的加载顺序控制全部都在PostProcessorRegistrationDelegate#registerBeanPostProcessors方法中,这里笔者就不展示具体的代码了,有兴趣的同学可以自行阅读一下。区别于自定义Bean的处理逻辑,只有实现了Ordered/PriorityOrdered接口的BeanPostProcessor才会进行排序处理,否则会按照启动时的顺序进行加载处理。

三、总结

  从上面的探讨中,我们可以发现Spring提供的顺序控制能力只适用于加载集合Bean中元素顺序的调整,通过加载顺序来决定最终集合Bean中遍历时的执行顺序。而对于启动时的初始化顺序,无论是Spring自身的注解或是接口,还是基于JSR规范的注解都是无法进行控制的,此时我们需要使用Spring提供的另一个注解——@DependsOn@DependsOn注解在上文引用的官方文档中也提到过,有兴趣的同学可以自行了解一下。

  最后,疫情还在继续,希望大家出行注意防护,身体健康,每天保持好心情~~