Java对象的生命周期

为了更好的理解Bean的声明周期,先来看看不用Spring时,Java对象的生命周期。

下面是一个Java对象创建的语句。

1
A a=new A();

对于普通Java环境下创建对象简要的步骤可以分为:

  1. java源码被编译为被编译为class文件
  2. 等到类需要被初始化时(比如说new、反射等)
  3. class文件被虚拟机通过类加载器加载到JVM

简单来说,可以理解为它是用Class对象作为「模板」进而创建出具体的实例。

注:第3步,其实就是JVM的类加载过程

  1. 加载。(由类加载器完成,双亲委派模型…)
  2. 验证。(防止恶意代码的注入,以及确保类文件的正确性)
  3. 准备。(JVM为类的静态变量分配内存空间(这些内存都将在方法区中分配),并设置默认初始值,但并没有赋值。)
  4. 解析。(将符号引用转化为直接引用)
  5. 类初始化。(对静态变量进行初始化和执行静态代码块)(静态代码块先于构造方法,父类先于子类。)
  6. 卸载。(即被GC的过程)

Spring Bean的生命周期的五个阶段

  1. 实例化Instantiation。
    • Bean的生命周期从实例化开始,这是创建Bean对象的阶段。
    • Spring容器会根据配置信息或注解创建Bean的实例。这通常涉及到使用构造函数或工厂方法来创建对象。
  2. 属性注入Populate。
    • 一旦Bean对象被实例化,Spring容器会注入Bean的属性,也就是依赖注入(DI)的过程。
    • 这个阶段涉及将属性值或引用设置到Bean中,以满足Bean的依赖关系。
  3. 初始化Initialization。
    • 初始化前回调。在Bean的属性被设置之后,Spring容器会调用初始化方法(如果有的话)。可以通过实现InitializingBean接口或在配置中指定自定义的初始化方法
    • 初始化。Bean可以执行一些额外的初始化操作,例如建立数据库连接、打开文件等。
    • 初始化后回调。在Bean的初始化方法被调用之后,Spring容器可以再次回调另一个方法(如果有的话)。可以通过实现InitializingBean接口或在配置中指定自定义的初始化后方法。
  4. 使用。
    • 此时Bean已经完全初始化,并且可以被应用程序的其他部分使用。
  5. 销毁Destruction。
    • 销毁前回调。当Bean不再需要时,Spring容器可以调用销毁前的回调方法(如果有的话)。可以通过实现DisposableBean接口或在配置中指定自定义的销毁前方法。
    • 销毁。这是Bean销毁的阶段,Bean可以执行一些资源清理操作,例如关闭数据库连接、释放文件资源等。
    • 销毁后回调。在Bean销毁之后,Spring容器可以再次回调另一个方法(如果有的话)。可以通过实现DisposableBean接口或在配置中指定自定义的销毁后方法。

Bean的生命周期指的就是在上面三个步骤中后置处理器BeanPostprocessor穿插执行的过程

Spring Bean的生命周期的详细介绍

普通Java对象是以Class对象作为「模板」进而创建出具体的实例。

Spring Bean除了Class对象之外,还会使用BeanDefinition的实例来描述对象的信息。(比如说,我们可以在Spring所管理的Bean有一系列的描述:@Scope、@Lazy、@DependsOn等等)

可以理解为,Class只描述了类的信息,而BeanDefinition描述了对象的信息。

BeanDefinition定义Bean的类信息

首先,Spring在启动的时候需要「扫描」在XML/注解中需要被Spring管理的Bean信息。随后,会将这些信息封装成BeanDefinition,最后会把这些信息放到一个beanDefinitionMap中,其中的key是beanName,value则是BeanDefinition对象。

接着会遍历这个beanDefinitionMap,执行BeanFactoryPostProcessor这个Bean工厂后置处理器的逻辑。

然后到了实例化对象啦。

实例化Instantiation

实例化在Spring里边是通过反射来实现的,一般情况下会通过反射选择合适的构造器来把对象实例化。

但是这里只是创建了对象,还没有属性注入。

比如我的对象是A,而A对象依赖着B对象,这时候的B还是null的.

下一步是属性注入.

属性注入Populate

这里就是Spring中 DI(依赖注入)的过程

可能问到的问题(见附录) :

  • 依赖注入的方式有哪些?区别?
  • 可以在哪些地方使用@Autowired注解?
  • 如何解决依赖循环(三级缓存)?

初始化Initialization

首先判断该Bean是否实现了Aware相关的接口,如果存在则填充相关的资源

Aware接口用于对Spring中Bean的扩展。

Aware相关的接口处理完之后,就会到BeanPostProcessor后置处理器,其中有before和after两个方法,分别在初始化方法之前和之后执行。

BeanPostProcessor后置处理器是AOP实现的关键 ,即使用BeanPostProcessor#after对Bean进行增强。

销毁Destruction

销毁的时候就看有没有配置相关的destroy方法,执行就完事了。

附录

Bean依赖注入的方式有哪些?区别?

注意与Spring管理Bean的方式(xml与注解)区分开。

