Spring Bean 全方位指南:从作用域、生命周期到自动配置详解

目录

1. Bean 的作用域

1.1 singleton

1.2 prototype

1.3 request

1.4 session

1.5 application

1.5.1 servletContext 和 applicationContext 区别

2. Bean 的生命周期

2.1 详解初始化

2.1.1 Aware 接口回调

2.1.2 执行初始化方法

2.2 代码示例

2.3 源码 [面试题]

3. SpringBoot 自动配置

3.1 问题复现

3.2 解决方法

 3.2.1 @ComponentScan

3.2.2 @Import({第三方类.class})

3.2.3 @Import(MySelector.class)

 3.2.4 第三方 jar 中定义注解

3.3 源码解读


1. Bean 的作用域

Bean 的作用域, 这里的 "作用域" 是指 Bean 在 Spring 框架中不同的行为模式.

举个例子, 当 Spring 容器中相同名称的 Bean 只有一个时, 我们无论是通过 context.getBean 获取还是通过 @Autowired 获取, 获取到的 Bean 都是同一个对象(内存地址相同).

这就是 Spring Bean 默认的作用域/行为模式 --- 单例作用域.

Bean 的作用域有以下 6 种:

作用域说明
singleton(单例作用域)每个 Spring IoC 容器内同名称的 bean 只有一个实例 (默认).
prototype(原型/多例作用域)每次获取 bean 时会创建新的实例 (非单例)
request(请求作用域)每个 HTTP 请求生命周期内,创建新的实例 (web 环境中,了解)
session(会话作用域)每个 HTTP Session 生命周期内,创建新的实例 (web 环境中,了解)
application(全局作用域)每个 ServletContext 生命周期内,创建新的实例 (web 环境中,了解)
websocket(HTTP WebSocket 作用域)每个 WebSocket 生命周期内,创建新的实例 (web 环境中,了解)

1.1 singleton

singleton 是 Spring 应用设计模式中单例模式的体现.

singleton 单例作用域, 也是 Spring 默认的作用域, 指在 Spring 容器中, 相同名称的 Bean 只有一个.

如上图所示, 只要获取的是同一个 Bean name 的 Bean, 不管是 @Resource 获取, 还是 getBean  获取, 又或者是不同的客户端(google, postman, ...)获取, 获取到的都是同一个 Bean.

1.2 prototype

多例模式, 每次获取 Bean 时, 都会创建一个新的实例(无论 Bean name 是否相同)

如上图所示, 每发送一个请求, 通过 applicationContext 获取的 bean 就发生了变化. 但是, 通过 @Resource 获取的 bean 却一直不变.

这是因为: 每请求一次, context 就会重新执行一次 getBean 方法(重新获取 bean), 而我们设置的作用域是 prototype, 自然每次获取到的都是一个新的 bean.

但是, 通过 @Resource 获取 bean 赋值给属性这一步骤, 是在项目启动时执行获取 bean 的, 只会执行这一次, 因此只会获取一次 bean. 因此无论请求多少次, 这个属性的值依然是之前的 bean, 是不会变的(除非项目重新启动).

@Resource/@Autowired 是在项目启动时, 底层调用的是 ApplicationContext.getBean 获取 bean 的.

1.3 request

作用域为 request 时, 每次请求, 都会创建一个全新的实例. 

但在同一个请求内是单例的(同一次请求中, 如果 bean name 相同, 那么获取到的是相同的 bean).

1.4 session

作用域为 session 时, 同一个会话中, 同一个 bean name, 获取到的都是同一个 bean.

1.5 application

作用域为 application 时, 同一个 servletContext 中, 获取到的同一个 bean.

就目前来看, application 和 singleton 的效果是一致的, 接下来为大家介绍一下两者区别.

1.5.1 servletContext 和 applicationContext 区别

servletContext 可以认为是一个 Tomcat 服务器, 一个 Tomcat 服务器中, 可以部署多个服务, 其中每一个服务是一个 applicationContext.

因此, 一个 servletContext 可以包含多个 applicationContext.

但是目前, 我们都是基于 Spring 进行开发, 而 Spring 集成了 Tomcat, 一个 Tomcat 中只运行一个服务, 因此目前的现状是: 一个 Tomcat 只部署一个服务.

因此, 对于 Spring 项目来说, 作用域为 application 和 singleton 的效果是一样的.


2. Bean 的生命周期

bean 的生命周期和我们人的生命周期, 从出生到死亡. 又或者说像一个戏剧演员, 从登台到谢幕...

