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

【Spring 3】深入剖析 Spring 的 Prototype Scope:何时以及如何使用非单例 Bean

在 Spring 框架的世界里,Bean 的作用域(Scope)是一个核心概念,它定义了 Bean 的生命周期和创建模式。绝大多数开发者最熟悉的是 singleton scope,它是 Spring 的默认设置,确保了整个应用中每个 IoC 容器只存在一个 Bean 实例。然而,当你的应用场景需要更高的灵活性或状态独立性时,singleton 就显得力不从心了。

今天,我们将把目光聚焦于 prototype scope,深入探讨这个“非单例”模式,揭示其工作原理、适用场景以及需要避开的陷阱。

1. 什么是 Prototype Scope?

简单来说,将一个 Bean 的 scope 设置为 prototype,就是告诉 Spring 容器:每次请求(获取)这个 Bean 时,都请创建一个全新的实例。

你可以将它类比为 Java 中的 new 关键字。每次调用 getBean() 或通过 @Autowired 注入时,容器都会执行一次初始化流程,为你返回一个独立的对象。

官方定义: Prototype scope 的 Bean,其生命周期是:创建 -> 依赖注入 -> 初始化 -> 返回给客户端 -> 容器不再管理其销毁。这意味着,Spring 负责“生”,但不负责“死”,prototype bean 的销毁逻辑需要由客户端代码或 GC 来处理。

2. 如何配置 Prototype Scope?

配置起来非常简单,有以下几种常见方式:

2.1 使用注解(推荐)

在类定义上使用 @Scope 注解:

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;@Component
@Scope("prototype") // 或者 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ShoppingCart {private List<Item> items = new ArrayList<>();public void addItem(Item item) {items.add(item);}// getters and other methods...
}
2.2 在 Java Config 中声明
@Configuration
public class AppConfig {@Bean@Scope("prototype")public ShoppingCart shoppingCart() {return new ShoppingCart();}
}
2.3 在 XML 配置中声明
<bean id="shoppingCart" class="com.example.ShoppingCart" scope="prototype"/>

3. 深入理解:Prototype 的工作机制与生命周期

