首先,Spring bean的默认加载顺序是怎么控制的

工程中有2个bean,A和B,其中必须先初始化A再初始化B,但是没有depend-on或者Order等方式去保证,只不过恰好刚好这么运行着没出事,但是突然增加了一个C之后,就先初始化B再初始化A导致问题,但是在主干版本上却没问题。

解决这个问题其实很简单,depend-on即可,但是为什么会分支版本上会增加C后就改变AB的初始化顺序?为什么主干版本上同样添加没问题呢?可以看spring的源码 DefaultListableBeanFactory 类中有

private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>(256);
这个map保存的就是xml中定义的bean结构,spring解析定义的bean之后,保存到这里,其中key为beanid,value为bean的详细信息,根据beanid算出hashCode分别为1505068002,1682286698,从定义可以看到这是一个ConcurrentHashMap,在获取到定义后,spring在初始化的时候是怎么初始化的呢?显然也是从这个定义里取值,为了简化问题,我们理解为是类似如下方式

for(Entry<String,BeanDefinition> entry : beanDefinitionMap)
去遍历bean然后根据BeanDefinition来初始化,考虑分支版本的是用JDK1.6,主干是用JDK1.8,不同ConcurrentHashMap的实现是否会导致取出来的顺序不一样呢?

JDK1.8中,ConcurrentHashMap 的实现是这样的,数组Node<K,V>[]table,每个元素为一个Node,每个元素直接落到Node上,去追加链表或者在红黑树上,总之是一次hash

JDK1.6中,ConcurrentHashMap 的实现是这样的,先Segment<K,V>[]segments来进行一个分段,然后每个Segment里再包含元素 HashEntry<K,V>[] table,即,会对每个元素会有2次hash,第一次定位到Segment,第二次在Segment内部定位到自己的位置userService

可以知道,在JDK1.8中2个bean的位置是固定的(所以主干版本同样添加但是AB初始化顺序不变),但是在JDK1.6中可能会发生这种情况:B在第一个Segment中,A在第二个Segment中,导致取出来的时候先获取到B,后取出来A,所以出现了空指针,同样,也可以解释,为什么新增了1个id为C的bean就导致了问题,但是之前没问题,因为新增bean导致ConcurrentHashMap中部分bean所在的Segment发生变化。

当然,对有依赖关系显然是显式指定出来的好,不然像这样坑后来人就不好了

使用Spring @DependsOn控制bean加载顺序

spring容器载入bean顺序是不确定的,spring框架没有约定特定顺序逻辑规范。但spring保证如果A依赖B(如beanA中有@Autowired B的变量),那么B将先于A被加载。但如果beanA不直接依赖B,我们如何让B仍先加载呢?

控制bean初始化顺序
可能有些场景中,bean A 间接依赖 bean B。如Bean B应该需要更新一些全局缓存,可能通过单例模式实现且没有在spring容器注册,bean A需要使用该缓存;因此,如果bean B没有准备好,bean A无法访问。

另一个场景中,bean A是事件发布者(或JMS发布者),bean B (或一些) 负责监听这些事件,典型的如观察者模式。我们不想B 错过任何事件,那么B需要首先被初始化。

简言之,有很多场景需要bean B应该被先于bean A被初始化,从而避免各种负面影响。我们可以在bean A上使用@DependsOn注解,告诉容器bean B应该先被初始化。下面通过示例来说明。

示例说明
示例通过事件机制说明,发布者和监听者,然后通过spring配置运行。为了方便说明,示例进行了简化。

EventManager.java
事件管理类,维护监听器列表,通过单例方法获取事件管理器,可以增加监听器或发布事件。

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer; public class EventManager {
private final List<Consumer<String>> listeners = new ArrayList<>(); private EventManager() {
} private static class SingletonHolder {
private static final EventManager INSTANCE = new EventManager();
} public static EventManager getInstance() {
return SingletonHolder.INSTANCE;
} public void publish(final String message) {
listeners.forEach(l -> l.accept(message));
} public void addListener(Consumer<String> eventConsumer) {
listeners.add(eventConsumer);
}
}

