一、引言

在Spring开发的世界中,Bean是每个应用程序的核心。它们就像是微小的齿轮,默默地驱动着整个系统的运行。然而,这些看似简单的Bean背后,隐藏着复杂的作用域与生命周期管理。你是否曾想过,Bean究竟是如何从一个普通的对象,经过各种作用域的洗礼,最终走向销毁的呢?在这篇文章中,我们将揭开Spring Bean的神秘面纱,深入探讨它们从出生到退役的每一步。无论你是初学者,还是经验丰富的开发者,这个探索之旅都将带你领略到Bean的独特魅力与无限可能。准备好了吗?就让我们一起开启这段充满惊喜的技术之旅了恰~


二、Bean 的作用域

2.1、概念

在Spring IoC 与 DI 的那篇博客中,我们知晓了 Spring 是如何帮助我们管理对象的。

  1. 通过 @Controller@Service@Repository@Component@Configuration@Bean 来声明 Bean 对象。
  2. 通过 ApplicationContext 或者 BeanFactory 来获取对象。
  3. 通过 @AutowiredSetter() 方法或者构造方法等来为应用程序注入所依赖的 Bean 对象。

【注】:不清楚的可以去看《IoC 与 DI : 简化 Spring 开发的秘密武器》这篇博客,里面介绍的很详细。

假设我们其中的一个代码是这样的(因为我也找不到之前的代码去哪儿了Spring Bean 的全面指南 - 理解作用域与生命周期_作用域):

DogBean dog = applicationContext.getBean(DogBean.class);
System.out.println(dog);

然后把这段代码改成这样:

Dog dog1 = applicationContext.getBean(Dog.class);
System.out.println(dog1);

Dog dog2 = applicationContext.getBean(Dog.class);
System.out.println(dog2);

然后输出了这一段信息:

Spring Bean 的全面指南 - 理解作用域与生命周期_spring_02

欸,大家发现了,这其实是一串地址信息,并且这一串输出的 bean 对象地址值是一样的,说明每次从 Spring 容器中取出来的对象都是同一个。

这就是 "单例模式" 下产生的效果。单例模式,确保一个类只有一个实例,多次创建也不会创建出多个实例。默认情况下,Spring 容器中的 bean 都是单例的,这种行为模式,我们就称之为 Bean 的作用域。

Bean 的作用域是指 Bean 在 Spring 框架中的某种行为模式。

比如单例作用域,表示的是 Bean 在整个 Spring 中只有一份,它是全局共享的,那么当其他人修改了这个值之后,那么另一个人读取到的就是被修改之后的值。

2.2、六种作用域

在 Spring 中支持六种作用域,后面四种在 Spring MVC 环境才会生效。

  1. singleton:单例作用域
  2. prototype:原型作用域(多例作用域)
  3. request:请求作用域
  4. session:会话作用域
  5. Application:全局作用域
  6. websocket:HTTP WebSocket 作用域

作用域

说明

singleton

每个 Spring IoC 容器内同名称的 bean 只有⼀个实例(单例)(默认)

prototype

每次使用该 bean 时会创建新的实例(非单例)

request

每个 HTTP 请求生命周期内,创建新的实例(web环境中,了解)

session

每个 HTTP Session 生命周期内, 创建新的实例(web环境中, 了解)

application

每个 ServletContext 生命周期内, 创建新的实例(web环境中, 了解)

websocket

每个 WebSocket 生命周期内, 创建新的实例(web环境中, 了解)

首先我们来简单定义一下这个作用域的代码:

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.ApplicationScope;
import org.springframework.web.context.annotation.RequestScope;
import org.springframework.web.context.annotation.SessionScope;

