热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

详解Spring中如何控制2个bean中的初始化顺序

开发过程中有这样一个场景,2个 bean 初始化逻辑中有依赖关系,需要控制二者的初始化顺序。实现方式可以有多种,本文结合目前对 Spring

开发过程中有这样一个场景,2个 bean 初始化逻辑中有依赖关系,需要控制二者的初始化顺序。实现方式可以有多种,本文结合目前对 Spring 的理解,尝试列出几种思路。

场景

假设A,B两个 bean 都需要在初始化的时候从本地磁盘读取文件,其中B加载的文件,依赖A中加载的全局配置文件中配置的路径,所以需要A先于B初始化,此外A中的配置改变后也需要触发B的重新加载逻辑,所以A,B需要注入彼此。

对于下面的模型,问题简化为:我们需要initA()先于initB()得到执行。

@Service
public class A {
  @Autowired
  private B b;

  public A() {
    System.out.println("A construct");
  }

  @PostConstruct
  public void init() {
    initA();
  }

  private void initA() {
    System.out.println("A init");
  }
}

@Service
public class B {
  @Autowired
  private A a;

  public B() {
    System.out.println("B construct");
  }

  @PostConstruct
  public void init() {
    initB();
  }

  private void initB(){
    System.out.println("B init");
  }
}

方案一:立Flag

我们可以在业务层自己控制A,B的初始化顺序,在A中设置一个“是否初始化的”标记,B初始化前检测A是否得以初始化,如果没有则调用A的初始化方法,所谓的check-and-act。对于上述模型,实现如下:

@Service
public class A {

  private static volatile boolean initialized;

  @Autowired
  private B b;

  public A() {
    System.out.println("A construct");
  }

  @PostConstruct
  public void init() {
    initA();
  }

  public boolean isInitialized() {
    return initialized;
  }

  public void initA() {
    if (!isInitialized()) {
      System.out.println("A init");
    }
    initialized = true;
  }
}

@Service
public class B {

  @Autowired
  private A a;


  public B() {
    System.out.println("B construct");
  }

  @PostConstruct
  public void init() {
    initB();
  }