EventPublisherBean.java
事件发布类,通过EventManager类发布事件。

import com.logicbig.example.EventManager;

public class EventPublisherBean {

    public void initialize() {
System.out.println("EventPublisherBean initializing");
EventManager.getInstance().publish("event published from EventPublisherBean");
}
}

EventListenerBean.java
事件监听者,可以增加监听器。

import com.logicbig.example.EventManager;

public class EventListenerBean {

    private void initialize() {
EventManager.getInstance().
addListener(s ->
System.out.println("event received in EventListenerBean : " + s));
}
}

AppConfig.java
配置运行类。

@Configuration
@ComponentScan("com.logicbig.example")
public class AppConfig { @Bean(initMethod = "initialize")
@DependsOn("eventListener")
public EventPublisherBean eventPublisherBean () {
return new EventPublisherBean();
} @Bean(name = "eventListener", initMethod = "initialize")
// @Lazy
public EventListenerBean eventListenerBean () {
return new EventListenerBean();
} public static void main (String... strings) {
new AnnotationConfigApplicationContext(AppConfig.class);
}
}

运行AppConfig的main方法,输出结果为:

EventListenerBean initializing
EventPublisherBean initializing
event received in EventListenerBean : event published from EventPublisherBean

总结
如果我们注释掉@DependsOn("eventListener"),我们可能不确定获得相同结果。尝试多次运行main方法,偶尔我们将看到EventListenerBean 没有收到事件。为什么是偶尔呢?因为容器启动过程中,spring按任意顺序加载bean。

那么当不使用@DependsOn可以让其100%确定吗?可以使用@Lazy注解放在eventListenerBean ()上。因为EventListenerBean在启动阶段不加载,当其他bean需要其时才加载。这次我们仅EventListenerBean被初始化。

EventPublisherBean initializing
现在从新增加@DependsOn,也不删除@Lazy注解,输出结果和第一次一致,虽然我们使用了@Lazy注解,eventListenerBean在启动时仍然被加载,因为@DependsOn表明需要EventListenerBean。

@Lazy

@Lazy用于指定该Bean是否取消预初始化。主要用于修饰Spring Bean类,用于指定该Bean的预初始化行为,

使用该Annotation时可以指定一个boolean型的value属性,该属性决定是否要预初始化该Bean

  • lazy代表延时加载,lazy=false,代表不延时,如果对象A中还有对象B的引用,会在A的xml映射文件中配置b的对象引用,多对一或一对多,不延时代表查询出对象A的时候,会把B对象也查询出来放到A对象的引用中,A对象中的B对象是有值的。
  • lazy=true代表延时,查询A对象时,不会把B对象也查询出来,只会在用到A对象中B对象时才会去查询,默认好像是false,你可以看看后台的sql语句的变化就明白了,一般需要优化效率的时候会用到
@Lazy(true)
@Component
public class Chinese implements Person{
//codes here
}

@DependsOn用于强制初始化其他Bean。可以修饰Bean类或方法,使用该Annotation时可以指定一个字符串数组作为参数,每个数组元素对应于一个强制初始化的Bean

@DependsOn({"steelAxe","abc"})
@Component
public class Chinese implements Person{
//codes here
}

@Order

@Order注解的源码如下,value用例赋值,默认赋值Ordered.LOWEST_PRECEDENCE,即默认优先级最低:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface Order { /**
* The order value. Default is {@link Ordered#LOWEST_PRECEDENCE}.
* @see Ordered#getOrder()
*/
int value() default Ordered.LOWEST_PRECEDENCE; }

再来看下Order接口的源码如下,一目了然,值越小优先级越高,即被优先加载(precedence即优先级):

