当前位置: 首页 > news >正文

千字详解Spring bean的作用域

引言

在默认情况下,应用上下文中的bean都是单例的形式创建的。 也就是不管给定的一个bean注入其他bean多少次,每次注入的都是同一个实例。

在大多数情况下,单例 bean是很理想的,可以反复重用。但是有时候我们所需要的bean是易变的,他会存储一些属于自己的状态,如果使用单例模式,每次操作同一个bean,会对bean造成污染,可能会出现预料不到的问题。

在Spring中定义了多种作用域,可以基于这些作用域创建 bean。

  • 单例(Singleton):在整个应用中,只创建bean的一个实例。
  • 原型(Prototype):每次注入或者通过Spring上下文获取的时候都会创建一个新的 bean实例。
  • 会话(Session):在Web应用中,为每个会话创建一个bean实例。
  • 请求(Request):在Web应用中,为每个请求创建一个bean实例。

配置bean的作用域

怎样指定bean的作用域,我们可以通过@Scope注解。如果是通过组件扫描方式发现和声明 bean,那么它可以和@Component注解搭配使用,将其声明为所配置作用域Bean

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)//将其声明为原型bean
public class Demo { ... }

若是通过java配置类声明bean,它可以与@Bean搭配使用

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Demo demo(){
    return new Demo();
}

若是使用XML来配置bean,我们可以使用<bean>元素的scope属性,配置bean的作用域

<bean id="demo" class="com.abiao.Demo" scope="prototype" />

我列举的这几个例子都是声明的prototype(原型)作用域,每次注入或从Spring应用上下文检索该bean时,都会创建新的实例,这样我们每次操作都会得到我们自己的bean实例。

会话作用域和请求作用域

以上我们介绍了,单例与原型作用域的声明。但是在有的场景我们使用前两种作用域,并不合适。例如,在Web应用中实现购物车。

若是使用单例作用域,我们每次往购物车添加商品都是往一个购物车添加,这并不合理。若是使用原型,我们每次添加商品都会创建一个购物车,下次获取购物车也不是同一个,也不合理。按照购物车bean来说,会话作用域是最适合的,为每个用户会话创建一个购物车。

声明会话作用域bean

指定会话作用域bean,我们也是使用@Scope注解。除了给@Scope注解配置value属性,我们还配置了 proxyMode 属性(代理模式),ScopedProxyMode.INTERFACES表示基于接口的动态代理模式(jdk)。配置 proxyMode(代理模式)可以帮我们解决会话作用域bean注入到单例bean所遇到的问题。

@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,//常量值为session
       proxyMode = ScopedProxyMode.INTERFACES)//基于接口实现的代理模式
public class Demo { ... }

我们以商店与购物车为例,商店 StoreService(单例bean),购物车 ShoppingCart(会话bean)。在 StoreService的setShoppingCart()方法注入 ShoppingCart bean。

@Component
public class StoreService {

    /** 购物车 */
    private ShoppingCart shoppingCart;

    @Autowired
    public void setShoppingCart(ShoppingCart shoppingCart){
        this.shoppingCart = shoppingCart;
    }
    
}    

因为 StoreService 是一个单例bean,会在Spring应用上下文加载时创建。在创建时,Spring会试图将 ShoppingCart 注入到 setShoppingCart()方法。但是 ShoppingCart 是会话作用域的bean,此时并不存在。只有当用户进入系统,创建会话之后,才会出现 ShoppingCart 实例。

那么问题来了,系统中会存在多个 ShoppingCart,每个用户进入系统,创建会话后,都会产生一个 ShoppingCart。但是 StoreService 是单例的,只能注入一个,我们并不想注入一个固定的 ShoppingCart 实例到 StoreService 中。我们所希望的是,当 StoreService 处理购物车时,注入的 ShoppingCart 恰好是当前会话所对应的那一个。