  private void initB() {
    if (!a.isInitialized()) {
      a.initA();
    }
    System.out.println("B init");
  }

执行效果:

A construct
B construct
A init
B init

这种立flag的方法好处是可以做到lazy initialization,但是如果类似逻辑很多的话代码中到处充斥着类似代码,不优雅,所以考虑是否框架本身就可以满足我们的需要。

方案二:使用DependsOn

Spring 中的 DependsOn 注解可以保证被依赖的bean先于当前bean被容器创建,但是如果不理解Spring中bean加载过程会对 DependsOn 有误解,自己也确实踩过坑。对于上述模型,如果在B上加上注解@DependsOn({"a"}),得到的执行结果是:

A construct
B construct
B init
A init

在这里问题的关键是:bean属性的注入是在初始化方法调用之前。

// 代码位置:AbstractAutowireCapableBeanFactory.doCreateBean
// 填充 bean 的各个属性,包括依赖注入
populateBean(beanName, mbd, instanceWrapper);
if (exposedObject != null) {
  // 调用初始化方法,如果是 InitializingBean 则先调用 afterPropertiesSet 然后调用自定义的init-method 方法
  exposedObject = initializeBean(beanName, exposedObject, mbd);
}

结合本例,发生的实际情况是,因为出现了循环依赖,A依赖B,加载B,B依赖A,所以得到了一个提前暴露的A,然后调用B的初始化方法,接着回到A的初始化方法。具体源码分析过程如下:

ApplicationContext 在 refresh 过程中的最后会加载所有的 no-lazy 单例。

本例中,先加载的bean A,最终通过无参构造器构造,然后,继续属性填充(populateBean),发现需要注入 bean B。所以转而加载 bean B(递归调用 getBean())。此时发现 bean B 需要 DependsOn("a"),在保存依赖关系(为了防止循环 depends)后,调用 getBean("a"),此时会得到提前暴露的 bean A ,所以继续 B 的加载,流程为: 初始化策略构造实例 -> 属性填充(同样会注入提前暴露的 bean A ) -> 调用初始化方法。

// 代码位置:AbstractBeanFactory.doGetBean
// Guarantee initialization of beans that the current bean depends on. 实例化依赖的 bean
    String[] dependsOn = mbd.getDependsOn();
    if (dependsOn != null) {
      for (String dep : dependsOn) {
        if (isDependent(beanName, dep)) {
          throw new BeanCreationException(mbd.getResourceDescription(),
              beanName, "Circular depends-on relationship between '"
              + beanName + "' and '" + dep + "'");
        }
        registerDependentBean(dep, beanName); // 缓存 bean 依赖的关系
        getBean(dep);
      }
    }

得到提前暴露的 bean A的过程为:

此时此刻,bean A 的属性注入完成了, 返回到调用初始化方法,所以表现的行为是:构造A -> 构造B -> B初始化 -> A初始化。

DependsOn只是保证的被依赖的bean先于当前bean被实例化,被创建,所以如果要采用这种方式实现bean初始化顺序的控制,那么可以把初始化逻辑放在构造函数中,但是复杂耗时的逻辑仿造构造器中是不合适的,会影响系统启动速度。

方案三:容器加载bean之前

Spring 框架中很多地方都为我们提供了扩展点,很好的体现了开闭原则(OCP)。其中 BeanFactoryPostProcessor 可以允许我们在容器加载任何bean之前修改应用上下文中的BeanDefinition(从XML配置文件或者配置类中解析得到的bean信息,用于后续实例化bean)。

在本例中,就可以把A的初始化逻辑放在一个 BeanFactoryPostProcessor 中。

@Component
public class ABeanFactoryPostProcessor implements BeanFactoryPostProcessor {
  @Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
    A.initA();
  }
}

执行效果:

A init
A construct
B construct
B init

这种方式把A中的初始化逻辑放到了加载bean之前,很适合加载系统全局配置,但是这种方式中初始化逻辑不能依赖bean的状态。

方案四:事件监听器的有序性

Spring 中的 Ordered 也是一个很重要的组件,很多逻辑中都会判断对象是否实现了 Ordered 接口,如果实现了就会先进行排序操作。比如在事件发布的时候,对获取到的 ApplicationListener 会先进行排序。

// 代码位置:AbstractApplicationEventMulticaster.ListenerRetriever.getApplicationListeners()
public Collection> getApplicationListeners() {
    LinkedList> allListeners = new LinkedList>();
    for (ApplicationListener<&#63;> listener : this.applicationListeners) {
      allListeners.add(listener);
    }
    if (!this.applicationListenerBeans.isEmpty()) {
      BeanFactory beanFactory = getBeanFactory();
      for (String listenerBeanName : this.applicationListenerBeans) {
        try {
          ApplicationListener<&#63;> listener = beanFactory.getBean(listenerBeanName, ApplicationListener.class);
          if (this.preFiltered || !allListeners.contains(listener)) {
            allListeners.add(listener);
          }
        } catch (NoSuchBeanDefinitionException ex) {
          // Singleton listener instance (without backing bean definition) disappeared -
          // probably in the middle of the destruction phase
        }
      }
    }
    AnnotationAwareOrderComparator.sort(allListeners); // 排序
    return allListeners;
  }

所以可以利用事件监听器在处理事件时的有序性,在应用上下文 refresh 完成后,分别实现A,B中对应的初始化逻辑。

@Component
public class ApplicationListenerA implements ApplicationListener, Ordered {
  @Override
  public void onApplicationEvent(ApplicationContextEvent event) {
    initA();
  }

  @Override
  public int getOrder() {
    return Ordered.HIGHEST_PRECEDENCE; // 比 ApplicationListenerB 优先级高
  }

  public static void initA() {
    System.out.println("A init");
  }
}

