Spring Boot攔截器(Interceptor)詳解

Interceptor 介紹Interceptor 做用自定義 Interceptor運行程序並測試效果應用性能監控登陸檢測參考資料html

Interceptor 介紹

攔截器(Interceptor)同 Filter 過濾器同樣,它倆都是面向切面編程——AOP 的具體實現(AOP切面編程只是一種編程思想而已)。git

你可使用 Interceptor 來執行某些任務,例如在 Controller 處理請求以前編寫日誌,添加或更新配置……web

Spring中,當請求發送到 Controller 時,在被Controller處理以前,它必須通過 Interceptors(0或多個)。spring

Spring Interceptor是一個很是相似於Servlet Filter 的概念 。apache

Interceptor 做用

  1. 日誌記錄:記錄請求信息的日誌,以便進行信息監控、信息統計、計算 PV(Page View)等;
  2. 權限檢查:如登陸檢測,進入處理器檢測是否登陸;
  3. 性能監控:經過攔截器在進入處理器以前記錄開始時間,在處理完後記錄結束時間,從而獲得該請求的處理時間。(反向代理,如 Apache 也能夠自動記錄)
  4. 通用行爲:讀取 Cookie 獲得用戶信息並將用戶對象放入請求,從而方便後續流程使用,還有如提取 Locale、Theme 信息等,只要是多個處理器都須要的便可使用攔截器實現。

自定義 Interceptor

若是你須要自定義 Interceptor 的話必須實現 org.springframework.web.servlet.HandlerInterceptor接口或繼承 org.springframework.web.servlet.handler.HandlerInterceptorAdapter類,而且須要重寫下面下面 3 個方法: 編程

  1. preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) 方法在請求處理以前被調用。該方法在 Interceptor 類中最早執行,用來進行一些前置初始化操做或是對當前請求作預處理,也能夠進行一些判斷來決定請求是否要繼續進行下去。該方法的返回至是 Boolean 類型,當它返回 false 時,表示請求結束,後續的 Interceptor 和 Controller 都不會再執行;當它返回爲 true 時會繼續調用下一個 Interceptor 的 preHandle 方法,若是已是最後一個 Interceptor 的時候就會調用當前請求的 Controller 方法。
  2. postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) 方法在當前請求處理完成以後,也就是 Controller 方法調用以後執行,可是它會在 DispatcherServlet 進行視圖返回渲染以前被調用,因此咱們能夠在這個方法中對 Controller 處理以後的 ModelAndView 對象進行操做。
  3. afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex) 方法須要在當前對應的 Interceptor 類的 preHandle 方法返回值爲 true 時纔會執行。顧名思義,該方法將在整個請求結束以後,也就是在 DispatcherServlet 渲染了對應的視圖以後執行。此方法主要用來進行資源清理。

接下來結合實際代碼進行學習。安全

LogInterceptor 類:springboot

public class LogInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        long startTime = System.currentTimeMillis();
        System.out.println("\n-------- LogInterception.preHandle --- ");
        System.out.println("Request URL: " + request.getRequestURL());
        System.out.println("Start Time: " + System.currentTimeMillis());

        request.setAttribute("startTime", startTime);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("\n-------- LogInterception.postHandle --- ");
        System.out.println("Request URL: " + request.getRequestURL());
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("\n-------- LogInterception.afterCompletion --- ");

        long startTime = (Long) request.getAttribute("startTime");
        long endTime = System.currentTimeMillis();
        System.out.println("Request URL: " + request.getRequestURL());
        System.out.println("End Time: " + endTime);

        System.out.println("Time Taken: " + (endTime - startTime));
    }
}
複製代碼

OldLoginInterceptor 類:服務器

public class OldLoginInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("\n-------- OldLoginInterceptor.preHandle --- ");
        System.out.println("Request URL: " + request.getRequestURL());
        System.out.println("Sorry! This URL is no longer used, Redirect to /admin/login");

        response.sendRedirect(request.getContextPath()+ "/admin/login");
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("\n-------- OldLoginInterceptor.postHandle --- ");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("\n-------- OldLoginInterceptor.afterCompletion --- ");
    }
}
複製代碼

配置攔截器 :markdown

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor());

        registry.addInterceptor(new OldLoginInterceptor()).addPathPatterns("/admin/oldLogin");

        registry.addInterceptor(new AdminInterceptor()).addPathPatterns("/admin/*").excludePathPatterns("/admin/oldLogin");
    }
}
複製代碼

