SpringBoot攔截器和動態代理有什麼區別?

2023-09-15 12:00:24

在 Spring Boot 中,攔截器和動態代理都是用來實現功能增強的,所以在很多時候,有人會認為攔截器的底層是通過動態代理實現的,所以本文就來盤點一下他們兩的區別,以及攔截器的底層實現。

1.攔截器

攔截器(Interceptor)準確來說在 Spring MVC 中的一個很重要的元件,用於攔截 Controller 的請求。
它的主要作用有以下幾個:

  1. 許可權驗證:驗證使用者是否登入、是否有許可權存取某個介面。
  2. 紀錄檔記錄:記錄請求資訊的紀錄檔,如請求引數,響應資訊等。
  3. 效能監控:監控系統的執行效能,如慢查詢介面等。
  4. 通用行為:插入一些通用的行為,比如開發環境忽略某些請求。

典型的使用場景是身份認證、授權檢查、請求紀錄檔記錄等。

1.1 攔截器實現

在 Spring Boot 中攔截器的實現分為兩步:

  1. 建立一個普通的攔截器,實現 HandlerInterceptor 介面,並重寫介面中的相關方法。
  2. 將上一步建立的攔截器加入到 Spring Boot 的組態檔中,並設定攔截規則。

具體實現如下。

① 實現自定義攔截器

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class TestInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("攔截器:執行 preHandle 方法。");
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("攔截器:執行 postHandle 方法。");
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("攔截器:執行 afterCompletion 方法。");
    }
}

其中:

  • boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handle):在請求方法執行前被呼叫,也就是呼叫目標方法之前被呼叫。比如我們在運算元據之前先要驗證使用者的登入資訊,就可以在此方法中實現,如果驗證成功則返回 true,繼續執行資料操作業務;否則就返回 false,後續運算元據的業務就不會被執行了。
  • void postHandle(HttpServletRequest request, HttpServletResponse response, Object handle, ModelAndView modelAndView):呼叫請求方法之後執行,但它會在 DispatcherServlet 進行渲染檢視之前被執行。
  • void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex):會在整個請求結束之後再執行,也就是在 DispatcherServlet 渲染了對應的檢視之後再執行。

② 設定攔截規則

然後,我們再將上面的攔截器注入到專案組態檔中,並設定相應攔截規則,具體實現程式碼如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class AppConfig implements WebMvcConfigurer {

    // 注入攔截器
    @Autowired
    private TestInterceptor testInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(testInterceptor) // 新增攔截器
                .addPathPatterns("/**"); // 攔截所有地址
        		.excludePathPatterns("/login"); // 放行介面
    }
}

這樣我們的攔截器就實現完了。

1.2 攔截器實現原理

Spring Boot 攔截器是基於 Java 的 Servlet 規範實現的,通過實現 HandlerInterceptor 介面來實現攔截器功能。

在 Spring Boot 框架的執行流程中,攔截器被註冊在 DispatcherServlet 的 doDispatch() 方法中,該方法是 Spring Boot 框架的核心方法,用於處理請求和響應。

程式每次執行時都會呼叫 doDispatch() 方法時,並驗證攔截器(鏈),之後再根據攔截器返回的結果,進行下一步的處理。如果返回的是 true,那麼繼續呼叫目標方法,反之則會直接返回驗證失敗給前端。

doDispatch 原始碼實現如下:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        try {
            ModelAndView mv = null;
            Object dispatchException = null;

            try {
                processedRequest = this.checkMultipart(request);
                multipartRequestParsed = processedRequest != request;
                mappedHandler = this.getHandler(processedRequest);
                if (mappedHandler == null) {
                    this.noHandlerFound(processedRequest, response);
                    return;
                }

                HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
                String method = request.getMethod();
                boolean isGet = HttpMethod.GET.matches(method);
                if (isGet || HttpMethod.HEAD.matches(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

                // 呼叫預處理【重點】
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // 執行 Controller 中的業務
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                this.applyDefaultViewName(processedRequest, mv);
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            } catch (Exception var20) {
                dispatchException = var20;
            } catch (Throwable var21) {
                dispatchException = new NestedServletException("Handler dispatch failed", var21);
            }

            this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
        } catch (Exception var22) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
        } catch (Throwable var23) {
            this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
        }

    } finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        } else if (multipartRequestParsed) {
            this.cleanupMultipart(processedRequest);
        }

    }
}

從上述原始碼可以看出在開始執行 Controller 之前,會先呼叫 預處理方法 applyPreHandle,而 applyPreHandle 方法的實現原始碼如下:

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
        // 獲取專案中使用的攔截器 HandlerInterceptor
        HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
        if (!interceptor.preHandle(request, response, this.handler)) {
            this.triggerAfterCompletion(request, response, (Exception)null);
            return false;
        }
    }
    return true;
}

從上述原始碼可以看出,在 applyPreHandle 中會獲取所有的攔截器 HandlerInterceptor 並執行攔截器中的 preHandle 方法,這樣就會咱們前面定義的攔截器對應上了,如下圖所示:

此時使用者登入許可權的驗證方法就會執行,這就是攔截器的執行過程。
因此,可以得出結論,攔截器的實現主要是依賴 Servlet 或 Spring 執行流程來進行攔截和功能增強的。

2.動態代理

動態代理是一種設計模式,它是指在執行時提供代理物件,來擴充套件目標物件的功能。
在 Spring 中的,動態代理的實現手段有以下兩種:

  1. JDK 動態代理:通過反射機制生成代理物件,目標物件必須實現介面。
  2. CGLIB 動態代理:通過生成目標類的子類來實現代理,不要求目標物件實現介面。

動態代理的主要作用包括:

  1. 擴充套件目標物件的功能:如新增紀錄檔、驗證引數等。
  2. 控制目標物件的存取:如進行許可權控制。
  3. 延遲載入目標物件:在需要時才範例化目標物件。
  4. 遠端代理:將請求轉發到遠端的目標物件上。

JDK 動態代理和 CGLIB 的區別詳見:www.javacn.site/interview/spring/jdk_cglib.html

3.攔截器 VS 動態代理

因此,我們可以得出結論,攔截器和動態代理雖然都是用來實現功能增強的,但二者完全不同,他們的主要區別體現在以下幾點:

  1. 使用範圍不同:攔截器通常用於 Spring MVC 中,主要用於攔截 Controller 請求。動態代理可以使用在 Bean 中,主要用於提供 bean 的代理物件,實現對 bean 方法的攔截。
  2. 實現原理不同:攔截器是通過 HandlerInterceptor 介面來實現的,主要是通過 afterCompletion、postHandle、preHandle 這三個方法在請求前後進行攔截處理。動態代理主要有 JDK 動態代理和 CGLIB 動態代理,JDK 通過反射生成代理類;CGLIB 通過生成被代理類的子類來實現代理。
  3. 加入時機不同:攔截器是在執行階段動態加入的;動態代理是在編譯期或執行期生成的代理類。
  4. 使用難易程度不同:攔截器相對簡單,通過實現介面即可使用。動態代理稍微複雜,需要了解動態代理的實現原理,然後通過相應的 api 實現。

小結

在 Spring Boot 中,攔截器和動態代理都是用來實現功能增強的,但二者沒有任何關聯關係,它的區別主要體現在使用範圍、實現原理、加入時機和使用的難易程度都是不同的。

本文已收錄到我的面試小站 www.javacn.site,其中包含的內容有:Redis、JVM、並行、並行、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、設計模式、訊息佇列等模組。