文章目录
- 前言
- 一、Bean 的作用域
- 1.1 被修改的 Bean 案例
- 1.2 作用域的定义
- 1.3 Bean 的六种作用域
- 1.4 Bean 作用域的设置
- 二、Spring 的执行流程 和 Bean 的生命周期
- 2.1 Spring 的执行流程
- 2.2 Bean 的生命周期
- 2.3 Bean 生命周期的演示
前言
Bean 是 Spring 框架中的一个核心概念,它是指由 Spring 容器管理的对象实例。在使用 Spring 进行开发时,我们通常会定义各种各样的 Bean,用于承载应用程序的不同功能和组件。然而,很多开发者可能只关注了 Bean 的定义和使用方式,而忽略了 Bean 的作用域和生命周期,这两者对于一个应用程序的性能、稳定性和可维护性都至关重要。
在本文中,我将深入讨论 Bean 的作用域和生命周期,并解释它们对于 Spring 应用程序的影响。我会尽量用简单明了的语言来阐述这些概念,以便读者能够轻松理解和应用到自己的开发实践中。
一、Bean 的作用域
初次看到 Bean 的作用域这个名词,可能会令我们一头雾水,但是没关系,接下来的一个简单案例将会告诉我们什么是 Bean 的作用域。
1.1 被修改的 Bean 案例
现在有一个公共的 Bean对象,可以供 A 和 B 两个用户使用,但是 A 在使用这个 Bean 对象的时候却悄悄的对这个 Bean 的数据进行了修改,那么会不会导致 B 用户在使用这个 Bean 对象的时候发生预期之外的结果呢?
创建一个 UserBean
的类,它的作用是通过 @Bean
注解的方式将 User
对象存储到 Spring 容器中,其中 User
包含一个 id
和 name
属性:
@Component
public class UserBeans {
@Bean
public User getUser(){
User user = new User();
user.setId(123);
user.setName("张三");
return user;
}
}
然后创建一个 UserController1
类,通过 @Controller
注解将其存储到 Spring 容器中,然后使用属性注解
获取 Spring 容器中的 Bean 对象。另外创建一个 printUser
方法,里面创建一个临时引用 myUser
指向 Bean 对象,然后对这个 Bean 的数据进行修改:
@Controller
public class UserController1 {
@Autowired
private User user;
public void printUser(){
System.out.println("user: " + user);
User myUser = user;
myUser.setName("李四");
System.out.println("myUser: " + myUser);
System.out.println("user: " + user);
}
}
另外再创建一个 UserController2
,同样使用 @Controller
注解将其存储到 Spring 容器中,然后使用属性注解
获取 Spring 容器中的 Bean 对象。另外创建一个 printUser
方法,只打印获取到的 Bean 对象:
@Controller
public class UserController2 {
@Resource
private User user;
public void printUser(){
System.out.println("UserController2: user -> " + user);
}
}
在启动类中的 main
方法分别通过 ApplicationContext
获取 UserController1
和 UserController2
,然后分别执行其中的方法,观察修改 Bean 对象后产生的影响。
public static void main(String[] args) {
ApplicationContext context
= new ClassPathXmlApplicationContext("spring-config.xml");
UserController1 userController1
= context.getBean("userController1", UserController1.class);
UserController2 userController2
= context.getBean("userController2", UserController2.class);
userController1.printUser();
System.out.println("=========");
userController2.printUser();
}
执行的结果如下:
从运行的结果来看,当 UserController1
修改了 Bean 在数据,此时通过 UserController2
获取的 Bean 的数据也被修改了,那么就说明一个 Bean 在 Spring 中的储存只有一份,并且是单例模式的。因此 Spring 容器中的 Bean 的默认作用域就是单例模式(singleton)的。
1.2 作用域的定义
作用域(Scope)是在编程中用于描述变量或标识符在程序中可访问的范围。换句话说,它规定了变量在哪些部分可以被引用和使用。作用域是一个重要的概念,因为它可以帮助程序员避免命名冲突和理解变量在代码中的生命周期。
在 Spring 框架中,作用域(Scope)就是用来定义 Bean 对象的生命周期和可见性规则。作用域决定了在不同的上下文环境中,Spring 容器如何管理和提供 Bean 对象
。例如,在定义一个 Bean 的时候,我们可以指定其作用域,从而决定它在应用程序中的行为表现。
1.3 Bean 的六种作用域
Spring 容器在初始化一个 Bean 的实例的时候,同时会指定该实例的作用域,如果我们不修改要指定的作用域,Spring 就会默认指定一个默认的作用域。以下是 Spring 中的六种作用域,其中最后四种是基于 Spring MVC 生效的,因此本文先不讨论。
-
单例作用域(singleton):这是 Spring 容器
默认
的作用域。在整个应用程序的生命周期中,只会创建一个该类型的 Bean 实例,并且所有对该 Bean 的引用都会执行同一个对象。这种作用域适用于哪些无状态、线程安全的 Bean 对象,例如工具类、配置类等。 -
原型作用域(prototype):每次请求 Bean 对象的时候,Spring 容器都会创建一个新的 Bean 实例,因此在不同的请求中,得到的是不同的对象实例。原型作用域适用于那先状态较多,需要频繁创建和销毁的对象,比如某些与会话相关的 Bean。
-
请求作用域(request):请求作用域是在 Web 应用中常用的作用域。它表示每次 HTTP 请求都会创建一个新的 Bean 实例,该实例仅在当前请求的处理过程中有效。当请求结束后,该 Bean 会被销毁。这样的作用域通常用于存储和处理与单个请求相关的数据。
-
会话作用域(session):会话作用域是在 Web 应用中基于用户会话的作用域。每个 HTTP 会话(Session)对应一个 Bean 实例,该实例在整个会话的生命周期内有效。这种作用域适用于需要在整个会话期间保持状态的对象,比如用户登录信息等。
-
全局作用域(application):全局作用域是指在整个 Web 应用程序的生命周期内只创建一个 Bean 实例。该作用域的 Bean 在整个应用中可见,适用于那些在整个应用中需要共享的状态信息。
-
HTTP WebSocket 作用域(websocket): HTTP WebSocket 作用域是基于 WebSocket 连接的作用域。每个 WebSocket 连接对应一个 Bean 实例,该实例在 WebSocket 连接的整个生命周期内有效。
单例作用域(singleton)和全局作用域(application)之间的区别:
单例作用域和全局作用域都只创建一个 Bean 对象,那么它们之间有什么区别呢?
1. 定义位置:
- 单例作用域是 Spring Core 的一部分,在整个 Spring IoC 容器中生效。它适用于任何类型的 Spring 应用,不仅限于 Web 应用。
- 全局作用域是 Spring Web 的一部分,在 Servlet 容器中生效。它是专门为 Web 应用设计的,通过 Spring Web 库提供支持。
2. 作用范围:
- 单例作用域只保证在 Spring IoC 容器中,每个 Bean 只会有一个实例。在整个应用程序中,不同的 Spring IoC 容器可能会有不同的实例。
- 全局作用域确保在整个 Web 应用程序中,每个 Bean 只会有一个实例。无论是在同一个 Servlet 容器中还是不同的 Servlet 容器中,都只有一个实例。
3. 应用场景:
- 单例作用域适用于那些无状态、线程安全的 Bean,例如工具类、配置类等。由于单例 Bean 在整个应用中只有一个实例,因此在性能和资源利用方面有一定的优势。
- 全局作用域适用于那些在整个 Web 应用中需要共享状态信息的 Bean,比如全局的配置对象、全局缓存等。通过全局作用域,我们可以确保这些对象在整个应用中只有一个实例,而不受多个 Servlet 容器的影响。
4. 管理容器:
- 单例作用域的管理是 Spring IoC 容器负责的,它由 Spring Core 提供的 IoC 容器管理,与 Web 应用无关。
- 全局作用域的管理需要由 Spring Web 库提供的 Servlet 容器来管理,它与 Web 应用的生命周期相关。
总结来说,单例作用域适用于整个 Spring IoC 容器,保证每个 Bean 在容器中只有一个实例;而全局作用域适用于整个 Web 应用,保证每个 Bean 在整个应用中只有一个实例。选择合适的作用域取决于具体的应用需求和设计考虑。
1.4 Bean 作用域的设置
在 Spring 中,设置 Bean 作用域可以通过两种方法:XML 配置和注解配置
。
1. XML 配置
在 XML 配置文件中的 <bean>
标签中,可以使用 scope
属性来设置 Bean 的作用域。例如,对于原型作用域的 Bean,可以这样配置:
<bean id="myBean" class="com.spring.demo.MyBean" scope="prototype">
<!-- Bean 的属性配置 -->
</bean>
其中,scope
指定的是作用域的名称,如:prototype、singleton。
2. 注解配置
使用注解配置 Bean 的作用域更加的简洁。在 Spring 中,可以使用 @Scope
注解来指定 Bean 的作用域,例如对刚才的 UserBeans
类指定原型作用域:
@Component
public class UserBeans {
@Bean(name = {"user1"})
// @Scope("prototype") // 原型模式
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 使用全局变量设置
public User getUser(){
User user = new User();
user.setId(123);
user.setName("张三");
return user;
}
}
此时,@Scope
中的参数可以说作用域的名称,如:prototype;也可以通过ConfigurableBeanFactory
类来指定。ConfigurableBeanFactory
类的源码:
可以发现ConfigurableBeanFactory
类也是对作用域名的封装。
当作用域设置为原型模型的时候,再次运行刚才修改 Bean 内容的代码:
此时就能够发现,两个 UserController
获取到的都是一个全新的 Bean 对象,即使一个修改也不会对另一个造成影响。
二、Spring 的执行流程 和 Bean 的生命周期
2.1 Spring 的执行流程
Spring 执行流程:
-
启动 Spring 容器:
- Spring 容器在启动时会读取配置文件或者扫描注解来获取 Bean 的定义信息。
-
实例化 Bean(分配内存空间,从无到有):
- Spring 容器根据配置信息或者注解的定义,实例化 Bean 对象。实例化过程可能会涉及构造函数的调用和依赖对象的创建。
-
Bean 注册到 Spring 容器(存操作):
- 实例化后的 Bean 被注册到 Spring 容器中,容器将其纳入管理范围,并为每个 Bean 分配一个唯一的标识符(通常是 Bean 的名称或者 ID)。
-
Bean 初始化(初始化操作):
- 如果 Bean 的定义中配置了初始化方法(例如使用
init-method
属性或者@PostConstruct
注解),Spring 容器会在实例化之后调用该初始化方法,用于执行一些初始化逻辑。
- 如果 Bean 的定义中配置了初始化方法(例如使用
-
将 Bean 装配到需要的类中(取操作):
- 当其他类需要使用某个 Bean 时,Spring 容器会根据依赖注入的配置,将对应的 Bean 自动注入到需要的类中。这样,其他类就可以直接使用该 Bean 的实例,而不需要关心 Bean 对象的创建和管理过程。
-
使用 Bean:
- 现在,Bean 已经被装配到需要的类中,可以在其他类中直接使用它了。
-
Bean 销毁(销毁操作):
- 如果 Bean 的定义中配置了销毁方法(例如使用
destroy-method
属性或者@PreDestroy
注解),Spring 容器会在容器关闭时调用该销毁方法,用于执行一些清理操作。
- 如果 Bean 的定义中配置了销毁方法(例如使用
需要注意的是,Bean 的生命周期在 Spring 容器的控制下,开发者无需手动管理 Bean 的创建和销毁过程,这就是 Spring IoC(控制反转)的核心思想。通过依赖注入,我们能够更专注于业务逻辑的实现,而不需要关心对象的创建和管理。
2.2 Bean 的生命周期
Bean 的生命周期是指一个 Bean 实例从被创建到被销毁的整个过程。在 Spring 容器中,Bean 的生命周期主要包含以下阶段:
-
实例化:这个阶段 Spring 容器会根据配置信息和注解创建 Bean 实例。这是 “从无到有” 的过程,即分配内存空间并调用构造方法来创建 Bean 实例。
-
设置属性(Bean 的注入和装配):在实例化后,Spring 容器会根据配置文件或注解,将属性值注入到 Bean 实例中。这是依赖注入(Dependency Injection)的过程,通过属性或构造方法来设置 Bean 的属性值。
-
Bean 初始化:在属性赋值完成后,Spring 容器会执行以下步骤来初始化 Bean:
- 各种通知:如果 Bean 实现了相应的 Aware 接口,Spring 会通过回调方式通知 Bean 相应的状态,例如
BeanNameAware
、BeanFactoryAware
、ApplicationContextAware
等。 - 初始化前置方法:执行
BeanPostProcessor
初始化前置方法,如postProcessBeforeInitialization
。 - 初始化方法(XML方式和注解方式):如果 Bean 定义了初始化方法(
@PostConstruct
注解或实现了InitializingBean
接口),Spring 容器会在设置属性后调用该方法进行进一步的初始化。 - 初始化后置方法:如果 Bean 定义了初始化后置方法,如
BeanPostProcessor
接口实现类的postProcessAfterInitialization
方法,Spring 容器会在 Bean 初始化完成后调用该方法。
- 各种通知:如果 Bean 实现了相应的 Aware 接口,Spring 会通过回调方式通知 Bean 相应的状态,例如
-
使用 Bean:在初始化完成后,Bean 实例就可以被应用程序使用了。它会被注入到其他类中,或者通过 Spring 容器获取并调用它的方法。
-
销毁 Bean 对象:在容器关闭或者程序结束时,Spring 容器会执行以下步骤来销毁 Bean:
- 销毁前置方法:如果 Bean 定义了销毁前置方法(destroy-method),Spring 容器会在 Bean 销毁前调用该方法。
- 销毁方法(XML方式和注解方式):如果 Bean 定义了销毁方法(
@PreDestroy
注解或实现了DisposableBean
接口),Spring 容器会在 Bean 销毁时调用该方法进行清理操作。
2.3 Bean 生命周期的演示
下面的代码演示了一个完整的 Bean 生命周期过程,并展示了 Spring 中 Bean 的各个阶段的回调方法。让我们来看一下 Bean 生命周期的执行流程:
@Component
public class BeanComponent implements BeanNameAware, BeanPostProcessor {
@Override
public void setBeanName(String s) {
System.out.println("执行了通知,Bean name -> " + s);
}
// xml 的初始化方法
public void myInit(){
System.out.println("XML 方式初始化");
}
@PostConstruct
public void doPostConstruct(){
System.out.println("注解 的初始化方法");
}
public void sayHi(){
System.out.println("do sayHi()");
}
@PreDestroy
public void preDestroy(){
System.out.println("do PreDestroy");
}
// 前置方法
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("do postProcessBeforeInitialization");
return bean;
}
// 后置方法
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("do postProcessAfterInitialization");
return bean;
}
}
首先需要在 spring-config.xml
配置文件中增加以下内容:
<!-- XML 配置方式 -->
<bean id="beanComponent"
class="com.spring.demo.component.BeanComponent"
scope="prototype"
init-method="myInit" ></bean>
模拟 Bean 的生命周期:
-
实例化 Bean:
- Spring 容器读取配置文件或扫描注解,发现了
BeanComponent
的定义,并实例化了该 Bean。此时,还没有调用 Bean 的构造方法。
- Spring 容器读取配置文件或扫描注解,发现了
-
设置属性(Bean 的注入和装配):
- 如果有需要,Spring 容器会将相应的属性注入到
BeanComponent
实例中。
- 如果有需要,Spring 容器会将相应的属性注入到
-
Bean 初始化:
- 执行各种通知:因为
BeanComponent
实现了BeanNameAware
接口,所以setBeanName
方法会被调用,打印输出执行了通知,Bean name -> beanComponent
。 - 初始化前置方法:如果是 XML 配置方式,
myInit
方法会被调用,打印输出XML 方式初始化
。 - 初始化方法(@PostConstruct 注解):
doPostConstruct
方法会被调用,打印输出注解 的初始化方法
。 - 初始化后置方法:
postProcessAfterInitialization
方法会被调用,打印输出do postProcessAfterInitialization
。
- 执行各种通知:因为
-
使用 Bean:
- 在初始化完成后,
BeanComponent
实例可以被应用程序使用,例如调用sayHi()
方法,打印输出do sayHi()
。
- 在初始化完成后,
-
销毁 Bean 对象:
- 在容器关闭或程序结束时,
preDestroy
方法会被调用,打印输出do PreDestroy
。
- 在容器关闭或程序结束时,
需要注意的是,在上述示例中,使用了 BeanPostProcessor
接口来实现前置和后置方法。这个接口提供了在 Bean 初始化前后对 Bean 进行额外处理的能力。在实际应用中,可以通过实现 BeanPostProcessor
接口来自定义一些特定的处理逻辑,例如 AOP 的代理生成。
启动类的main
方法:
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
BeanComponent beanComponent= context.getBean("beanComponent", BeanComponent.class);
beanComponent.sayHi();
}
执行结果:
这里发现执行了两次通知,第一次通常是因为在创建 Spring 上下文的时候会执行 Bean 的实例化;第二次执行通常是因为采用的是原型模式,当执行getBean
的会创建一个新的 Bean 对象。