
在spring boot应用中,默认情况下,通过`@bean`注解声明的对象是单例的,即所有注入点共享同一个实例。然而,对于`resttemplatebuilder`等有状态对象,这种共享可能导致意外的副作用。本文将详细介绍如何利用`@scope(“prototype”)`注解来创建非共享的、每次注入都生成新实例的bean,从而有效管理有状态组件,避免潜在的并发问题和状态污染。
理解Spring Bean的默认作用域
spring框架在管理Bean时,默认采用“单例”(Singleton)作用域。这意味着,当您在一个配置类中使用@Bean注解声明一个方法时,Spring IoC容器只会创建该Bean的一个实例。所有后续对该Bean的注入请求(通过@Autowired等)都将获得并共享这同一个实例。这种设计对于无状态的Bean来说非常高效,因为它减少了对象的创建和垃圾回收的开销。
然而,当Bean本身是有状态的,或者其内部状态在不同使用场景下需要独立维护时,默认的单例作用域就会带来问题。例如,RestTemplateBuilder是一个常见的Spring应用组件,用于构建RestTemplate实例。RestTemplateBuilder是有状态的,如果在构建过程中对其进行配置(如添加拦截器、设置超时等),这些配置会影响到所有使用该单例RestTemplateBuilder实例的地方,可能导致意料之外的副作用和行为冲突。
创建非共享(原型)Bean
为了解决有状态Bean的共享问题,Spring提供了“原型”(Prototype)作用域。当一个Bean被定义为原型作用域时,spring容器在每次请求该Bean时都会创建一个全新的实例。这确保了每个注入点都拥有一个独立的对象实例,从而避免了状态污染和副作用。
要将一个Bean定义为原型作用域,只需在@Bean注解的同时,添加@Scope(“prototype”)注解即可。
示例代码
以下是如何在spring boot中定义一个原型作用域的Bean的示例:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.beans.factory.config.ConfigurableBeanFactory; @Configuration public class appConfig { /** * 定义一个原型作用域的Person Bean。 * 每次注入或请求该Bean时,Spring都会创建一个新的Person实例。 */ @Bean @Scope("prototype") public Person personPrototype() { System.out.println("Creating a new Person prototype instance."); return new Person(); } /** * 另一种定义原型作用域的方式,使用ConfigurableBeanFactory常量。 * 这种方式更具类型安全性,推荐使用。 */ @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public MyService myServicePrototype() { System.out.println("Creating a new MyService prototype instance."); return new MyService(); } } // 示例实体类 class Person { private String name; // ... getters and setters } // 示例服务类 class MyService { private String config; // ... getters and setters }
在上面的示例中,personPrototype()方法和myServicePrototype()方法都创建了原型作用域的Bean。每次有其他组件通过@Autowired请求Person或MyService时,Spring容器都会执行相应的@Bean方法,并返回一个新的对象实例。
使用ConfigurableBeanFactory常量
为了提高代码的可读性和类型安全性,推荐使用ConfigurableBeanFactory.SCOPE_PROTOTYPE常量来指定原型作用域,而不是直接使用字符串”prototype”。
import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; // ... 其他导入 @Bean @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) public MyService myServicePrototype() { return new MyService(); }
原型Bean的生命周期管理
与单例Bean不同,Spring容器对原型Bean的生命周期管理有限。Spring负责创建原型Bean并将其交给客户端,但此后,容器不再跟踪其生命周期。这意味着,对于原型Bean的销毁回调(如@PreDestroy注解或DisposableBean接口),Spring容器不会自动调用。客户端代码需要自行负责原型Bean的清理工作,例如手动释放资源。
何时使用原型作用域
原型作用域主要适用于以下场景:
- 有状态的Bean:当Bean内部包含需要在不同使用场景下独立维护的状态时,如RestTemplateBuilder、StringBuilder、date对象等。
- 线程不安全的Bean:如果一个Bean不是线程安全的,并且需要在多线程环境中并发使用,将其定义为原型可以为每个线程提供独立的实例。
- 每次请求都需要全新实例的场景:例如,在Web应用中,每个http请求可能需要一个全新的、独立的业务处理对象。
注意事项
- 性能开销:由于每次请求都会创建新实例,原型Bean的创建和垃圾回收开销会比单例Bean高。因此,不应滥用原型作用域,只在确实需要时使用。
- 依赖注入:Spring容器在创建原型Bean时,会解析并注入其依赖。但如果原型Bean依赖于一个单例Bean,该单例Bean只会在原型Bean首次创建时注入一次。
- AOP代理:如果原型Bean需要被AOP代理,Spring会为每个原型实例创建一个独立的代理对象。
总结
Spring Bean的作用域是管理对象生命周期的核心概念。虽然默认的单例作用域在大多数情况下表现良好,但对于有状态或需要独立实例的Bean,如RestTemplateBuilder,理解并正确使用原型(@Scope(“prototype”))作用域至关重要。通过为这些特定Bean配置原型作用域,我们可以有效地避免潜在的状态冲突和副作用,确保应用程序的稳定性和可预测性。在决定Bean的作用域时,务必根据其实际需求和特性进行权衡。


