99%的人都答错了!Spring MVC 控制器到底是不是单例?怎么破局?

23 阅读5分钟



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

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

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!

我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!