1. 程式人生 > 程式設計 >Spring Boot實踐——三種攔截器的建立

Spring Boot實踐——三種攔截器的建立

Spring中的攔截器

  在web開發中,攔截器是經常用到的功能。它可以幫我們驗證是否登陸、許可權認證、資料校驗、預先設定資料以及統計方法的執行效率等等。今天就來詳細的談一下spring中的攔截器。spring中攔截器主要分種,一個是HandlerInterceptor,一個是MethodInterceptor。

一、HandlerInterceptor攔截器

  HandlerInterceptor是springMVC專案中的攔截器,它攔截的目標是請求的地址,比MethodInterceptor先執行。實現一個HandlerInterceptor攔截器可以直接實現HandlerInterceptor介面,也可以繼承HandlerInterceptorAdapter類。這兩種方法殊途同歸,其實HandlerInterceptorAdapter也就是宣告瞭HandlerInterceptor介面中所有方法的預設實現,而我們在繼承他之後只需要重寫必要的方法。下面就是HandlerInterceptorAdapter的程式碼,可以看到一個方法只是預設返回true,另外兩個是空方法:

/** * 自定義攔截器-基於springmvc
 * @ClassName: CustomInterceptor 
 * @Description: springMVC專案中的攔截器,它攔截的目標是請求的地址,比MethodInterceptor先執行。
 *                 該攔截器只能過濾action請求,SPring允許多個攔截器同時存在,通過攔截器鏈管理。
 *                 當preHandle return true時,執行下一個攔截器,直到所有攔截器執行完,再執行被攔截的請求。
 *                 當preHandle return false時,不再執行後續的攔截器鏈及被攔截的請求。
 * @author OnlyMate
 * @Date 2018年8月28日 下午2:30:22  
 * */
public class CustomInterceptor implements HandlerInterceptor  {

    @Override public boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws Exception { // TODO Auto-generated method stub
        return HandlerInterceptor.super.preHandle(request,response,handler);
    }

    @Override public void postHandle(HttpServletRequest request,Object handler,ModelAndView modelAndView) throws Exception { // TODO Auto-generated method stub
        HandlerInterceptor.super.postHandle(request,handler,modelAndView);
    }

    @Override public void afterCompletion(HttpServletRequest request,Exception ex) throws Exception { // TODO Auto-generated method stub
        HandlerInterceptor.super.afterCompletion(request,ex);
    }

}複製程式碼

這三個方法都是幹什麼的,有什麼作用,什麼時候呼叫,不同的攔截器之間是怎樣的呼叫順序呢?這還得參考一下DispatcherServlet的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 {
            ModelAndView mv = null;
            Exception dispatchException = null;

            try {
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);

                // Determine handler for the current request.
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null) {
                    noHandlerFound(processedRequest,response);
                    return;
                }

                // Determine handler adapter for the current request.
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                // Process last-modified header,if supported by the handler.
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request,mappedHandler.getHandler());
                    if (logger.isDebugEnabled()) {
                        logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                    }
                    if (new ServletWebRequest(request,response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

                if (!mappedHandler.applyPreHandle(processedRequest,response)) {
                    return;
                }

                // Actually invoke the handler.
                mv = ha.handle(processedRequest,mappedHandler.getHandler());

                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                applyDefaultViewName(processedRequest,mv);
                mappedHandler.applyPostHandle(processedRequest,mv);
            }
            catch (Exception ex) {
                dispatchException = ex;
            }
            catch (Throwable err) {
                // As of 4.3,we're processing Errors thrown from handler methods as well,// making them available for @ExceptionHandler methods and other scenarios.
                dispatchException = new NestedServletException("Handler dispatch failed",err);
            }
            processDispatchResult(processedRequest,mappedHandler,mv,dispatchException);
        }
        catch (Exception ex) {
            triggerAfterCompletion(processedRequest,ex);
        }
        catch (Throwable err) {
            triggerAfterCompletion(processedRequest,new NestedServletException("Handler processing failed",err));
        }
        finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                // Instead of postHandle and afterCompletion
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest,response);
                }
            }
            else {
                // Clean up any resources used by a multipart request.
                if (multipartRequestParsed) {
                    cleanupMultipart(processedRequest);
                }
            }
        }
    }複製程式碼

  程式碼有點長,但是它封裝了springMVC處理請求的整個過程。首先根據請求找到對應的HandlerExecutionChain,它包含了處理請求的handler和所有的HandlerInterceptor攔截器;然後在呼叫hander之前分別呼叫每個HandlerInterceptor攔截器的preHandle方法,若有一個攔截器返回false,則會呼叫triggerAfterCompletion方法,並且立即返回不再往下執行;若所有的攔截器全部返回true並且沒有出現異常,則呼叫handler返回ModelAndView物件;再然後分別呼叫每個攔截器的postHandle方法;最後,即使是之前的步驟丟擲了異常,也會執行triggerAfterCompletion方法。關於攔截器的處理到此為止,接下來看看triggerAfterCompletion做了什麼