public interface Ordered {

/**
* Useful constant for the highest precedence value.
* @see java.lang.Integer#MIN_VALUE
*/
int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;//值越小,优先级越高 /**
* Useful constant for the lowest precedence value.
* @see java.lang.Integer#MAX_VALUE
*/
int LOWEST_PRECEDENCE = Integer.MAX_VALUE;//值越大,优先级越低
/**
* Return the order value of this object, with a
* higher value meaning greater in terms of sorting.
* <p>Normally starting with 0, with {@code Integer.MAX_VALUE}
* indicating the greatest value. Same order values will result
* in arbitrary positions for the affected objects.
* <p>Higher values can be interpreted as lower priority. As a
* consequence, the object with the lowest value has highest priority
* (somewhat analogous to Servlet "load-on-startup" values).
* @return the order value
*/
int getOrder();

拓展①:为何在类上加@Order就可以对类(映射到容器中就是bean)有效排序了?

先了解下Java强大的注解功能,可以参考Java注解教程及自定义注解

拓展②:给类@Order注解后spring容器具体怎么给bean排序的呢?

在@Order注解的源码上,作者写了相关信息,感兴趣可以查看下

拓展③:@Order的作用域可以是类、方法、类成员,方法和类成员暂时没有测试,保留。

Springboot的@AutoConfigureAfter注解,手动的指定Bean的实例化顺序

Springboot的@AutoConfigureAfter注解,手动的指定Bean的实例化顺序。了解Spring内Bean的解析,加载和实例化顺序机制有助于我们更好的使用Spring/Springboot,避免手动的去干预Bean的加载过程,搭建更优雅的框架。

Spring容器在实例化时会加载容器内所有非延迟加载的单例类型Bean,看如下源码:

public abstract class AbstractApplicationContext extends DefaultResourceLoader
implements ConfigurableApplicationContext, DisposableBean { //刷新Spring容器,相当于初始化
public void refresh() throws BeansException, IllegalStateException {
......
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
}
} public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { /** List of bean definition names, in registration order */
private volatile List<String> beanDefinitionNames = new ArrayList<String>(256); public void preInstantiateSingletons() throws BeansException {
List<String> beanNames = new ArrayList<String>(this.beanDefinitionNames);
for (String beanName : beanNames) {
......
getBean(beanName); //实例化Bean
}
} }

ApplicationContext内置一个BeanFactory对象,作为实际的Bean工厂,和Bean相关业务都交给BeanFactory去处理。
在BeanFactory实例化所有非延迟加载的单例Bean时,遍历beanDefinitionNames 集合,按顺序实例化指定名称的Bean。beanDefinitionNames 属性是Spring在加载Bean Class生成的BeanDefinition时,为这些Bean预先定义好的名称,看如下代码:

public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
......
this.beanDefinitionNames.add(beanName);
}
}

BeanFactory在加载一个BeanDefinition(也就是加载Bean Class)时,将相应的beanName存入beanDefinitionNames属性中,在加载完所有的BeanDefinition后,执行Bean实例化工作,此时会依据beanDefinitionNames的顺序来有序实例化Bean,也就是说Spring容器内Bean的加载和实例化是有顺序的,而且近似一致,当然仅是近似。

Spring在初始化容器时,会先解析和加载所有的Bean Class,如果符合要求则通过Class生成BeanDefinition,存入BeanFactory中,在加载完所有Bean Class后,开始有序的通过BeanDefinition实例化Bean。

我们先看加载Bean Class过程,零配置下Spring Bean的加载起始于ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry(BeanDefinitionRegistry)方法,我总结了下其加载解析Bean Class的流程:

配置类可以是Spring容器的起始配置类,也可以是通过@ComponentScan扫描得到的类,也可以是通过@Import引入的类。如果这个类上含有@Configuration,@Component,@ComponentScan,@Import,@ImportResource注解中的一个,或者内部含有@Bean标识的方法,那么这个类就是一个配置类,Spring就会按照一定流程去解析这个类上的信息。

在解析的第一步会校验当前类是否已经被解析过了,如果是,那么需要按照一定的规则处理(@ComponentScan得到的Bean能覆盖@Import得到的Bean,@Bean定义的优先级最高)。

如果未解析过,那么开始解析:

