1. 程式人生 > >spring MVC——攔截器實現登入檢測和效能監控

spring MVC——攔截器實現登入檢測和效能監控

1. 攔截器簡介

Spring MVC中的攔截器,類似於Servlet開發中的過濾器Filter,主要用來攔截使用者的請求並進行相應的處理,可以用來做日誌記錄、許可權驗證或者登陸檢測。

(1) 常見的應用場景

日誌記錄:記錄請求資訊的日誌,以便進行資訊監控、資訊統計、計算PV(Page View)等。

許可權檢查:如登入檢測,進入處理器檢測檢測是否登入,如果沒有直接返回到登入頁面;

效能監控:有時候系統在某段時間莫名其妙的慢,可以通過攔截器在進入處理器之前記錄開始時間,在處理完後記錄結束時間,從而得到該請求的處理時間;

通用行為:讀取cookie得到使用者資訊並將使用者物件放入請求,從而方便後續流程使用,只要是多個處理器都需要的即可使用攔截器實現。

OpenSessionInView:如Hibernate,在進入處理器開啟Session,在完成後關閉Session。

攔截器本質上也是AOP(面向切面程式設計),也就是說符合橫切關注點的所有功能都可以放入攔截器實現。

(2) 攔截器的介面

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

有3個方法,下面是這些方法的說明:

preHandle:預處理回撥方法,實現處理器的預處理(如登入檢查),第三個引數為響應的處理器(Controller);返回值:true表示繼續流程(如呼叫下一個攔截器或處理器);

false表示流程中斷(如登入檢查失敗),不會繼續呼叫其他的攔截器或處理器,此時我們需要通過response來產生響應;

postHandle:後處理回撥方法,實現處理器的後處理(但在渲染檢視之前),此時我們可以通過modelAndView(模型和檢視物件)對模型資料進行處理或對檢視進行處理,modelAndView也可能為null。

afterCompletion:整個請求處理完畢回撥方法,即在檢視渲染完畢時回撥,如效能監控中我們可以在此記錄結束時間並輸出消耗時間,還可以進行一些資源清理,類似於try-catch-finally中的finally,但僅呼叫處理器執行鏈中preHandle返回true的攔截器的afterCompletion。

(3) 攔截器介面卡

有時我們可能只需要實現三個回撥方法中的某一個,如果實現HandlerInterceptor介面的話,三個方法必須實現,不管你需不需要,此時spring提供了一個HandlerInterceptorAdapter介面卡(一種介面卡設計模式的實現),允許我們只實現需要的回撥方法。

public abstract class HandlerInterceptorAdapter implements AsyncHandlerInterceptor {

    public HandlerInterceptorAdapter() {
    }

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }

    public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    }

}

2、登入檢測

在訪問某些資源時,需要使用者登入後才能檢視,因此需要進行登入檢測。

流程:

1、訪問需要登入的資源時,由攔截器重定向到登入頁面;

2、如果訪問的是登入頁面,攔截器不應該攔截;

3、使用者登入成功後,往cookie/session新增登入成功的標識(如使用者編號);

4、下次請求時,攔截器通過判斷cookie/session中是否有該標識來決定繼續流程還是到登入頁面;

5、在此攔截器還應該允許遊客訪問的資源。

攔截器實現:

package com.etc.interceptor;

import org.springframework.web.servlet.AsyncHandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginCheckInterceptor extends HandlerInterceptorAdapter{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1、如果使用者已經登入 放行
        if(request.getSession().getAttribute("user") != null){
            return true;
        }else {
            //2、非法請求 即這些請求需要登入後才能訪問,重定向到登入頁面
            response.sendRedirect(request.getContextPath()+"/login");
            return false;
        }
    }
}

攔截器配置:   

         <mvc:interceptor>
            <!-- 攔截所有的請求,這個必須寫在前面,也就是寫在【不攔截】的上面 -->
            <mvc:mapping path="/**"/>
            <!-- 但是排除下面這些,也就是不攔截請求 -->
            <mvc:exclude-mapping path="/login"/>
            <mvc:exclude-mapping path="/logout"/>
            <mvc:exclude-mapping path="/doLogin" />
            <bean class="com.etc.interceptor.LoginCheckInterceptor"/>
        </mvc:interceptor>

3、效能監控

如何記錄請求的處理時間,得到一些慢請求(如處理時間超過500毫秒),從而進行效能改進,我們來看如何使用攔截器來實現。

實現分析:

1.在進入處理器之前記錄開始時間,即在攔截器的preHandle記錄開始時間;

2.在結束請求處理之後記錄結束時間,即在攔截器的afterCompletion記錄結束實現,並用結束時間-開始時間得到這次請求的處理時間。

問題:

我們的攔截器是單例,因此不管使用者請求多少次都只有一個攔截器實現,即執行緒不安全,那我們應該怎麼記錄時間呢?

解決方案:

使用ThreadLocal,它是執行緒繫結的變數,提供執行緒區域性變數(一個執行緒一個ThreadLocal,A執行緒的ThreadLocal只能看到A執行緒的ThreadLocal,不能看到B執行緒的ThreadLocal)。

Spring提供的一個命名的ThreadLocal實現(NamedThreadLocal)。

攔截器實現:

package com.etc.interceptor;

import org.springframework.core.NamedThreadLocal;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class PerformanceMonitorInterceptor extends HandlerInterceptorAdapter{

    private NamedThreadLocal<Long> threadLocal = new NamedThreadLocal<>("PerformanceMonitor");

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //請求開始時間
        long startTime = System.currentTimeMillis();
        //執行緒繫結變數(該資料只有當前請求的執行緒可見)
        threadLocal.set(startTime);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //請求結束時間
        long endTime = System.currentTimeMillis();
        //獲取執行緒繫結的區域性變數(開始時間)
        long startTime = threadLocal.get();
        long elapse = endTime - startTime;
        //為了測試只要大於0就記錄,一般設定為一個比較大的數,比如300,代表300毫秒
        if(elapse > 0){
            System.out.println(String.format("%s elapse %d 毫秒", request.getRequestURI(), elapse));
        }

    }
}

攔截器配置:

在配置時將該攔截器放在攔截器鏈的第一個,這樣得到的時間才是比較準確。

        <mvc:interceptor>
            <!-- 攔截所有的請求,這個必須寫在前面,也就是寫在【不攔截】的上面 -->
            <mvc:mapping path="/**"/>
            <!-- 但是排除下面這些,也就是不攔截請求 -->
            <mvc:exclude-mapping path="/login"/>
            <mvc:exclude-mapping path="/logout"/>
            <mvc:exclude-mapping path="/doLogin" />
            <bean class="com.etc.interceptor.PerformanceMonitorInterceptor"/>
        </mvc:interceptor>

測試:

4. Spring MVC中的攔截器和Servlet中的過濾器的區別

(1) 攔截器是基於Java的反射機制的,而過濾器是JavaEE標準,基於函式回撥

(2) 攔截器不依賴於Servlet容器,過濾器依賴於Servlet容器。

(3) 攔截器可以獲取IOC容器中的各個bean,而過濾器就不行,在攔截器裡注入一個Service,可以呼叫業務邏輯。

5. 總結

推薦能使用Servlet規範中的過濾器Filter實現的功能就用Filter實現,因為HandlerInteceptor只有在Spring MVC環境下才能使用,因此Filter是最通用的、最先應該使用的。比如如登入檢測攔截器最好使用Filter來實現。