在 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 {
@Autowired
private ApplicationContext applicationContext;
@Test
public void testPrototypeScope() {
// 第一次请求 Bean
ShoppingCart cart1 = applicationContext.getBean(ShoppingCart.class);
cart1.addItem(new Item("Book"));
// 第二次请求同一个 Bean
ShoppingCart 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()); // 输出:1
System.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
@Autowired
private 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(...); } @Lookup protected abstract ShoppingCart getShoppingCart(); }Spring 会通过 CGLIB 生成子类来实现
getShoppingCart()方法,使其每次调用都返回新的prototypebean。 -
使用
ObjectFactory或Provider(推荐):@Component public class OrderService { @Autowired private 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来按需获取新实例。 - 管理资源: 牢记你需要负责
prototypebean 生命周期的结尾部分,做好资源清理工作。
6. 总结
prototype scope 是 Spring 提供的一个强大工具,它打破了“一切皆单例”的思维定式,为处理有状态、非线程安全或需要独立会话的场景提供了完美的解决方案。
然而,能力越大,责任越大。使用 prototype 意味着你需要更深入地理解其生命周期,并小心处理它与 singleton bean 的依赖关系,以及潜在的内存泄漏风险。希望本篇博客能帮助你在未来的 Spring 开发中,更加自信和正确地运用 prototype scope,让你的应用架构更加清晰和健壮。
693

被折叠的 条评论
为什么被折叠?



