Spring高手之路4——深度解析Spring内置作用域及其在实践中的应用

文章目录

  • 1. Spring的内置作用域
  • 2. singleton作用域
    • 2.1 singleton作用域的定义和用途
    • 2.2 singleton作用域线程安全问题
  • 3. prototype作用域
    • 3.1 prototype作用域的定义和用途
    • 3.2 prototype作用域在开发中的例子
  • 4. request作用域(了解)
  • 5. session作用域(了解)
  • 6. application作用域(了解)
  • 7. websocket作用域(了解)

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通信场景。

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

2. singleton作用域

2.1 singleton作用域的定义和用途

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

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

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

  在这个例子中,Plaything是一个singleton作用域的bean。无论我们在应用中的哪个地方请求这个beanSpring都会返回同一个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;@Autowiredpublic 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 {@Beanpublic Kid kid1(Plaything plaything1) {Kid kid = new Kid();kid.setPlaything(plaything1);return kid;}@Beanpublic 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定义还没有对应的实例),并注入相应的依赖。

运行结果:
在这里插入图片描述

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

为什么会有3kid

  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的线程安全问题。

首先,我们创建一个名为Countersingleton 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;}
}

然后,我们创建一个名为CounterServicesingleton 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 {@Autowiredprivate 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。每次我们请求这个beanSpring都会创建一个新的Plaything实例。

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

打印结果:

在这里插入图片描述

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

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();......
}

  有人可能会问这里为什么使用context.getBean,而不是@Resource@Autowired注解,@Resource@Autowired注解只会在注入时创建一个新的实例,这里并不会反复注入。ApplicationContext.getBean()方法是在每次调用时解析的,所以它会在每次调用时创建一个新的AsyncUploadHandler实例。

AsyncUploadHandler.java

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

  AsyncUploadHandler类是一个prototype作用域的bean,它被用来处理上传的Excel数据。由于并发上传的每个任务可能需要处理不同的数据,并且可能需要在不同的用户上下文中执行,因此每个任务都需要有自己的AsyncUploadHandler bean。这就是为什么需要将AsyncUploadHandler定义为prototype作用域的原因。

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

  把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;}@Overridepublic 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实例。这个BeanWeb应用的生命周期内都是有效的,当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的所有调用(包括gettersetter)都将影响到同一个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



欢迎一键三连~

有问题请留言,大家一起探讨学习

----------------------Talk is cheap, show me the code-----------------------

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/8583.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NGINX+Tomcat负载均衡、动静分离集群

目录 前言 一、NGINX正向代理与反向代理 1.1、NGINX正向代理 1.2、NGINX反向代理 1. 2.1Nginx配置反向代理的主要参数 二、负载均衡 三、NGINX动静分离集群 3.1动静分离原理 四、NginxTomcat动静分离 4.1搭建nginx代理服务器192.168.14.100 4.1.1安装 NGINX依赖环境 …

阿里云国际站:云原生数据库2.0时代,阿里云如何将云原生进行到底?

【猎云网上海】11月3日报道&#xff08;文/孙媛&#xff09; “PolarDB将云原生进行到底&#xff01;” 在2021年云栖大会上&#xff0c;阿里巴巴集团副总裁、阿里云智能数据库事业部总负责人李飞飞宣布了PolarDB实现三层解耦的重磅升级以及引领云原生数据库技术持续创新的态…

【python】数据处理

1.按照间隔生成时间戳,并保存为csv文件 ##### 按照间隔生成时间戳,并保存为csv文件 import pandas as pd from datetime import datetime, time, timedelta times [] ts datetime(2023, 6, 17, 9, 10, 0) while ts < datetime(2023, 6, 17, 9, 26, 40):# times.append(t…

博客相关推荐在线排序学习实践

现有固定槽位的填充召回策略在相关线上推荐服务中缺乏有效的相关性排序&#xff0c;存在较硬的排列顺序&#xff0c;各个策略之间互相影响&#xff0c;导致线上基于规则的拓扑图比较复杂&#xff0c;因此设计在线推理服务&#xff0c;通过学习用户行为完成在线排序。 1. 博客相…

数通王国历险记之TCP协议下的三大协议的验证实验

系列文章目录 数通王国历险记&#xff08;1&#xff09; 前言 一&#xff0c;我们要先知道PDU是什么&#xff1f; 二、TCP协议下的三大协议的验证实验 1.FTP的验证实验 1&#xff0c;拓扑图 2.将lsw4配置一下 3&#xff0c;FTP服务器端开启FTP服务&#xff1a; 4&#x…

本地新项目推送至gitlab仓库

1. gitlab上新建一个空白项目 gitlab上点击new project按钮&#xff0c;新建一个项目 新建空白项目 项目名称与本地新建项目名称相同&#xff0c;其余根据具体需要选择 2. 初始化本地仓库并commit项目 进入本地项目根目录下&#xff0c;右击 git bash here打开命令窗口 初始化…

机器学习21:机器学习工程落地注意事项-I

目录 1.静态训练与动态训练 1.1 如何选择训练方式&#xff1f; 2.静态与动态推理 2.1 离线推理的优缺点 2.2 在线推理的优缺点 3.数据依赖性 3.1 可靠性 3.2 版本控制 3.3 必要性 3.4 相关性 3.5 反馈回路 4.参考文献 到目前为止&#xff0c;【机器学习1&#xff5e…

小型中文版聊天机器人

入门小菜鸟&#xff0c;希望像做笔记记录自己学的东西&#xff0c;也希望能帮助到同样入门的人&#xff0c;更希望大佬们帮忙纠错啦~侵权立删。 目录 一、简单介绍与参考鸣谢 二、数据集介绍 三、数据预处理 1、重复标点符号表达 2、英文标点符号变为中文标点符号 3、繁…

中北大学 - 信息对抗大三下学习课程设计(爬取招标网站,进行招标分析,数据保存execl中)

文章目录 1. 题目描述2. 项目细节分析定时爬取任务思路避免多次爬取数据重复问题网站结构根据爬取信息确认招标地区 3. 项目代码4. 运行截图 1. 题目描述 中北大学信息安全技术爬虫课程设计 题目 5&#xff1a;招投标信息分析系统 &#xff08;20050441 2005031113&#xff09…

在 7 月 4 日,PoseiSwap 治理通证 $POSE 上线了 BNB Chain 上的头部

在 7 月 4 日&#xff0c;PoseiSwap 治理通证 $POSE 上线了 BNB Chain 上的头部 DEX PancakeSwap&#xff08;POSE/ZBC 交易对&#xff09;&#xff0c;在 $POSE 开盘交易的 10 分钟内&#xff0c;其最高涨幅达到了 2169.22%&#xff0c;所有的早期投资者基本都从中获得了不菲的…

Kafka入门, 消费者工作流程(十八)

kafka消费方式 pull(拉)模式&#xff1a; consumer采用从broker中主动拉取数据。 Kafka采用这种方式。 push(推)模式&#xff1a; Kafka没有采用这种方式&#xff0c;因为由broker决定消息发送速率&#xff0c;很难适应所有消费者的速率。例如推送速度是50m/s&#xff0c;consu…

Ant Design4中Form.List和shouldUpdate一起使用的坑

背景 在antd3.x版本中&#xff0c;如果要实现一组表单增加删除的功能&#xff0c;需要Array.map()加上state状态来控制&#xff0c;代码比较复杂&#xff0c;而且非常不优雅。 其次在antd3.x中&#xff0c;表单中任何一个表单项的内容更新都会触发页面重新渲染&#xff0c;这在…