Spring MVC 控制器到底是不是单例?怎么破局?

软件求生 2周前 (05-01) 阅读数 0 #科技



嗨大家好呀,我是小米,一个喜欢边学边分享,把坑踩过一遍再告诉你怎么绕开的技术宅控!

最近我在准备换工作的社招面试(真是社畜中的社畜啊……),被问了一个超级经典但又能坑死人的问题:

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!

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

软件求生

软件求生

从事软件开发,分享“技术”、“运营”、“产品”等。