private void triggerAfterCompletion(HttpServletRequest request,@Nullable HandlerExecutionChain mappedHandler,Exception ex) throws Exception {

        if (mappedHandler != null) {
            mappedHandler.triggerAfterCompletion(request,ex);
        }
        throw ex;
    }複製程式碼

  根據以上的程式碼,分析一下不同攔截器及其方法的執行順序。假設有5個攔截器編號分別為12345,若一切正常則方法的執行順序是12345的preHandle,54321的postHandle,54321的afterCompletion。若編號3的攔截器的preHandle方法返回false或者丟擲了異常,接下來會執行的是21的afterCompletion方法。這裡要注意的地方是,我們在寫一個攔截器的時候要謹慎的處理preHandle中的異常,因為這裡一旦有異常丟擲就不會再受到這個攔截器的控制。12345的preHandle的方法執行過之後,若handler出現了異常或者某個攔截器的postHandle方法出現了異常,則接下來都會執行54321的afterCompletion方法,因為只要12345的preHandle方法執行完,當前攔截器的攔截器就會記錄成編號5的攔截器,而afterCompletion總是從當前的攔截器逆向的向前執行。  另外,實現HandlerInterceptor攔截器還有一個方法,就是實現WebRequestInterceptor介面。其實它和剛才的兩種方法也是殊途同歸,最終還是被spring適配成HandlerInterceptor。有一點不同,它的preHandle方法最終只會返回true。

這裡可以根據自己的需求在對應方法中寫自己業務處理邏輯

/**
 * 自定義攔截器-基於springmvc
 * @ClassName: CustomInterceptor 
 * @Description: springMVC專案中的攔截器,它攔截的目標是請求的地址,比MethodInterceptor先執行。
 *                 該攔截器只能過濾action請求,SPring允許多個攔截器同時存在,通過攔截器鏈管理。
 *                 當preHandle return true時,執行下一個攔截器,直到所有攔截器執行完,再執行被攔截的請求。
 *                 當preHandle return false時,不再執行後續的攔截器鏈及被攔截的請求。
 * @author OnlyMate
 * @Date 2018年8月28日 下午2:30:22  
 *
 */
public class CustomInterceptor implements HandlerInterceptor  {
    private Logger logger = LoggerFactory.getLogger(CustomInterceptor.class);
    
    /**
     * 在請求處理之前執行,主要用於許可權驗證、引數過濾等
     */
    @Override
    public boolean preHandle(HttpServletRequest request,Object handler)
            throws Exception {
        logger.info("CustomInterceptor ==> preHandle method: do request before");
        return true;
    }

    /**
     * 當前請求進行處理之後執行,主要用於日誌記錄、許可權檢查、效能監控、通用行為等
     */
    @Override
    public void postHandle(HttpServletRequest request,ModelAndView modelAndView) throws Exception {
        logger.info("CustomInterceptor ==> postHandle method: do request after");
    }

    /**
     * 當前對應的interceptor的perHandle方法的返回值為true時,postHandle執行完成並渲染頁面後執行,主要用於資源清理工作
     */
    @Override
    public void afterCompletion(HttpServletRequest request,Exception ex)
            throws Exception {
        logger.info("CustomInterceptor ==> afterCompletion method: do request finshed");
    }
}複製程式碼

配置如下:

/**
 * Web MVC 配置介面卡
 * @ClassName: WebAppConfigurer 
 * @Description: 
 * @author OnlyMate
 * @Date 2018年8月28日 下午2:39:31  
 * 
 * WebAppConfigurer extends WebMvcConfigurerAdapter 在Spring Boot2.0版本已過時了,用官網說的新的類替換
 *
 */
@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {
    /**
     * 注入自定義攔截器
     * @Title: addInterceptors 
     * @Description: 先add的攔截器會越靠外,即越靠近瀏覽器
     * @Date 2018年8月28日 下午2:47:28 
     * @author OnlyMate
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        WebMvcConfigurer.super.addInterceptors(registry);
        registry.addInterceptor(new CustomInterceptor()).addPathPatterns("/**");//攔截所有請求
    }

}複製程式碼

二、MethodInterceptor攔截器

  MethodInterceptor是AOP專案中的攔截器,它攔截的目標是方法,即使不是controller中的方法。實現MethodInterceptor攔截器大致也分為兩種,一種是實現MethodInterceptor介面,另一種利用AspectJ的註解或配置。

1、實現MethodInterceptor介面

/**
 * 自定義攔截器-方法攔截器,基於spring aop
 * @ClassName: CustomMethodInterceptor 
 * @Description: AOP專案中的攔截器,它攔截的目標是方法
 *                 配置在applicationContext.xml中
 * @author OnlyMate
 * @Date 2018年8月29日 下午3:35:24  
 *
 */
public class CustomMethodInterceptor implements MethodInterceptor {
    private Logger logger = LoggerFactory.getLogger(CustomMethodInterceptor.class);
    
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        logger.info("CustomMethodInterceptor ==> invoke method: process method name is {}",invocation.getMethod().getName());
        
        //TODO 處理操作
        
        return invocation.proceed();
    }

}複製程式碼

配置說明

<bean id="customMethodInterceptor" class="com.onlymate.springboot.interceptor.CustomMethodInterceptor" />
    
    <aop:config proxy-target-class="false">
        <!-- 方法攔截器,基於spring aop 實現配置 -->
        <!-- 掃描使用了註解的方法進行攔截 -->
        <aop:advisor pointcut="@annotation(com.onlymate.springboot.annotation.CustomAnnotation)" advice-ref="customMethodInterceptor" />
        <!-- 指定包路徑下的方法 -->
        <aop:advisor pointcut="execution(* com.onlymate.springboot.controller.*.*(..))" advice-ref="customMethodInterceptor" />
    </aop:config>複製程式碼

CustomAnnotation自定義註解

/**
 * 自定義註解物件
 * @ClassName: TableSplit 
 * @Description: TODO
 * @author OnlyMate
 * @Date 2018年5月22日 上午11:43:57  
 *
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotation {
    /** 需攔截方法名描述 */
    String name() default "";
    
    /** 加密 */
    String[] encrypt() default {};

    /** 解密 */
    String[] decrypt() default {};

}複製程式碼

2、利用AspectJ的註解或配置

 a、基於AspectJ註解

/**
 * 自定義攔截器-方法攔截器,基於註解的AspectJ方式
 * @ClassName: CustomAutoAspectJInterceptor 
 * @Description: 配置在applicationContext.xml中
 * @author OnlyMate
 * @Date 2018年8月29日 下午4:03:49  
 *
 */
@Component
@Aspect
public class CustomAutoAspectJInterceptor {
    private Logger logger = LoggerFactory.getLogger(CustomAutoAspectJInterceptor.class);
    
    @Around("execution (* com.onlymate.springboot.controller.*.*(..))")
    public Object around(ProceedingJoinPoint point) throws Throwable{
        logger.info("CustomAutoAspectJInterceptor ==> invoke method: process method class is {}",point.getTarget().getClass());
        
        //TODO 處理操作
        
        return point.proceed();
    }
}複製程式碼

b、基於AspectJ配置

/**
 * 自定義攔截器-方法攔截器,基於AspectJ方式
 * @ClassName: CustomAspectJInterceptor 
 * @Description: 配置在applicationContext.xml中
 * @author OnlyMate
 * @Date 2018年8月29日 下午4:03:49  
 *
 */
public class CustomAspectJInterceptor {
    private Logger logger = LoggerFactory.getLogger(CustomAspectJInterceptor.class);
    
    public Object around(ProceedingJoinPoint point) throws Throwable{
        logger.info("CustomAspectJInterceptor ==> invoke method: process method class is {}",point.getTarget().getClass());
        
        //TODO 處理操作
        
        return point.proceed();
    }
}複製程式碼

c、配置說明

<bean id="customAspectJInterceptor" class="com.onlymate.springboot.interceptor.CustomAspectJInterceptor"/>
    <aop:config proxy-target-class="false">
        <!-- 方法攔截器,基於AspectJ實現方式一 -->
        <aop:aspect ref="customAspectJInterceptor">
            <aop:around method="around" pointcut="execution(* com.onlymate.springboot.controller.*.*(..))"/>
        </aop:aspect>
        
    </aop:config>

    <!-- 方法攔截器,基於AspectJ實現方式二 -->
    <!-- 自動掃描使用了aspectj註解的類 -->
    <aop:aspectj-autoproxy/>複製程式碼

三、效果圖

201908161453001

四、談一談區別

  上面的兩種攔截器都能起到攔截的效果,但是他們攔截的目標不一樣,實現的機制不同,所以有的時候適用不同的場景。HandlerInterceptoer攔截的是請求地址,所以針對請求地址做一些驗證、預處理等操作比較合適。當你需要統計請求的響應時間時MethodInterceptor將不太容易做到,因為它可能跨越很多方法或者只涉及到已經定義好的方法中一部分程式碼。MethodInterceptor利用的是AOP的實現機制,在本文中只說明瞭使用方式,關於原理和機制方面介紹的比較少,因為要說清楚這些需要講出AOP的相當一部分內容。在對一些普通的方法上的攔截HandlerInterceptoer就無能為力了,這時候只能利用AOP的MethodInterceptor。利用MethodInterceptor就可以很容易的實現一個日誌攔截處理。另外,還有一個跟攔截器類似的東西----Filter。Filter是Servlet規範規定的,不屬於spring框架,也是用於請求的攔截。但是它適合更粗粒度的攔截,在請求前後做一些編解碼處理、日誌記錄等。而攔截器則可以提供更細粒度的,更加靈活的,針對某些請求、某些方法的組合的解決方案。