在 Servlet API 中,若要在 Servlet 處理請求的前後做點橫切主要流程的服務,可以使用過濾器,在使用 Spring MVC 之後,雖然說請求實際上會經由 DispatcherServlet
,然後依設定決定要呼叫哪個控制器中的方法,然而,你還是可以使用過濾器來處理這類服務。
例如,從〈準備 gossip 專案〉開始,雖然一路重構 gossip 應用程式,然而,在 gossip 應用程式中,有個使用 Java HTML Sanitizer 來消毒訊息內容的元件 cc.openhome.web.HtmlSanitizer
,就是使用過濾器實作的。
這是因為過濾器一定是在 Servlet 前執行,而 DispatcherServlet
是個 Servlet,因此 HttpServletRequest
的替換,一定會在 DispatcherServlet
前進行,自然地,後面取得的請求參數值,就是消毒後的訊息內容。
然而,若過濾器對你來說粒度太大,新增訊息主要邏輯,是在 MemberController
的 newMessage
方法實作,你想要在該方法前進行消毒呢?
單就攔截控制器中的處理器方法的話,有個古老的 API 自 Spring 1.x 時代就存在,那就是 HandlerInterceptor
,在我舊版的 Spring 文件中 〈Handler Interceptor〉也介紹過,HandlerInterceptor
介面有三個方法要實作:
boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception;
void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception;
void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception;
正如舊文件中談過的,preHandler
會在控制器處理請求之前被呼叫,HandlerInterceptor
類似 Servlet API 中的過濾器,以鏈狀的方式組織,傳回的 boolean
決定是(傳回 true
)否(傳回 false
)呼叫接下來的 HandlerInterceptor
或是控制器來處理請求;postHandler
會在控制器處理完請求之後被呼叫,afterCompletion
方法會在頁面繪製完成之後被呼叫。
(關於 ModelAndView
,可參考舊版文件中的〈ModelAndView〉。)
在 Spring 5.1.2 中,HandlerInterceptor
依然可以使用,只不過因為支援 Java 8 介面預設方法實作,你可以在實作介面時只實作感興趣的方法(preHandle
預設傳回 true
),不必再使用 HandlerInterceptorAdapter
,另一方面,可以透過 JavaConfig 的方式來註冊 HandlerInterceptor
也比較方便了,例如,若 LoggingInterceptor
實作了 HandlerInterceptor
,而你想介入 MemberController
標註 @PostMapping("new_message")
的方法,可以如下設定:
...略
public class WebConfig implements WebMvcConfigurer, ApplicationContextAware {
...略
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggingInterceptor())
.addPathPatterns("/new_message");
}
}
不過,HandlerInterceptor
雖然可以透過 preHandle
傳回 false
來攔截請求,也傳入了 HttpServletRequest
,然而,你無法消毒請求參數的值,因為不若過濾器中 FilterChain
的 doFilter
方法,可傳入請求包裹器。
這時就可以用上之前談 AOP 時的 @Around
了:
package cc.openhome.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.owasp.html.PolicyFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class HtmlSanitizer {
@Autowired
private PolicyFactory policy;
@Around("execution(* cc.openhome.controller.MemberController.newMessage(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object[] args = proceedingJoinPoint.getArgs();
args[0] = policy.sanitize(args[0].toString());
return proceedingJoinPoint.proceed(args);
}
}
在這邊取得了 newMessage
的第一個參數,也就是使用者發送的訊息,在 PolicyFactory
的 sanitize
消毒過後,重新設定為第一個引數,然後呼叫 ProceedingJoinPoint
的 proceed
時傳入,這樣就完成了訊息的過澸。
由於消毒策略是會變動的,上頭使用了 @Autowired
來自動綁定 PolicyFactory
,為了便於設定,可以在 WebConfig
組態:
...略
@EnableAspectJAutoProxy
@ComponentScan(basePackages = {"cc.openhome.controller", "cc.openhome.aspect"})
public class WebConfig implements WebMvcConfigurer, ApplicationContextAware {
...略
@Bean
public PolicyFactory htmlPolicy() {
return new HtmlPolicyBuilder()
.allowElements("a", "b", "i", "del", "pre", "code")
.allowUrlProtocols("http", "https")
.allowAttributes("href").onElements("a")
.requireRelNofollowOnLinks()
.toFactory();
}
...略
}
別忘了加上 @EnableAspectJAutoProxy
,而且掃描 Aspect 所在的套件,現在原本消毒訊息用的過濾器可以刪掉了,訊息處理的粒度縮小在 MemberController
的 newMessage
方法上,並且仍是個橫切主要流程的服務。
如果 HandlerInterceptor
與符合 Pointcut 的 Aspect 同時存在,那麼 HandlerInterceptor
會是在 Aspect 的前後執行。
你可以在 gossip 找到以上的範例專案。