【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 的工作机制与生命周期
让我们通过一个测试来直观感受 prototype
与 singleton
的区别。
@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}
}
输出结果将会证明,cart1
和 cart2
是两个完全不同的对象,拥有各自独立的状态。
生命周期关键点:
- 初始化: 每次创建新实例时,
@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。 -
使用
ObjectFactory
或Provider
(推荐):@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 最佳实践总结
- 审慎使用: 不要因为“感觉可能需要”就使用
prototype
。singleton
在无状态服务中因其高性能和低内存开销,仍然是绝大多数情况下的最佳选择。 - 明确状态: 只有当 Bean 确实需要维护独立的状态,并且该状态在多个请求间不能共享时,才考虑使用
prototype
。 - 解决注入问题: 当在
singleton
中依赖prototype
时,优先使用ObjectFactory
或Provider
来按需获取新实例。 - 管理资源: 牢记你需要负责
prototype
bean 生命周期的结尾部分,做好资源清理工作。
6. 总结
prototype
scope 是 Spring 提供的一个强大工具,它打破了“一切皆单例”的思维定式,为处理有状态、非线程安全或需要独立会话的场景提供了完美的解决方案。
然而,能力越大,责任越大。使用 prototype
意味着你需要更深入地理解其生命周期,并小心处理它与 singleton
bean 的依赖关系,以及潜在的内存泄漏风险。希望本篇博客能帮助你在未来的 Spring 开发中,更加自信和正确地运用 prototype
scope,让你的应用架构更加清晰和健壮。