解析内部类,查看内部类是否应该被定义成一个Bean,如果是,递归解析。
解析@PropertySource,也就是解析被引入的Properties文件。
解析配置类上是否有@ComponentScan注解,如果有则执行扫描动作,通过扫描得到的Bean Class会被立即解析成BeanDefinition,添加进beanDefinitionNames属性中。之后查看扫描到的Bean Class是否是一个配置类(大部分情况是,因为标识@Component注解),如果是则递归解析这个Bean Class。
解析@Import引入的类,如果这个类是一个配置类,则递归解析。
解析@Bean标识的方法,此种形式定义的Bean Class不会被递归解析
解析父类上的@ComponentScan,@Import,@Bean,父类不会被再次实例化,因为其子类能够做父类的工作,不需要额外的Bean了。
在1,3,4,6中都有递归操作,也就是在解析一个Bean Class A时,发现其上能够获取到其他Bean Class B信息,此时会递归的解析Bean Class B,在解析完Bean Class B后再接着解析Bean Class A,可能在解析B时能够获取到C,那么也会先解析C再解析B,就这样不断的递归解析。

在第3步中,通过@ComponentScan扫描直接得到的Bean Class会被立即加载入beanDefinitionNames中,但@Import和@Bean形式定义的Bean Class则不会,也就是说正常情况下面@ComponentScan直接得到的Bean其实例化时机比其他两种形式的要早。

通过@Bean和@Import形式定义的Bean Class不会立即加载,他们会被放入一个ConfigurationClass类中,然后按照解析的顺序有序排列,就是图片上的 “将配置类有序排列“。一个ConfigurationClass代表一个配置类,这个类可能是被@ComponentScan扫描到的,则此类已经被加载过了;也可能是被@Import引入的,则此类还未被加载;此类中可能含有@Bean标识的方法。

Spring在解析完了所有Bean Class后,开始加载ConfigurationClass。如果这个ConfigurationClass是被Import的,也就是说在加载@ComponentScan时其未被加载,那么此时加载ConfigurationClass代表的Bean Class。然后加载ConfigurationClass内的@Bean方法。

顺序总结:@ComponentScan > @Import > @Bean

下面看实际的启动流程:

此图顺序验证小框架:Spring Bean解析,加载及实例化顺序验证小框架

Bean Class的结构图如上所示,A是配置类的入口,通过A能直接或间接的引入一个模块。

此时启动Spring容器,将A引入容器内。

如果A是通过@ComponentScan扫描到的,那么此时的加载顺序是:
A > D > F > B > E > G > C

如果A是通过@Import形式引入的,那么此时的加载顺讯是:
D > F > B > E > G > A > C

当然以上仅仅代表着加载Bean Class的顺序,实际实例化Bean的顺序和加载顺序大体相同,但还是会有一些差别。
Spring在通过getBean(beanName)形式实例化Bean时,会通过BeanDefinition去生成Bean对象。在这个过程中,如果BeanDefinition的DependsOn不为空,从字面理解就是依赖某个什么,其值一般是某个或多个beanName,也就是说依赖于其他Bean,此时Spring会将DependsOn指定的这些名称的Bean先实例化,也就是先调用getBean(dependsOn)方法。我们可以通过在Bean Class或者@Bean的方法上标识@DependsOn注解,来指定当前Bean实例化时需要触发哪些Bean的提前实例化。

当一个Bean A内部通过@Autowired或者@Resource注入Bean B,那么在实例化A时会触发B的提前实例化,此时会注册A>B的dependsOn依赖关系,实质和@DependsOn一样,这个是Spring自动为我们处理好的。

了解Spring Bean的解析,加载及实例化的顺序机制能够加深对Spring的理解,搭建更优雅简介的Spring框架。

