Spring之B:spring初始化顺序

首先,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框架。

原文地址:https://www.cnblogs.com/duanxz/p/3668423.html