Bean 作用域

Bean 作用域(Scope)决定 Spring 容器何时创建创建几个生命周期持续多久。不同作用域适用于不同场景。


作用域速查

作用域实例数生命周期适用环境
singleton(默认)每容器 1 个容器启动 → 容器关闭通用
prototype每次注入/获取新建 1 个使用方负责管理有状态对象
request每个 HTTP 请求 1 个请求开始 → 请求结束Web
session每个 HTTP Session 1 个Session 创建 → Session 过期Web
application每个 ServletContext 1 个应用启动 → 应用关闭Web
websocket每个 WebSocket 连接 1 个连接建立 → 连接关闭WebSocket

Singleton(单例,默认)

容器中只存在一个共享实例,所有注入点拿到的是同一个对象:

// 无需任何注解,默认即为 singleton
@Service
public class UserService {
    // 整个应用生命周期内只有一个实例
}
 
// 显式声明(冗余,仅作说明用)
@Bean
@Scope("singleton")
public OrderService orderService() {
    return new OrderService();
}

适用场景:无状态服务、工具类、配置类。
注意:单例 Bean 中不应保存请求级别的状态(如当前用户信息),线程不安全的字段会引发并发问题。


Prototype(原型)

每次注入或调用 getBean()新建一个实例

@Component
@Scope("prototype")
public class ReportTask {
 
    private String reportId;
    private List<String> results = new ArrayList<>();
 
    public void setReportId(String id) { this.reportId = id; }
    public void addResult(String result) { results.add(result); }
    public List<String> getResults() { return results; }
}
@Service
@RequiredArgsConstructor
public class ReportService {
 
    private final ApplicationContext context;
 
    public void generateReport(String reportId) {
        // 每次获取全新实例,避免状态污染
        ReportTask task = context.getBean(ReportTask.class);
        task.setReportId(reportId);
        task.addResult("数据加载完成");
        // ...
    }
}

Spring 不管理 Prototype Bean 的销毁@PreDestroyDisposableBean.destroy() 不会被调用。若持有资源(连接、线程),需由使用方手动释放。

单例 Bean 注入 Prototype Bean 的陷阱

单例 Bean 只初始化一次,其中注入的 Prototype Bean 也只创建一次,后续不再新建,失去 Prototype 意义:

// ❌ 错误:每次调用 getTask() 返回的是同一个 prototype 实例
@Service
public class BadService {
    @Autowired
    private ReportTask task;   // 只在 BadService 初始化时注入一次
 
    public ReportTask getTask() { return task; }
}

解决方案一:注入 ApplicationContext,每次手动 getBean(见上例)

解决方案二:@Lookup 方法注入(Spring 通过 CGLIB 覆盖该方法)

@Service
public abstract class ReportService {
 
    // Spring 自动覆盖此方法,每次调用返回新的 Prototype 实例
    @Lookup
    public abstract ReportTask createTask();
 
    public void run(String reportId) {
        ReportTask task = createTask();   // 每次调用都是新实例
        task.setReportId(reportId);
    }
}

解决方案三:ObjectProvider(推荐,类型安全)

@Service
@RequiredArgsConstructor
public class ReportService {
 
    private final ObjectProvider<ReportTask> taskProvider;
 
    public void run(String reportId) {
        ReportTask task = taskProvider.getObject();   // 每次获取新实例
        task.setReportId(reportId);
    }
}

Web 作用域

Web 作用域仅在 Web 应用(Servlet 容器)中可用,需要 spring-webmvcspring-webflux

Request(请求作用域)

每个 HTTP 请求独立一个实例,请求结束后销毁:

@Component
@RequestScope                       // 等价于 @Scope("request")
// 或 @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
 
    private String traceId;
    private String userId;
    private long startTime = System.currentTimeMillis();
 
    // getter / setter
}
@RestController
@RequiredArgsConstructor
public class OrderController {
 
    private final RequestContext requestContext;   // 注入的是代理对象
 
    @PostMapping("/orders")
    public Order createOrder(@RequestBody CreateOrderRequest req) {
        requestContext.setUserId(SecurityContextHolder.getContext()
            .getAuthentication().getName());
        // 在同一请求的任何地方都能获取到相同的 RequestContext 实例
        return orderService.create(req, requestContext);
    }
}

Session(会话作用域)

每个 HTTP Session 独立一个实例,Session 过期后销毁:

@Component
@SessionScope                       // 等价于 @Scope("session")
public class ShoppingCart {
 
    private final List<CartItem> items = new ArrayList<>();
 
