Spring MVC 控制器到底是不是单例?怎么破局?
嗨大家好呀,我是小米,一个喜欢边学边分享,把坑踩过一遍再告诉你怎么绕开的技术宅控!
最近我在准备换工作的社招面试(真是社畜中的社畜啊……),被问了一个超级经典但又能坑死人的问题:
Spring MVC 的控制器(Controller)是单例的吗?如果是,会有什么问题?怎么解决?
听到这个问题那一刻,我表面微笑,内心咯噔:
“完了,要是答得不清不楚,面试官又要画叉叉了……”
好在我之前踩过这个坑,一口气讲了个通透,还得到了面试官的点赞!
今天我就把完整的故事和解题思路分享给大家。
拿好小板凳,咱们从头讲起!
记忆中的第一个坑:Controller 的单例本质!记得我刚学 Spring MVC 的时候,脑子里想当然觉得:
“控制器嘛,就是处理一个请求,创建一个对象,处理完就丢掉,多清晰!”
结果呢?查源码一看,啪啪打脸!
实际上,Spring MVC 默认把 Controller 当作单例(Singleton)来管理的!
也就是说,咱们写的这个:
默认是单例模式(Singleton Scope),由 Spring 容器托管,启动时创建一个实例,整个应用生命周期共用这一份!
所以,记住一句话:
Spring 中的 @Controller 本质上是一个单例 Bean!
那为啥 Spring 要这么搞呢?
其实很简单,节省资源,提高性能!
如果每来一个请求就 new 一个 Controller,想想服务器内存得爆炸成啥样子?而且 Controller 通常是无状态的(处理逻辑、调用 Service),并不需要为每次请求新建实例。
所以,单例是合理的默认选择!
但是——
事情到这里,才刚刚开始。
单例带来的隐患:线程安全问题!单例 + 多线程,听着就危险,对吧?
没错,Controller 是单例,但是 用户请求是多线程并发的。
一旦 Controller 里写了成员变量,而且这个成员变量又是可变的、共享的,那简直是灾难现场!
比如:
看着没啥问题对吧?
但是注意啊!
用户A提交了一个orderId:1001
用户B紧接着提交了一个orderId:1002
因为Controller是单例的,他们共用同一个 lastOrderId!
结果:
A本来想处理自己提交的1001,结果处理到一半,lastOrderId 被 B 改成了1002……
数据错乱、请求串台、诡异Bug,分分钟爆炸!
这就是典型的线程安全问题!
总结一下:
Spring MVC Controller 单例本身没问题,问题在于如果 Controller 里保存了【有状态的可变成员变量】,就会引发线程安全问题!
面试官想听的:怎么解决?好,既然知道问题了,那接下来最重要的就是——怎么解决?
思路一:保证 Controller 无状态
不要在 Controller 里写可变的成员变量!
所有数据都通过方法参数传递。
比如刚才的 lastOrderId,正确写法应该是:
这样,每个请求进来,拿的是自己方法参数里的数据,不会互相污染。
记住一句话:
Controller 要像一潭死水一样冷静,不要有变化,保持无状态!
思路二:必要时改变作用域
如果业务场景确实需要保存一些请求级别的数据,比如一步步流程操作,那么可以考虑改变 Bean 的作用域!
使用 @Scope("request")
让每个请求有自己的 Controller 实例。
比如:
加上 @Scope("request"),
Spring 会给每个请求创建一个新的 Controller 实例,互不影响!
当然啦,这样就失去了单例带来的性能优势了,要慎重选择。
大部分场景下,通过方法参数传递就够了,很少需要改变作用域。
思路三:使用 ThreadLocal如果真的需要存 per-request 数据,还可以用ThreadLocal。
ThreadLocal 保证每个线程有独立副本,互不干扰。
注意,用完一定要 remove()!不然可能会导致内存泄漏,尤其是在线程池环境下。
小米的社招总结答法(亲测有效)最后,总结一下,社招面试我怎么答的:
面试官问:“Spring MVC 控制器是单例的吗?如果是,有什么问题?怎么解决?”
我答:
Spring MVC 的控制器默认是单例的,由 Spring 容器管理。
单例本身没问题,但如果 Controller 里存在可变的成员变量,在多线程并发请求下会引发线程安全问题。
解决办法有:
最推荐:保持 Controller 无状态,只通过方法参数传递数据;
必要时可以将 Controller 设为请求作用域(@Scope("request"));
或者使用 ThreadLocal 保存每个请求的独立数据,但注意清理。
面试官点头微笑,
我心里一阵狂喜,暗搓搓给自己比了个✌️。
总结一下今天的故事今天我们讲了:
Spring MVC 控制器是默认单例的(Singleton Scope);
单例会引发线程安全问题(成员变量共享导致数据错乱);
最好保持 Controller 无状态;
特殊场景下可以使用 @Scope("request") 或 ThreadLocal;
关键思路:
Controller 要无状态,数据传参走,线程安全稳如老狗!
最后的小结尾如果你看到这里,恭喜你,已经把社招面试中一个超常见又容易被问挂的坑彻底掌握啦!
未来遇到这个问题,不用慌,拿出小米今天的这套答法,一套流程走完,面试官都得点头认同!
当然啦,社招之路不易,咱们一起加油呀!
如果你觉得这篇文章对你有帮助,别忘了【分享】、【在看】、【点赞】三连,或者分享给你正在准备面试的朋友们~
下次我会继续分享更多社招高频面试题解析,咱们一起轻松拿下心仪的 Offer!
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。