依赖注入(Dependency Injection,DI)是一种将组件的依赖关系动态注入到组件中的设计模式,它有多种主要的实现方式,每种方式有其自身的优点和用途。以下是依赖注入的主要方式及其区别:

  1. 构造函数注入(Constructor Injection)

    • 工作原理:通过类的构造函数将依赖项传递给需要的组件。

    • 区别:构造函数注入通常用于传递必需的依赖项,确保对象在创建时就具有所有必需的依赖项。这促使了不可变性和清晰的构造函数签名。

    • 示例

      1
      2
      3
      4
      5
      6
      7
      public class ExampleService {
      private final Dependency dependency;

      public ExampleService(Dependency dependency) {
      this.dependency = dependency;
      }
      }
  2. Setter方法注入(Setter Injection)

    • 工作原理:通过提供Setter方法,允许动态设置依赖项。

    • 区别:Setter方法注入提供了更灵活的方式来注入依赖项,可以在运行时更改依赖。这对可选的和可变的依赖项非常有用。

    • 示例

      1
      2
      3
      4
      5
      6
      7
      public class ExampleService {
      private Dependency dependency;

      public void setDependency(Dependency dependency) {
      this.dependency = dependency;
      }
      }
  3. 字段注入(Field Injection)

    • 工作原理:通过直接注入依赖项到类的字段中来实现依赖注入。

    • 区别:字段注入通常用于简化Bean的代码,但可能会降低可测试性,因为字段通常是私有的,难以进行模拟和替换。

    • 示例

      1
      2
      3
      4
      public class ExampleService {
      @Autowired
      private Dependency dependency;
      }
  4. 接口注入(Interface Injection)

    • 工作原理:通过实现一个接口,在接口中定义依赖注入的方法。
    • 区别:接口注入较少使用,通常在一些特殊情况下,例如在Java EE环境中使用javax.annotation.Resource注解。
  5. 方法参数注入

    • 工作原理:通过将依赖作为方法参数传递给需要依赖的方法来实现依赖注入。

    • 区别:这种方式通常用于配置方法,例如@Bean方法或在编程式的ApplicationContext中。

    • 示例

      1
      2
      3
      4
      5
      6
      7
      @Configuration
      public class AppConfig {
      @Bean
      public ExampleService exampleService(Dependency dependency) {
      return new ExampleService(dependency);
      }
      }

每种依赖注入方式都有其自身的优点和适用场景。选择哪种方式通常取决于项目需求、代码清晰性、可测试性和可维护性等因素。通常来说,构造函数注入和Setter方法注入是最常见和常用的方式,用于不同的依赖注入需求。

可以在哪些地方使用@Autowired注解?

  • 可以使用:对成员变量、方法和构造函数进行标注,来完成自动装配的工作。
  • 不可以使用:静态字段或方法、局部变量

如何解决依赖循环(三级缓存)?

如果现在有个A对象,它的属性是B对象,而B对象的属性也是A对象。说白了就是A依赖B,而B又依赖A,Spring是怎么做的?

大致过程

  1. 首先A对象实例化,然后对属性进行注入,发现依赖B对象
  2. B对象此时还没创建出来,所以转头去实例化B对象
  3. B对象实例化之后,发现需要依赖A对象,那A对象已经实例化了嘛,所以B对象最终能完成创建
  4. B对象返回到A对象的属性注入的方法上,A对象最终完成创建

原理(三级缓存)

所谓的三级缓存其实就是三个Map…三级缓存定义:

  • singletonObjects(一级,日常实际获取Bean的地方);

  • earlySingletonObjects(二级,还没进行属性注入,由三级缓存放进来);

  • singletonFactories(三级,Value是一个对象工厂,实例化后初始化之前);

具体的过程:

  1. A对象的创建请求:当应用程序首次请求创建A对象时,Spring会开始创建A对象。
  2. 创建A对象的实例:Spring会创建A对象的一个实例,但在属性注入之前,会将A对象的实例放入一级缓存(singletonObjects)中。
  3. A注入B对象:在A对象创建的过程中,发现需要注入B对象作为属性,Spring会继续创建B对象。
  4. 创建B对象的实例:Spring会创建B对象的一个实例,并将其放入一级缓存中。
  5. B注入A对象:在B对象创建的过程中,发现需要注入A对象作为属性,Spring会继续创建A对象。
  6. 检测到循环依赖:在A对象创建的过程中,当Spring尝试注入B对象时,它会在一级缓存中找到B对象的实例。然后,Spring会检测到B对象依赖A对象,而A对象还未完成初始化,这时会发现循环依赖。
  7. B的初始化:为了解决循环依赖,Spring会将A对象的早期实例(还未完全初始化)放入二级缓存(earlySingletonObjects)中,并将B对象的属性引用指向A对象的早期实例,实现B的初始化,B创建成功,存入一级缓存singletonObjects
  8. 完成A对象的初始化:回到A对象初始化,因为B对象已经创建完成,则可以直接注入B,A创建成功存入一次缓存singletonObjects

为什么要三层?

首先从第三级缓存说起(就是key是BeanName,Value为ObjectFactory)。我们的对象是单例的,有可能A对象依赖的B对象是有AOP的(B对象需要代理)。

假设没有第三级缓存,只有第二级缓存(Value存对象,而不是工厂对象)。

那如果有AOP的情况下,岂不是在存入第二级缓存之前都需要先去做AOP代理?这不合适嘛

这里肯定是需要考虑代理的情况的,比如A对象是一个被AOP增量的对象,B依赖A时,得到的A肯定是代理对象的

所以,三级缓存的Value是ObjectFactory,可以从里边拿到代理对象

而二级缓存存在的必要就是为了性能,从三级缓存的工厂里创建出对象,再扔到二级缓存(这样就不用每次都要从工厂里拿)

即 :

  • 第三级缓存考虑代理

  • 第二级缓存考虑性能