其实Spring并不会将 ShoppingCart bean 注入到 StoreService 中,而是注入一个 ShoppingCart bean 的代理。这个代理会暴露与 ShoppingCart 同样的方法,所以 StoreService 会将他看做一个购物车。当 StoreService 调用 ShoppingCart 的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的 ShoppingCart bean。

proxyMode属性设置

  • 若 ShoppingCart 是个接口,其实现类 @Scope的 proxyMode 属性设置为 ScopedProxyMode.INTERFACES ,表示这个代理要实现 ShoppingCart 接口,并将调用委托给实现bean。该代理模式也是最为理想的。

  • 若 ShoppingCart 是个类,Spring就没办法创建基于接口的代理,这时,他必须使用CGLib来生成基于类的代理。将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,Spring会生成基于目标类扩展类的方式创建代理。

以上我们了解了会话作用域可能产生这类问题,同样的请求作用域bean也会面临相同的装配问题。因此,请求作用域的bean也应该使用作用域代理的方式注入。

如图:作用域代理延迟注入请求和会话作用域的bean

XML中声明作用域代理

如果不使用@Scope注解声明作用域代理,我们还可以使用XML文件来声明。 在XML中作用域可通过scope属性直接声明,但是声明作用域代理需要使用Spring aop 命名空间的元素<aop:scoped-proxy />。 <aop:scoped-proxy />默认采用CGLib方式创建目标类的代理。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/beans/spring-aop.xsd
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="cart" 
          class="com.abiao._03_scope.ShoppingCart" 
          scope="session">
         <aop:scoped-proxy /> 
    </bean>

</beans>

我们也可以通过设置 <aop:scoped-proxy /> 标签的 proxy-target-class 属性值为 false,让它基于接口创建作用域代理。

<bean id="cart"
      class="com.abiao._03_scope.ShoppingCart"
      scope="session">
     <aop:scoped-proxy proxy-target-class="false"/>
</bean>

总结

Spring 支持多种 Bean 作用域:单例(默认)、原型(每次创建新实例)、会话(Web 环境中每个会话独立)、请求(每个请求独立),其中会话和请求作用域需通过 @Scope 注解或 XML 配置代理模式(proxyMode)解决注入到单例 Bean 时的依赖问题,确保延迟解析并动态获取当前作用域实例。

相关文章:

  • std::set详解与应用:生成不重复的字符,确保每个字符唯一
  • Linux驱动学习(三)--字符设备架构与注册
  • 理解 Rust 的并发特性:`Send` 和 `Sync` 特征
  • 《Qt窗口动画实战:Qt实现呼吸灯效果》
  • 爬虫获取阿里巴巴 item_search 接口:根据关键字获取在售商品数据
  • 3-1 WPS JS宏工作簿的新建与保存(批量新建工作簿)学习笔记
  • yolov8_pose模型,使用rknn在安卓RK3568上使用
  • 如何正确的用 Cursor 打开 Unity 项目
  • Android 12系统源码_多屏幕(四)自由窗口模式
  • [原创]openwebui解决searxng通过接口请求不成功问题
  • Linux mount命令
  • C# 中 Array、ArrayList 和 List 的比较
  • 从“记住我”到 Web 认证:Cookie、JWT 和 Session 的故事
  • MySQL 8.4 SQL 全攻略:所有知识点与实战场景
  • Python常见面试题的详解25
  • ISIS(中间系统到中间系统)——基础
  • header在spring boot中解析
  • 什么是元数据管理?为什么数据治理的第一步是整理元数据?
  • docker container 修改 Mounts
  • Visual Studio 使用 GitHub Copilot 与 IntelliCode 辅助编码 【AI辅助开发系列】
  • 做网站还是博客/seo品牌优化百度资源网站推广关键词排名
  • 网站重定向怎么做/网页百度
  • 车公庙网站建设/专业seo网站优化推广排名教程
  • 网络公司网站报价/seo做的好的网站
  • 做网站赚广告/关键词组合工具
  • flashfxp 网站/网上怎么推广产品