摘要:本文详细解析了Spring的内置作用域,包括Singleton、Prototype、Request、Session、Application和WebSocket作用域,并通过实例讲解了它们在实际开发中的应用。

本文分享自华为云社区《Spring高手之路4——深度解析Spring内置作用域及其在实践中的应用》,作者:砖业洋__ 。

本文详细解析了Spring的内置作用域,包括Singleton、Prototype、Request、Session、Application和WebSocket作用域,并通过实例讲解了它们在实际开发中的应用。特别是Singleton和Prototype作用域,我们深入讨论了它们的定义、用途以及如何处理相关的线程安全问题。通过阅读本文,读者可以更深入地理解Spring作用域,并在实际开发中更有效地使用

1. Spring的内置作用域

我们来看看Spring内置的作用域类型。在5.x版本中,Spring内置了六种作用域:

  • singleton:在IOC容器中,对应的Bean只有一个实例,所有对它的引用都指向同一个对象。这种作用域非常适合对于无状态的Bean,比如工具类或服务类。
  • prototype:每次请求都会创建一个新的Bean实例,适合对于需要维护状态的Bean。
  • request:在Web应用中,为每个HTTP请求创建一个Bean实例。适合在一个请求中需要维护状态的场景,如跟踪用户行为信息。
  • session:在Web应用中,为每个HTTP会话创建一个Bean实例。适合需要在多个请求之间维护状态的场景,如用户会话。
  • application:在整个Web应用期间,创建一个Bean实例。适合存储全局的配置数据等。
  • websocket:在每个WebSocket会话中创建一个Bean实例。适合WebSocket通信场景。

我们需要重点学习两种作用域:singleton和prototype。在大多数情况下singleton和prototype这两种作用域已经足够满足需求。

2. singleton作用域

2.1 singleton作用域的定义和用途

Singleton是Spring的默认作用域。在这个作用域中,Spring容器只会创建一个实例,所有对该bean的请求都将返回这个唯一的实例。

例如,我们定义一个名为Plaything的类,并将其作为一个bean:

@Component
public class Plaything {
public Plaything() {
System.out.println("Plaything constructor run ...");
}
}

在这个例子中,Plaything是一个singleton作用域的bean。无论我们在应用中的哪个地方请求这个bean,Spring都会返回同一个Plaything实例。

下面的例子展示了如何创建一个单实例的Bean:

package com.example.demo.bean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class Kid {
private Plaything plaything;
@Autowired
public void setPlaything(Plaything plaything) {
this.plaything = plaything;
}
public Plaything getPlaything() {
return plaything;
}
}
package com.example.demo.bean;
import org.springframework.stereotype.Component;
@Component
public class Plaything {
public Plaything() {
System.out.println("Plaything constructor run ...");
}
}

这里可以在Plaything类加上@Scope(BeanDefinition.SCOPE_SINGLETON),但是因为是默认作用域是Singleton,所以没必要加。

package com.example.demo.configuration;
import com.example.demo.bean.Kid;
import com.example.demo.bean.Plaything;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanScopeConfiguration {
@Bean
public Kid kid1(Plaything plaything1) {
Kid kid = new Kid();
kid.setPlaything(plaything1);
return kid;
}
@Bean
public Kid kid2(Plaything plaything2) {
Kid kid = new Kid();
kid.setPlaything(plaything2);
return kid;
}
}
package com.example.demo.application;
import com.example.demo.bean.Kid;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("com.example")
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(DemoApplication.class);
context.getBeansOfType(Kid.class).forEach((name, kid) -> {
System.out.println(name + " : " + kid.getPlaything());
});
}
}

在Spring IoC容器的工作中,扫描过程只会创建bean的定义,真正的bean实例是在需要注入或者通过getBean方法获取时才会创建。这个过程被称为bean的初始化。

这里运行 ctx.getBeansOfType(Kid.class).forEach((name, kid) -> System.out.println(name + " : " + kid.getPlaything())); 时,Spring IoC容器会查找所有的Kid类型的bean定义,然后为每一个找到的bean定义创建实例(如果这个bean定义还没有对应的实例),并注入相应的依赖。

