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

news2025/2/1 1:00:19

文章目录

  • 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;

    @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定义还没有对应的实例),并注入相应的依赖。

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

  三个 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 {
	@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。每次我们请求这个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();
	......
}

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

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

  如果直接新建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.coloradmin.cn/o/631528.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

002docker 安装

官网安装https://docs.docker.com/engine/install/ 系统要求 Centos7 Linux 内核&#xff1a;官方建议 3.10 以上查看Linux内核版本 用于打印当前系统的相关信息(内核版本号,硬件架构,主机名称和操作系统类型等 cat /proc/version uname -a 更新YUM源 生产环境中此步操作…

Spring Cloud Feign实战

概述 Feign是一种声明式、模板化的HTTP Client&#xff0c;目标是使编写Java HTTP Client变得更简单。Feign通过使用Jersey和CXF等工具实现一个HTTP Client&#xff0c;用于构建REST或SOAP的服务。Feign还支持用户基于常用的HTTP工具包&#xff08;OkHTTP、HTTPComponents&…

2023网安面试题170道,轻松应对面试

最近有不少小伙伴跑来咨询&#xff1a; 想找网络安全工作&#xff0c;应该要怎么进行技术面试准备&#xff1f; 工作不到 2 年&#xff0c;想跳槽看下机会&#xff0c;有没有相关的面试题呢&#xff1f; 为了更好地帮助大家高薪就业&#xff0c;今天就给大家分享两份网络安全工…

仙境传说ro:如何在地图上刷怪教程

仙境传说ro&#xff1a;如何在地图上刷怪教程 大家好我是艾西&#xff0c;在仙境传说这个游戏中我们知道了怎么创建NPC添加商品售卖后&#xff0c;那么对于游戏的怪肯定也得有自己的想法以及对游戏的设定以及理解&#xff0c;今天我跟大家分享的是怎么在地图中刷怪教程。 我们…

STL——string、vector、deque容器

初识STL **STL的基本概念****vector容器存放内置数据类型****vector容器中存放自定义数据类型****vector容器嵌套vector容器****string容器——构造函数****string容器——赋值操作****string容器——字符串拼接****string容器——字符串的查找和替换****string容器——字符串比…

人工智能该如何学习?详细的AI学习

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 1.TomChat 地址&#xff1a;https://www.ridderchat.com/ 该网站非常简介好看&#xff0c;界面看着很舒服&#xff0c;可以帮助你快速编写、编辑和讨论代码。 2.强大的AI辅助 链接…

模拟实现strstr函数,通俗易懂!!!

函数介绍 函数声明 函数声明&#xff1a;char *strstr(const char *str1, const char *str2) 头 文 件&#xff1a;#include <string.h> 返 回 值&#xff1a; 返回值为char * 类型&#xff08; 返回指向 str1 中第一次出现的 str2 的指针&#xff09;&#xff1b…

Webots R2021a教程

文章目录 Windows安装设置中文打开世界添加贴图 为外部控制器配置Anaconda解决报错&#xff1a;CondaSSLError: Encountered an SSL error. Most likely a certificate verification issue.调用Python API Windows 安装 进入下载页面 https://github.com/cyberbotics/webots/r…

CSS实现几种常见布局

CSS实现几种常见布局 两列左窄右宽型布局 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" co…

【面试题】2023前端面试系列-- Vue 篇

大厂面试题分享 面试题库 前后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 web前端面试题库 VS java后端面试题库大全 Vue 常见面试题总结 MVVM模型&#xff1f; MVVM&#xff0c;是Model-View-ViewModel的简写…

001docker架构介绍

docker 官网Choose the best method for you to install Docker Engine. This client-server application is available on Linux, Mac, Windows, and as a static binary.https://docs.docker.com/engine/install/ 介绍 Docker用Go编程语言编写&#xff0c;并利用Linux内核的…

算法刷题-链表-两两交换链表中的节点

两两交换链表中的节点 24. 两两交换链表中的节点思路其他语言版本 24. 两两交换链表中的节点 力扣题目链接 给定一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后的链表。 你不能只是单纯的改变节点内部的值&#xff0c;而是需要实际的进行节点交换。 思…

黄金回收小程序开发功能有哪些?

一、用户端&#xff1a; 1、实时查询&#xff1a;通过对接三方接口实现实时金价动态查看&#xff1b; 2、多种类珠宝实时回收&#xff1a;小程序支持多品类珠宝的实时回收包含黄金饰品、金条、铂金、K金、白银等&#xff0c;同步实现价格实时更新&#xff1b; …

计算机内核态、用户态和零拷贝技术详解

存储介质的性能 话不多说&#xff0c;先看一张图&#xff0c;下图左边是磁盘到内存的不同介质&#xff0c;右边形象地描述了每种介质的读写速率。一句话总结就是越靠近cpu&#xff0c;读写性能越快。了解了不同硬件介质的读写速率后&#xff0c;你会发现零拷贝技术是多么的香&a…

JDBC 事务和批处理 详解(通俗易懂)

目录 一、前言 二、事务 1.事务介绍 : 2.事务处理 : Δ准备工作 Δ不使用事务的情况 Δ使用事务的情况 三、批处理 1.介绍 : 2.常用方法 : 3.应用 : 4.源码分析(JDK17.0版本&#xff09; : 四、总结 一、前言 第四节内容&#xff0c;up主要和大家…

阿里背调,征信不好也会被pass

大厂背调&#xff0c;我一直认为是唬人的&#xff0c;走下流程而已&#xff0c;没想到这么严格。这次提供的背调信息&#xff0c;我填写了上家公司三个联系人&#xff0c;HR、领导、同事&#xff1b;上上家公司三个联系人&#xff0c;HR、领导、同事。根据朋友的反馈来看&#…

python 第五章 列表list [ ]

系列文章目录 第一章 初识python 第二章 变量 第三章 基础语句 第四章 字符串str 文章目录 5.1列表的应用场景5.2列表的格式5.3列表的常用操作查找下标函数查找函数index()count()len() 判断是否存在innot in 增加append()extend()insert() 删除delpop()remove()clear() 清空列…

Java并发回顾

树叶柔和爽朗的呼吸 诗人一路吹着口哨回家 一路踢着石子妙想连篇 感到夕阳和晚风自古多情 自己现在和将来 都是个幸福的人 系列文章目录 Java常见知识点汇总Java集合回顾Java并发回顾… 文章目录 系列文章目录什么是线程和进程?线程与进程的关系,区别及优缺点&#xff1f;图解…

Tcp的三次握手及netty和实际开发如何设置全连接队列参数

上图 第一次握手&#xff0c;client 发送 SYN 到 server&#xff0c;状态修改为 SYN_SEND&#xff0c;server 收到&#xff0c;状态改变为 SYN_REVD&#xff0c;并将该请求放入 sync queue 队列 第二次握手&#xff0c;server 回复 SYN ACK 给 client&#xff0c;client 收到…

【Prometheus】mysqld_exporter采集+Grafana出图+AlertManager预警

前提环境&#xff1a;已经安装和配置好prometheus server 所有组件对应的版本&#xff1a; prometheus-2.44.0 mysqld_exporter-0.14.0 grafana-enterprise-9.1.2-1.x86_64.rpm alertmanager-0.25.0 prometheus-webhook-dingtalk-2.1.0 简介 mysql_exporter是用来收集MysQL或…