徹徹底底給你講明白啥是SpringMvc異步處理
生活在這個世界上,我們必須承認任何事物都是運動變化著的,沒有什么東西是一成不變的。
不僅因為這句話是出自馬克思主義哲學的唯物辯證法,而且事實確實如此。下面就來描述這樣的一個變化。
分工的產(chǎn)生與協(xié)作
想必大家都買過房或終將會買房,那自然離不開裝修,就以裝修這個話題來展開吧。假設有一個搞裝修的人叫小王。
小王活兒做得特別好,但目前還是一個人單打獨斗。他正在干活兒的這家業(yè)主的鄰居發(fā)現(xiàn)了他的活兒好,于是專門來找他咨詢情況。
小王不得不停下手頭的工作,來為潛在的客戶解答疑惑或訴說方案。最終靠實力把潛在客戶變成了真正的客戶。小王的名氣就這樣傳開了。
附近小區(qū)的人聞名而來,找他溝通裝修方案或確認時間安排,小王不得不每次都停下手頭工作,專心為客戶解答。等客戶走后,他再開始繼續(xù)工作。
隨著來找他的人與日俱增,漸漸的小王發(fā)現(xiàn)他真正用于干活兒的時間越來越少了,因為他在接待客戶上花費了太多的時間。
但沒有辦法,為了能夠接到更多的活兒,他必須為客戶提供這個咨詢,必須要花掉這些時間。結果導致干活兒的時間被壓縮了,活兒干不完了,該咋辦呢?
其實解決方法很簡單,所有人都能想得到,那就是小王招了一個工人專門給他干活。工人干完活后先過小王這一關,小王覺得可以了才會交付給業(yè)主。
小王呢則繼續(xù)為客戶提供咨詢服務,在空閑的時候也會去和僅有的一個工人一起干活??磥韱栴}已得到解決,一切重新回到平衡狀態(tài)中。
由于小王走了狗屎運,每天來的客戶實在是太多了,這個剛剛建立的平衡又被打破了。小王不得不全天候的提供咨詢,索性不再干活了。
而且由于接的活兒越來越多,無奈不得不又招了第二個工人,第三個工人,第四個。。。
我們可以看到很多事情都在逐漸的發(fā)生著改變,小王從一個只會干活的工人逐步過渡到只負責接待客戶的顧問,不再干活。
為了適應自己的這個新角色,小王不得不招了一個又一個工人為自己干活,這樣自己才能得以從原來的工作中徹底解脫。
可見隨著事物的發(fā)展,分工的產(chǎn)生是一種必然,這就導致了崗位種類的增加。原來只有一個崗位,既負責干活又負責咨詢。
現(xiàn)在變?yōu)榱藘蓚€崗位,一個專門負責咨詢和監(jiān)工,其實就是工長了,一個專門負責干活和施工,其實就是工人。
這種“工長 + 工人”的模式在現(xiàn)實生活中使用的非常普遍,工長為工人分配工作和驗收工人的工作以及解答工人的問題,其實就是兩個崗位之間的協(xié)作。
因此,分工的產(chǎn)生與協(xié)作代表著一種更加先進和高效的生產(chǎn)方式,至少從理論上來看是這樣的,他們各司其職,然后再強強聯(lián)合,結果可想而知。
Web容器對異步的支持
看完上一小節(jié),希望大家記住這八個字,“各司其職、相互協(xié)作”。OK,馬上進入程序世界。
SpringMvc的實質就是對Java Web的封裝,因此為了更容易的理解SpringMvc的異步,必須先了解Java Web的異步。
Java Web的實質就是一套標準(接口),Web容器實現(xiàn)了這套標準,所以我們站在Web容器的立場來說Java Web的異步,非常好理解。
無論采用什么框架開發(fā)的Java Web應用,最終都是在Web容器中運行的,如tomcat就是最常用的Web容器。
啟動SpringBoot時,可以看看日志中打印出的線程名稱,其實就是tomcat線程池里的線程??梢远嗾埱髱状危l(fā)現(xiàn)每次處理請求的線程Id都不一樣。
我們知道,其實每一個請求過來后,Web容器都會從自己的線程池中拿出一個線程來運行Java Web應用以處理請求。當請求處理完后,這個線程會被還回到線程池中以便處理下一個請求。
如果請求能在非常短的時間內(nèi)處理完成,是沒有問題的。我們設想一下,如果請求執(zhí)行的非常慢,那么這個線程將無法還回去,于是線程池中可用的線程數(shù)目將少一個。
由于線程池的容量是有限的,如果很多執(zhí)行的很慢的請求同時到來,那么線程池中的線程將會用光,而且短時間內(nèi)都還不回來。此時的其它請求都將無法被處理。
我們發(fā)現(xiàn)執(zhí)行很快的請求和非常耗時的請求是屬于兩種不同性質的事物,現(xiàn)在它們都由Web容器的線程池的線程來從頭處理到尾,就像當初的小王。
既要接待客戶做咨詢工作,又要等客戶走了自己干活。這顯然是低效的,唯一的方法就是將這兩種工作分開,分別交由兩個崗位去做。
于是我們就看到,小王招了很多工人,自己專業(yè)做咨詢接活兒,轉手把活兒交給工人去做,工人做好后把活兒再交回給小王,小王再向業(yè)主交付。
我們可以把這個思路套到Web容器上,就是Web容器線程池的線程只用來處理執(zhí)行速度很快的請求,這對應于小王只負責咨詢接活兒。
對于非常耗時的請求,Web容器線程池的線程將把這個請求交給其它專門的線程池的線程去處理,這對應于小王把接到的活兒交給他的工人去做。
Web容器線程池的線程將再去接受其它請求,因為耗時的請求已經(jīng)被交出去了。這對應于小王再去接別的活兒,因為上一個活兒已經(jīng)交給工人去干了。
專門的線程池的線程處理完這個請求后會把它還回給Web容器線程池的線程,這對應于工人干完活兒后會把活兒先交給小王去驗收。
Web容器線程池的線程拿到處理結果,把結果寫入響應,這個請求就被處理完畢了。這對應于小王把工人干好的活兒交付給業(yè)主,施工就算完畢了。
我們來分析一下,小王由于角色轉變而帶來的工作轉變都有哪些。一開始小王單打獨斗,小王工作有三種,小王從業(yè)主接活兒,小王自己干活,小王向業(yè)主交付。
到最后小王成了工長,此時的工作是四種,小王從業(yè)主接活兒,小王把活兒交給工人去做,工人把做好的活兒交回給小王,小王向業(yè)主交付。
對比后發(fā)現(xiàn),小王為了使自己不干活,所以引入了工人,以及由此產(chǎn)生的他與工人之間的一去一來的協(xié)調(diào)交互。
同樣來分析一下,Web容器線程池的線程前前后后有哪些變化。一開始線程處理所有的請求,可以分為三個階段,線程接受請求,線程處理請求,線程寫回響應。
到最后線程的工作分為四個階段,線程接受請求,線程把待處理的請求交給專門的線程池,專門的線程池把處理好的請求交回給線程,線程寫回響應。
對比后發(fā)現(xiàn),Web容器線程池的線程為了使自己不處理耗時請求,所以引入了專門的線程池,以及由此產(chǎn)生的它與專門的線程池的一去一來的協(xié)調(diào)交互。
是不是發(fā)現(xiàn)線程池和小王的行為是完全對應的。而且他們面臨的問題都一樣,就是把需要處理的任務交出去給別人,別人把處理完畢的任務再還回給他。
好了,現(xiàn)在我來告訴你,這就是Java Web異步處理的整體邏輯思想?;睘楹喓?,其實就是一去一來這兩個動作罷了。
自己把執(zhí)行流程交出去
1AsyncContext startAsync()
別人把執(zhí)行流程還回來
1void dispatch()
上面這兩個方法就完成了一去一來這兩個動作,僅此而已。
看到這些,可能有些人大失所望,傳說中“牛X和高大上”的異步處理,到最后也不過區(qū)區(qū)十四個字,“執(zhí)行流程交出去,執(zhí)行流程還回來”。
可能還會有人覺得,異步處理不應該和客戶端也有關系嗎?其實并沒有,純粹是服務器端的把戲。而且對于單個請求的處理時間也不會減少。
就像你去飯店吃飯,你點的這份飯在后廚是一個廚師完成的還是多個廚師協(xié)作完成的,其實你并不知道,一般情況下你也并不關心。
SpringMvc對異步的支持
上面描述的異步處理邏輯只是一個規(guī)范(接口),是不帶實現(xiàn)的。任何想要支持Java Web異步處理的,需要在遵守這個規(guī)范的前提下,自己提供一套實現(xiàn)。
說白了,就是也要采用交出去還回來的模式,但是如何交出去,怎樣還回來,要自己去實現(xiàn),還有這個專門的線程池也要自己來提供。
SpringMvc提供了對異步的支持,所以它遵守了這個規(guī)范,而且它也定義了相似的接口來與Java Web規(guī)范交互。
這個接口就是AsyncWebRequest,它的實現(xiàn)類是StandardServletAsyncWebRequest。同樣的兩個方法。
交出去
1void startAsync()
還回來
1void dispatch()
在這兩個方法的實現(xiàn)中,分別去調(diào)用了Java Web規(guī)范中的對應方法,這就是與Java Web規(guī)范的交互。
同時SpringMvc自己是有線程池的,所以在第一個方法里實現(xiàn)了把執(zhí)行流程交出去的操作,在第二個方法里實現(xiàn)了把執(zhí)行流程還回來的操作。
還有一個問題就是,并不是所有的方法都需要異步處理,所以就需要有一種方式來告訴SpringMvc,哪個方法需要異步處理。
SpringMvc選擇了采用方法返回值的類型來進行區(qū)分,凡是@RequestMapping方法的返回值類型是以下這些的,就表明開發(fā)人員想進行異步處理,于是SpringMvc就進行異步處理。
- 1Callable<V>
- 2
- 3DeferredResult<T>
- 4
- 5WebAsyncTask<V>
- 6
- 7StreamingResponseBody
- 8
- 9ResponseEntity<StreamingResponseBody>
- 10
- 11ResponseBodyEmitter
- 12
- 13ResponseEntity<ResponseBodyEmitter>
對于異步處理的方式,其實也包括兩大類,一類是開發(fā)人員不管,完全交給SpringMvc去處理,一類是開發(fā)人員自己把控,不用SpringMvc參與。
對于第一類,我們返回Callable
對于第二類,我們返回DeferredResult
因此是由開發(fā)人員來決定的,開發(fā)人員需要自己弄個線程去執(zhí)行,并且一定要記住最后要設置一下結果才行。
我們看到耗時的代碼都跑到別的線程里去執(zhí)行了,那么SpringMvc的處理主流程自然就結束了,這就是執(zhí)行流程交出去的過程。
只不過有一點需要注意,這個請求的處理并沒有完成,只是暫時離開了SpringMvc的主流程,在別的線程池里運行著呢。
那么一段時間后,別的線程池運行結束,已經(jīng)取得了結果,那這個結果和執(zhí)行流程又該如何還回來呢?
直接還回來嗎?顯然是不可能的,因為SpringMvc的處理主流程在把任務交出去的那一刻就已經(jīng)結束了、不存在了。
其實是別的線程池在處理結束并得到結果后,處理流程連同結果會返回到Web容器中,由Web容器再次分派這個請求到SpringMvc中來。
這就是為什么上面第二個方法的名字叫做dispatch的原因,就是再次分派這個請求到Web應用中來嘛,因此會再次進入到SpringMvc中,這不就把執(zhí)行流程還回來了嘛。
可能會有人產(chǎn)生疑惑,如果SpringMvc再次處理這個請求那不就亂套了嗎?答案是顯然不會的,因為這次異步的結果已經(jīng)存在了,自然不會再異步處理了,而是把它跳過去直接進入后續(xù)處理。
也就是對方法返回值(ReturnValue)的處理,比如序列化成JSON寫入響應,或進行視圖渲染,把渲染后的視圖寫入響應,這樣這個請求就算處理完畢了。