运行结果:

三个 Kid 的 Plaything bean是相同的,说明默认情况下 Plaything 是一个单例bean,整个Spring应用中只有一个 Plaything bean被创建。

为什么会有3个kid?

  1. Kid: 这个是通过在Kid类上标注的@Component注解自动创建的。Spring在扫描时发现这个注解,就会自动在IOC容器中注册这个bean。这个Bean的名字默认是将类名的首字母小写kid。
  2. kid1: 在 BeanScopeConfiguration 中定义,通过kid1(Plaything plaything1)方法创建,并且注入了plaything1。
  3. kid2: 在 BeanScopeConfiguration 中定义,通过kid2(Plaything plaything2)方法创建,并且注入了plaything2。

2.2 singleton作用域线程安全问题

需要注意的是,虽然singleton Bean只会有一个实例,但Spring并不会解决其线程安全问题,开发者需要根据实际场景自行处理。

我们通过一个代码示例来说明在多线程环境中出现singleton Bean的线程安全问题。

首先,我们创建一个名为Counter的singleton Bean,这个Bean有一个count变量,提供increment方法来增加count的值:

package com.example.demo.bean;
import org.springframework.stereotype.Component;
@Component
public class Counter {
private int count = 0;
public int increment() {
return ++count;
}
}

然后,我们创建一个名为CounterService的singleton Bean,这个Bean依赖于Counter,在increaseCount方法中,我们调用counter.increment方法:

package com.example.demo.service;
import com.example.demo.bean.Counter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CounterService {
@Autowired
private final Counter counter;
public void increaseCount() {
counter.increment();
}
}
我们在多线程环境中调用counterService.increaseCount方法时,就可能出现线程安全问题。因为counter.increment方法并非线程安全,多个线程同时调用此方法可能会导致count值出现预期外的结果。

要解决这个问题,我们需要使counter.increment方法线程安全。

这里可以使用原子变量,在Counter类中,我们可以使用AtomicInteger来代替int类型的count,因为AtomicInteger类中的方法是线程安全的,且其性能通常优于synchronized关键字。

package com.example.demo.bean;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public int increment() {
return count.incrementAndGet();
}
}

尽管优化后已经使Counter类线程安全,但在设计Bean时,我们应该尽可能地减少可变状态。这是因为可变状态使得并发编程变得复杂,而无状态的Bean通常更容易理解和测试。

什么是无状态的Bean呢? 如果一个Bean不持有任何状态信息,也就是说,同样的输入总是会得到同样的输出,那么这个Bean就是无状态的。反之,则是有状态的Bean。

3. prototype作用域

3.1 prototype作用域的定义和用途

在prototype作用域中,Spring容器会为每个请求创建一个新的bean实例。

例如,我们定义一个名为Plaything的类,并将其作用域设置为prototype:

package com.example.demo.bean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class Plaything {
public Plaything() {
System.out.println("Plaything constructor run ...");
}
}

在这个例子中,Plaything是一个prototype作用域的bean。每次我们请求这个bean,Spring都会创建一个新的Plaything实例。

我们只需要修改上面的Plaything类,其他的类不用动。

打印结果:

这个@Scope(BeanDefinition.SCOPE_PROTOTYPE)可以写成@Scope("prototype"),按照规范,还是利用已有的常量比较好。

3.2 prototype作用域在开发中的例子

以我个人来说,我在excel多线程上传的时候用到过这个,当时是EasyExcel框架,我给一部分关键代码展示一下如何在Spring中使用prototype作用域来处理多线程环境下的任务(实际业务会更复杂),大家可以对比,如果用prototype作用域和使用new对象的形式在实际开发中有什么区别。

使用prototype作用域的例子

