Spring的核心模块:Bean的生命周期(内含依赖循环+业务场景)。
创始人
2024-06-01 08:34:30
0

Bean的生命周期

    • 前言
    • 为什么要学习Bean的生命周期
    • 前置知识
      • Spring Post-processor(后置处理器)
      • Aware接口
    • 简单介绍
    • Bean的实例化过程
      • 为什么会有bean的实例化?
      • 过程
    • Bean的初始化阶段
      • 为什么会有Bean的初始化?
      • Bean的初始化目的是什么?
    • 依赖注入
      • 基本概念
      • 依赖注入的几种方式
      • 重点问题——依赖循环
        • 那么,为什么会出现循环引用问题呢?
      • 如何解决Spring依赖循环问题
        • Spring自身的策略——提供三级缓存
      • 生命周期总结
    • 总结

前言

上篇文章我们介绍了Spring的一个核心模块BeanDifinition。
那么趁热打铁,我们再来复习一下Bean的整个生命周期是如何工作的。
Spring具有高度的灵活性和可扩展性,在Spring框架中,有一些对象是由Spring容器管理的,而这些对象的生命周期受Spring容器的管理。
我们先回进行一些前置知识的学习,帮助我们更好的理解。
您的关注是我最大的动力,我会不断打磨自己的笔记和知识点,希望可以一直给您带来帮助,关注交个朋友吧。

为什么要学习Bean的生命周期

在 Spring 框架中,Bean 是一个非常重要的概念,Bean 是 Spring 中的基本组件,它们由 Spring 容器创建、管理和维护,Bean 的生命周期是在容器中创建、初始化、使用和销毁的过程。
理解 Bean 的生命周期可以帮助开发人员更好地利用 Spring 框架,更好地管理和控制应用程序的组件。
具体而言呢,学习Bean的生命周期可以帮助我们理解:
1.理解 Spring 容器如何创建、管理和维护 Bean,从而可以更好地设计和实现应用程序中的组件。
2.理解 Bean 生命周期中各个阶段的意义,从而可以在 Bean 的生命周期的不同阶段进行一些必要的处理,比如在初始化阶段执行一些特定的操作。
3.学习如何自定义 Bean 生命周期中的各个阶段,从而可以控制 Bean 的创建和销毁过程。
后面等我们学习后,这三点我们会通过业务场景分别举例说明,确保我们已经掌握了。

前置知识

Spring Post-processor(后置处理器)

如果不太了解。麻烦先看这篇文章:后置处理器

Aware接口

如果不太了解。麻烦先看这篇文章:Aware接口

如果这两块不太了解,务必去看一下,大概会花费30分钟时间去理解,磨刀不误砍柴工。

简单介绍

Spring的生命周期是从Bean实例化之后,即通过反射创建出对象之后,到Bean成为一个完整对象,最终存储到单例池中,这个过程被称为Spring Bean的生命周期。Spring Bean的生命周期大体上分为三个阶段:

  • Bean的实例化阶段:Spring框架会取出BeanDefinition信息进行判断当前Bean的范围是否是singleton的,是否是延迟加载的,是否是FactoryBean等,最终将一个普通的singleton的Bean通过反射进行实例化;
  • Bean的初始化阶段:Bean创建之后还仅仅是个“半成品”,还需要对Bean实例的属性进行填充,执行一些Aware接口方法,执行BeanPostProcessor方法,执行InitializingBean接口的初始化方法,执行自定义初始化init方法等。该阶段是Spring最具技术含量和复杂度等阶段,Aop增强功能,后面的Spring注解功能等。
  • Bean的完成阶段:经过初始化阶段,Bean就成了一个完整的SpringBean,被存储到单例池singletonObjects中去了,即完成了SpringBean的整个生命周期。

Bean的实例化过程

为什么会有bean的实例化?

Bean的实例化是创建Bean对象,并将其放置在Sprig容器中,以便在需要时可以使用它们。
Bean的实例化使得开发者可以通过声明Bean定义来描述Bean的行为和属性,无需关心具体的实例化过程。

过程

