Spring - bean 的加载

一 加载配置文件

xml方式注入

ClassPathXmlApplicationContext(“xxx.xml”)

构造函数重点源码

  1. 构造器(最终会执行该构造器)
	public ClassPathXmlApplicationContext(
			String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
			throws BeansException {

		super(parent);
		setConfigLocations(configLocations);
		if (refresh) {
			refresh(); // 重点方法
		}
	}
  1. 暂停

有空看一看

bean的创建流程

1. 概述

超链接
大佬2

Spring 作为 Ioc 框架,实现了依赖注入,由一个中心化的 Bean 工厂来负责各个 Bean 的实例化和依赖管理。各个 Bean 可以不需要关心各自的复杂的创建过程,达到了很好的解耦效果

两大环节

  1. 解析,读取xml配置,扫描类文件,从配置或者注解中获取Bean的定义信息,注册一些扩展功能
  2. 加载,通过解析完的定义信息获取Bean实例
    在这里插入图片描述

两个概念

  1. 作用域,单例作用域或者原型作用域,单例的话需要全局实例化一次,原型每次创建都需要重新实例化。
  2. 依赖关系,一个 Bean 如果有依赖,我们需要初始化依赖,然后进行关联。如果多个 Bean 之间存在着循环依赖,A 依赖 B,B 依赖 C,C 又依赖 A,需要解这种循环依赖问题。

2. 加载过程简介

  1. 获取 BeanName,对传入的name进行解析,转化为可以从Map中获取到BeanDefinition的bean name
  2. 合并 Bean 定义,对父类的定义进行合并和覆盖,如果父类还有父类,会进行递归合并,以后去完整的Bean定义信息
  3. 实例化,使用构造或者工厂方法创建Bean实例
  4. 属性填充,寻找并且注入依赖,依赖的Bean还会递归调用getBean方法获取
  5. 初始化,调用自定义的初始化方法
  6. 获取最终的Bean,如果是FactoryBean需要调用getObject方法,如果需要类型转换调用TypeConverter进行转化

3. 细节分析

3.1 转化 BeanName

  1. 解析完配置后会创建Map,用beanName作为key,BeanDefinition作为value
  2. BeanFactor.getBean中传入的name,有可能是以下几种情况
  • bean name:可以直接获取到定义BeanDefinition
  • alias name:别名需要转化
  • factorybean name:带&前缀,所以需要去除&前缀

在这里插入图片描述

3.2 合并 RootBeanDefinition

  1. 从配置文件读取到的 BeanDefinition 是 GenericBeanDefinition。它记录了一些当前类声明的属性或构造参数,但是对于父类只用一个parentName来记录
  2. 而在实例化的时候。使用的BeanDefinition是RootBeanDefinition,而非GenericBeanDefinition
  • 如果不存在继承关系,GenericBeanDefinition 存储的信息是完整的,可直接转换为RootBeanDefinition
  • 如果存在继承关系,GenericBeanDefintion存储的是增量信息而不是全量信息
  1. 为了能够正确初始化对象,需要完整的信息才行。需要递归合并父类的定义
    在这里插入图片描述

3.3 处理循环依赖

在这里插入图片描述
循环依赖根据注入的实际分成两种类型

  • 构造器循环依赖,依赖的对象是通过构造器注入的,发生在实例化Bean的时候
  • 设值循环依赖,依赖的对象是通过setter方法传入的,对象已经实例化,发生在属性填充和依赖注入的时候
  1. 如果是构造器循环依赖,本质上是无法解决的
  2. 如果是设置循环依赖,Spring框架只支持单例下的设置循环依赖。Spring通过对还在创建过程中的单例,缓存并提前暴露该单例,使得其他实例可以引用该依赖。

使用了一个 ThreadLocal 变量 prototypesCurrentlyInCreation 来记录当前线程正在创建中的 Bean 对象

	/** Names of beans that are currently in creation. */
	private final ThreadLocal<Object> prototypesCurrentlyInCreation =
			new NamedThreadLocal<>("Prototype beans currently in creation");

3.3.1 原型模式的循环依赖(prototypes)

Spring 不支持原型模式的任何循环依赖。检测到循环依赖会直接抛出BeanCurrentlyInCreationException异常

  • 无论是构造函数的循环依赖还是设置循环依赖,在需要注入依赖的对象时,会继续调用 beanFactory.getBean 去加载对象,形成一个递归操作

  • 而每次调用 beanFactory.getBean 进行实例化前后,都使用了 prototypesCurrentlyInCreation 这个变量做记录。按照这里的思路走,整体效果等同于 建立依赖对象的构造链。

  • prototypesCurrentlyInCreation 中的值的变化如下:在这里插入图片描述

  • 在原型模式下,构造函数循环依赖和设值循环依赖,本质上使用同一种方式检测出来。Spring 无法解决,直接抛出 BeanCurrentlyInCreationException 异常。

3.3.2 单例模式的构造循环依赖

Spring 也不支持单例模式的构造循环依赖,检测到构造循环依赖也会抛出BeanCurrentlyInCreationException异常

  • 加载 A 的单例,和原型模式类似,单例模式也会调用匹配到要使用的构造函数,发现构造函数有参数 B,然后使用 BeanDefinitionValueResolver 来检索 B 的实例,根据上面的分析,继续调用 beanFactory.getBean 方法
  • 拿 A,B,C 的例子来举例 singletonsCurrentlyInCreation 的变化,这里可以看到和原型模式的循环依赖判断方式的算法是一样
    在这里插入图片描述
  • 加载A,记录 singletonsCurrentlyInCreation = [a],构造依赖 B,开始加载 B
  • 加载 B,记录 singletonsCurrentlyInCreation = [a, b],构造依赖 C,开始加载 C
  • 加载 C,记录 singletonsCurrentlyInCreation = [a, b, c],构造依赖 A,又开始加载 A
  • 加载 A,执行到 DefaultSingletonBeanRegistry.beforeSingletonCreation ,singletonsCurrentlyInCreation 中 a 已经存在了,检测到构造循环依赖,直接抛出异常结束操作

3.3.3 单例模式的设置循环依赖

单例模式下,构造函数的循环依赖无法解决,但设值循环依赖是可以解决的。

  • 此处有个重要的设计:提前暴露创建中的单例

在这里插入图片描述

  1. A 创建 -> A 构造完成,开始注入属性,发现依赖 B,启动 B 的实例化
  2. B 创建 -> B 构造完成,开始注入属性,发现依赖 C,启动 C 的实例化
  3. C 创建 -> C 构造完成,开始注入属性,发现依赖 A(假设)

此时在阶段1中,A已经构造完成,Bean对象在堆中也分配好内存了,即使后续往A中填充属性(比如填充依赖的B对象),也不会修改到A的引用地址,所以这个时候,可以先提前拿A实例的引用来先注入到C,先完成C的实例化

  1. C 创建 -> C 构造完成,开始注入依赖,发现依赖 A,发现 A 已经构造完成,直接引用,完成 C 的实例化
  2. C 完成实例化后,B 注入 C 也完成实例化,A 注入 B 也完成实例化

3.3.4实现单例的提前暴露:三级缓存

/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

singletonObjects :单例缓存,存储已经实例化完成的单例
singletonFactories :生产单例的工厂的缓存,存储工厂
earlySingletonObjects:提前暴露的单例缓存,这时候的单例刚刚创建完,但还没有注入依赖

  • 先尝试从singletonObjects或者singletonFactories读取,没有数据,然后尝试singletonFactories读取singletonFactory,执行getEarlyBeanReference获取到引用后,存储到earlySingletonObjects

  • 这个earlySingletonObjects的好处是,如果此时又有其他地方尝试获取未初始化的单例,可从earlySingletonObjects直接取出而不需要再调用getEarlyBeanReference

3.4 创建实例

获取到完整的 RootBeanDefintion 后,就可以拿这份定义信息来实例具体的 Bean

具体实例创建见AbstractAutowireCapableBeanFactory.createBeanInstance,返回Bean的包装类BeanWrapper,一共有三种策略:

  • 使用工厂方法创建, instantiateUsingFactoryMethod
  • 使用有参构造函数构建, autowireConstructor
  • 使用无参构造函数创建, instantiateBean
  1. 使用工厂方法创建,会先使用getBean获取工厂类,然后通过参数找到匹配的工厂方法,调用实例化方法实现实例化,具体见ConstructorResolver.instantiateUsingFactoryMethod
  2. 使用有参构造函数创建,整个过程比较复杂,涉及到参数和构造器的匹配,为了找到匹配的构造器,Spring花了大量的工作,具体见ConstructorResolver.autowireConstructor
  3. 使用无参构造函数创建时最简单的方式,见AbstractAutowireCapableBeanFactory.instantiateBean

这三个实例化方式,最后都会走 getInstantiationStrategy().instantiate(...),可见实现类SimpleInstantiationStrategy.instantiate

  • 虽然拿到了构造函数,但并没有立即实例化,因为用户使用了replace和lookup的配置方法,用到了动态代理加入对应的逻辑。如果没有的话,直接使用反射来创建实例

  • 创建实例后,就可以开始注入属性和初始化等操作

  • 这里的Bean还不是最终的Bean,返回给调用方时,如果是FactoryBean的话需要使用getObject方法来创建实例,见AbstractBeanFactory.getObjectFromBeanInstance,会执行到doGetObjectFromFactoryBean

3.5 注入属性

实例创建完后开始进行属性的注入,如果涉及到外部以来的实例,会自动检索并关联到该当前实例
此处主要的环节是:

  • 应用 InstantiationAwareBeanPostProcessor处理器,在属性注入前后进行处理。假设使用了@Autowire注解,这里会调用到AutowiredAnnotationBeanPostProcessor来对依赖的实例进行检索和注入,它是InstantiationAwareBeanPostProcessor的子类
  • 根据名称或者类型进行自动注入,存储结果到PropertyValues中。
  • 应用PropertyValues,填充到BeanWrapper。这里检索依赖实例的引用的时候,会递归调用BeanFactory.getBean来获得

3.6 初始化

3.6.1 触发Aware

  1. 有时候Bean需要用到容器的一些资源 ,如BeanFactory、ApplicationContext,Spring提供了Aware系列接口来解决这些问题
  • BeanFactoryAware:用来获取BeanFactory
  • ApplicationContextAware:用来获取ApplicationContext
  • ResourceLoaderAware:用来获取ResourceLoadAware
  • ServletContextAware:用来获取ServletContext
  1. Spring在初始化阶段,如果判断Bean实现了这几个接口之一,就会往Bean中注入它关心的资源

3.6.2 触发 BeanPostProcessor(重点)

Spring项目启动时打印日志

  1. 在Bean的初始化前或者初始化后,需要进行一些增强操作,如打日志,做校验,属性修改,耗时检测等。
  2. Spring框架提供了BeanPostProcessor来达成这个目标,比如用@Autowire来声明依赖,就是使用AutowiredAnnotationBeanPostProcessor来实现依赖的查询和注入的,接口定义如下:
public interface BeanPostProcessor {

    // 初始化前调用
    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;

    // 初始化后调用
    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;

}
  1. 实现该接口的Bean都会被Spring注册到beanPostProcessors中,见AbstractBeanFactory
/** BeanPostProcessors to apply in createBean */
private final List<BeanPostProcessor> beanPostProcessors = new ArrayList<BeanPostProcessor>();

只要 Bean 实现了 BeanPostProcessor 接口,加载的时候会被 Spring 自动识别这些 Bean,自动注册,然后在Bean实例化前后,Spring会去调用我们注册的beanPostProcessors把处理器都执行一遍

public abstract class AbstractAutowireCapableBeanFactory ... {
        
    ...
    
    @Override
    public Object applyBeanPostProcessorsBeforeInitialization ... {

        Object result = existingBean;
        for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
            result = beanProcessor.postProcessBeforeInitialization(result, beanName);
            if (result == null) {
                return result;
            }
        }
        return result;
    }

    @Override
    public Object applyBeanPostProcessorsAfterInitialization ... {

        Object result = existingBean;
        for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
            result = beanProcessor.postProcessAfterInitialization(result, beanName);
            if (result == null) {
                return result;
            }
        }
        return result;
    }
    
    ...
}

