springmvc学习笔记--Interceptor机制和实践
前言:
Spring的AOP理念, 以及j2ee中責任鏈(過濾器鏈)的設計模式, 確實深入人心, 處處可以看到它的身影. 這次借項目空閑, 來總結一下SpringMVC的Interceptor機制, 并以用戶登陸和日志記錄作為案例, 以做實踐.
原理及類圖:
攔截器的使用, 其實非常的廣泛, 尤其對通用普適的功能調用, 提取到攔截器層中實現.
常見的攔截器有如下幾種: 用戶登陸/日志記錄/性能評估/權限控制等等.
攔截器Interceptor鏈, 橫亙在控制器Controller(Action)前, 具體的接口定義如下所示:
摘錄了開濤老師的原話和圖文解說:
preHandle: 預處理回調方法, 在controller層之前調用.返回值: true 表示繼續流程(如調用下一個攔截器或處理器).false 表示流程中斷, 不會繼續調用其他的攔截器或處理器. postHandle: 后處理回調方法, 在controller層之后調用(但在渲染視圖之前), 我們可以通過modelAndView(模型和視圖對象)對模型數據進行處理或對視圖進行處理, modelAndView也可能為null. afterCompletion: 整個請求處理完畢回調方法, 即在視圖渲染完畢時回調, 類似于try-catch-finally中的finally.當然前提是該攔截器的preHandle返回true. 正常流程和異常流程的圖說明:
注: 圖摘自開濤老師的博客, <<第五章 處理器攔截器詳解——跟著開濤學SpringMVC>>.?
但有多個攔截器的時候, 其配置順序也特別重要, preHandle是順序執行, postHandle則是逆序執行, afterCompletion也是逆序執行.
集成于springmvc時, 配置也非常的簡潔, 如下樣例即可:
注: 在最外層定義的Interceptor類, 對所有的url映射都進行攔截, 而mvc:interceptor標簽申明的interceptor則通過mvc:mapping來自定義過濾規則.
用戶登陸:用戶登陸驗證, 是最常見的一種需求, 也是很多開發者第一次使用攔截器使用的對象. 因此我們就以此作為案例.
比如我們編寫如下代碼: @Controller @RequestMapping("/") public class HelloController {@RequestMapping(value="/login", method={RequestMethod.POST, RequestMethod.GET})@ResponseBodypublic String login(@RequestParam("username") String username,@RequestParam("password") String password,HttpSession session) {session.setAttribute("user", "...");return "ok";}@RequestMapping(value="/echo", method={RequestMethod.POST, RequestMethod.GET})public ModelAndView echo(@RequestParam("message") String message,HttpSession session, HttpServletResponse response) {ModelAndView mav = new ModelAndView();// *) 判斷是否已經登陸Object obj = session.getAttribute("user");if ( obj == null ) {try {response.sendRedirect("/html/login.html");} catch (IOException e) {e.printStackTrace();}}mav.addObject("message", message);mav.setViewName("/echo");return mav;}} 比如echo函數, 需要添加一段判斷用戶是否登陸的代碼, 若沒登陸, 需要重定向到登陸頁面上去.
當類似這樣的接口很多, 這段登陸判斷的代碼, 就會被粘貼復制很多, 若登陸判斷邏輯有變動, 難免形成蝴蝶效應.
我們可以抽象到攔截器中去實現, 添加UserVerifyInterceptor類. @Componentpublic class UserVerifyIntercptor extends HandlerInterceptorAdapter {private String[] allowUrls = new String[] {// *) 用戶登陸相關的接口"/login",};@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String uri = request.getRequestURI();for ( String allowUri : allowUrls ) {if ( allowUri.equalsIgnoreCase(uri) ) {return true;}}// *) 判斷是否已經登陸HttpSession session = request.getSession();Object obj = session.getAttribute("user");if ( obj == null ) {response.sendRedirect("/html/login.html");return false;}// *)return true;}} 注: 有些url不需要登陸判斷, 可以添加排除數組以實現白名單機制, 類似上邊代碼的allowUrls數組.
然后在springmvc的dispatcher-servlet.xml中添加如下配置: <!-- 攔截器列表 --><mvc:interceptors><!-- 用戶登陸的驗證攔截器 --><mvc:interceptor><mvc:mapping path="/**" /><mvc:exclude-mapping path="/html/**" /><bean class="com.springapp.mvc.interceptor.UserVerifyIntercptor" /></mvc:interceptor></mvc:interceptors> 注: 對于mvc:mapping和mvc:exclude-mapping, 很好地調控了攔截器作用對象的范圍.
同時, 這樣之前的echo函數, 就可以簡化為: @RequestMapping(value="/echo", method={RequestMethod.POST, RequestMethod.GET})public ModelAndView echo(@RequestParam("message") String message) {ModelAndView mav = new ModelAndView();mav.addObject("message", message);mav.setViewName("/echo");return mav;} 這樣就比之前的代碼要簡潔很多了. 日志記錄:
其實, 這邊我希望到達的一個目的是, 一個完整的rest api請求, 單獨輸出一條日志, 里面包含各類信息, 包括各個子過程的調用過程(耗時, 返回結果), 請求參數, 最終結果等. 這樣的好處顯而易見, 能夠避免多個點的日志, 分散在多行, 當請求量多得時候, 難以尋找和聚合.
這個實現機制, 大致和我之前寫過的一篇文章類似: Thrift 個人實戰--Thrift RPC服務框架日志的優化.
大致的代碼示例效果如下所示: @RequestMapping(value="/sample", method={RequestMethod.GET, RequestMethod.POST})@ResponseBodypublic String sample(@RequestParam("message") String message) {// *) 記錄請求參數RestLoggerUtility.noticeLog("[params: {message:%s}]", message);// serviceA.call(),// 記錄調用的子過程/子服務, 結果是什么, 總共耗時多少等等RestLoggerUtility.noticeLog("[serviceA.call, params: xxx, result: xxx, consume xs]");// serviceB.call(),// 記錄調用的子過程/子服務, 結果是什么, 總共耗時多少等等RestLoggerUtility.noticeLog("[serviceB.call, params: xxx, result: xxx, consume xs]");// *) 記錄最終的響應結果RestLoggerUtility.noticeLog("[response: ok]");return "ok";} 其最終的日志輸出如下所示: [params: {message:10}][serviceA.call, params: xxx, result: xxx, consume xs][serviceB.call, params: xxx, result: xxx, consume xs][response: ok] 我們可以借助, 線程私有變量ThreadLocal來組裝日志, 然后在Action的外層做攔截, 并做日志的準備和輸出.
1). 添加借助ThreadLocal實現的日志聚合工具類
對RestLoggerUtility類的設計如下: public class RestLoggerUtility {private static final Logger restLogger = LoggerFactory.getLogger("rest");public static final ThreadLocal<StringBuilder> threadLocals = new ThreadLocal<StringBuilder>();public static void beforeInvoke() {StringBuilder sb = threadLocals.get();if (sb == null) {sb = new StringBuilder();threadLocals.set(sb);}sb.delete(0, sb.length());}public static void returnInvoke() {StringBuilder sb = threadLocals.get();if (sb != null && sb.length() > 0) {restLogger.info(sb.toString());}}public static void throwableInvoke(String fmt, Object... args) {StringBuilder sb = threadLocals.get();if (sb != null) {restLogger.info(sb.toString() + " " + String.format(fmt, args));}}public static void noticeLog(String fmt, Object... args) {StringBuilder sb = threadLocals.get();if (sb != null) {// *) 對長度進行限定if ( sb.length() < 1024 ) {sb.append(String.format(fmt, args));}}}} 2). 實現日志攔截器
然后, 我們定義攔截器類RestLoggerInterceptor, 其具體的類代碼如下: public class RestLoggerInterceptor extends HandlerInterceptorAdapter {private static final Logger restLogger = LoggerFactory.getLogger("rest");@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// *) 日志準備RestLoggerUtility.beforeInvoke();return super.preHandle(request, response, handler);}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {super.postHandle(request, response, handler, modelAndView);}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {super.afterCompletion(request, response, handler, ex);// *) 進行日志的刷新RestLoggerUtility.returnInvoke();}} 根據springmvc攔截器的原理, 我們需要把日志初始化工作, 放在preHandle中實現. 把日志整體輸入, 放在afterCompletion函數中實現.
3). 添加攔截器配置
再次添加攔截器配置, 并把它置于首位. <!-- 攔截器列表 --><mvc:interceptors><!-- 日志攔截器, 更好地記錄整個請求過程 --><mvc:interceptor><mvc:mapping path="/**"/><mvc:exclude-mapping path="/html/**" /><bean class="com.springapp.mvc.interceptor.RestLoggerInterceptor" /></mvc:interceptor><mvc:interceptor><mvc:mapping path="/**" /><mvc:exclude-mapping path="/html/**" /><bean class="xxx.xxx.XXXIntercptor" /></mvc:interceptor></mvc:interceptors> 4). 完善異常的處理
對異常的攔截, 需要再補充, 定義一個ControlAdvice, 在處理異常的代碼中, 添加異常日志記錄的pointcut. @ControllerAdvicepublic class RestApiControlAdvice {private static final Logger restLogger = LoggerFactory.getLogger("rest");@ExceptionHandler(value=Exception.class)@ResponseBodypublic String handle(Exception e) {restLogger.warn("exception", e);RestLoggerUtility.throwableInvoke("[exception: msg:%s]", e.getMessage());return "error";}} 這樣, 我們想要實現的基本目標就能達到了.
示例代碼:
樣例代碼的下載:http://pan.baidu.com/s/1jH1ggZ0.
代碼類組織如下:
總結:
好久想寫這篇文章了,算是對springmvc攔截器機制的一份整理和自身理解. 希望能對讀者有益,對自己而言,權當學習筆記.
個人微信公眾號:?木目的H5游戲世界 個人游戲作品集站點(尚在建設中...):?www.mmxfgame.com, ?也可直接ip訪問:?http://120.26.221.54/.
總結
以上是生活随笔為你收集整理的springmvc学习笔记--Interceptor机制和实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用auditd监控Linux的文件变化
- 下一篇: 浮动路由