内容纲要
是的,SpringBoot 中的 @Controller
(以及 @RestController
)默认是 单例(Singleton) 的。
原因在于:
-
Spring Bean 默认作用域
- Spring 容器管理的所有 Bean 默认作用域是
singleton
。 @Controller
只是一个特殊的 Bean 注解,本质上和@Component
一样,默认作用域同样是单例。
- Spring 容器管理的所有 Bean 默认作用域是
-
请求处理的线程安全问题
- 每次请求并不会创建新的 Controller 实例,而是由多个线程共享同一个 Controller 对象。
- 这意味着 不要在 Controller 中定义有状态的成员变量(例如:可变的 List、Map、计数器等),否则在并发访问时可能会发生线程安全问题。
- 正确做法是:把状态交给方法参数、本地变量,或者交给 Service / Repository 层处理。
-
如何修改作用域
如果确实有需求让 Controller 不是单例,可以用@Scope
修改,比如:@Controller @Scope("prototype") // 每次请求都会创建一个新的实例 public class MyController { }
但这种情况比较少见,因为 Spring MVC 的设计理念就是 Controller 是无状态的,单例即可。
👉 总结:
- 默认单例,所有请求共享同一个 Controller 实例。
- 注意线程安全,避免在 Controller 中定义有状态成员变量。
- 特殊需求可以用
@Scope("prototype")
改成多例。
🌐 SpringMVC 请求到 Controller 的流程(简化版)
这张图会帮你更直观理解为什么是单例
sequenceDiagram
participant Browser as 浏览器/客户端
participant Dispatcher as DispatcherServlet
participant HandlerMapping as HandlerMapping
participant Controller as @Controller (单例Bean)
participant Service as Service层
participant View as ViewResolver
Browser->>Dispatcher: 发送HTTP请求
Dispatcher->>HandlerMapping: 查找对应的Controller方法
HandlerMapping->>Dispatcher: 返回Handler(Controller+Method)
Dispatcher->>Controller: 调用Controller方法 (同一个单例对象)
Controller->>Service: 调用业务逻辑
Service-->>Controller: 返回结果数据
Controller-->>Dispatcher: 返回ModelAndView/ResponseBody
Dispatcher->>View: 渲染视图
View-->>Browser: 返回HTTP响应
↑↑↑Spring MVC 请求到 Controller 的生命周期示意图
🔑 关键点说明
-
Controller 是单例
- DispatcherServlet 每次请求过来时,会根据 HandlerMapping 找到已经注册好的 Controller Bean(单例),然后调用它的方法。
- 不是每个请求都
new
一个 Controller。
-
线程安全风险
- 因为同一个 Controller 实例会被多个线程调用,如果你在类里定义了共享的可变成员变量,可能会造成数据混乱。
- 正确做法:方法内使用局部变量,或者把状态托管到
Service
、DB
、Redis
等。
-
生命周期对比
- 单例 Controller:应用启动时就实例化,整个应用共享。
- 原型 Controller (
@Scope("prototype")
):每次注入或请求都会新建实例,但几乎没人这样用,会有性能和管理成本。
Controller 单例 vs 多例 对比表
📊 SpringBoot Controller 作用域对比
特性 | 默认单例(@Controller ) |
多例(@Controller @Scope("prototype") ) |
---|---|---|
生命周期 | Spring 容器启动时创建一个实例,整个应用中共享 | 每次注入 / 调用都会新建一个实例 |
内存开销 | 较低(只保留一个实例) | 较高(频繁创建和销毁对象) |
线程安全 | ⚠️ 有风险:多个线程共享同一个对象,若存在成员变量则可能冲突 | ✅ 安全:每个请求独立实例,不会共享成员变量 |
常见使用场景 | 绝大多数情况(Controller 方法无状态) | 少见:需要在 Controller 内维护独立的会话级 / 请求级状态时 |
性能 | 高效(单例复用) | 低效(频繁创建销毁) |
推荐实践 | 保持 Controller 无状态,把状态交给方法参数、Service、数据库 | 尽量避免;如果需要请求隔离,更推荐 @Scope("request") |
💡 结论
- 绝大部分业务场景下,单例足够且推荐。
-
如果确实要存储和隔离请求状态,可以考虑:
- 使用
@Scope("request")
,保证每个 HTTP 请求独立一个 Controller 实例。 - 使用
ThreadLocal
保存线程级变量(例如登录用户信息)。 - 将有状态的数据下放到 Service / Session / Redis,而不是放在 Controller 成员变量里。
- 使用