0


Spring高手之路4——深度解析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

@ComponentpublicclassPlaything{publicPlaything(){System.out.println("Plaything constructor run ...");}}

  在这个例子中,

Plaything

是一个

singleton

作用域的

bean

。无论我们在应用中的哪个地方请求这个

bean

Spring

都会返回同一个

Plaything

实例。

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

Bean

packagecom.example.demo.bean;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;@ComponentpublicclassKid{privatePlaything plaything;@AutowiredpublicvoidsetPlaything(Plaything plaything){this.plaything = plaything;}publicPlaythinggetPlaything(){return plaything;}}
packagecom.example.demo.bean;importorg.springframework.stereotype.Component;@ComponentpublicclassPlaything{publicPlaything(){System.out.println("Plaything constructor run ...");}}

  这里可以在

Plaything

类加上

@Scope(BeanDefinition.SCOPE_SINGLETON)

,但是因为是默认作用域是

Singleton

,所以没必要加。

packagecom.example.demo.configuration;importcom.example.demo.bean.Kid;importcom.example.demo.bean.Plaything;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;@ConfigurationpublicclassBeanScopeConfiguration{@BeanpublicKidkid1(Plaything plaything1){Kid kid =newKid();
        kid.setPlaything(plaything1);return kid;}@BeanpublicKidkid2(Plaything plaything2){Kid kid =newKid();
        kid.setPlaything(plaything2);return kid;}}
