文章目录
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
?
- 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
的值:
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")
,按照规范,还是利用已有的常量比较好。
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-----------------------
版权归原作者 砖业洋__ 所有, 如有侵权,请联系我们删除。