前言:
📕作者简介:热爱编程的小七,致力于C、Java、Python等多编程语言,热爱编程和长板的运动少年!
📘相关专栏Java基础语法,JavaEE初阶,数据库,数据结构和算法系列等,大家有兴趣的可以看一看。
😇😇😇有兴趣的话关注博主一起学习,一起进步吧!
一、通过一个案例来看 Bean 作用域的问题
假设现在有一个公共的 Bean,提供给 A 用户和 B 用户使用,然而在使用的途中 A 用户却“悄悄”地修改了公共 Bean 的数据,导致 B 用户在使用时发生了预期之外的逻辑错误。
我们预期的结果是,公共 Bean 可以在各自的类中被修改,但不能影响到其他类。
1.1被修改的 Bean 案例
公共 Bean:
@Component
public class Users {
@Bean
public User user1() {
User user = new User();
user.setId(1);
user.setName("Java"); // 【重点:名称是 Java】
return user;
}
}
A 用户使用时,进行了修改操作:
@Controller
public class BeanScopesController {
@Autowired
private User user1;
public User getUser1() {
User user = user1;
System.out.println("Bean 原 Name:" + user.getName());
user.setName("悟空"); // 【重点:进行了修改操作】
return user;
}
}
B 用户再去使用公共 Bean 的时候:
@Controller
public class BeanScopesController2 {
@Autowired
private User user1;
public User getUser1() {
User user = user1;
return user;
}
}
打印 A 用户和 B 用户公共 Bean 的值:
public class BeanScopesTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("s
pring-config.xml");
BeanScopesController beanScopesController = context.getBean(BeanSc
opesController.class);
System.out.println("A 对象修改之后 Name:" + beanScopesController.ge
tUser1().toString());
BeanScopesController2 beanScopesController2 = context.getBean(Bean
ScopesController2.class);
System.out.println("B 对象读取到的 Name:" + beanScopesController2.g
etUser1().toString());
}
}
执行结果如下:
1.2原因分析
操作以上问题的原因是因为 Bean 默认情况下是单例状态(singleton),也就是所有人的使用的都是同一个对象,之前我们学单例模式的时候都知道,使用单例可以很大程度上提高性能,所以在 Spring 中Bean 的作用域默认也是 singleton 单例模式。
二、作用域定义
限定程序中变量的可用范围叫做作用域,或者说在源代码中定义变量的某个区域就叫做作用域。
而 Bean 的作用域是指 Bean 在 Spring 整个框架中的某种行为模式,比如 singleton 单例作用域,就表示 Bean 在整个 Spring 中只有一份,它是全局共享的,那么当其他人修改了这个值之后,那么另一个人读取到的就是被修改的值。
2.1Bean 的 6 种作用域
Spring 容器在初始化一个 Bean 的实例时,同时会指定该实例的作用域。Spring有 6 种作用域,最后四种是基于 Spring MVC 生效的:
1. singleton:单例作用域
2. prototype:原型作用域(多例作用域)
3. request:请求作用域
4. session:回话作用域
5. application:全局作用域
6. websocket:HTTP WebSocket 作用域
注意后 4 种状态是 Spring MVC 中的值,在普通的 Spring 项目中只有前两种。
1.singleton
singleton 是 Spring 中默认的 Bean 作用域,它表示在整个应用程序中只存在一个 Bean 实例。每次请求该 Bean 时,都会返回同一个实例。
思考:Bean的单例模式是线程安全的吗?
不是。如何保证线程安全?通过ThreadLocal(本地线程变量)
2.prototype
prototype 表示每次请求该 Bean 时都会创建一个新的实例。每个实例都有自己的属性值和状态,因此它们之间是相互独立的。
3.request
request 表示在一次 HTTP 请求中只存在一个 Bean 实例。在同一个请求中,多次请求该 Bean 时都会返回同一个实例。不同的请求之间,该 Bean 的实例是相互独立的。
4.session
session 表示在一个 HTTP Session 中只存在一个 Bean 实例。在同一个 Session 中,多次请求该 Bean 时都会返回同一个实例。不同的 Session 之间,该 Bean 的实例是相互独立的。
5.application
application 表示在一个 ServletContext 中只存在一个 Bean 实例。该作用域只在 Spring ApplicationContext 上下文中有效。
6.websocket
websocket 表示在一个 WebSocket 中只存在一个 Bean 实例。该作用域只在 Spring ApplicationContext 上下文中有效。
单例作用域(singleton) VS 全局作用域(application)
- singleton 是 Spring Core 的作用域;application 是 Spring Web 中的作用域;
- singleton 作 于 IoC 的容器, application 作 于 Servlet 容器。
2.2Bean的单例模式是线程安全的吗?
无状态的单例 Bean 是线程安全的,而有状态的单例 Bean 是非线程安全的,所以总的来说单例 Bean 还是非线程安全的。
2.2.1什么是有状态和无状态?
有状态的 Bean 是指 Bean 中包含了状态,比如成员变量,而无状态的 Bean 是指 Bean 中不包含状态,比如没有成员变量,或者成员变量都是 final 的。
2.2.2为什么非线程安全?
Spring 默认的 Bean 是单例模式,意味着容器中只有一个 Bean 实例,所有的线程都会使用并操作这个唯一的 Bean 实例,那么多个线程同时调用修改这个单例 Bean,就会产生线程安全问题。 举个例子:
@Component
public class SingletonBean {
private int counter = 0;
public int getCounter() {
return counter++;
}
}
这是一个简单的单例 Bean,有一个计数器,每调用一次加 1,当多个线程同时调用这个 Bean 的 getCounter() 方法时,因为 counter++ 是非原子性操作(先查询再加等),所以最终的结果就会比实际的加等次数少,这就是线程安全问题。
2.2.3如何保证线程安全?
Spring 中保证单例 Bean 线程安全的手段有以下几个:
- 变为原型 Bean:在 Bean 上添加 @Scope("prototype") 注解,将其变为多例 Bean。这样每次注入时返回一个新的实例,避免竞争。
- 加锁:在 Bean 中对需要同步的方法或代码块添加同步锁 @Synchronized 或使用 Java 中的线程同步工具 ReentrantLock 等。
- 使用线程安全的集合:如 Vector、Hashtable 代替 ArrayList、HashMap 等非线程安全集合。
- 变为无状态 Bean:不在 Bean 中保存状态,让 Bean 成为无状态 Bean。无状态的 Bean 没有共享变量,自然也无须考虑线程安全问题。
- 使用线程局部变量 ThreadLocal:在方法内部使用线程局部变量 ThreadLocal,因为 ThreadLocal 是线程独享的,所以也不存在线程安全问题。
2.3设置作用域
使用@Scope标签就可以来声明 Bean 的作用域, 如设置 Bean 的作用域,如下代码所示:
@Component
public class Users {
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Bean(name = "u1")
public User user1() {
User user = new User();
user.setId(1);
user.setName("Java"); // 【重点:名称是 Java】
return user;
}
}
@Scope 标签既可以修饰方法也可以修饰类,@Scope 有两种设置方式:
1. 直接设置值:@Scope("prototype")
2. 使用枚举设置:@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
三、Spring 执行流程和 Bean 的生命周期
3.1Spring 执行流程
Bean 执行 流程(Spring 执行流程):启动 Spring 容器 -> 实例化 Bean(分配内存空间,从 到有) -> Bean 注册到 Spring 中(存操作) -> 将 Bean 装配到需要的类中(取操作)。
3.2 Bean 生命周期
所谓的生命周期指的是一个对象从诞生到销毁的整个生命过程,我们把这个过程就叫做一个对象的生命周期。
Bean 的生命周期分为以下 5 部分:
3.2.1. 实例化
在 Spring 容器启动时,会根据配置文件或注解等方式创建 Bean 的实例,也就是说实例化就是为 Bean 对象分配内存空间。根据 Bean 的作用域不同,实例化的方式也不同。例如,singleton 类型的 Bean 在容器启动时就会被实例化,而 prototype 类型的 Bean 则是在每次请求时才会被实例化。
3.2.2. 属性赋值
在 Bean 实例化后,Spring 容器会自动将配置文件或注解中指定的属性值注入到 Bean 中。属性注入可以通过构造函数注入、Setter 方法注入、注解注入等方式实现。
3.2.3. 初始化
在属性注入完成后,Spring 容器会调用 Bean 的初始化方法。Bean 的初始化方法可以通过实现 InitializingBean 接口、@PostConstruct 注解等方式实现。在初始化方法中,可以进行一些初始化操作,例如建立数据库连接、加载配置文件等。
3.2.4. 使用
在 Bean 初始化完成后,Bean 就可以被应用程序使用了。在应用程序中,可以通过 Spring 容器获取 Bean 的实例,并调用 Bean 的方法。
3.2.5. 销毁
在应用程序关闭时,Spring 容器会自动销毁所有的 Bean 实例。Bean 的销毁方法可以通过实现 DisposableBean 接口、@PreDestroy 注解等方式实现。在销毁方法中,可以进行一些清理操作,例如释放资源、关闭数据库连接等。
3.2.6实例化和初始化的区别
实例化和属性设置是 Java 级别的系统“事件”,其操作过程不可人为干预和修改; 初始化是给开发者提供的,可以在实例化之后,类加载完成之前进 定义“事件”处理。
生命流程的“故事”:
Bean 的生命流程看似繁琐,但咱们可以用生活中的场景来理解它,比如我们现在需要买栋房 ,那么我们的流程是这样的:
1.先买房(实例化,从无到有);
2.装修(设置属性);
3.买家电,如洗 机、冰箱、电视、空调等([各种]初始化);
4. 入住(使用Bean);
5.卖出去(Bean 销毁)。