在Spring框架中,IoC容器管理的Bean默认都是单例的,也就是说无论你在多少个地方注入同一个Bean,拿到的都是同一个对象实例。这种设计在大多数场景下能节省内存并保证无状态Bean的复用,但当业务逻辑需要每个使用方持有独立的Bean实例时,默认的单例模式就会成为问题。Spring提供了@Scope注解来改变Bean的作用域,其中prototype(原型)模式可以让容器在每次获取Bean时都创建一个新实例。本文将围绕@Scope注解的用法、原理以及常见的坑点展开详细讲解。
一、@Scope注解基础用法与作用域类型
@Scope注解可以标注在类上或方法上,用于告诉Spring容器该Bean的作用域类型。当标注在@Configuration配置类中的@Bean方法上时,作用于该方法返回的Bean;当标注在@Component及其派生注解(如@Service、@Repository)修饰的类上时,作用于该类的实例。
Spring内置了多种作用域,最常用的有两种:singleton(默认值,容器中只存在一个实例)和prototype(每次获取都创建新实例)。此外,在Web环境下还有request(每个HTTP请求一个实例)、session(每个HTTP会话一个实例)、application(整个ServletContext一个实例)和websocket(每个WebSocket会话一个实例)。
下面通过一个简单的例子演示prototype作用域的基本用法:
// 方式一:标注在组件类上
@Component
@Scope("prototype")
public class OrderProcessor {
public OrderProcessor() {
System.out.println("OrderProcessor 实例化:" + this.hashCode());
}
}
// 方式二:标注在@Bean方法上
@Configuration
public class BeanConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public OrderProcessor orderProcessor() {
return new OrderProcessor();
}
}
上述代码中,每次从容器中获取OrderProcessor,都会触发构造方法执行,打印出不同的hashCode,说明确实是多实例。需要注意的是,@Scope的值既可以用字符串常量,也可以使用ConfigurableBeanFactory和WebApplicationContext中定义的常量,后者在编译期更安全。
二、prototype与singleton的行为差异
singleton与prototype的核心区别不仅在于实例数量,还体现在生命周期的管理责任上。对于singleton Bean,容器负责其完整的生命周期管理,包括实例化、依赖注入、初始化回调以及容器关闭时的销毁回调。
而prototype Bean则不同:容器只负责创建和初始化它,一旦创建完成并交给使用方之后,容器就不再跟踪它的生命周期。这意味着标注在prototype Bean上的销毁回调(如@PreDestroy修饰的方法、实现DisposableBean接口的destroy方法)不会被容器自动调用。如果prototype Bean持有数据库连接、文件句柄等需要释放的资源,使用方必须自己负责清理,这是很多开发者容易忽略的坑。
可以通过如下代码验证两种作用域的差异:
@SpringBootApplication
public class ScopeDemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(ScopeDemoApplication.class).run(args);
// prototype:两次获取,hashCode不同
Object bean1 = context.getBean("orderProcessor");
Object bean2 = context.getBean("orderProcessor");
System.out.println(bean1 == bean2); // 输出 false
// singleton:两次获取,同一个对象
Object bean3 = context.getBean("singletonService");
Object bean4 = context.getBean("singletonService");
System.out.println(bean3 == bean4); // 输出 true
}
}
另外还要注意懒加载与作用域的关系。prototype Bean在默认情况下不会随容器启动而实例化,只有真正被请求时才创建;而singleton Bean默认在容器启动时就会创建(除非加上@Lazy注解)。这也解释了为什么有些prototype Bean的构造方法在启动日志中看不到。
三、经典坑点:单例Bean中注入原型Bean失效问题
这是@Scope使用中最常见的陷阱。假设一个singleton的Controller中注入了一个prototype的Service,直觉上每次调用Controller方法时,Service都应该是新实例,但实际上注入只发生一次,Controller持有的始终是同一个Service对象。原因在于:依赖注入发生在singleton Bean初始化的时候,容器创建Controller时注入了一次Service,之后不会再注入。
@RestController
public class OrderController {
@Autowired
private OrderService orderService; // OrderService是prototype
@GetMapping("/order")
public String createOrder() {
// 无论调用多少次,orderService都是同一个对象
return "hash: " + orderService.hashCode();
}
}
解决这个问题的方案有三种。第一种是使用ObjectProvider,这是Spring官方推荐的现代方案:
@RestController
public class OrderController {
private final ObjectProvider<OrderService> orderServiceProvider;
public OrderController(ObjectProvider<OrderService> provider) {
this.orderServiceProvider = provider;
}
@GetMapping("/order")
public String createOrder() {
// 每次调用getObject都会获取新的prototype实例
OrderService service = orderServiceProvider.getObject();
return "hash: " + service.hashCode();
}
}
第二种是使用@Lookup注解。在singleton Bean中定义一个抽象方法并用@Lookup修饰,Spring会在运行时通过CGLIB生成子类重写该方法,每次调用都从容器中获取新实例:
@Component
public abstract class OrderCreator {
// 每次调用都会返回容器中新的prototype实例
@Lookup
public abstract OrderService getOrderService();
public void process() {
OrderService service = getOrderService();
System.out.println("当前实例:" + service.hashCode());
}
}
第三种是直接注入ApplicationContext,在需要时手动调用getBean方法。这种方式最直接,但会让业务代码耦合Spring容器,一般不推荐在规范的分层架构中使用。相比之下,ObjectProvider类型安全且解耦程度高,是首选方案。
四、Web作用域与proxyMode代理模式
在Spring Boot的Web应用中,request和session作用域非常实用。例如需要在一次HTTP请求内共享某个有状态对象,可以将其声明为request作用域。但这里有一个技术难点:如果把这个Bean注入到singleton Bean中,由于singleton只初始化一次,而request作用域的Bean生命周期依附于具体的HTTP请求,两者生命周期不匹配。
Spring通过代理模式解决这个问题。设置proxyMode属性后,容器注入的实际上是该Bean的代理对象,方法调用时代理会根据当前线程绑定的RequestContext找到真正的实例再转发调用。用法如下:
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSessionContext {
private String currentUser;
public String getCurrentUser() {
return currentUser;
}
public void setCurrentUser(String currentUser) {
this.currentUser = currentUser;
}
}
proxyMode有几个可选值:NO(不使用代理,默认值)、INTERFACES(基于JDK动态代理,要求Bean实现了接口)、TARGET_CLASS(基于CGLIB代理类)。在实际项目中,如果没有明确的接口抽象,建议使用TARGET_CLASS。需要注意的是,使用代理后,被代理的Bean不能是final类,方法也不能是final或private的,否则CGLIB无法重写。
五、使用建议与总结
在决定是否使用prototype作用域之前,先审视一下Bean是否真的需要持有可变状态。Spring社区一直倡导将业务组件设计为无状态的,无状态Bean天然线程安全且可以安全复用,这也是singleton成为默认作用域的原因。很多时候,真正需要多实例的可能只是一个数据对象,用普通Java对象或记录类即可,没必要上升为容器管理的Bean。
总结几点核心内容:第一,@Scope注解可以灵活切换Bean作用域,prototype实现每次获取新实例;第二,prototype Bean的销毁回调不会被容器调用,资源清理需要使用方自行负责;第三,singleton中直接注入prototype会失效,应使用ObjectProvider或@Lookup解决;第四,Web作用域需要配合proxyMode代理才能正确注入到singleton Bean中。掌握这些要点,就能在Spring Boot项目中游刃有余地管理Bean的生命周期与作用域了。
Spring BootScope注解Bean作用域修改时间:2026-08-31 06:48:49