乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

@Scope 详解

定义与作用

@Scope 是 Spring 提供的**作用域声明(Scope Declaration)**注解,用于控制 Bean 实例的创建数量和生命周期可见范围。默认情况下,Spring 容器中的所有 Bean 都是 singleton 作用域——每个容器只创建一个实例,全局共享。但在某些场景下,这种默认行为并不合适:有状态的对象需要每个线程独立一份,Web 应用需要请求级隔离的数据封装,@Scope 让开发者能够精确控制这些行为。

在飞翔科技的学生管理系统中,小崔遇到了一个典型问题:StudentReportBuilder 在生成报表时维护了当前报表的标题、页码等状态。由于默认 singleton 作用域,多个用户同时请求报表时,状态互相覆盖,导致页码错乱、标题串扰。小崔通过在 StudentReportBuilder 上标注 @Scope("prototype"),让每个请求都获得全新的实例,彻底解决了并发状态污染问题。

适用位置与常用属性

属性类型说明
value / scopeNameString作用域名称,如 singleton、prototype、request、session、application、websocket
proxyModeScopedProxyMode作用域代理模式,用于解决单例 Bean 注入短作用域 Bean 的问题

适用位置:类级别(与 @Component 等一起使用)、方法级别(与 @Bean 一起使用)。

核心原理

Spring 容器在注册 BeanDefinition 时,会记录每个 Bean 的 scope 属性。当容器收到 getBean() 请求时,根据作用域决定返回策略:

  • singleton:从 singletonObjects 缓存池中直接返回已存在的实例,不存在则创建并缓存
  • prototype:每次都调用 createBean() 创建新实例,不缓存,不管理完整生命周期(不调用销毁回调)
  • request / session / application / websocket:依赖 Web 环境的 RequestContextListener 或 RequestContextFilter,将实例绑定到对应的 HTTP 生命周期域

六种作用域详解

作用域说明适用场景生命周期管理
singleton默认,每个 Spring 容器一个实例无状态服务、配置类、工具类容器启动时创建(或首次获取时),容器关闭时销毁
prototype每次请求都创建新实例有状态对象、多线程独立上下文、不可变构建器容器仅负责创建,销毁由 JVM GC 处理
request每个 HTTP 请求一个实例请求级数据封装、表单处理对象请求开始时创建,请求结束时销毁
session每个 HTTP Session 一个实例用户登录状态、购物车Session 创建时初始化,Session 过期或销毁时清理
application每个 ServletContext 一个实例应用级全局配置、缓存管理器应用启动时创建,应用关闭时销毁
websocket每个 WebSocket 会话一个实例长连接会话状态、实时消息处理器WebSocket 连接建立时创建,断开时销毁

注意:request、session、application、websocket 四个作用域需要在 Web-aware ApplicationContext(如 AnnotationConfigWebApplicationContext)中使用,且需要配置 RequestContextListener 或 RequestContextFilter 来绑定线程上下文。

完整示例

场景简述

飞翔科技的学生管理系统中,StudentReportBuilder 用于生成 PDF 学生成绩单。该对象在构建过程中维护了 reportTitle、currentPage、watermarkText 等状态。由于默认 singleton,并发请求时状态互相覆盖,导致 A 同学的成绩单上出现了 B 同学的姓名水印。小崔决定用 @Scope("prototype") 解决。

操作前:默认 singleton 导致的状态污染

@Component
public class StudentReportBuilder {

    private String reportTitle;
    private int currentPage;
    private String watermarkText;

    public void setReportTitle(String title) {
        this.reportTitle = title;
    }

    public void setWatermarkText(String text) {
        this.watermarkText = text;
    }

    public byte[] build(Long studentId) {
        // 使用 reportTitle、currentPage、watermarkText 生成报表
        currentPage++;
        return new byte[0]; // 模拟 PDF 数据
    }
}
@Service
public class ReportService {

    @Autowired
    private StudentReportBuilder reportBuilder; // singleton,全局共享

    public byte[] generateReport(Long studentId, String studentName) {
        reportBuilder.setReportTitle(studentName + " 的成绩单");
        reportBuilder.setWatermarkText("飞翔科技版权所有");
        return reportBuilder.build(studentId);
    }
}