简洁来说, bean 的生命周期为: 实例化 ➝ 填充属性/依赖注入 ➝ 初始化 ➝ 使用 ➝ 销毁

  1. 实例化: 为 bean 分配内存空间(执行构造方法) 
  2. 属性赋值: 对属性 bean 进行依赖注入
  3. 初始化: 1. 执行各种 aware 通知(BeanNameAware, BeanFactoryAware, ...) 2. 执行初始化方法(@Bean, @PostConstruct, ...)
  4. 使用 bean
  5. 销毁 bean(执行 @PreDestroy 方法)

先打个比方:

  1. 实例化: 买房
  2. 属性赋值: 装修
  3. 初始化: 买家电
  4. 使用 bean: 住房
  5. 销毁 bean: 卖房

2.1 详解初始化

初始化较难理解, 我们这里聚焦初始化这一步.

初始化中, 有两个关键动作:

  1. 执行 aware 接口回调
  2. 执行初始化方法

2.1.1 Aware 接口回调

如果 Bean 实现了 Spring 提供的一些特定的 Aware 接口, 如: BeanNameAware, BeanFactoryAware, ApplicationContextAware, ... 那么在初始化时, Spring 会在初始化阶段调用这些 Aware 接口中定义的方法, 使得 Bean 可以获取到 Spring 容器的相关信息.

这几个接口的作用如下:

  1. BeanNameAware: 获取当前 Bean 的 bean name(告诉你你叫什么名字)

  2. BeanFactoryAware: 获取 BeanFactory, 允许你在 Bean 内部动态访问容器中的其他 Bean.(相当于 Spring 在初始化你时, 告诉你: 这是我们的 Bean 管理中心钥匙, 需要时你可以自己去开柜子拿其他 Bean)

  3. ApplicationContextAware: 获取 ApplicationContext(获取 Spring 配置信息, 告诉你你生长在什么配置环境下)

Aware 接口名要干嘛?你要怎么用?
BeanNameAware想知道自己的名字实现接口:重写 setBeanName(String name)
BeanFactoryAware想拿到 Bean 工厂引用实现接口:重写 setBeanFactory(BeanFactory)
ApplicationContextAware想拿到上下文容器实现接口:重写 setApplicationContext(...)
EnvironmentAware想获取环境变量实现接口:重写 setEnvironment(...)
ResourceLoaderAware想读取资源文件实现接口:重写 setResourceLoader(...)

2.1.2 执行初始化方法

初始化中的第二个关键动作, 即: 执行初始化方法.

定义初始化方法, 有以下三种方式:

方式是否需要你主动声明如何声明
@PostConstruct 注解✅ 要加注解@PostConstruct public void init() {...}
实现 InitializingBean 接口✅ 要重写方法重写 afterPropertiesSet() 方法
initMethod 属性✅ 要在配置里写@Bean(initMethod = "xxx")或XML

注意: 无论是 Aware 回调,还是初始化方法,确实都需要你“主动声明” —— 要么实现接口,要么加注解,要么在配置中写明 !!

  • Aware 回调:靠你“实现接口”,Spring 才知道你“想要这个功能”
  • 初始化方法:你也得“声明想执行”,才会触发

2.2 代码示例

@Component
public class BeanLifeComponent implements BeanNameAware {
    // 注入 bean: 1. @Autowired... 2. 构造方法 3. set
    // 这里通过 set 方法注入
    private DogComponent dogComponent;

    // 1. 实例化
    public BeanLifeComponent() {
        System.out.println("1. 实例化, 执行构造方法");
    }

    // 2. 构造方法注入属性 bean
    @Autowired
    public void setDogComponent(DogComponent dogComponent) {
        this.dogComponent = dogComponent;
        System.out.println("2. 属性注入");
    }

    // 3. 执行 Aware 回调
    @Override
    public void setBeanName(String name) {
        System.out.println("3. Aware 回调, bean name: " + name);
    }

    // 4. 执行初始化方法
    @PostConstruct
    public void init() {
        System.out.println("4. 执行初始化方法");
    }

    // 5. 使用 Bean
    public void use() {
        System.out.println("5. 使用 Bean");
    }

    // 6. 销毁 Bean
    @PreDestroy
    public void destroy() {
        System.out.println("6. 销毁 bean");
    }
}

执行结果如下:

2.3 源码 [面试题]

Bean 生命周期的核心流程在 AbstractAutowireCapableBeanFactory 的 createBean 方法中.

1. 实例化: 根据类的全限定名, 获取类的 .class, 从 BeanFactory 中执行 Bean 的构造方法, 创建 Bean 的实例.

2. 属性注入: 先查看有没有属性, 如果有的话, 判断 Bean 是根据 Bean name 注入的还是根据类型注入的, 分别执行不同的方法, 进行属性注入

3. 初始化过程, 在源码中分为四部分:

  1. 检查 Bean 实现了哪些 Aware 接口, 并执行其中的执行 Aware 回调方法
  2. 执行初始化前置方法
  3. 执行初始化方法
  4. 执行初始化后置方法

 4. Bean 使用

