1. 程式人生 > >記一次Controller改造,及SpringMVC處理流程

記一次Controller改造,及SpringMVC處理流程

概述

由於工作需要,需實現這樣一個功能的controller框架:

1,Restful API

2,請求引數校驗(請求中需要攜帶指定的引數,才能進入控制器方法。一次請求會攜帶一些基本資訊,以及請求資料,此處校驗的是請求資料的攜帶情況)

3,請求格式校驗(請求格式需要符合規定,才能進入控制器方法。此處校驗的是基本資訊的攜帶情況)

4,資料繫結(通過@RequestBody註解能直接繫結請求資料到POJO中。此POJO有一些欄位,用以儲存請求的基本資訊,以及一個Map,用以儲存請求資料)

5,請求資料的解密和返回資料的加密

探索之旅

第一思路

開始我對spring mvc的請求流程不太熟悉,我構思,先經過HttpMessageConverter,再經過Intercepter。

由前者解析請求資料,轉換為我們自定義的POJO,並且解密請求資料。後者做格式校驗,引數校驗。很完美。

但實際情況是,Intercepter在HttpMessageConverter之前執行。更具體地說,spring mvc的攔截器是在轉換器外層的,也就是請求進來的時候,先進攔截器,再進轉換器;返回的時候,先進轉換器,再進攔截器。

第一思路GG

第二思路

第一思路不行了,我開始尋找spring mvc中能在轉換器(HttpMessageConverter)之後打斷整個請求流程的辦法。

看了官方文件和一些部落格,我找到了@ControllerAdvice這個註解,以及RequestBodyAdvice這個介面,該介面有四個方法:

boolean supports(MethodParameter methodParameter, Type targetType,
		Class<? extends HttpMessageConverter<?>> converterType);  


Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
		Type targetType, Class<? extends HttpMessageConverter<?>> converterType);  


HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
		Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;  


Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
		Type targetType, Class<? extends HttpMessageConverter<?>> converterType);  

乍一看,可以指定支援型別,afterBodyRead方法可以在HttpMessageConverter轉換之後拿到POJO。好像很完美。

但afterBodyRead方法不能打斷流程!!!這個方法沒有丟擲異常,也就是說,程式走到這裡,不管你在afterBodyRead裡做了什麼,控制器方法都註定要被呼叫了,如此以來,引數校驗和格式校驗就無法實現。(如果你不用奇淫巧技的話,是這樣。但其實你可以在這裡丟擲一個uncheck的Exception,然後使用@ControllerAdvice配合@ExceptionHandler註解打斷流程,並跳到被@ExceptionHandler註釋的控制器方法中。只是這個方法太極客而且醜陋了,我沒用)

本來我想在HttpMessageConverter中轉換一次POJO,整個程式就使用這個POJO,但這個願望似乎是無法實現了。如果有高手知道解決辦法,還望指出。

第二思路GG

第三思路

拋棄了“一次轉換,終身使用”的執念,我開始思考,多次轉換的解決辦法

還記得第二思路里那個接口裡的beforeBodyRead方法嗎,它丟擲了一個IOException!!!沒錯,你可以在這裡轉換資料,並校驗引數和格式,丟擲IOException,並配合@ControllerAdvice的@ExceptionHandler。

IOException!?哎,我是一個有程式碼潔癖的人,你要硬說請求格式錯誤,引數錯誤是一種IO錯誤,也沒問題。你要硬把一堆驗證程式碼塞在這個小小的beforeBodyRead方法裡,也沒問題。但是我拒絕。

第四思路

既然Spring mvc提供了攔截器,它就應該有用武之地,咱們再回過頭來考慮一下它吧。

現在我已經摒棄了“一次轉換,終身使用”的執念,那麼讓我來考慮一下,攔截器轉換請求,並做格式校驗,引數校驗。

沒有問題,只要你提供統一的解析工具和解密工具給攔截器。唯一的缺點是,我將請求格式校驗,請求引數校驗拆開成兩個攔截器,如此,對Http請求的解析和轉換將會發生兩次。

如果用思路三的做法,這個多次轉換就可以避免,但攔截器的url過濾,攔截器排序這樣的功能就享受不到了。

對於校驗發現請求錯誤,有兩種辦法做處理,1是丟擲異常,要知道preHandle方法是throws Exception的,此時配合@ControllerAdvice的@ExceptionHandler來處理;2是當發生異常時,讓preHandle返回false,返回前用request.getRequestDispatcher(...).forward(request, response)發起轉發,你可以轉發到一個你專門用來返回錯誤資訊的控制器方法上。

最後我選擇了思路四的方案,其實思路三也是完全沒有問題的。如果有大神知道更好的方案,請指教。

對返回的處理

對返回資料的處理是比較簡單的,沒有這麼多周折。利用spring mvc提供的@ResponseBody註解,寫一個HttpMessageConverter就行,此處我使用了繼承AbstractHttpMessageConverter的方法,較為簡單。

值得一提的是,對控制器方法的返回值,在進入轉換器之前,有兩種辦法去做統一的處理,1是@ControllerAdvice註解配合ResponseBodyAdvice介面;2是HandlerMethodReturnValueHandler。

如果你用方式2實現,要注意,HttpMessageConverter的呼叫需要你在HandlerMethodReturnValueHandler中手動實現,否則轉換器不會被呼叫。例如spring mvc自己實現的AbstractMessageConverterMethodProcessor抽象類,就提供了writeWithMessageConverters()方法。該抽象類實現了HandlerMethodReturnValueHandler介面,繼承自AbstractMessageConverterMethodArgumentResolver抽象類。

所以你如果實現自己的HandlerMethodReturnValueHandler,可以通過實現AbstractMessageConverterMethodProcessor抽象類。

我使用的是方式1。

最後在HttpMessageConverter中去做加密就OK了。

總結

spring mvc對流程的控制我知道的有3個。1是攔截器;2是@ControllerAdvice配合RequestBodyAdvice介面的beforeBodyRead方法丟擲異常;3是HttpMessageConverter的readInternal方法丟擲異常。除了1之外,都需要@ExceptionHandler做配合。(如果有其他方法,望指出)

轉發的時候,新的請求會從最開始重走一遍,也就是說你的所有攔截器都會再走一遍;轉換器,控制器增強器等等這些東西等於都是自動複用的。配置類中註冊攔截器的InterceptorRegistry的addInterceptor方法返回的InterceptorRegistration例項有excludePathPatterns等方法,可以用來控制攔截器作用的url範圍。

很多東西spring mvc的官方文件裡寫的不是很清楚,還是要走一走原始碼才能搞明白,感覺有待完善。