导读:本期聚焦于比特币程序员创作的《Spring Boot中如何实现Bean多实例?@Scope注解详解与常见坑点分析》,敬请观看详情。为什么同一个Spring Bean注入两次拿到的是同一个对象?这背后是IoC容器默认的单例模式在起作用。当业务需要多个独立实例时,@Scope注解就成了关键工具。本文系统讲解@Scope的基本用法,重点分析prototype和singleton两种作用域的行为差异,说明@Scope在@Component和@Bean两种注册方式下的使用细节,并深入剖析单例Bean中注入原型Bean时产生的坑点,给出@Lookup注解、ObjectFactory和ObjectProvider三种解决方案。同时还会介绍request、session等Web作用域的适用场景,以及proxyMode代理模式的作用与选择建议,帮助你彻底掌握Spring Bean生命周期管理。

在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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。