5. 执行 Bean 销毁注解的方法


3. SpringBoot 自动配置

通过 Maven 将第三方 JAR 包的依赖引入项目中后, 然后这些第三方 jar 提供的 Bean, 就能通过注解 "自动地" 注入到我们的项目, 而不需要我们手动写 @ComponentScan 指定它的包, 或者写 @Import 去导入它的配置类(不受启动类目录限制/不受 Spring 扫描路径的限制) —— 这就是 Spring Boot 自动配置.

3.1 问题复现

为了更好的让大家理解 Spring 的自动配置, 我们先模拟一下没有 "自动配置" 的场景.

我们新建一个 config 包, 模拟第三方 jar 代码:

我们使用 @Configuration 将第三方代码中的 Bean 交给 Spring 管理.

接着, 在启动类中使用 getBean 获取这些第三方 Bean, 观察是否能执行成功:

答案是显而易见的, Spring 只会扫描启动类所在目录, 不会扫描第三方 jar 代码, 因此 Spring 容器根本没有执行那个第三方 JAR 中的 @Configuration 类, 因此也根本没有管理其中定义的 Bean, 更不会获取到这些 Bean.

3.2 解决方法

解决方法有以下四种:

  1. @ComponentScan(basePackages = "com.config"): 在启动类上, 明确指定要扫描的第三方 JAR 的包路径
  2. @Import({DemoConfig.class, DemoConfig2.class}): 在启动类上, 手动地导入第三方 JAR 中的 @Configuration 类
  3. @Import(MySelector.class): 第三方 jar 实现 ImportSelector 接口, 一次性导入所有第三方 jar 类
  4. 第三方 jar 中自定义注解, 在该注解的定义中使用 @Import 来引用同一个 JAR 包中的其他配置类

 3.2.1 @ComponentScan

@ComponentScan(basePackages = "com.config"): 在启动类上, 明确指定要扫描的第三方 JAR 的包路径.

3.2.2 @Import({第三方类.class})

@Import({DemoConfig.class, DemoConfig2.class}): 在启动类上, 手动地导入第三方 JAR 中的 @Configuration 类

3.2.3 @Import(MySelector.class)

@Import(MySelector.class): 在第三方 jar 中实现 ImportSelector 接口, 一次性导入所有第三方 jar 类

创建自定类 MySelector 实现 ImportSelector 接口, 重写方法, 对要导入的第三方类统一封装.

 3.2.4 第三方 jar 中定义注解

第三方 jar 中自定义注解, 在该注解的定义中使用 @Import 来引用同一个 JAR 包中的其他配置类

Spring 底层也是采用注解的方式, 来完成 自动配置 功能的.

3.3 源码解读

我们知道, Spring 项目启动时, 只会默认扫描启动类所在的目录及其子孙目录, 可是我们通过 Maven 引入的第三方 jar 包中的 Bean 是怎么被 Spring 扫描到, 并交由 Spring 管理的呢?

我们来看源码是怎么做的.

总结:

Spring 的自动配置, 是通过 @SpringBootApplication 注解实现的. @SpringBootApplication 中包含三个关键注解:

  1. @SpringBootConfiguration
  2. @EnableAutoConfiguration
  3. @ComponentScan

其中, @SpringBootConfiguration 可以认为是 @Configuration 的封装, 标识启动类为配置类, 本质也是交给 Spring 管理.

而 @ComponentScan, 指定了 Spring 的默认扫描路径, 也就是扫描启动类所在目录.

最核心的是 @EnableAutoConfiguration 注解, 这个注解中又包含了两个注解:

  1. @AutoConfigurationPackage
  2. @Import(提供了一个 ImportSelector)

其中, @Import 中的 ImportSelector, 让 Spring 能够扫描第三方 jar 包提供的 import 文件中的类(第三方会提供一个 import 文件, 其中包含了要被 Spring 管理的类), 但是 Spring 不会将 import 文件中的所有类都进行管理, 而是会通过 @Condition 条件注解进行筛选, 筛选出要管理的 Bean.

而 @AutoConfigurationPackage 和 @ComponentScan 有点相似, 共同作用使 Spring 默认扫描启动类所在包及其子包.

@AutoConfigurationPackage 和 @ComponentScan 区别:

  • @ComponentScan: 这是真正执行扫描动作的注解, 如果没有指定扫描路径, 会默认按照 @AutoConfigurationPackage 提供的路径即启动类路径进行扫描.
  • @AutoConfigurationPackage: 注册启动类所在的包. 它本身不执行扫描, 它的主要作用是告诉 @ComponentScan 启动类在哪里

面试时, 先总结给出一个高层次的概述, 如果面试官追问细节, 再用这些内容详细去阐述 @ComponentScan 和 @AutoConfigurationPackage 的具体区别.


END 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值