99%的人都答錯了!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 里存在可變的成員變量,在多線程并發請求下會引發線程安全問題。
- 解決辦法有:
- 或者使用 ThreadLocal 保存每個請求的獨立數據,但注意清理。
- 必要時可以將 Controller 設為請求作用域(@Scope("request"));
- 最推薦:保持 Controller 無狀態,只通過方法參數傳遞數據;
面試官點頭微笑,
我心里一陣狂喜,暗搓搓給自己比了個??。
總結一下今天的故事
今天我們講了:
- Spring MVC 控制器是默認單例的(Singleton Scope);
- 單例會引發線程安全問題(成員變量共享導致數據錯亂);
- 最好保持 Controller 無狀態;
- 特殊場景下可以使用 @Scope("request") 或 ThreadLocal;
關鍵思路:
- Controller 要無狀態,數據傳參走,線程安全穩如老狗!