并发问题:

  • 线程 A 调用 generateReport(1L, "小崔"),设置了 reportTitle = "小崔 的成绩单"
  • 线程 B 调用 generateReport(2L, "黄俪"),覆盖了 reportTitle = "黄俪 的成绩单"
  • 线程 A 继续执行 build(),发现标题变成了 "黄俪 的成绩单",数据错乱

使用该注解的完整代码

package com.feixiang.student.report;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("prototype")
public class StudentReportBuilder {

    private String reportTitle;
    private int currentPage;
    private String watermarkText;

    public StudentReportBuilder setReportTitle(String title) {
        this.reportTitle = title;
        return this;
    }

    public StudentReportBuilder setWatermarkText(String text) {
        this.watermarkText = text;
        return this;
    }

    public byte[] build(Long studentId) {
        // 模拟 PDF 生成过程
        currentPage = 1;
        System.out.println("生成报表:" + reportTitle);
        System.out.println("页码:" + currentPage);
        System.out.println("水印:" + watermarkText);
        System.out.println("实例哈希:" + System.identityHashCode(this));
        return new byte[1024];
    }
}
package com.feixiang.student.service;

import com.feixiang.student.report.StudentReportBuilder;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
public class ReportService {

    private final ObjectProvider<StudentReportBuilder> builderProvider;

    // 通过 ObjectProvider 每次获取新的 prototype 实例
    public ReportService(ObjectProvider<StudentReportBuilder> builderProvider) {
        this.builderProvider = builderProvider;
    }

    public byte[] generateReport(Long studentId, String studentName) {
        StudentReportBuilder builder = builderProvider.getObject();
        return builder
                .setReportTitle(studentName + " 的成绩单")
                .setWatermarkText("飞翔科技版权所有")
                .build(studentId);
    }
}
package com.feixiang.student;

import com.feixiang.student.config.AppConfig;
import com.feixiang.student.service.ReportService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class StudentApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx =
                new AnnotationConfigApplicationContext(AppConfig.class);

        ReportService reportService = ctx.getBean(ReportService.class);

        // 模拟两次独立的报表生成
        reportService.generateReport(1L, "小崔");
        reportService.generateReport(2L, "黄俪");

        ctx.close();
    }
}

操作后运行结果及分析

生成报表:小崔 的成绩单
页码:1
水印:飞翔科技版权所有
实例哈希:12345678

生成报表:黄俪 的成绩单
页码:1
水印:飞翔科技版权所有
实例哈希:87654321

变化分析:

  • 两次报表生成的 StudentReportBuilder 实例哈希不同,证明确实是两个独立对象
  • 每个实例的 currentPage 都从 1 开始,不存在状态串扰
  • ObjectProvider.getObject() 是 Spring 5.x 推荐的获取 prototype Bean 的方式,比直接 @Autowired 注入 prototype 更可靠(见下方易错场景)

Web 作用域示例

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {

    private String requestId;
    private long startTime;

    public void init(String requestId) {
        this.requestId = requestId;
        this.startTime = System.currentTimeMillis();
    }

    public String getRequestId() {
        return requestId;
    }

    public long getElapsedTime() {
        return System.currentTimeMillis() - startTime;
    }
}
@Service
public class StudentService {

    @Autowired
    private RequestContext requestContext; // singleton 注入 request 作用域 Bean

    public Student getStudent(Long id) {
        System.out.println("当前请求ID:" + requestContext.getRequestId());
        // ... 查询逻辑
        return new Student(id, "小崔", 20, "计算机科学");
    }
}

proxyMode 的必要性:StudentService 是 singleton,它在容器启动时就需要注入依赖。但 RequestContext 是 request 作用域,在启动时还没有 HTTP 请求。ScopedProxyMode.TARGET_CLASS 会为 RequestContext 生成 CGLIB 代理,实际的目标实例在每次 HTTP 请求时才创建。StudentService 持有的是代理对象,每次调用代理方法时,代理会从当前线程绑定的 Request 域中获取真实实例。

易错场景与面试考点

易错场景一:singleton Bean 直接注入 prototype Bean

@Component
@Scope("prototype")
public class StudentReportBuilder { ... }