让我们通过一个测试来直观感受 prototypesingleton 的区别。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ScopeTest {@Autowiredprivate ApplicationContext applicationContext;@Testpublic void testPrototypeScope() {// 第一次请求 BeanShoppingCart cart1 = applicationContext.getBean(ShoppingCart.class);cart1.addItem(new Item("Book"));// 第二次请求同一个 BeanShoppingCart cart2 = applicationContext.getBean(ShoppingCart.class);cart2.addItem(new Item("Pen"));// 验证是否为两个不同的实例System.out.println("cart1 instance: " + cart1);System.out.println("cart2 instance: " + cart2);System.out.println("Are they the same? " + (cart1 == cart2)); // 输出:false// 验证它们的状态是独立的System.out.println("cart1 items count: " + cart1.getItems().size()); // 输出:1System.out.println("cart2 items count: " + cart2.getItems().size()); // 输出:1}
}

输出结果将会证明,cart1cart2 是两个完全不同的对象,拥有各自独立的状态。

生命周期关键点

  • 初始化: 每次创建新实例时,@PostConstruct 方法都会被调用。
  • 销毁@PreDestroy 方法不会被 Spring 容器调用。因为容器将实例交给请求者后,就放弃了对它的管理。

4. 经典应用场景:为什么需要 Prototype?

4.1 持有状态的场景

最典型的例子就是 ShoppingCart。每个用户的购物车都应该是独立的、有状态的。如果使用 singleton,所有用户都会共享同一个购物车对象,导致数据混乱。

4.2 线程不安全类的封装

例如,SimpleDateFormat 是著名的非线程安全类。你可以定义一个 prototype 的 Bean 来包装它,确保每个需要它的服务都能获得一个独立的实例,避免并发问题。

@Component
@Scope("prototype")
public class DateFormatter {private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");public String format(Date date) {return sdf.format(date);}
}
4.3 高并发计算或处理

假设有一个 ReportGenerator,用于生成复杂的报表。在并发请求下,如果使用 singleton,生成器的内部状态可能会被相互覆盖。使用 prototype 可以为每个生成请求提供一个干净的、独立的处理器。

5. 常见的陷阱与最佳实践

5.1 陷阱 1:在 Singleton Bean 中注入 Prototype Bean

这是一个非常经典的陷阱!

@Component
public class OrderService { // 默认是 singleton@Autowiredprivate ShoppingCart shoppingCart; // 这是一个 prototype!public void processOrder() {shoppingCart.addItem(...);// 问题:由于 OrderService 是单例,它只在初始化时被注入了一次 ShoppingCart。// 后续所有通过 OrderService 调用的 processOrder 方法,操作的都是同一个 ShoppingCart 实例!}
}

解决方案

  • 方法注入(@Lookup:

    @Component
    public abstract class OrderService {public void processOrder() {ShoppingCart cart = getShoppingCart(); // 每次调用都获取新的实例cart.addItem(...);}@Lookupprotected abstract ShoppingCart getShoppingCart();
    }
    

    Spring 会通过 CGLIB 生成子类来实现 getShoppingCart() 方法,使其每次调用都返回新的 prototype bean。

  • 使用 ObjectFactoryProvider (推荐):

    @Component
    public class OrderService {@Autowiredprivate ObjectFactory<ShoppingCart> shoppingCartFactory;// 或者使用 javax.inject.Provider: private Provider<ShoppingCart> shoppingCartProvider;public void processOrder() {ShoppingCart cart = shoppingCartFactory.getObject(); // 每次调用 getObject()// ShoppingCart cart = shoppingCartProvider.get(); // 使用 Provider 的方式cart.addItem(...);}
    }
    

    这种方式更加灵活且对代码侵入性小,是当前的首选方案。

  • 通过 ApplicationContext:
    直接注入 ApplicationContext,然后在方法中调用 getBean(ShoppingCart.class)。这种方式虽然可行,但将代码与 Spring API 紧耦合,不推荐。

5.2 陷阱 2:内存泄漏

由于 Spring 不管理 prototype bean 的销毁,如果这个 bean 持有昂贵资源(如数据库连接、文件句柄等),你必须确保在使用完毕后能正确释放这些资源。这通常需要客户端代码实现类似 close() 的方法并主动调用。

5.3 最佳实践总结
  1. 审慎使用: 不要因为“感觉可能需要”就使用 prototypesingleton 在无状态服务中因其高性能和低内存开销,仍然是绝大多数情况下的最佳选择。
  2. 明确状态: 只有当 Bean 确实需要维护独立的状态,并且该状态在多个请求间不能共享时,才考虑使用 prototype
  3. 解决注入问题: 当在 singleton 中依赖 prototype 时,优先使用 ObjectFactoryProvider 来按需获取新实例。
  4. 管理资源: 牢记你需要负责 prototype bean 生命周期的结尾部分,做好资源清理工作。

6. 总结

prototype scope 是 Spring 提供的一个强大工具,它打破了“一切皆单例”的思维定式,为处理有状态、非线程安全或需要独立会话的场景提供了完美的解决方案。

然而,能力越大,责任越大。使用 prototype 意味着你需要更深入地理解其生命周期,并小心处理它与 singleton bean 的依赖关系,以及潜在的内存泄漏风险。希望本篇博客能帮助你在未来的 Spring 开发中,更加自信和正确地运用 prototype scope,让你的应用架构更加清晰和健壮。

http://www.dtcms.com/a/456824.html

相关文章:

  • asp.net+mvc+网站开发wordpress 手机端页面
  • 【开题答辩全过程】以 爱篮球app为例,包含答辩的问题和答案
  • 深入理解跨域问题与解决方案
  • 从零搭建 RAG 智能问答系统1:基于 LlamaIndex 与 Chainlit实现最简单的聊天助手
  • Redis核心通用命令解析
  • 后端(JavaWeb)学习笔记(CLASS 1):maven
  • 后端_Redis 分布式锁实现指南
  • K8s学习笔记(十六) 探针(Probe)
  • 企业个人网站口碑营销策略
  • c语言网站三星网上商城分期
  • Gradient Descent and Its Implementation in TensorFlow|梯度下降及其在 TensorFlow 中的实现
  • 大模型解码策略深度解析:从原理到工程实践
  • 【Java并发】揭秘Lock体系 -- 深入理解ReentrantReadWriteLock
  • xedu和5070
  • gitlab 在centos7 下的安装和基本使用
  • 优化GitHub访问问题
  • 二、项目结构与版本控制规范
  • 快消存量竞争时代:洗衣液 “三级加速器” 成行业新范本
  • 网站建设实训致谢语电商网站运营策划
  • 三分钟做网站网站访客统计代码
  • Arduino开发ESP32点亮一个LED【适合新手】
  • 【心理分析】好为人师
  • 离线二维码生成器,无需网络自制专属二维码
  • OpenCV(六):TrackBar控件
  • 网站开发 验收模板手机网站案例 鸿
  • 向量化编码和RAG增强搜索
  • 分布式场景下防止【缓存击穿】的不同方案
  • 《Cargo 参考手册》第二章:工作区(Workspaces)
  • 2025山西旅游攻略(个人摩旅版-国庆从北京到山西)
  • 博弈论——一些概念