spring初始化顺序的更多相关文章

  1. 一次Spring Bean初始化顺序问题排查记录

    最近在使用Springboot的时候需要通过静态的方法获取到Spring容器托管的bean对象,参照一些博文里写的,新建了个类,并实现ApplicationContextAware接口.代码大致如下: ...

  2. Spring boot变量的初始化顺序

    起因是Spring建议”总是在您的bean中使用构造函数建立依赖注入.总是使用断言强制依赖”,而且之前用@Autowired时idea总是给警告,于是全部改成了构造器注入,运行时发生了循环注入,于是找 ...

  3. Spring初始化ApplicationContext为null

    1. ApplicationContextAware初始化 通过它Spring容器会自动把上下文环境对象调用ApplicationContextAware接口中的setApplicationConte ...

  4. SpringBoot系列教程之Bean之指定初始化顺序的若干姿势

    上一篇博文介绍了@Order注解的常见错误理解,它并不能指定 bean 的加载顺序,那么问题来了,如果我需要指定 bean 的加载顺序,那应该怎么办呢? 本文将介绍几种可行的方式来控制 bean 之间 ...

  5. spring初始化源码浅析之关键类和扩展接口

    目录 1.关键接口和类 1.1.关键类之 DefaultListableBeanFactory 1.2.关键类之XmlBeanDefinitionReader 1.3.关键类之ClassPathXml ...

  6. java类的初始化顺序

    在java中,当我们new一个对象时,对象中的成员,初始化块以及构造方法的加载是有一定的顺序的,看下面一副图: 一.单类(无基类)下的初始化顺序: public class Parent { stat ...

  7. java类初始化顺序

    一.概述 了解类的初始化顺序,可以更灵活.方便的构造一个类. 二.类初始化顺序 2.1 示例 public class InitialOrderTest { public static void ma ...

  8. java 初始化顺序

    java 变量类型如下: 实例变量: 类变量: 初始化途经如下: 实例变量 --声明时,初始化: --非静态初始化块内,初始化: --构造函数内,初始化: 实例1: public class bean ...

  9. Java初始化顺序

    package com.helei.init; class Log { public static String fatherStaticString(){System.out.println(&qu ...

随机推荐

  1. zabbix_get 命令介绍

    zabbix_get 是 zabbix 服务端的一个命令,用于检测 agent 端的配置是否正确,可以很方便地知道 key 是否能正常获取到数据,在测试自定义监控的时候特别有用 [root@crazy ...

  2. 洛谷 4035 [JSOI2008]球形空间产生器

    题目戳这里 一句话题意 给你 n+1 个 n 维点,需要你求出这个n维球的球心.(n<=10) Solution 这个题目N维的话确实不好想,反正三维就已经把我搞懵了,所以只好拿二维类比. 首先 ...

  3. VIM复制粘贴 的快捷键是什么

    yy复制游标所在行整行.或大写一个Y. 2yy或y2y复制两行. ㄟ ,请举一反三好不好! :-) y^复制至行首,或y0.不含游标所在处字元. y$复制至行尾.含游标所在处字元. yw复制一个wor ...

  4. 牛客练习赛13 B 幸运数字Ⅱ 【暴力】【二分】

    题目链接 https://www.nowcoder.com/acm/contest/70/B 思路 没有代码限制 先打表 打出 幸运数字的表 然后 二分查找 第一个 大于 r 的幸运数字 然后 往 L ...

  5. java搭建 SpringMVC+Mybatis(SMM)+mybatis-generate

    搭建SSM系统,首先要了解整个过程: 1.创建spring-mvc项目 2.在maven中添加要引用的jar包(使用框架都是较新的版本:) 3. jdbc.xml +spring-mybatis.xm ...

  6. 【leetcode刷题笔记】Edit Distance

    Given two words word1 and word2, find the minimum number of steps required to convert word1 to word2 ...

  7. java常用注解(更新中)

    注解根据来源可分为: 系统注解(自带的,取决于JDK版本).自定义注解及第三方注解 系统注解根据用途又可分为: java内置注解和元注解 根据运行机制(保留到什么时候)可分为: 源码注解.编译注解和运 ...

  8. 算法(Algorithms)第4版 练习 2.1.24

    代码实现: package com.qiusongde; import edu.princeton.cs.algs4.In; import edu.princeton.cs.algs4.StdOut; ...

  9. POJ 1679 The Unique MST:次小生成树【倍增】

    题目链接:http://poj.org/problem?id=1679 题意: 给你一个图,问你这个图的最小生成树是否唯一. 题解: 求这个图的最小生成树和次小生成树.如果相等,则说明不唯一. 次小生 ...

  10. Struts调用DMI

    <constant name="struts.enable.DynamicMethodInvocation" value="true"/>