这里使用了责任链模式,Bean会在处理器链中进行传递和处理。当我们调用BeanFactory.getBean后,执行到Bean的初始化方法AbstractAutowireCapableBeanFactory.initializeBean会启动这些处理器

protected Object initializeBean ... {   
    ...
    wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    ...
    // 触发自定义 init 方法
    invokeInitMethods(beanName, wrappedBean, mbd);
    ...
    wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    ...
}

3.6.3 触发自定义 init

自定义初始化有两种方式可以选择:

  • 实现 InitializingBean。在属性设置完成后再加入自己的初始化逻辑
  • 定义init方法。自定义初始化逻辑

3.7 类型转换

Bean已经加载完成、属性也填充好了,初始化也完成了,在返回给调用者之前,还留有一个机会对Bean实例进行类型的转换。见AbstractBeanFactory.doGetBean

protected <T> T doGetBean ... {
    ...
    if (requiredType != null && bean != null && !requiredType.isInstance(bean)) {
        ...
        return getTypeConverter().convertIfNecessary(bean, requiredType);
        ...
    }
    return (T) bean;
}

4. 小总结

抛开一些细节处理和扩展功能,一个Bean的创建过程无非是:
获取完整定义 -> 实例化 -> 依赖注入 -> 初始化 -> 类型转换

