99%的人都答錯了!Spring MVC 控制器到底是不是單例?怎么破局?
引言
嗨大家好呀,我是小米,一個喜歡邊學(xué)邊分享,把坑踩過一遍再告訴你怎么繞開的技術(shù)宅控!
最近我在準(zhǔn)備換工作的社招面試,被問了一個超級經(jīng)典但又能坑死人的問題:
Spring MVC 的控制器(Controller)是單例的嗎?如果是,會有什么問題?怎么解決?
聽到這個問題那一刻,我表面微笑,內(nèi)心咯噔:
“完了,要是答得不清不楚,面試官又要畫叉叉了……”
好在我之前踩過這個坑,一口氣講了個通透,還得到了面試官的點(diǎn)贊!
今天我就把完整的故事和解題思路分享給大家。
記憶中的第一個坑:Controller 的單例本質(zhì)!
記得我剛學(xué) Spring MVC 的時候,腦子里想當(dāng)然覺得:
“控制器嘛,就是處理一個請求,創(chuàng)建一個對象,處理完就丟掉,多清晰!”
結(jié)果呢?查源碼一看,啪啪打臉!
實(shí)際上,Spring MVC 默認(rèn)把 Controller 當(dāng)作單例(Singleton)來管理的!
也就是說,咱們寫的這個:
圖片
默認(rèn)是單例模式(Singleton Scope),由 Spring 容器托管,啟動時創(chuàng)建一個實(shí)例,整個應(yīng)用生命周期共用這一份!
所以,記住一句話:
Spring 中的 @Controller 本質(zhì)上是一個單例 Bean!
那為啥 Spring 要這么搞呢?
其實(shí)很簡單,節(jié)省資源,提高性能!
如果每來一個請求就 new 一個 Controller,想想服務(wù)器內(nèi)存得爆炸成啥樣子?而且 Controller 通常是無狀態(tài)的(處理邏輯、調(diào)用 Service),并不需要為每次請求新建實(shí)例。
所以,單例是合理的默認(rèn)選擇!
但是——
事情到這里,才剛剛開始。
單例帶來的隱患:線程安全問題!
單例 + 多線程,聽著就危險(xiǎn),對吧?
沒錯,Controller 是單例,但是 用戶請求是多線程并發(fā)的。
一旦 Controller 里寫了成員變量,而且這個成員變量又是可變的、共享的,那簡直是災(zāi)難現(xiàn)場!
比如:
圖片
看著沒啥問題對吧?
但是注意啊!
- 用戶A提交了一個orderId:1001
- 用戶B緊接著提交了一個orderId:1002
- 因?yàn)镃ontroller是單例的,他們共用同一個 lastOrderId!
結(jié)果:
A本來想處理自己提交的1001,結(jié)果處理到一半,lastOrderId 被 B 改成了1002……
數(shù)據(jù)錯亂、請求串臺、詭異Bug,分分鐘爆炸!
這就是典型的線程安全問題!
總結(jié)一下:
Spring MVC Controller 單例本身沒問題,問題在于如果 Controller 里保存了【有狀態(tài)的可變成員變量】,就會引發(fā)線程安全問題!
面試官想聽的:怎么解決?
好,既然知道問題了,那接下來最重要的就是——怎么解決?
思路一:保證 Controller 無狀態(tài)
- 不要在 Controller 里寫可變的成員變量!
- 所有數(shù)據(jù)都通過方法參數(shù)傳遞。
比如剛才的 lastOrderId,正確寫法應(yīng)該是:
圖片
這樣,每個請求進(jìn)來,拿的是自己方法參數(shù)里的數(shù)據(jù),不會互相污染。
記住一句話:
Controller 要像一潭死水一樣冷靜,不要有變化,保持無狀態(tài)!
思路二:必要時改變作用域
如果業(yè)務(wù)場景確實(shí)需要保存一些請求級別的數(shù)據(jù),比如一步步流程操作,那么可以考慮改變 Bean 的作用域!
- 使用 @Scope("request")
- 讓每個請求有自己的 Controller 實(shí)例。
比如:
圖片
加上 @Scope("request"),
Spring 會給每個請求創(chuàng)建一個新的 Controller 實(shí)例,互不影響!
當(dāng)然啦,這樣就失去了單例帶來的性能優(yōu)勢了,要慎重選擇。
大部分場景下,通過方法參數(shù)傳遞就夠了,很少需要改變作用域。
思路三:使用 ThreadLocal
如果真的需要存 per-request 數(shù)據(jù),還可以用ThreadLocal。
圖片
ThreadLocal 保證每個線程有獨(dú)立副本,互不干擾。
注意,用完一定要 remove()!不然可能會導(dǎo)致內(nèi)存泄漏,尤其是在線程池環(huán)境下。
小米的社招總結(jié)答法(親測有效)
最后,總結(jié)一下,社招面試我怎么答的:
面試官問:“Spring MVC 控制器是單例的嗎?如果是,有什么問題?怎么解決?”
我答:
- Spring MVC 的控制器默認(rèn)是單例的,由 Spring 容器管理。
- 單例本身沒問題,但如果 Controller 里存在可變的成員變量,在多線程并發(fā)請求下會引發(fā)線程安全問題。
- 解決辦法有:
- 或者使用 ThreadLocal 保存每個請求的獨(dú)立數(shù)據(jù),但注意清理。
- 必要時可以將 Controller 設(shè)為請求作用域(@Scope("request"));
- 最推薦:保持 Controller 無狀態(tài),只通過方法參數(shù)傳遞數(shù)據(jù);
面試官點(diǎn)頭微笑,
我心里一陣狂喜,暗搓搓給自己比了個??。
總結(jié)一下今天的故事
今天我們講了:
- Spring MVC 控制器是默認(rèn)單例的(Singleton Scope);
- 單例會引發(fā)線程安全問題(成員變量共享導(dǎo)致數(shù)據(jù)錯亂);
- 最好保持 Controller 無狀態(tài);
- 特殊場景下可以使用 @Scope("request") 或 ThreadLocal;
關(guān)鍵思路:
- Controller 要無狀態(tài),數(shù)據(jù)傳參走,線程安全穩(wěn)如老狗!