Bean 作用域与使用注意
结论先行:Bean 默认作用域是 singleton(每个容器内每个 beanName 只有一个实例);prototype 每次获取都 new 一个新实例;Web 环境另有 request / session / application / websocket。使用注意三条:① 大对象/有状态对象慎用 singleton 做共享可变状态;② 长生命周期 Bean 注入短生命周期 Bean(如 singleton 注入 prototype/session)时,必须用作用域代理或每次现取,否则拿到的永远是第一个实例;③ prototype Bean 不受容器销毁回调管理。
一、作用域速查
| 作用域 | 生命周期 | 说明 |
|---|---|---|
| singleton(默认) | 容器启动/首次创建,容器关闭销毁 | 每容器每 beanName 一个实例,共享需防并发 |
| prototype | 每次 getBean/注入创建 | 容器不跟踪其销毁(destroy 回调不执行),需自行清理 |
| request | 一次 HTTP 请求 | 仅 Web 容器,请求结束销毁 |
| session | 一次 HTTP 会话 | 仅 Web 容器 |
| application / websocket | ServletContext / WebSocket 生命周期 | 仅 Web 容器 |
二、跨作用域注入的坑
// 正确做法:在 Bean 定义处声明"作用域 + 代理模式"(也可用 @SessionScope/@RequestScope 便捷注解)
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS) // 生成 CGLIB 作用域代理
public class LoginContext {
private Long userId;
// getter/setter ...
}
@Component
public class UserService {
// singleton 注入 session Bean:实际拿到的是"作用域代理",
// 每次调用方法都委托给当前会话的真实 LoginContext,而不是固定第一个用户的实例
@Autowired
private LoginContext ctx;
}
// 错误做法:LoginContext 本身是 singleton,却想在注入点用 @Scope 改变它 → 无效且误导
// 备选:注入 ObjectProvider<LoginContext>,调用时 getObject() 每次现取
// 备选:prototype 场景可用 @Lookup 方法注入,每次调用返回新实例
三、使用注意清单
- singleton 中的共享可变字段有线程安全问题,应设计为无状态或使用 ThreadLocal/加锁;
- prototype 每次新建有开销,不要在循环里高频 getBean;
- 定义作用域:XML 用 scope 属性,注解用 @Scope("prototype"),常量建议 ConfigurableBeanFactory.SCOPE_PROTOTYPE;
- 验证方式:容器 getBean 两次,prototype 的 hashCode 不同、singleton 相同。
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public TaskWorker taskWorker() { return new TaskWorker(); }
常见追问 / 记忆点
- 追问 1:Spring 的 singleton 与 GoF 单例模式一样吗?答:不一样——Spring 单例是"容器作用域内每 beanName 一个实例",不是 JVM 全局,且由容器管理生命周期。
- 追问 2:singleton 注入 prototype 依赖为什么不生效?答:singleton 在启动时实例化并固定注入引用,之后不再重新注入;解法是作用域代理/ObjectProvider/@Lookup。
- 记忆点:"默认单例省心但共享要防并发;跨作用域注入必上代理或现取"。