Spring容器根据Bean的定义读取配置文件或者注解信息,找到Bean的定义。
2.
创建BeanDefinition对象:在加载配置文件后,Spring会为每个Bean定义一个BeanDefinition对象。BeanDefinition对象包含了Bean的信息,例如Bean的类名、依赖关系等。
3.
实例化Bean:在BeanDefinition对象创建完成后,Spring会使用BeanDefinition对象中的信息来创建Bean实例。Bean实例化的方式有多种,例如通过构造函数、工厂方法、对象池等方式来创建Bean实例。。
4.
如果Bean的作用域为singleton且不是懒加载模式,Spring容器会在容器启动时进行实例化。
如果作用域为prototype或者是singleton 且为懒加载模式,Spring容器会在第一次请求该Bean的时候进行实例化。
换句话说:Bean实例化后,Spring会根据Bean的作用域(scope)进行相应的处理,如果Bean的作用域是singleton,Spring会在容器初始化时创建Bean实例并将其放入容器中;如果Bean的作用域是prototype,Spring会在每次请求时创建新的Bean实例。

1.Bean的定义:
假设有一个名为 “userService” 的 Bean,在配置文件中的定义如下所示:



2.Bean的实例化
当容器启动时,如果 “userService” Bean 是 singleton 作用域且不是懒加载模式,则容器会进行实例化。
如果是 prototype 作用域或者是 singleton 且懒加载模式,则容器会在第一次请求该 Bean 的时候进行实例化。
3.依赖注入
在上述 Bean 定义中,“userService” Bean 的构造函数需要一个名为 “userDao” 的依赖对象。Spring 容器会自动进行依赖注入,例如在上述例子中,容器会先实例化名为 “userDao” 的 Bean,然后将该 Bean 注入到 “userService” Bean 的构造函数中。

Spring 容器在销毁 MyBean 实例之前,会调用其 cleanup() 方法进行清理操作。我们需要在 Bean 类中定义该方法。注意,如果 Bean 同时实现了 DisposableBean 接口和指定了 destroy-method 属性,则 destroy-method 属性中指定的方法会覆盖接口中定义的方法。

总体来说:
Bean 的实例化过程比较复杂,其中涉及到多个步骤和相关的接口和类。这些步骤和接口可以帮助我们在 Bean 的生命周期中执行自定义的逻辑和操作,增加了 Spring 框架的灵活性和扩展性。

Bean的初始化阶段

为什么会有Bean的初始化?

在Spring框架中,Bean的初始化是必要的,因为Spring的核心思想是IoC(控制反转)和DI(依赖注入)。在IoC中,对象的创建和维护不再由应用程序负责,而是由框架来完成。这意味着,应用程序只需要声明它需要哪些对象,而不必关心它们的创建和维护。

为了实现这个目标,Spring容器需要能够创建和管理所有声明的Bean。在容器启动时,Spring会读取配置文件(或注解)中的Bean定义,并创建相应的Bean实例。这个过程称为Bean的初始化。
Bean的初始化是Spring框架实现IoC和DI的必要步骤之一。

Bean的初始化目的是什么?

初始化节点:Bean实例化之后,属性注入之前,以及使用之前执行的操作。
目的:使用Bean之前,对Bean进行一些配置,检查,预处理等操作,以确保能够正确地工作。
意义:Bean的初始化给了我们一个机会,可以在Bean初始化之前或者之后对Bean进行一些扩展操作(例如:实现BeanPostProcessor接口的后置处理器,可以在Bean实例化后、属性注入前或后、初始化方法前或后等各个阶段对Bean进行操作。)。
大致的流程为:
1.Bean实例的属性填充:
当Spring容器创建一个bean实例时,它会自动注入该实例的依赖项,这些依赖项可以是其他的bean实例或者是基本类型的值。属性注入可以通过构造函数注入、setter方法注入或字段注入来完成。
2.Aware接口属性注入:
当一个bean实现了Aware接口时,Spring容器在创建bean的过程中会自动调用相应的接口方法,并将相关的依赖项传递给这些方法。这样可以让bean在创建时得到一些额外的信息,例如ApplicationContext、BeanFactory等。
3.BeanPostProcessor的before()方法回调
4.InitializingBean接口的初始化方法回调
5.自定义初始化方法init回调
6.BeanPostProcessor的after()方法回调

针对于上面的每个步骤,我们来看个例子就可以更加了解了。
1.
a. 构造函数注入:

public class Person {private String name;private int age;public Person(String name, int age) {this.name = name;this.age = age;}// getters and setters
}

b.setter方法注入:

public class Person {private String name;private int age;public void setName(String name) {this.name = name;}public void setAge(int age) {this.age = age;}// getters
}


上面的例子中,我们定义了一个名为"person"的bean实例,并且通过字段注入了name和age属性。注入是通过使用@Value注解和XML属性元素的value属性来完成的。

注意,这些示例只是展示了属性填充的一些常见方式,实际上Spring还提供了其他的属性填充方式,例如注解注入和自动装配。
spring的自动装配,后面boot的时候我们会详细说。感兴趣可以点个关注哈哈。
3.字段注入

