在Servlet容器中,談到請求攔截,通常我們會想到過濾器,若進一步使用Spring MVC,視不同的需求而定,此時,我們可能會想到古老的HandlerInterceptor,或者是相對較新的@ControllerAdvice元件,那麼Spring AOP呢?它又應用在哪個位置?這麼多方案該怎麼選?

Servlet請求前後的過濾器

在Servlet API中,若要在Servlet處理請求的前後,做點橫切主要流程的服務,可以使用過濾器,在使用Spring MVC之後,雖然請求實際上會經由DispatcherServlet,之後,依設定決定要呼叫哪個控制器中的方法,然而,還是可以使用過濾器來處理這類服務。

這是因為過濾器一定是在Servlet前執行,而DispatcherServlet是個Servlet,過濾器必然是在DispatcherServlet前進行,自然地,若要攔截請求,例如防護某些頁面、過濾輸入訊息、處理壓縮、編碼轉換等,只要是粒度是在Servlet前、後進行處理,使用過濾器並不會有什麼問題。

例如,若想採用Spring Security,雖然在這個方案的封裝之下,可以不用處理過濾器細節,然而在Web環境中的運作仍是基於過濾器,這也就是為何我們必須設置DelegatingFilterProxy,或是繼承WebSecurityConfigurerAdapter,來進行相關設定的原因。

不用只是為了避免Servlet API,而想盡辦法避免Servlet API,因為,這只是自找麻煩,Spring MVC終究是基於Servlet的技術堆疊,若使用Servlet API可以簡單地解決需求,維護上也方便,使用Servlet API並沒有什麼不好,例如,若想令後續的請求處理流程,使用自定義的HttpServletRequest、HttpServletResponse包裹器,過濾器仍是最為直覺而簡單的方式。

以控制器為範疇的@ControllerAdvice

在應用程式中,有些例外的確是可以採用橫切的方式處理。

例如,要對傳播至Web容器的例外進行處理,可在Web容器層面可以在web.xml中設定,指定例外類型與處理頁面等資訊,目標若為JSP頁面,可以設定isErrorPage屬性為true,如此就可以使用exception隱含物件來提取例外資訊。

這類例外處理的對象,通常是底層無法處理的執行時期例外,而傳播至容器之目的,是希望頁面呈現出對使用者有用的資訊,知道到底發生了什麼事情,適合採取橫切主要商務流程的方式來處理。

在web.xml設定,顯然是個針對整個應用程式,粒度較大。在使用Spring MVC時,或許會想以控制器為單位來處理例外,這時,我們可以使用@ControllerAdvice,透過它的basePackages、basePackageClasses、annotations等屬性,指定要橫切至哪些控制器之中,如果不予以設定,那就是適用於全部的控制器。

針對例外處理來說,在@ControllerAdvice中,可以使用@ExceptionHandler來標示特定的例外,而被標示的方法,將在例外發生且型態相符時執行,也可以決定由哪個頁面來呈現錯誤訊息,進一步地,亦可搭配@ResponseStatus指定回應狀態碼。

實際上,也不僅止於處理例外的部分,我們在@ControllerAdvice當中,可以使用@InitBinder──在控制器中的處理器被呼叫之前,被標註的方法可以接受WebDataBinder實例,而且,這個實例擁有許多可以處理請求欄位的方法,像是能夠設定請求參數的白名單或黑名單,也可以註冊自定義PropertyEditor或Validator等。

在ControllerAdvice中,我們也可以使用@ModelAttribute。被標註的方法可以接受Model實例,會在控制器中每個處理器方法被執行前呼叫,如果處理器使用的Model,在處理器執行前都會進行某些屬性設定,就可以集中至@ModelAttribute標註的方法中進行。

不過,對於頁面的防護,@ControllerAdvice基本上做不到,對使用者輸入的訊息進行過濾、消毒等動作,也是無法進行。技術上來說,雖然這些可以使用PropertyEditor,然而並不適合,因為在橫切入請求處理流程之後,針對的並不一定是某個請求參數,你希望的是,過濾、消毒的元件能更為通用。

HandlerInterceptor和@Around

單就攔截控制器中的處理器方法而言,Spring MVC有個古老的API,自1.x時代就存在著,那就是HandlerInterceptor介面,至今依舊可以使用,只不過在Spring支援Java 8介面預設方法實作之後,可以在實作該介面時只實作感興趣的方法,不必再繼承HandlerInterceptorAdapter,就能達到目的,另一方面,可以透過JavaConfig的方式,來註冊HandlerInterceptor也比較方便。

單看HandlerInterceptor所提供的preHandle、postHandle、afterCompletion這三個方法,會覺得它跟Web容器的過濾器功能相近,也可以透過InterceptorRegistry來設定路徑模式,決定哪些URI要套用,藉由preHandle傳回true或false,也可以決定是否攔截請求,達到防護頁面的功能。

HandlerInterceptor是作用在DispatcherServlet之後,每個控制器的處理器之前,粒度上來說,是比較適合處理器;然而,HandlerInterceptor定義的三個方法,雖然都傳入了HttpServletRequest、HttpServletResponse,然而並沒有如過濾器中FilterChain的角色存在,也就無法替換自定義包裹器,因此,過濾、消毒輸入訊息的元件,無法使用HandlerInterceptor實作。

這時,就可以使用Spring AOP來定義Aspect了。實際上,Spring AOP可以設置的Advice,有Around、Before、After、After Throwing、After Returning等,然而,大部份的情況下,Spring MVC中,我們也能使用@ControllerAdvice、HandlerInterceptor,而不一定要用上這些Advice,畢竟Spring AOP有比較多的觀念與技術細節,必須掌握,若是使用HandlerInterceptor,相對來說還是比較簡單。

然而,過濾、消毒輸入訊息的元件,或者是替換處理器方法上參數值的動作,就可以用上@Around來定義Around Advice了。因為@Around標示的方法可接受ProceedingJoinPoint,其proceed方法類似於FilterChain的doFilter,可以替換引數,並且透過Pointcut的定義,可決定哪些處理器方法要套用。

技術熟悉度與適當位置

同樣是想實作橫切主要流程的服務時,你會選擇哪個技術呢?其實你首先要選擇的,應該是最熟悉的技術!只要是能簡單實現需求,並且對將來維護也不造成麻煩,就算是舊時代代表的技術(像是過濾器),也沒什麼不好!

當然,雖說別為了新技術而新技術,相對而言,也別為了堅守熟悉的技能點,而拒絕任何新觀念與實作,此時,該思考的是,這些技術擺放的適當位置,例如,粒度大小是個考量,就像過濾器是在Servlet前後,@ControllerAdvice是在控制器前後,至於HandlerInterceptor、@Around,是在處理器方法前後。

當然,這些技術在可解決的需求上會有些重疊,此時思考技術擺放的位置就更為重要了,在思考的過程中,也有助於釐清Web應用程式本身的各個關注點,使元件之間職責更為清晰,對於技術的熟悉度也會增加,就不致於迷失在一堆技術細節之中!

 

作者簡介


Advertisement

更多 iThome相關內容