解析Spring内置作用域及其在实践中的应用
摘要:本文详细解析了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?
- Kid: 这个是通过在Kid类上标注的@Component注解自动创建的。Spring在扫描时发现这个注解,就会自动在IOC容器中注册这个bean。这个Bean的名字默认是将类名的首字母小写kid。
- kid1: 在 BeanScopeConfiguration 中定义,通过kid1(Plaything plaything1)方法创建,并且注入了plaything1。
- 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内置作用域及其在实践中的应用的更多相关文章
- Spring —— 三种配置数据源的方式:spring内置、c3p0、dbcp
01.Spring内置数据源配置Class:DriverManagerDataSource全限定名:org.springframework.jdbc.datasource.DriverManagerD ...
- webstorm快捷键 webstorm keymap内置快捷键英文翻译、中英对照说明
20160114参考网络上的快捷键,整理自己常用的: 查找/代替shift+shift 快速搜索所有文件,简便ctrl+shift+N 通过文件名快速查找工程内的文件(必记)ctrl+shift+al ...
- 内置Jetty配置JSP支持过程中的常见报错
目录 1. 常见报错及解决 1.1 JSP support not configured 1.2 JSTL标签解析 1.3 JSP编译 1.4 JSP实现依赖 1.5 EL表达式支持 2. 小结 1. ...
- Android:源码环境下移植第三方的apk内置到ROM(System Image)中
1. 首先在vendor目录下新建一个the3rdapk的目录,将需要内置的apk丢进去,目录名自己随意定. 2. 在 build/target/product/common.mk最后面,在$(cal ...
- 解析本内置Linux目录结构
使用声明:1.此版本采用官方原版ISO+俄罗斯HunterTik 的Debian包制作而成2.此IMG包未进行Crack,资源来源于网络,如果你下载的是Crack版,与原作者无关,请自行分辨.“就看人 ...
- thinkphp中的内置操作数据库与mysql中的函数汇总
8.4.4 Model类getModelName() 获取当前Model的名称getTableName() 获取当前Model的数据表名称switchModel(type,vars=array()) ...
- Python的 counter内置函数,统计文本中的单词数量
counter是 colletions内的一个类 可以理解为一个简单的计数 import collections str1=['a','a','b','d'] m=collections.Counte ...
- JavaScript中的内置对象-8--4.date对象中-获取,设置日期时间的方法; 获取,设置年月日时分秒及星期的方法;
学习目标 1.掌握创建日期对象的方法 2.掌握date对象中获取日期时间的方法 3.掌握date对象中设置日期时间的方法 如何创建一个日期对象 语法:new Date(); 功能:创建一个日期时间对象 ...
- Web-request内置对象在JSP编程中的应用
- Javascript初识之流程控制、函数和内置对象
一.JS流程控制 1. 1.if else var age = 19; if (age > 18){ console.log("成年了"); }else { console. ...
随机推荐
- bootstrap响应式原理
Bootstrap 框架的网格系统工作原理如下:1 .数据行 (.row) 必须包含在容器( .container )中,以便为其赋予合适的对齐方式和内距 (padding) . 如: <div ...
- DP 杂题选做
部分详见: 概率期望 DP 学习笔记 树形 DP 学习笔记 其余题就不具体分类了. P1220 关路灯 题解说这是区间 DP 经典题,但我以前居然没听说过,这下尴尬了. 设 \(f_{i,j,0/1} ...
- Leetcode.456单调栈
给你一个整数数组 nums ,数组中共有 n 个整数.132 模式的子序列 由三个整数 nums[i].nums[j] 和 nums[k] 组成,并同时满足:i < j < k 和 num ...
- 2023寒鹭Tron-CTF迎新赛 CRYPTO Misc 全WP
CRYPTO 简简单单 1.题目信息 U2FsdGVkX1+2gTXPuTetdM1p+IETUDXAHe2eC33jQfgdJoOmmrJq 2.解题方法 兔子密码,在线工具直接解 简简单单2 1. ...
- WebViewJavascriptBridge.js代码学习
//notation: js file can only use this kind of comments //since comments will cause error when use in ...
- 2021 ICPC济南 J Determinant
题意就是给定一个矩阵,然后给出他的行列式的绝对值,这个值是精确的,然后让我们判断行列式的正负. 思路来源:一个Acmer 首先做这个题要明白一个性质才可以做,一个数和它的相反数对一个奇数的取模一定不同 ...
- 基于资源编排服务(ROS)实现存量资源的IaC化
背景 如今,基础设施即代码(Infrastructure as code,IaC)是云资源管理和编排的趋势,基于 IaC 的管理模式,在提升云资源自动化管理能力,降低管理成本的同时,可以大大降低云资源 ...
- 三菱PLC 轻松数采
目前市面上数采的软件有很多,但是用的最为省力最为简单的就是kepserver了,在kepserver的应用中,有对应的三菱驱动针对于三菱PLC,三菱驱动支持多个Mitsubishi 协议,包括 MEL ...
- 聊聊分布式 SQL 数据库Doris(九)
优化器的作用是优化查询语句的执行效率,它通过评估不同的执行计划并选择最优的执行计划来实现这一目标. CBO: 一种基于成本的优化器,它通过评估不同查询执行计划的成本来选择最优的执行计划.CBO会根据数 ...
- 【让AI女友跟我表白】大白话说Python+Flask入门(四)Flask Sijax的使用
写在前面 先吐槽两句,搞个mysql安装配置弄了4个小时,怎么都是外网无法访问,我靠,我特么也是服了. 当然,后来我投降了,明天再说,学什么不是学,娘的,换个方向,状态依然在! Sijax是什么? 代 ...