public class Person {@Value("John Doe")private String name;@Value("30")private int age;// getters
}


2.Aware接口注入
如果 Bean 实现了 Aware 接口,例如 BeanNameAware 接口,则 Spring 容器会调用相应的方法将容器中的信息注入到该 Bean 中。例如:

public class MyBean implements BeanNameAware {private String beanName;@Overridepublic void setBeanName(String beanName) {this.beanName = beanName;}
}

当容器实例化 “MyBean” Bean 时,会调用 setBeanName() 方法将该 Bean 在容器中的名称注入到 “beanName” 属性中。
3.BeanPostProcessor前置处理
如果 Bean 实现了 BeanPostProcessor 接口,例如:

public class MyBeanPostProcessor implements BeanPostProcessor {@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {// 在 Bean 初始化之前进行处理return bean;}
}

则 Spring 容器会在 Bean 实例化后,调用其 postProcessBeforeInitialization() 方法进行前置处理。例如可以在该方法中修改 Bean 的属性值或者生成代理对象等。
4.初始化方法
如果 Bean 实现了 InitializingBean 接口,例如:

public class MyBean implements InitializingBean {@Overridepublic void afterPropertiesSet() throws Exception {// 在 Bean 初始化完成之后进行操作}
}

或者指定了自定义的初始化方法,例如:



则 Spring 容器会在 Bean 实例化后,调用其 afterPropertiesSet() 方法或者指定的自定义初始化方法进行初始化操作。
6.BeanPostProcessor后置处理
如果 Bean 实现了 BeanPostProcessor 接口,例如:

public class MyBeanPostProcessor implements BeanPostProcessor {@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {// 在 Bean 初始化完成之后进行处理return bean;}
}

则 Spring 容器会在 Bean 初始化之后,调用其 postProcessAfterInitialization() 方法进行后置处理。例如可以在该方法中为 Bean 增加一些功能或者修改其属性值等。
8.DisposableBean

public class MyBean implements DisposableBean {@Overridepublic void destroy() throws Exception {// 在 Bean 销毁之前进行清理操作}
}

当 Spring 容器销毁 MyBean 实例时,会调用其 destroy() 方法进行清理操作。开发人员可以在该方法中进行一些清理工作,例如关闭数据库连接、释放资源等。

另外,在 XML 配置文件中也可以通过指定 destroy-method 属性来定义 Bean 的销毁方法,例如:


依赖注入

基本概念

依赖注入(Dependency Injection,DI)是一种设计模式,它的核心思想是通过依赖将依赖关系从应用程序代码中抽离出来,由容器来负责和实例化和管理对象之间的依赖关系。
优点:
1.可测试性:通过将依赖项注入到组件中,可以轻松地模拟和替换这些依赖项,从而使得测试变得更加容易。
2.可扩展性:通过注入依赖项,组件的实现可以从外部配置和管理,从而使得应用程序更加灵活和可扩展。
3.可维护性:通过解耦组件之间的依赖关系,可以使得代码更加易于维护和修改。
4.可读性:通过使用依赖注入,可以使得组件之间的依赖关系更加明确和易于理解。

依赖注入的几种方式

Spring在进行属性填充的时候,会分如下几种情况:

  • 注入普通属性:
    String,int或存储基本类型的集合时,直接通过set方法的反射设置进去;
  • 注入单向对象引用属性:
    从容器中getBean获取后通过set方法反射设置进去,如果容器中没有,则先创建被注入对象Bean实例(完成整个生命周期)后,再进行注入操作;
  • 注入双向对象引用属性时:
    比较复杂了,双向对象引用属性指的是两个对象之间相互引用。涉及到了循环引用(循环依赖)问题,下面我们详细来聊一下这个问题。

需要注意的是,在使用Spring进行属性注入时,应该尽量避免使用双向对象引用属性,因为这样可能会导致复杂度的增加,代码难以维护。同时,需要根据实际情况选择合适的注入方式,保证代码的可读性和可维护性。

我们对于上面三种情况分别举出例子来说明:
1.注入普通属性的示例:

public class User {private String name;private int age;public void setName(String name) {this.name = name;}public void setAge(int age) {this.age = age;}// ...
}


2.注入单向对象引用属性的示例:

public class Order {private User user;public void setUser(User user) {this.user = user;}// ...
}





3.注入双向对象引用属性的示例:

public class Department {private List employees;public void addEmployee(Employee employee) {employees.add(employee);employee.setDepartment(this);}// ...
}
public class Employee {private Department department;public void setDepartment(Department department) {this.department = department;}// ...
}




Department和Employee对象之间形成了双向引用关系,Department持有一个Employee的List,Employee持有一个Department的引用。

重点问题——依赖循环

那么,为什么会出现循环引用问题呢?

循环引用问题通常出现在双向引用关系中,例如A对象持有B对象的引用,B对象持有A对象的引用。在这种情况下,如果不加以限制,可能会导致对象的创建和销毁出现问题。
请添加图片描述

具体来说,当Spring容器在创建A对象时,需要先创建B对象并将B对象注入到A对象中。而在创建B对象时,又需要先创建A对象并将A对象注入到B对象中。这样就会形成一个循环依赖的情况,导致对象无法创建成功。

为了避免循环引用问题,可以使用一些技巧来限制对象之间的依赖关系。
例如,可以使用@Lazy注解来延迟注入,或者使用Setter方法注入来解决循环引用问题,当然spring自带的也有缓存也是解决方案。同时,需要根据实际情况选择合适的注入方式,保证代码的可读性和可维护性。
请添加图片描述

为了更好懂一些,我们举一个订单系统的 业务场景:
假设有一个简单的订单系统,包括订单(Order)和用户(User)两个实体类。
其中,一个订单只属于一个用户,而一个用户可以有多个订单。
因此,订单类需要持有一个用户对象的引用,用户类需要持有一个订单列表的引用。

代码如下:

public class Order {private User user;public void setUser(User user) {this.user = user;}// ...
}public class User {private List orders;public void setOrders(List orders) {this.orders = orders;}// ...
}

如果使用Spring进行属性注入时,可能会出现循环引用问题。具体来说,当Spring容器在创建User对象时,需要先创建Order对象并将Order对象添加到User对象的订单列表中。而在创建Order对象时,又需要先创建User对象并将User对象注入到Order对象中。这样就形成了一个循环依赖,导致对象创建失败。

如何解决Spring依赖循环问题

Spring自身的策略——提供三级缓存

在Bean实例属性填充上:
Spring提供了三级缓存存储完整Bean实例半程品Bean实例,用于解决循环引用问题。
DefaultListableBeanFactory的上四级父类DefaultSingletonBeanRegistry中提供了如下三个Map:

/** Cache of singleton objects: bean name to bean instance. */
//1.存储单例Bean成品的容器,即实例化和初始化都完成的Bean,称之为“一级缓存”private final Map singletonObjects = new ConcurrentHashMap<>(256);/** Cache of singleton factories: bean name to ObjectFactory. */
//2.单例Bean的工厂池,缓存半成品对象,对象未被引用,使用时再通过工厂创建Bean,称之为“三级缓存”private final Map> singletonFactories = new HashMap<>(16);/** Cache of early singleton objects: bean name to bean instance. */
//3.早期Bean单例池,缓存半成品对象,且当前对象已经被其他对象引用了,称之为“二级缓存”。private final Map earlySingletonObjects = new ConcurrentHashMap<>(16);

我们首先给一个图来看:
在这里插入图片描述
这里简单聊一下大概的过程:
1.首先一个userService实例化创建完毕之后,并不是直接存到singletonFactories三级缓存里面,这个userService会用ObjectFactory进行一个包装,换句话说就是为它创建一个ObjectFactory,然后添加到三级缓存中,我们来看一下这个类:

@FunctionalInterface
public interface ObjectFactory {/*** Return an instance (possibly shared or independent)* of the object managed by this factory.* @return the resulting instance* @throws BeansException in case of creation errors*/T getObject() throws BeansException;}

所以等到我们真正去用的时候,再去调用里面的getObject方法。
我们也很清楚的明白,这个bean是未被引用的。
2.我们判断容器中是否有UserDao时,原先是从单例池找,但现在有了三级缓存(这里指的是三个缓存,即:一级,二级,三级),就会把三级缓存整个找下来看有没有UserDao。
3.找不到后,进去下半部分的路线,直到执行到判断(容器中是否有UserService),发现第三级缓存(这里只单指第三级缓存)之前存过了,那么就进行注入。
4.UserService被注入了,那么就放入到二级缓存

完整流程如下:

  1. UserService实例化对象,但还没有初始化,将UserService存储到第三级缓存。
  2. UserService属性注入,需要UserDao,从缓存中获取,没有UserDao;
  3. UserDao实例化对象,但尚未初始化,将UserDao存储到三级缓存;
  4. UserDao属性注入,需要UserService,从三级缓存获取UserService,UserService从三级缓存移入二级缓存。
  5. UserDao执行其他生命周期过程,最终成为一个完成Bean,存储到一级缓存,删除二三级缓存。
  6. UserService注入UserDao。
  7. UserService执行其他生命周期过程,最终成为一个完成Bean,存储到一级缓存,删除二三级缓存。

这块目前我们只说原理,源码后面等我们聊完refresh后,我们再一块梳理一下整个Bean生命周期的源码流程。

生命周期总结

一张图,无需多言,这个老师真的非常牛!!!!非常清晰。
如果觉得哪里看不明白,欢迎留言。
请添加图片描述

总结

Spring Bean生命周期上文涉及的大多是原理,等到后面源码篇时,我们会聊里面具体的代码实现。如果你对后面的内容感兴趣,麻烦点个关注,后面更新后方便更快查看。

相关内容

热门资讯

张岱的人物生平 张岱的人物生平张岱(414年-484年),字景山,吴郡吴县(今江苏苏州)人。南朝齐大臣。祖父张敞,东...
西游西后传演员女人物 西游西后传演员女人物西游西后传演员女人物 孙悟空 六小龄童 唐僧 徐少华 ...
名人故事中贾岛作诗内容简介 名人故事中贾岛作诗内容简介有一次,贾岛骑驴闯了官道.他正琢磨着一句诗,名叫《题李凝幽居》全诗如下:闲...
和男朋友一起优秀的文案? 和男朋友一起优秀的文案?1.希望是惟一所有的人都共同享有的好处;一无所有的人,仍拥有希望。2.生活,...
戴玉手镯的好处 戴玉手镯好还是... 戴玉手镯的好处 戴玉手镯好还是碧玺好 女人戴玉?戴玉好还是碧玺好点佩戴手镯,以和田玉手镯为佳!相嫌滑...
依然什么意思? 依然什么意思?依然(汉语词语)依然,汉语词汇。拼音:yī    rán基本解释:副词,指照往常、依旧...
高尔基的散文诗 高尔基的散文诗《海燕》、《大学》、《母亲》、《童年》这些都是比较出名的一些代表作。
心在飞扬作者简介 心在飞扬作者简介心在飞扬作者简介如下。根据相关公开资料查询,心在飞扬是一位优秀的小说作者,他的小说作...
卡什坦卡的故事赏析? 卡什坦卡的故事赏析?讲了一只小狗的故事, 我也是近来才读到这篇小说. 作家对动物的拟人描写真是惟妙...
林绍涛为简艾拿绿豆糕是哪一集 林绍涛为简艾拿绿豆糕是哪一集第三十二集。 贾宽认为是阎帅间接导致刘映霞住了院,第二天上班,他按捺不...
小爱同学是女生吗小安同学什么意... 小爱同学是女生吗小安同学什么意思 小爱同学,小安同学说你是女生。小安是男的。
内分泌失调导致脸上长斑,怎么调... 内分泌失调导致脸上长斑,怎么调理内分泌失调导致脸上长斑,怎么调理先调理内分泌,去看中医吧,另外用好的...
《魔幻仙境》刺客,骑士人物属性... 《魔幻仙境》刺客,骑士人物属性加点魔幻仙境骑士2功1体质
很喜欢她,该怎么办? 很喜欢她,该怎么办?太冷静了!! 太理智了!爱情是需要冲劲的~不要考虑着考虑那~否则缘...
言情小说作家 言情小说作家我比较喜欢匪我思存的,很虐,很悲,还有梅子黄时雨,笙离,叶萱,还有安宁的《温暖的玄》 小...
两个以名人的名字命名的风景名胜... 两个以名人的名字命名的风景名胜?快太白楼,李白。尚志公园,赵尚志。
幼儿教育的代表人物及其著作 幼儿教育的代表人物及其著作卡尔威特的《卡尔威特的教育》,小卡尔威特,他儿子成了天才后写的《小卡尔威特...
海贼王中为什么说路飞打凯多靠霸... 海贼王中为什么说路飞打凯多靠霸气升级?凯多是靠霸气升级吗?因为之前刚到时确实打不过人家因为路飞的实力...
运气不好拜财神有用吗运气不好拜... 运气不好拜财神有用吗运气不好拜财神有没有用1、运气不好拜财神有用。2、拜财神上香前先点蜡烛,照亮人神...
“犹如……,好像……。”如何造... “犹如……,好像……。”如何造句?用“犹如……,好像……。”造句例子如下:岁月犹如长河,人生如同奔流...