【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 {

    @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
    }
}

输出结果将会证明,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

    @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() 方法,使其每次调用都返回新的 prototype bean。

  • 使用 ObjectFactoryProvider (推荐):

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

6. 总结

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AllenBright

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值