@Resource
private ApplicationContext context;
@PostMapping("/user/upload")
public ResultModel upload(@RequestParam("multipartFile") MultipartFile multipartFile) {
......
ExecutorService es = new ThreadPoolExceutor(10, 16, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(2000), new ThreadPoolExecutor.CallerRunPolicy());
......
EasyExcel.read(multipartFile.getInputStream(), UserDataUploadVO.class,
new PageReadListener<UserDataUploadVO>(dataList ->{
......
// 多线程处理上传excel数据
Future<?> future = es.submit(context.getBean(AsyncUploadHandler.class, user, dataList, errorCount));
......
})).sheet().doRead();
......
}

AsyncUploadHandler.java

@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class AsyncUploadHandler implements Runnable {
private User user; private List<UserDataUploadVO> dataList; private AtomicInteger errorCount; @Resource
private RedisService redisService; ...... @Resource
private CompanyManagementMapper companyManagementMapper;
public AsyncUploadHandler(user, List<UserDataUploadVO> dataList, AtomicInteger errorCount) {
this.user = user;
this.dataList = dataList;
this.errorCount = errorCount;
}
@Override
public void run() {
......
} ......
}
AsyncUploadHandler类是一个prototype作用域的bean,它被用来处理上传的Excel数据。由于并发上传的每个任务可能需要处理不同的数据,并且可能需要在不同的用户上下文中执行,因此每个任务都需要有自己的AsyncUploadHandler bean。这就是为什么需要将AsyncUploadHandler定义为prototype作用域的原因。

由于AsyncUploadHandler是由Spring管理的,我们可以直接使用@Resource注解来注入其他的bean,例如RedisService和CompanyManagementMapper。

把AsyncUploadHandler交给Spring容器管理,里面依赖的容器对象可以直接用@Resource注解注入。如果采用new出来的对象,那么这些对象只能从外面注入好了再传入进去。

不使用prototype作用域改用new对象的例子

@PostMapping("/user/upload")
public ResultModel upload(@RequestParam("multipartFile") MultipartFile multipartFile) {
......
ExecutorService es = new ThreadPoolExceutor(10, 16, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(2000), new ThreadPoolExecutor.CallerRunPolicy());
......
EasyExcel.read(multipartFile.getInputStream(), UserDataUploadVO.class,
new PageReadListener<UserDataUploadVO>(dataList ->{
......
// 多线程处理上传excel数据
Future<?> future = es.submit(new AsyncUploadHandler(user, dataList, errorCount, redisService, companyManagementMapper));
......
})).sheet().doRead();
......
}

AsyncUploadHandler.java

public class AsyncUploadHandler implements Runnable {
private User user; private List<UserDataUploadVO> dataList; private AtomicInteger errorCount; private RedisService redisService; private CompanyManagementMapper companyManagementMapper; ......
public AsyncUploadHandler(user, List<UserDataUploadVO> dataList, AtomicInteger errorCount,
RedisService redisService, CompanyManagementMapper companyManagementMapper) {
this.user = user;
this.dataList = dataList;
this.errorCount = errorCount;
this.redisService = redisService;
this.companyManagementMapper = companyManagementMapper;
}
@Override
public void run() {
......
} ......
}

如果直接新建AsyncUploadHandler对象,则需要手动传入所有的依赖,这会使代码变得更复杂更难以管理,而且还需要手动管理AsyncUploadHandler的生命周期。

4. request作用域(了解)

request作用域:Bean在一个HTTP请求内有效。当请求开始时,Spring容器会为每个新的HTTP请求创建一个新的Bean实例,这个Bean在当前HTTP请求内是有效的,请求结束后,Bean就会被销毁。如果在同一个请求中多次获取该Bean,就会得到同一个实例,但是在不同的请求中获取的实例将会不同。

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedBean {
// 在一次Http请求内共享的数据
private String requestData;
public void setRequestData(String requestData) {
this.requestData = requestData;
}
public String getRequestData() {
return this.requestData;
}
}

上述Bean在一个HTTP请求的生命周期内是一个单例,每个新的HTTP请求都会创建一个新的Bean实例。