@Component
public class DogBeanConfig {
    @Bean
    public Dog dog() {
        Dog dog = new Dog();
        dog.setName("旺旺");
        return dog;
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    public Dog singleDog() {
        Dog dog = new Dog();
        return dog;
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Dog prototypeDog() {
        Dog dog = new Dog();
        return dog;
    }

    @Bean
    @RequestScope
    public Dog requestDog() {
        Dog dog = new Dog();
        return dog;
    }

    @Bean
    @SessionScope
    public Dog sessionDog() {
        Dog dog = new Dog();
        return dog;
    }

    @Bean
    @ApplicationScope
    public Dog applicationDog() {
        Dog dog = new Dog();
        return dog;
    }
}

这个代码就定义了六种作用域,Dog类这样写:

import lombok.Getter;
import lombok.Setter;
import org.springframework.context.annotation.Configuration;

@Getter
@Setter
@Configuration
public class Dog {
    private String name;
}

然后我们主函数这样写:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/blogger")
public class BlogController {
    @Autowired
    private Dog singleDog;
    @Autowired
    private Dog prototypeDog;
    @Autowired
    private Dog requestDog;
    @Autowired
    private Dog sessionDog;
    @Autowired
    private Dog applicationDog;
    @Autowired
    private ApplicationContext applicationContext;

    @RequestMapping("/single")
    public String single() {
        Dog contextDog = (Dog) applicationContext.getBean("singleDog");
        return "dog:" + singleDog.toString() + " ---||--- contextDog:" + contextDog;
    }

    @RequestMapping("/prototype")
    public String prototype() {
        Dog contextDog = (Dog) applicationContext.getBean("prototypeDog");
        return "dog:" + prototypeDog.toString() + " ---||--- contextDog:" + contextDog;
    }

    @RequestMapping("/request")
    public String request() {
        Dog contextDog = (Dog) applicationContext.getBean("requestDog");
        return "dog:" + requestDog.toString() + " ---||--- contextDog:" + contextDog.toString();
    }

    @RequestMapping("/session")
    public String session() {
        Dog contextDog = (Dog) applicationContext.getBean("sessionDog");
        return "dog:" + sessionDog + " ---||--- contextDog:" + contextDog;
    }

    @RequestMapping("/application")
    public String application() {
        Dog contextDog = (Dog) applicationContext.getBean("applicationDog");
        return "dog:" + applicationDog.toString() + " ---||--- contextDog:" + contextDog.toString();
    }
}

a)单例作用域

首先我们来看第一个单例的 singleton 作用域的,结果如下:

Spring Bean 的全面指南 - 理解作用域与生命周期_Spring Bean解析_03

我们可以看到,这确实是一样的地址哈。

b)多例作用域

再接着来看第二个 - prototype,多例作用域,结果如下:

Spring Bean 的全面指南 - 理解作用域与生命周期_spring_04

观察 ContextDog,每次获取的对象都不⼀样(注入的对象在 Spring 容器启动时,就已经注入了,所以多次请求也不会发生变化)。

c)请求作用域

故名思意,每次请求,就会重新创建对象,结果如下:

Spring Bean 的全面指南 - 理解作用域与生命周期_作用域_05

这是因为在⼀次请求中,@AutowiredapplicationContext.getBean() 也是同⼀个对象。但是每次请求,都会重新创建对象。

d)会话作用域

这个就相当于是每个 Session 就是一个新的对象,如下:

Spring Bean 的全面指南 - 理解作用域与生命周期_初始化方法_06

这两个都是在 Chrome 浏览器中获取的,所以结果都一样,下面我换 Edge 获取:

Spring Bean 的全面指南 - 理解作用域与生命周期_作用域_07

e)全局作用域

这个就用的比较少了,你不管怎么去访问,发现始终都是那个 bean,那这和单例作用域有什么区别呢?就这么说吧,一个单例作用域只管着那一个 ApplicationContext,但是全局作用域管的是整个 ServletContext,一个 ServletContext 中可以布置多个 ApplicationContext,这么说就很清晰了。

f)Http WebSocket 作用域

这个作用域的话解释清楚的很少,官方文档上虽然说有这个作用域,但也没有解释到底是什么场景使用的(可能是我没找到),以下是我问 gpt4o 的答案,可自行参考(了解即可)。

Spring Bean 的全面指南 - 理解作用域与生命周期_Spring Bean解析_08

【注】

相信大家还有个疑问,就是说为什么 Dog 类中,不是使用 @Mapper 注解,要使用 @Getter、@Setter 注解呢?因为在这里,我们不能使用他的 toString() 方法,其实我们这些都是通过代理的方式访问的,如果说使用的话打印的地址就是代理地址了,我画个图就能明白了,如下:

Spring Bean 的全面指南 - 理解作用域与生命周期_初始化方法_09

当使用 toString() 方法访问地址时,由于是基于代理模式访问到目标类的,所以会打印代理类的地址,但是代理类又是不改变的,所以打印出来的地址就一直不会变,不信大家可以改改然后试试请求和会话作用域。(内存划分这块傻傻分不清的话可以看看《JVM:内存、类与垃圾》这篇博客)Spring Bean 的全面指南 - 理解作用域与生命周期_作用域_10


三、Bean 的生命周期

我认为的 Bean 的生命周期是分六块,分别是

  1. Bean 的实例化(为 Bean 分配内存空间)
  2. 属性赋值(就比如说属性注入和装配啊这些)
  3. 执行各种通知
  4. 执行初始化方法
  5. 使用 Bean
  6. 销毁 Bean

其他的都没什么好说的,稍微说明一下如何执行通知和初始化方法这些。请看源码:

Spring Bean 的全面指南 - 理解作用域与生命周期_初始化方法_11

这个不用太深入,知道个大概就好,执行通知不用讲吧?可以理解为内部通知,比如说一个项目在一个公司中分工完成,你完成了一个板块要给其他人说一声。然后下一个初始化呢可以理解为他在找你有没有自定义初始化的方式呀这些,可以通过在 XML 配置或注解中指定的 init-method 方法,或者使用 @PostConstruct 注解的方法。

3.1、初始化方法执行顺序

  1. @PostConstruct 注解的方法:这是最先执行的初始化方法。如果 Bean 中使用了该注解,Spring 会在 Bean 的所有依赖注入完成后立即调用它。
  2. afterPropertiesSet() 方法:如果 Bean 实现了 InitializingBean 接口,Spring 会在调用 @PostConstruct 方法之后调用 afterPropertiesSet() 方法。
  3. init-method 方法:最后,Spring 会调用在 XML 配置或 @Bean 注解中指定的初始化方法。这一步是在 afterPropertiesSet() 方法执行后进行的。

3.2、初始化方法的用途

初始化方法的主要作用包括:

  • 资源的分配:比如初始化数据库连接、开启文件流等。
  • 属性的检查与修正:检查依赖注入的属性值是否满足要求,如果不满足可以进行调整。
  • 初始化缓存或数据:比如将某些数据加载到缓存中,以便后续的快速访问。
  • 其他启动逻辑:包括启动线程、初始化外部服务连接等。

这些初始化方法使得开发者可以在 Bean 准备就绪之前执行必要的逻辑,确保 Bean 处于良好状态,能够正确地参与应用程序的工作。


四、总结

在这篇博客中,我们深入探讨了 Spring 中 Bean 的作用域与生命周期管理。首先,我们详细介绍了 Spring 中 Bean 的六种作用域,包括单例作用域、原型作用域、请求作用域、会话作用域、全局作用域以及 WebSocket 作用域。通过示例代码和运行结果的分析,我们了解了这些作用域的特性和使用场景。

接着,我们探讨了 Bean 的生命周期,从实例化、属性赋值到执行各种通知、初始化方法,以及最后的销毁过程。我们特别强调了初始化方法的执行顺序及其在 Bean 生命周期中的重要作用。通过分析 Spring 源码,我们了解到如何自定义初始化方法,并理解了这些方法在资源分配、属性检查与修正、缓存数据初始化等方面的实际应用。


五、结语

Spring 中的 Bean 就像是编程世界中的生命体,它们在容器中经历了从无到有,从初始化到销毁的完整生命周期。掌握这些过程,不仅能让我们更好地理解和运用 Spring 框架,更能在开发中游刃有余地应对各种复杂需求。

学习 Spring Bean 的生命周期和作用域,就像是在探索生命的奥秘。每一个 Bean 的诞生、成长和消亡,都让我们对代码世界有了更深的理解。在不断学习和实践的过程中,我们不仅是在完善技术能力,更是在培养对编程的热爱与敬畏之心。

记住,编程的世界里,没有一成不变的规则,只有不断创新和进步的可能性。愿你在探索技术的道路上,不断挑战自我,拥抱变化,迎接更大的成功!