其他

@Resource @Autowired @Autowired

老师兄写的

@Autowired 构造器注入和set方法注入区别

  • @autowired可以写在变量和构造器上,注入bean,但是有的时候写在变量上会报空指针异常,然后通过写在构造器上就解决了此问题。
public class UserController {
    @Autowired
    public UserService userService;
    
    List<User> users = userService.findAll();
    
    UserController() {}
}

这个例子会出现空指针异常,但是换成以下写法就无问题

public class UserController {
    
    public UserService userService;
    
    List<User> users = userService.findAll();
    
    @Autowired
    UserController(UserService userService) {
        this.userService = userService;
    }
}

原因

  • 其实这两种方式都可以使用,但报错的原因是加载顺序的问题,@autowired写在变量上的注入要等到类完全加载完,才会将相应的bean注入,而users变量是在加载类的时候按照相应顺序加载的,所以users变量的加载要早于@autowired变量的加载,那么在类初始化时获取调用userService的findAll()方法,此时他是还没有被注入的,所以报空指针,而使用构造器就在加载类的时候将userService加载了,这样在内部使用userService给user变量赋值就完全没有问题。
  • 如果不使用构造器注入,那么也可以不给users赋值,而是在接下来的代码使用的地方,通过userService.findAll()进行赋值,这时的对userService的使用是在类完全加载之后,即userService被注入了,所以也是可以的。
  • 总之,@Autowired一定要等本类构造完成后,才能从外部引用设置进来。所以@Autowired的注入时间一定会晚于构造函数的执行时间。但在初始化变量的时候就使用了还没注入的bean,所以导致了NPE。若果在初始化其它变量时不使用这个要注入的bean,而是在以后的方法调用的时候去赋值,是可以使用这个bean的,因为那时类已初始化好,即已注入好了。

AA

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值