5. session作用域(了解)

session作用域:Bean是在同一个HTTP会话(Session)中是单例的。也就是说,从用户登录开始,到用户退出登录(或者Session超时)结束,这个过程中,不管用户进行了多少次HTTP请求,只要是在同一个会话中,都会使用同一个Bean实例。

@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionScopedBean {
// 在一个Http会话内共享的数据
private String sessionData;
public void setSessionData(String sessionData) {
this.sessionData = sessionData;
}
public String getSessionData() {
return this.sessionData;
}
}

这样的设计对于存储和管理会话级别的数据非常有用,例如用户的登录信息、购物车信息等。因为它们是在同一个会话中保持一致的,所以使用session作用域的Bean可以很好地解决这个问题。

但是实际开发中没人这么干,会话id都会存在数据库,根据会话id就能在各种表中获取数据,避免频繁查库也是把关键信息序列化后存在Redis。

6. application作用域(了解)

application作用域:在整个Web应用的生命周期内,Spring容器只会创建一个Bean实例。这个Bean在Web应用的生命周期内都是有效的,当Web应用停止后,Bean就会被销毁。

@Component
@Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ApplicationScopedBean {
// 在整个Web应用的生命周期内共享的数据
private String applicationData;
public void setApplicationData(String applicationData) {
this.applicationData = applicationData;
}
public String getApplicationData() {
return this.applicationData;
}
}

如果在一个application作用域的Bean上调用setter方法,那么这个变更将对所有用户和会话可见。后续对这个Bean的所有调用(包括getter和setter)都将影响到同一个Bean实例,后面的调用会覆盖前面的状态。

7. websocket作用域(了解)

websocket作用域:Bean 在每一个新的 WebSocket 会话中都会被创建一次,就像 session 作用域的 Bean 在每一个 HTTP 会话中都会被创建一次一样。这个Bean在整个WebSocket会话内都是有效的,当WebSocket会话结束后,Bean就会被销毁。

