springmvc學習筆記--Interceptor機制和實踐


前言:
  Spring的AOP理念, 以及j2ee中責任鏈(過濾器鏈)的設計模式, 確實深入人心, 處處可以看到它的身影. 這次借項目空閑, 來總結一下SpringMVC的Interceptor機制, 並以用戶登陸和日志記錄作為案例, 以做實踐.

原理及類圖:
  攔截器的使用, 其實非常的廣泛, 尤其對通用普適的功能調用, 提取到攔截器層中實現.
  常見的攔截器有如下幾種: 用戶登陸/日志記錄/性能評估/權限控制等等.
  攔截器Interceptor鏈, 橫亘在控制器Controller(Action)前, 具體的接口定義如下所示:

package org.springframework.web.servlet;

public interface 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;
}

  摘錄了開濤老師的原話和圖文解說:

preHandle: 預處理回調方法, 在controller層之前調用.
    返回值: true 表示繼續流程(如調用下一個攔截器或處理器).
        false 表示流程中斷, 不會繼續調用其他的攔截器或處理器.
postHandle: 后處理回調方法, 在controller層之后調用(但在渲染視圖之前), 
        我們可以通過modelAndView(模型和視圖對象)對模型數據進行處理或對視圖進行處理, 
        modelAndView也可能為null.
afterCompletion: 整個請求處理完畢回調方法, 即在視圖渲染完畢時回調, 
        類似於try-catch-finally中的finally.
        當然前提是該攔截器的preHandle返回true.

  正常流程和異常流程的圖說明:

  注: 圖摘自開濤老師的博客, <<第五章 處理器攔截器詳解——跟着開濤學SpringMVC>>. 
  但有多個攔截器的時候, 其配置順序也特別重要, preHandle是順序執行, postHandle則是逆序執行, afterCompletion也是逆序執行.
  集成於springmvc時, 配置也非常的簡潔, 如下樣例即可:

    <mvc:interceptors>
        <!-- 使用bean定義一個Interceptor,直接定義在mvc:interceptors根下面的Interceptor將攔截所有的請求 -->
        <bean class="com.host.app.web.interceptor.AllInterceptor"/>
        <mvc:interceptor>
            <!-- 定義在mvc:interceptor下面的表示是對特定的請求才進行攔截的 -->
            <mvc:mapping path="/**"/>
            <bean class="xxx.xxx.XXXInterceptor"/>
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean class="yyy.yyy.YYYInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>

  注: 在最外層定義的Interceptor類, 對所有的url映射都進行攔截, 而mvc:interceptor標簽申明的interceptor則通過mvc:mapping來自定義過濾規則.

用戶登陸:
用戶登陸驗證, 是最常見的一種需求, 也是很多開發者第一次使用攔截器使用的對象. 因此我們就以此作為案例.
比如我們編寫如下代碼:
@Controller
@RequestMapping("/")
public class HelloController {

   @RequestMapping(value="/login", method={RequestMethod.POST, RequestMethod.GET})
   @ResponseBody
   public 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類.
    @Component
    public class UserVerifyIntercptor extends HandlerInterceptorAdapter {

        private String[] allowUrls = new String[] {
                // *) 用戶登陸相關的接口
                "/login",
        };

        @Override
        public 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})
   @ResponseBody
   public 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");

        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // *) 日志准備
            RestLoggerUtility.beforeInvoke();
            return super.preHandle(request, response, handler);
        }

        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            super.postHandle(request, response, handler, modelAndView);
        }

        @Override
        public 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.
    @ControllerAdvice
    public class RestApiControlAdvice {

        private static final Logger restLogger = LoggerFactory.getLogger("rest");

        @ExceptionHandler(value=Exception.class)
        @ResponseBody
        public 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/.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM