Spring之手写模拟Spring Bean的创建

543 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详情

一、前言

在前面, 我们通过文章Spring底层核心原理解析知道了在Spring中 bean的创建大致经历了以下步骤

  • 通过推断构造方法来实例化一个对象
  • 对该对象进行依赖注入(属性赋值)
  • 依赖注入后,Spring会判断该对象是否实现了BeanNameAware接口、BeanClassLoaderAware接口、BeanFactoryAware接口,如果实现了,就表示当前对象必须实现该接口中所定义的setBeanName()、setBeanClassLoader()、setBeanFactory()方法,那Spring就会调用这些方法并传入相应的参数(Aware回调
  • 初始化前( @PostConstruct)
  • 初始化(继承InitializingBean类)
  • 初始化后(AOP)

本篇文章会手写模拟实现spring创建bean的过程(简单实现):

  • 推断构造方法(只有无参)
  • 依赖注入
  • 初始化
  • 初始化后(AOP)

Bean的种类如下:

  • 单例Bean: 默认的bean
  • 懒加载Bean: @Lazy, context.getBean()的时候才会去加载
  • 原型Bean: @Scope("prototype") 每次getBean的时候都会返回一个新的bean对象

bean的创建

在main方法中, 主要的是以下两行代码

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        NingxuanService ningxuanService = (NingxuanService)context.getBean("ningxuanService");
  • 所有的单例bean会在执行第一行代码, new AnnotationConfigApplicationContext()的时候加载出来
  • 所有懒加载的bean会在执行第二行代码的时候加载出来

环境搭建

pom, 仅留java 1.8

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.ningxuan</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>
</project>

项目结构如下

  • spring目录: 手写spring相关代码
  • ningxuan目录: 测试目录
  • service: 业务层
  • Test: main方法

二、完整代码

具体实现可定位到标题三、具体实现

项目结构

image.png

具体代码请进入gitee仓库查看

三、具体实现

1、bean加载第一步: 获取所有的bean并加载到bean存储池

main方法中 创建非懒加载的单例bean

NingxuanApplicationContext context = new NingxuanApplicationContext(AppConfig.class);

主要思路为:

  • 获取扫描路径
    • (ComponentScan) appConfig.getAnnotation(ComponentScan.class)
  • 取出扫描路径下的类
    • File file = new File(resource.getFile());
    • 测试结果在下方
  • 遍历路径下的类, 判断是否有@Component注解
    • clazz.isAnnotationPresent(Component.class)
  • 获取beanName
    • String beanName = clazz.getAnnotation(Component.class).value();
  • 生成 beanDefinition
    • BeanDefinition beanDefinition = new BeanDefinition();
  • 判断是否为单例Bean修改 beanDefinition的 Scope属性
    • if (clazz.isAnnotationPresent(Scope.class))
  • 将 beanName和 beanDefinition加入 beanDefinitionMap Bean存储池中存储
    • beanDefinitionMap.put(beanName, beanDefinition);

测试扫描路径结果:

image.png

单例Bean执行结果, 多次执行 Bean的地址不变

image.png

原型Bean执行结果, 每次getBean之后地址都发生改变

image.png

2、创建加载单例bean

bean存储池的存储结构如下:

private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();

存储池的 key为 beanName, value为 BeanDefinition, BeanDefinition的数据结构如下

image.png

所以, 我们想要获取所有的单例bean只要遍历我们的 bean存储池就可以了, 具体过程如下:

  • 遍历bean存储池
    • for (Map.Entry<String, BeanDefinition> entry : beanDefinitionMap.entrySet())
  • 判断是否为 单例
    • definition.getScope().equals("singleton")
  • 通过单例BeanDefinition生成 bean对象
    • Object bean = createBean(beanName, definition);
  • 将bean对象放入单例bean存储池中
    • singletonObjects.put(beanName, bean);

bean对象生成主要看下图就可以了

image.png

bean的默认名称

我们声明类为bean的时候通常会使用 @Component注解

image.png

这个时候Spring会自动为我们的bean起名为首字母小写的名称, 如上图为我们生成的bean名称为: 'orderService', 具体实现在 scan方法中, 如下图所示:

image.png

3、getBean(String beanName)方法

getBean(String beanName)方法放到这里边讲解, 因为下面依赖注入会用到

getBean方法的具体实现是:

  • 通过beanName去我们的 bean存储池(beanDefinitionMap)中查找
  • 若不存在则抛出异常
  • 若存在则判断是否为单例bean
  • 若为单例bean则查找单例bean存储池 singletonObjects 返回value
  • 若不为单例bean则通过 createBean(String beanName, BeanDefinition beanDefinition)方法创建bean实例, 如下图所示 image.png

getBean方法如下图所示

image.png

4、依赖注入(最简单的实现, 通过属性名字去查找)

依赖注入主要流程:

  • 取出类中的属性
  • 判断是否有注解 @Autowired
  • 开启反射
  • 在 getBean中找出 bean对象
    • getBean(String beanName)方法实际上就是在 beanDefinitionMap存储池中查找bean是否存在, 具体后面再讲

image.png

上边我们贴过getBean(String beanName)方法的截图了, 这里就不水图了.

但是我们的getBean代码并不完善, 当出现如下图的情况, 我们先创建了 JuejinService的bean对象, 当依赖注入orderService这个bean且这个bean为单例bean的时候, 我们的bean存储池中可能还没有对其进行添加, 就会爆出空指针异常

throw new NullPointerException();

image.png

这是我们要对getBean(String beanName)方法进行更改, 手动添加这个bean

image.png

依赖注入注意

  • 所有被 @Component注解修饰的类都会遍历添加进 bean存储池(beanDefinitionMap)
  • 在遍历 bean存储池(beanDefinitionMap)生成单例bean对象的时候可能会存在依赖也为单例bean, 但是还未被添加进 单例bean存储池(singletonObjects)中的情况
  • 这个时候我们要手动创建添加进去, 防止依赖注入失败

5、初始化

我们新建 InitializingBean 接口, 里面有一个方法为afterPropertiesSet

image.png

具体流程: 在创建bean的方法 createBean()中判断是否实现了 InitializingBean 接口, 若实现了则执行相应的方法

image.png

6、AOP(简单实现)

还是一样的, 我们创建一个接口, 里面两个方法, 一个是执行前的, 一个执行后的

image.png 在我们初始化bean存储池的时候就去判断, 我们的bean类是否实现了 BeanPostProcessor接口 如果实现了, 则加入缓存

private List<BeanPostProcessor> beanPostProcessorList = new ArrayList<>();

同时在我们的 createBean 方法执行的时候去对我们的缓存进行一个遍历, 去执行aop的相关方法

image.png

我这边也新建了一个类去实现 BeanPostProcessor 接口, 具体的实现代码都是在 NingxuanPostProcessor 中做的, 这边我没有去做

流程大概是:

  • 根据传入的 beanName执行方法, 然后返回回来

image.png

三、总结

回到我们的最初研究Spring的bean加载中, 现在我们可以对每一行代码进行详细的解答了

第一行代码:

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

他主要做的几件事:

  • 扫描注解定义包下的类
    • 扫描过程中把所有的bean放入 bean存储池
    • 扫描过程中将实现了 BeanPostProcessor 接口的类放入缓存池中
  • 遍历 bean存储池, 对单例bean创建bean对象, 并添加进 单例bean存储池中
    • 创建bean对象过程中进行
    • 获取实例对象
    • 依赖注入
    • 初始化
    • AOP

以上就是我本期的全部内容了, 具体代码可查看代码库

本文内容到此结束了

如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。

如有错误❌疑问💬欢迎各位大佬指出。

我是 宁轩 , 我们下次再见