@Service
public class ReportService {

    @Autowired
    private StudentReportBuilder reportBuilder; // 错误!
}

后果:ReportService 是 singleton,在容器启动时注入 StudentReportBuilder。Spring 只会在启动时创建一次 prototype 实例并注入,之后 ReportService 始终持有同一个 StudentReportBuilder,prototype 作用域完全失效。

解决方案:

  • 使用 ObjectProvider<T> 延迟获取(推荐,Spring 5.x+)
  • 使用 javax.inject.Provider<T>(JSR-330 标准)
  • 在 @Scope 上设置 proxyMode = ScopedProxyMode.TARGET_CLASS,让 Spring 生成代理
// 正确做法一:ObjectProvider
@Service
public class ReportService {
    private final ObjectProvider<StudentReportBuilder> builderProvider;
    public ReportService(ObjectProvider<StudentReportBuilder> builderProvider) {
        this.builderProvider = builderProvider;
    }
    public byte[] generateReport(...) {
        StudentReportBuilder builder = builderProvider.getObject(); // 每次获取新实例
        // ...
    }
}

// 正确做法二:Scoped Proxy
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class StudentReportBuilder { ... }

易错场景二:prototype Bean 的销毁回调不执行

@Component
@Scope("prototype")
public class TemporaryResource implements DisposableBean {

    private File tempFile;

    public TemporaryResource() {
        this.tempFile = new File("/tmp/student_" + System.currentTimeMillis() + ".tmp");
    }

    @Override
    public void destroy() {
        tempFile.delete(); // 清理临时文件
    }
}

后果:destroy() 永远不会被调用。Spring 容器对 prototype Bean 只负责创建,创建后就不再持有引用,因此无法触发销毁回调。临时文件会残留在磁盘上,造成资源泄漏。

正确做法:在业务代码中手动管理 prototype Bean 的生命周期,或改用 ObjectProvider 并在使用完毕后显式清理:

public void processData() {
    TemporaryResource resource = provider.getObject();
    try {
        // 使用资源
    } finally {
        resource.destroy(); // 手动调用清理
    }
}

面试考点

Q:singleton 是否线程安全?

singleton 本身不保证线程安全。线程安全取决于 Bean 内部状态是否可变。如果 Bean 是无状态的(如 Service 层只调用 DAO,不维护字段状态),则天然线程安全;如果 Bean 是有状态的(如维护了 currentUser、counter 等字段),多线程并发访问时需要同步机制,或改用 prototype/request 作用域。

Q:prototype 作用域的 Bean 何时销毁?

Spring 容器不管理 prototype Bean 的销毁。容器创建实例后就不再持有引用,实例的销毁由 JVM 垃圾回收器决定。如果 prototype Bean 持有需要显式释放的资源(如数据库连接、临时文件),必须在业务代码中手动处理,或实现自定义的 BeanPostProcessor 来跟踪 prototype 实例。

Q:@Scope 的 proxyMode 有哪些选项?

NO(默认,不生成代理)、INTERFACES(JDK 动态代理,要求目标类实现接口)、TARGET_CLASS(CGLIB 代理,适用于类)。当 singleton Bean 需要注入短作用域(request/session/prototype)Bean 时,必须使用代理模式,否则短作用域会在容器启动时就被"固化"为单例。

Q:Spring Boot 中 request 作用域 Bean 在单元测试中如何获取?

在非 Web 环境的单元测试中,没有真实的 HTTP 请求上下文。可以使用 RequestContextListener 或 MockHttpServletRequest 手动绑定请求上下文,或使用 @WebAppConfiguration 标注测试类加载 WebApplicationContext。Spring Boot Test 还提供了 @AutoConfigureMockMvc 来模拟 Web 环境。

Q:application 作用域和 singleton 有什么区别?

在单个 Spring 应用上下文中,两者行为相同(都是一个实例)。区别在于:如果应用中存在多个 ApplicationContext(如 Spring MVC 的 Root Context 和 Servlet Context),singleton 是每个 Context 一个实例,而 application 是整个 ServletContext 一个实例,跨 Context 共享。在典型的 Spring Boot 应用中,两者没有实际差异。

上一页
@Value 详解
下一页
@Lazy 详解