    public void addItem(CartItem item) { items.add(item); }
    public void removeItem(Long productId) {
        items.removeIf(i -> i.getProductId().equals(productId));
    }
    public List<CartItem> getItems() { return Collections.unmodifiableList(items); }
    public BigDecimal getTotal() {
        return items.stream()
            .map(i -> i.getPrice().multiply(BigDecimal.valueOf(i.getQuantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

Application(应用作用域)

整个 ServletContext 共享一个实例(与 Singleton 类似,但绑定的是 ServletContext 而非 Spring 容器):

@Component
@ApplicationScope
public class GlobalConfig {
    private volatile String maintenanceMessage = "";
    // ...
}

作用域代理(Scoped Proxy)

核心问题:单例 Bean 依赖短生命周期 Bean(如 Request/Session/Prototype)时,注入时机不匹配。

单例 Bean 只初始化一次,而 Request Bean 每个请求都是新实例。直接注入会拿到一个过期的固定实例

解决方案:作用域代理。Spring 注入的是一个 CGLIB 代理,每次方法调用时代理自动路由到当前作用域的真实实例

// 显式指定代理模式
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private String userId;
    // ...
}
 
// @RequestScope / @SessionScope 已内置了 ScopedProxyMode.TARGET_CLASS
// 直接使用这两个注解无需额外配置
// 单例 Bean 安全注入 Request 作用域 Bean
@Service
@RequiredArgsConstructor
public class AuditService {
 
    // requestContext 是代理对象,每次方法调用自动委托给当前请求的真实实例
    private final RequestContext requestContext;
 
    public void audit(String action) {
        // 正确:每次调用都拿到当前请求的 userId
        log.info("用户 {} 执行了: {}", requestContext.getUserId(), action);
    }
}
代理模式说明适用场景
ScopedProxyMode.NO不使用代理(默认)仅在同作用域内注入
ScopedProxyMode.INTERFACESJDK 动态代理(目标需实现接口)有接口的 Bean
ScopedProxyMode.TARGET_CLASSCGLIB 代理无接口的普通类(推荐)

自定义作用域

实现 Scope 接口并注册,可创建任意生命周期的作用域:

// 示例:线程作用域(每个线程一个实例)
public class ThreadScope implements Scope {
 
    private final ThreadLocal<Map<String, Object>> threadScope =
        ThreadLocal.withInitial(HashMap::new);
 
    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Map<String, Object> scope = threadScope.get();
        return scope.computeIfAbsent(name, k -> objectFactory.getObject());
    }
 
    @Override
    public Object remove(String name) {
        return threadScope.get().remove(name);
    }
 
    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        // 线程结束时调用(可通过 ThreadLocal 的 remove 扩展)
    }
 
    @Override
    public Object resolveContextualObject(String key) { return null; }
 
    @Override
    public String getConversationId() {
        return String.valueOf(Thread.currentThread().getId());
    }
}
 
// 注册自定义作用域
@Configuration
public class ScopeConfig {
 
    @Bean
    public static CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        configurer.addScope("thread", new ThreadScope());
        return configurer;
    }
}
 
// 使用自定义作用域
@Component
@Scope("thread")
public class ThreadLocalContext {
    private String operatorId;
    // ...
}

Spring 内置 SimpleThreadScope,可以直接注册使用:

@Bean
public static CustomScopeConfigurer threadScopeConfigurer() {
    CustomScopeConfigurer configurer = new CustomScopeConfigurer();
    configurer.addScope("thread", new SimpleThreadScope());
    return configurer;
}

作用域选型指南

Bean 有状态?
├── 否 → Singleton(默认,无需配置)
└── 是 → 状态绑定到哪里?
         ├── 方法/任务调用 → Prototype(配合 @Lookup 或 ObjectProvider)
         ├── HTTP 请求    → @RequestScope
         ├── 用户会话     → @SessionScope
         ├── 当前线程     → thread scope(SimpleThreadScope)
         └── 全局共享状态 → Singleton(加锁保护并发写入)

与生命周期的关系

作用域初始化时机销毁时机@PreDestroy 是否生效
Singleton容器启动时(默认)/ 首次使用(@Lazy)容器关闭
Prototype每次请求时使用方负责
Request每个 HTTP 请求请求结束
SessionSession 创建Session 过期
Application容器启动容器关闭

Singleton 的延迟初始化(@Lazy)详见 延迟加载。Bean 销毁回调的完整机制详见 Bean生命周期


相关链接

  • Bean生命周期 — 初始化/销毁回调的完整执行顺序
  • IOC与DI — 三种依赖注入方式与作用域的配合
  • 延迟加载@Lazy 推迟 Singleton Bean 的初始化时机
  • 循环依赖 — 作用域代理与三级缓存的关系
  • AOP — CGLIB 代理(Scoped Proxy)的底层实现
  • Session管理 — Session 作用域与 HttpSession 的配合