@Component
public class ApplicationListenerB implements ApplicationListener, Ordered{
  @Override
  public void onApplicationEvent(ApplicationContextEvent event) {
    initB();
  }

  @Override
  public int getOrder() {
    return Ordered.HIGHEST_PRECEDENCE -1;
  }

  private void initB() {
    System.out.println("B init");
  }
}

执行效果:

A construct
B construct
A init
B init

这种方式就是站在事件响应的角度,上下文加载完成后,先实现A逻辑,然后实现B逻辑。

总结

在平时的开发中使用的可能都是一个语言,一个框架的冰山一角,随着对语言,对框架的不断深入,你会发现更多的可能。本文只是基于目前对于 Spring 框架的理解做出的尝试,解决一个问题可能有多种方式,其中必然存在权衡选择,取决于对业务对技术的理解。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


推荐阅读
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 本文详细介绍了如何使用Spring Boot进行高效开发,涵盖了配置、实例化容器以及核心注解的使用方法。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • 在当前众多持久层框架中,MyBatis(前身为iBatis)凭借其轻量级、易用性和对SQL的直接支持,成为许多开发者的首选。本文将详细探讨MyBatis的核心概念、设计理念及其优势。 ... [详细]
  • RecyclerView初步学习(一)
    RecyclerView初步学习(一)ReCyclerView提供了一种插件式的编程模式,除了提供ViewHolder缓存模式,还可以自定义动画,分割符,布局样式,相比于传统的ListVi ... [详细]
  • 2023年京东Android面试真题解析与经验分享
    本文由一位拥有6年Android开发经验的工程师撰写,详细解析了京东面试中常见的技术问题。涵盖引用传递、Handler机制、ListView优化、多线程控制及ANR处理等核心知识点。 ... [详细]
  • 本文介绍如何在 Unity 的 XML 配置文件中,将参数传递给自定义生命周期管理器的构造函数。我们将详细探讨 CustomLifetimeManager 类的实现及其配置方法。 ... [详细]
  • 实体映射最强工具类:MapStruct真香 ... [详细]
  • 深入解析 Apache Shiro 安全框架架构
    本文详细介绍了 Apache Shiro,一个强大且灵活的开源安全框架。Shiro 专注于简化身份验证、授权、会话管理和加密等复杂的安全操作,使开发者能够更轻松地保护应用程序。其核心目标是提供易于使用和理解的API,同时确保高度的安全性和灵活性。 ... [详细]
  • 探讨如何真正掌握Java EE,包括所需技能、工具和实践经验。资深软件教学总监李刚分享了对毕业生简历中常见问题的看法,并提供了详尽的标准。 ... [详细]
  • 本文探讨了在 ASP.NET MVC 5 中实现松耦合组件的方法。通过分离关注点,应用程序的各个组件可以更加独立且易于维护和测试。文中详细介绍了依赖项注入(DI)及其在实现松耦合中的作用。 ... [详细]
  • Startup 类配置服务和应用的请求管道。Startup类ASP.NETCore应用使用 Startup 类,按照约定命名为 Startup。 Startup 类:可选择性地包括 ... [详细]
  • 网易严选Java开发面试:MySQL索引深度解析
    本文详细记录了网易严选Java开发岗位的面试经验,特别针对MySQL索引相关的技术问题进行了深入探讨。通过本文,读者可以了解面试官常问的索引问题及其背后的原理。 ... [详细]
  • 本文将深入探讨如何在不依赖第三方库的情况下,使用 React 处理表单输入和验证。我们将介绍一种高效且灵活的方法,涵盖表单提交、输入验证及错误处理等关键功能。 ... [详细]
  • 本文探讨了如何在日常工作中通过优化效率和深入研究核心技术,将技术和知识转化为实际收益。文章结合个人经验,分享了提高工作效率、掌握高价值技能以及选择合适工作环境的方法,帮助读者更好地实现技术变现。 ... [详细]
author-avatar
大绿
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有