@Component
@Scope(value = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class WebSocketScopedBean {
// 在一个WebSocket会话内共享的数据
private String socketData;
public void setSocketData(String socketData) {
this.socketData = socketData;
}
public String getSocketData() {
return this.socketData;
}
}

上述Bean在一个WebSocket会话的生命周期内是一个单例,每个新的WebSocket会话都会创建一个新的Bean实例。

这个作用域需要Spring Websocket模块支持,并且应用需要配置为使用websocket。

点击关注,第一时间了解华为云新鲜技术~

解析Spring内置作用域及其在实践中的应用的更多相关文章

  1. Spring —— 三种配置数据源的方式:spring内置、c3p0、dbcp

    01.Spring内置数据源配置Class:DriverManagerDataSource全限定名:org.springframework.jdbc.datasource.DriverManagerD ...

  2. webstorm快捷键 webstorm keymap内置快捷键英文翻译、中英对照说明

    20160114参考网络上的快捷键,整理自己常用的: 查找/代替shift+shift 快速搜索所有文件,简便ctrl+shift+N 通过文件名快速查找工程内的文件(必记)ctrl+shift+al ...

  3. 内置Jetty配置JSP支持过程中的常见报错

    目录 1. 常见报错及解决 1.1 JSP support not configured 1.2 JSTL标签解析 1.3 JSP编译 1.4 JSP实现依赖 1.5 EL表达式支持 2. 小结 1. ...

  4. Android:源码环境下移植第三方的apk内置到ROM(System Image)中

    1. 首先在vendor目录下新建一个the3rdapk的目录,将需要内置的apk丢进去,目录名自己随意定. 2. 在 build/target/product/common.mk最后面,在$(cal ...

  5. 解析本内置Linux目录结构

    使用声明:1.此版本采用官方原版ISO+俄罗斯HunterTik 的Debian包制作而成2.此IMG包未进行Crack,资源来源于网络,如果你下载的是Crack版,与原作者无关,请自行分辨.“就看人 ...

  6. thinkphp中的内置操作数据库与mysql中的函数汇总

    8.4.4 Model类getModelName() 获取当前Model的名称getTableName() 获取当前Model的数据表名称switchModel(type,vars=array()) ...

  7. Python的 counter内置函数,统计文本中的单词数量

    counter是 colletions内的一个类 可以理解为一个简单的计数 import collections str1=['a','a','b','d'] m=collections.Counte ...

  8. JavaScript中的内置对象-8--4.date对象中-获取,设置日期时间的方法; 获取,设置年月日时分秒及星期的方法;

    学习目标 1.掌握创建日期对象的方法 2.掌握date对象中获取日期时间的方法 3.掌握date对象中设置日期时间的方法 如何创建一个日期对象 语法:new Date(); 功能:创建一个日期时间对象 ...

  9. Web-request内置对象在JSP编程中的应用

  10. Javascript初识之流程控制、函数和内置对象

    一.JS流程控制 1. 1.if else var age = 19; if (age > 18){ console.log("成年了"); }else { console. ...

随机推荐

  1. [MAUI]深入了解.NET MAUI Blazor与Vue的混合开发

    @ 目录 Vue在混合开发中的特点 创建MAUI项目 创建Vue应用 使用element-ui组件库 JavaScript和原生代码的交互 传递根组件参数 从设备调用Javascript代码 从Vue ...

  2. 深入探讨I/O模型:Java中的阻塞和非阻塞和其他高级IO应用

    引言 I/O(Input/Output)模型是计算机科学中的一个关键概念,它涉及到如何进行输入和输出操作,而这在计算机应用中是不可或缺的一部分.在不同的应用场景下,选择正确的I/O模型是至关重要的,因 ...

  3. 关于react提问以及解答

    1. 请教个工程问题. 团队运用webpack打包前端代码,转译后的文件每次都需要push到代码库远端:从开发角度而言,是不希望这部分代码在代码库的:两个原因:1是不方便代码review,2是代码仓库 ...

  4. 使用openpyxl库读取Excel文件数据

    在Python中,我们经常需要读取和处理Excel文件中的数据.openpyxl是一个功能强大的库,可以轻松地实现Excel文件的读写操作.本文将介绍如何使用openpyxl库读取Excel文件中的数 ...

  5. 来世再不选Java!

    危机感 距离上一次找工作面试已经过去快2年了,那时候正值疫情肆虐,虽然还未感受到"寒潮来临"的苗头,但最终还是成功通过了几轮面试,顺利签约.在目前公司待了2年了,在大环境的影响下, ...

  6. Ubuntu18虚拟机远程开发

    Ubuntu18 虚拟机远程开发 1. 安装 VMware 和 Ubuntu18 虚拟机 (1)VMware 官网上下载免费版本 一路 next 安装就行(中间也许需要改一下存放路径) (2)Ubun ...

  7. JS判断点是否在线段上

    本文利用向量的点积和叉积来判断点是否在线段上. 基础知识补充 从零开始的高中数学--向量.向量的点积.带你一次搞懂点积(内积).叉积(外积).Unity游戏开发--向量运算(点乘和叉乘 说明 点积可以 ...

  8. 2. Shell 条件测试

    重点: 条件测试. read. Shell 环境配置. case. for. find. xargs. gzip,bzip2,xz. tar. sed. 1)位置 变量 位置变量:在 bash She ...

  9. HelloJs

    JS 轻量级脚本语言,也是嵌入式语言,是一种对啊想模型语言,简称JS 想要实现复杂的效果,得依靠宿主环境提供API,最常见的是浏览器,还有服务器环境(操作系统) 语言机构+宿主环境提供的API 写js ...

  10. 通过 VS Code 优雅地编辑 Pod 内的代码(非 NodePort)

    目录 1. 概述 2. NodePort 方式 3. Ingress 方式 4. 救命稻草 5. 其他 1. 概述 今天聊点啥呢,话说,你有没有想过怎样用 VS Code 连上 K8s 集群内的某个 ...