LogInterceptor 攔截器用於攔截全部請求; OldLoginInterceptor 用來攔截連接 「 / admin / oldLogin」,它將重定向到新的 「 / admin / login」。AdminInterceptor用來攔截連接 「/admin/*」,除了連接 「 / admin / oldLogin」

自定義 Controller 驗證攔截器

@Controller
public class LoginController {

    @RequestMapping("/index")
    public String index(Model model){
        return "index";
    }

    @RequestMapping(value = "/admin/login")
    public String login(Model model){
        return "login";
    }
}
複製代碼

同時依賴 thymeleaf 模板構建兩個頁面。

index.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8" />
    <title>Spring Boot Mvc Interceptor example</title>
</head>

<body>
<div style="border: 1px solid #ccc;padding: 5px;margin-bottom:10px;">
    <a th:href="@{/}">Home</a>
    &nbsp;&nbsp; | &nbsp;&nbsp;
    <a th:href="@{/admin/oldLogin}">/admin/oldLogin (OLD URL)</a>
</div>

<h3>Spring Boot Mvc Interceptor</h3>

<span style="color:blue;">Testing LogInterceptor</span>
<br/><br/>

See Log in Console..

</body>
</html>
複製代碼

login.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Spring Boot Mvc Interceptor example</title>
</head>
<body>

<div style="border: 1px solid #ccc;padding: 5px;margin-bottom:10px;">
    <a th:href="@{/}">Home</a>
    &nbsp;&nbsp; | &nbsp;&nbsp;
    <a th:href="@{/admin/oldLogin}">/admin/oldLogin (OLD URL)</a>
</div>

<h3>This is Login Page</h3>

<span style="color:blue">Testing OldLoginInterceptor &amp; AdminInterceptor</span>
<br/><br/>
See more info in the Console.

</body>

</html>
複製代碼

運行程序並測試效果

一切準備完畢,啓動該項目。打開網址: http://localhost:8080/index

關於該請求在後臺的執行過程,用圖解的方式進行展現:

若是此時點擊 /admin/oldLogin (OLD URL) 或者在網址欄輸入:http://localhost:8080/admin/oldLogin

控制檯打印結果:

-------- LogInterception.preHandle --- 
Request URL: http://localhost:8080/admin/oldLogin
Start Time1576329730709

-------- OldLoginInterceptor.preHandle --- 
Request URLhttp://localhost:8080/admin/oldLogin
Sorry! This URL is no longer used, Redirect to /admin/login

-------- LogInterception.afterCompletion --- 
Request URLhttp://localhost:8080/admin/oldLogin
End Time1576329730709
Time Taken: 0

-------- LogInterception.preHandle --- 
Request URLhttp://localhost:8080/admin/login
Start Time1576329730716

-------- AdminInterceptor.preHandle --- 

-------- AdminInterceptor.postHandle --- 

-------- LogInterception.postHandle --- 
Request URLhttp://localhost:8080/admin/login

-------- AdminInterceptor.afterCompletion --- 

-------- LogInterception.afterCompletion --- 
Request URLhttp://localhost:8080/admin/login
End Time1576329730718
Time Taken: 2
複製代碼

一樣咱們用圖解的形式分析:

應用

性能監控

如記錄一下請求的處理時間,獲得一些慢請求(如處理時間超過500毫秒),從而進行性能改進,通常的反向代理服務器如 apache 都具備這個功能,但此處咱們演示一下使用攔截器怎麼實現。

實現分析:

一、在進入處理器以前記錄開始時間,即在攔截器的 preHandle 記錄開始時間;

二、在結束請求處理以後記錄結束時間,即在攔截器的 afterCompletion 記錄結束實現,並用結束時間-開始時間獲得此次請求的處理時間。

問題:

咱們的攔截器是單例,所以無論用戶請求多少次都只有一個攔截器實現,即線程不安全,那咱們應該怎麼記錄時間呢?

解決方案是使用 ThreadLocal,它是線程綁定的變量,提供線程局部變量(一個線程一個 ThreadLocal,A線程的ThreadLocal 只能看到A線程的 ThreadLocal,不能看到B線程的 ThreadLocal)。

代碼實現:

public class StopWatchHandlerInterceptor extends HandlerInterceptorAdapter {
    private NamedThreadLocal<Long> startTimeThreadLocal = new NamedThreadLocal<>("StopWatch-StartTime");
    private Logger logger = LoggerFactory.getLogger(StopWatchHandlerInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        long beginTime = System.currentTimeMillis();//一、開始時間
        startTimeThreadLocal.set(beginTime);//線程綁定變量(該數據只有當前請求的線程可見)
        return true;//繼續流程
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        long endTime = System.currentTimeMillis();//二、結束時間
        long beginTime = startTimeThreadLocal.get();//獲得線程綁定的局部變量(開始時間)
        long consumeTime = endTime - beginTime;//三、消耗的時間
        if(consumeTime > 500) {//此處認爲處理時間超過500毫秒的請求爲慢請求
            //TODO 記錄到日誌文件
            logger.info(String.format("%s consume %d millis", request.getRequestURI(), consumeTime));
        }
        //測試的時候因爲請求時間未超過500,因此啓用該代碼
//        logger.info(String.format("%s consume %d millis", request.getRequestURI(), consumeTime));

    }
}
複製代碼

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

在測試時須要把 stopWatchHandlerInterceptor 放在攔截器鏈的第一個,這樣獲得的時間纔是比較準確的。

攔截器配置類

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new StopWatchHandlerInterceptor());

        registry.addInterceptor(new OldLoginInterceptor()).addPathPatterns("/admin/oldLogin");

    }
}
複製代碼

和上述操做步驟一致,控制檯打印結果爲:

2019-12-14 21:51:43.881  INFO 4616 --- [nio-8080-exec-3] c.e.i.StopWatchHandlerInterceptor        : /index consume 14 millis

-------- OldLoginInterceptor.preHandle --- 
Request URL: http://localhost:8080/admin/oldLogin
Sorry! This URL is no longer used, Redirect to /admin/login
2019-12-14 21:51:54.055  INFO 4616 --- [nio-8080-exec-5] c.e.i.StopWatchHandlerInterceptor        : /admin/oldLogin consume 1 millis
2019-12-14 21:51:54.070  INFO 4616 --- [nio-8080-exec-6] c.e.i.StopWatchHandlerInterceptor        : /admin/login consume 9 millis
複製代碼

登陸檢測

在訪問某些資源時(如訂單頁面),須要用戶登陸後才能查看,所以須要進行登陸檢測。

流程:

一、訪問須要登陸的資源時,由攔截器重定向到登陸頁面;

二、若是訪問的是登陸頁面,攔截器不該該攔截;

三、用戶登陸成功後,往 cookie/session 添加登陸成功的標識(如用戶編號);

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

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

攔截器代碼以下所示:

public class MyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        boolean flag = true;
        String ip = request.getRemoteAddr();
        long startTime = System.currentTimeMillis();
        request.setAttribute("requestStartTime", startTime);
        if (handler instanceof ResourceHttpRequestHandler) {
            System.out.println("preHandle這是一個靜態資源方法!");
        } else if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            System.out.println("用戶:" + ip + ",訪問目標:" + method.getDeclaringClass().getName() + "." + method.getName());
        }

        //若是用戶未登陸
        User user = (User) request.getSession().getAttribute("user");
        if (null == user) {
            //重定向到登陸頁面
            response.sendRedirect("toLogin");
            flag = false;
        }
        return flag;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if (handler instanceof ResourceHttpRequestHandler) {
            System.out.println("postHandle這是一個靜態資源方法!");
        } else if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            long startTime = (long) request.getAttribute("requestStartTime");
            long endTime = System.currentTimeMillis();
            long executeTime = endTime - startTime;

            int time = 1000;
            //打印方法執行時間
            if (executeTime > time) {
                System.out.println("[" + method.getDeclaringClass().getName() + "." + method.getName() + "] 執行耗時 : "
                        + executeTime + "ms");
            } else {
                System.out.println("[" + method.getDeclaringClass().getSimpleName() + "." + method.getName() + "] 執行耗時 : "
                        + executeTime + "ms");
            }
        }
    }

}
複製代碼

參考資料

https://snailclimb.gitee.io/springboot-guide/#/./docs/basis/springboot-interceptor

https://www.cnblogs.com/junzi2099/p/8260137.html#_label3_0

相關文章
相關標籤/搜索