packagecom.example.demo.application;importcom.example.demo.bean.Kid;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.ApplicationContext;importorg.springframework.context.annotation.AnnotationConfigApplicationContext;importorg.springframework.context.annotation.ComponentScan;@SpringBootApplication@ComponentScan("com.example")publicclassDemoApplication{publicstaticvoidmain(String[] args){ApplicationContext context =newAnnotationConfigApplicationContext(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

的值:

packagecom.example.demo.bean;importorg.springframework.stereotype.Component;@ComponentpublicclassCounter{privateint count =0;publicintincrement(){return++count;}}

然后,我们创建一个名为

CounterService

singleton Bean

,这个

Bean

依赖于

Counter

,在

increaseCount

方法中,我们调用

counter.increment

方法:

packagecom.example.demo.service;importcom.example.demo.bean.Counter;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;@ServicepublicclassCounterService{@AutowiredprivatefinalCounter counter;publicvoidincreaseCount(){
        counter.increment();}}

  我们在多线程环境中调用

counterService.increaseCount

方法时,就可能出现线程安全问题。因为

counter.increment

方法并非线程安全,多个线程同时调用此方法可能会导致

count

值出现预期外的结果。

  要解决这个问题,我们需要使

counter.increment

方法线程安全。

  这里可以使用原子变量,在

Counter

类中,我们可以使用

AtomicInteger

来代替

int

类型的

count

,因为

AtomicInteger

类中的方法是线程安全的,且其性能通常优于

synchronized

关键字。

packagecom.example.demo.bean;importorg.springframework.stereotype.Component;importjava.util.concurrent.atomic.AtomicInteger;@ComponentpublicclassCounter{privateAtomicInteger count =newAtomicInteger(0);publicintincrement(){return count.incrementAndGet();}}

  尽管优化后已经使

Counter

类线程安全,但在设计

Bean

时,我们应该尽可能地减少可变状态。这是因为可变状态使得并发编程变得复杂,而无状态的

Bean

通常更容易理解和测试。

什么是无状态的Bean呢? 如果一个

Bean

不持有任何状态信息,也就是说,同样的输入总是会得到同样的输出,那么这个

Bean

就是无状态的。反之,则是有状态的

Bean

3. prototype作用域

3.1 prototype作用域的定义和用途

prototype

作用域中,

Spring

容器会为每个请求创建一个新的

bean

实例。

例如,我们定义一个名为

Plaything

的类,并将其作用域设置为

prototype

packagecom.example.demo.bean;importorg.springframework.beans.factory.config.BeanDefinition;importorg.springframework.context.annotation.Scope;importorg.springframework.stereotype.Component;@Component@Scope(BeanDefinition.SCOPE_PROTOTYPE)publicclassPlaything{publicPlaything(){System.out.println("Plaything constructor run ...");}}

在这个例子中,

Plaything

是一个

prototype

作用域的

bean

。每次我们请求这个

bean

Spring

都会创建一个新的

Plaything

实例。

我们只需要修改上面的

Plaything

类,其他的类不用动。

打印结果:

在这里插入图片描述

这个

@Scope(BeanDefinition.SCOPE_PROTOTYPE)

可以写成

@Scope("prototype")

,按照规范,还是利用已有的常量比较好。
prototype作用域

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

  以我个人来说,我在

excel

多线程上传的时候用到过这个,当时是

EasyExcel

框架,我给一部分关键代码展示一下如何在

Spring

中使用

prototype

作用域来处理多线程环境下的任务(实际业务会更复杂),大家可以对比,如果用

prototype

作用域和使用

new

对象的形式在实际开发中有什么区别。

**使用

prototype

作用域的例子**

@ResourceprivateApplicationContext context;@PostMapping("/user/upload")publicResultModelupload(@RequestParam("multipartFile")MultipartFile multipartFile){......ExecutorService es =newThreadPoolExceutor(10,16,0L,TimeUnit.MILLISECONDS,newLinkedBlockingQueue<>(2000),newThreadPoolExecutor.CallerRunPolicy());......EasyExcel.read(multipartFile.getInputStream(),UserDataUploadVO.class,newPageReadListener<UserDataUploadVO>(dataList ->{......// 多线程处理上传excel数据Future<?> future = es.submit(context.getBean(AsyncUploadHandler.class, user, dataList, errorCount));......})).sheet().doRead();......}

AsyncUploadHandler.java

@Component@Scope(BeanDefinition.SCOPE_PROTOTYPE)publicclassAsyncUploadHandlerimplementsRunnable{privateUser user;privateList<UserDataUploadVO> dataList;privateAtomicInteger errorCount;@ResourceprivateRedisService redisService;@ResourceprivateCompanyManagementMapper companyManagementMapper;publicAsyncUploadHandler(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

,例如

RedisService

CompanyManagementMapper

  把

AsyncUploadHandler

交给

Spring

容器管理,里面依赖的容器对象可以直接用

@Resource

注解注入。如果采用

new

出来的对象,那么这些对象只能从外面注入好了再传入进去。

**不使用

prototype

作用域改用

new

对象的例子**

@PostMapping("/user/upload")publicResultModelupload(@RequestParam("multipartFile")MultipartFile multipartFile){......ExecutorService es =newThreadPoolExceutor(10,16,0L,TimeUnit.MILLISECONDS,newLinkedBlockingQueue<>(2000),newThreadPoolExecutor.CallerRunPolicy());......EasyExcel.read(multipartFile.getInputStream(),UserDataUploadVO.class,newPageReadListener<UserDataUploadVO>(dataList ->{......// 多线程处理上传excel数据Future<?> future = es.submit(newAsyncUploadHandler(user, dataList, errorCount, redisService, companyManagementMapper));......})).sheet().doRead();......}

AsyncUploadHandler.java

publicclassAsyncUploadHandlerimplementsRunnable{privateUser user;privateList<UserDataUploadVO> dataList;privateAtomicInteger errorCount;privateRedisService redisService;privateCompanyManagementMapper companyManagementMapper;publicAsyncUploadHandler(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)publicclassRequestScopedBean{// 在一次Http请求内共享的数据privateString requestData;publicvoidsetRequestData(String requestData){this.requestData = requestData;}publicStringgetRequestData(){returnthis.requestData;}}

上述

Bean

在一个

HTTP

请求的生命周期内是一个单例,每个新的

HTTP

请求都会创建一个新的

Bean

实例。

5. session作用域(了解)

session

作用域:

Bean

是在同一个

HTTP

会话(

Session

)中是单例的。也就是说,从用户登录开始,到用户退出登录(或者

Session

超时)结束,这个过程中,不管用户进行了多少次

HTTP

请求,只要是在同一个会话中,都会使用同一个

Bean

实例。

@Component@Scope(value =WebApplicationContext.SCOPE_SESSION, proxyMode =ScopedProxyMode.TARGET_CLASS)publicclassSessionScopedBean{// 在一个Http会话内共享的数据privateString sessionData;publicvoidsetSessionData(String sessionData){this.sessionData = sessionData;}publicStringgetSessionData(){returnthis.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)publicclassApplicationScopedBean{// 在整个Web应用的生命周期内共享的数据privateString applicationData;publicvoidsetApplicationData(String applicationData){this.applicationData = applicationData;}publicStringgetApplicationData(){returnthis.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)publicclassWebSocketScopedBean{// 在一个WebSocket会话内共享的数据privateString socketData;publicvoidsetSocketData(String socketData){this.socketData = socketData;}publicStringgetSocketData(){returnthis.socketData;}}

上述

Bean

在一个

WebSocket

会话的生命周期内是一个单例,每个新的

WebSocket

会话都会创建一个新的

Bean

实例。

这个作用域需要

Spring Websocket

模块支持,并且应用需要配置为使用

websocket


欢迎一键三连~

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

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


本文转载自: https://blog.csdn.net/qq_34115899/article/details/131109608
版权归原作者 砖业洋__ 所有, 如有侵权,请联系我们删除。

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

还没有评论