SpringMVC之八:基於SpringMVC攔截器和注解實現controller中訪問權限控制,及異步模式


一、SpringMVC定義interceptor方式

在SpringMVC 中定義一個Interceptor是比較非常簡單,主要有兩種方式:
第一種:實現HandlerInterceptor 接口,或者是繼承實現了HandlerInterceptor 接口的類,例如HandlerInterceptorAdapter; 
第二種:實現Spring的WebRequestInterceptor接口,或者是繼承實現了WebRequestInterceptor的類。

1.1、HandlerInterceptorAdapter

1.1.1、 HandlerInterceptor接口

SpringMVC的攔截器HandlerInterceptor對應提供了三個preHandle,postHandle,afterCompletion方法:

  1.  boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handle)方法:該方法將在請求處理之前進行調用,只有該方法返回true,才會繼續執行后續的Interceptor和Controller,當返回值為true 時就會繼續調用下一個Interceptor的preHandle 方法,如果已經是最后一個Interceptor的時候就會是調用當前請求的Controller方法; 
  2. void postHandle (HttpServletRequest request, HttpServletResponse response, Object handle, ModelAndView modelAndView)方法:該方法將在請求處理之后,DispatcherServlet進行視圖返回渲染之前進行調用,可以在這個方法中對Controller 處理之后的ModelAndView 對象進行操作。 
  3. void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)方法:該方法也是需要當前對應的Interceptor的preHandle方法的返回值為true時才會執行,該方法將在整個請求結束之后,也就是在DispatcherServlet 渲染了對應的視圖之后執行。用於進行資源清理。

1.1.2、HandlerInterceptorAdapter抽象類

  HandlerInterceptorAdapter它實現了AsyncHandlerInterceptor接口,為每個方法提供了空實現。這樣下來HandlerInterceptorAdapter比HandlerInterceptor多了一個實現方法afterConcurrentHandlingStarted(),它來自HandlerInterceptorAdapter的直接實現類AsyncHandlerInterceptor,AsyncHandlerInterceptor接口直接繼承了HandlerInterceptor,並新添了afterConcurrentHandlingStarted()方法用於處理異步請求。

afterConcurrentHandlingStarted()執行時機:???

1.2、WebRequestInterceptor

1.2.1、 WebRequestInterceptor接口

WebRequestInterceptor接口同HandlerInterceptor接口一樣定義了三個方法,preHandle 、postHandle 以及afterCompletion。兩個接口的方法名都相同,調用次序也相同。即preHandle是在請求處理之前調用;postHandle實在請求處理之后,視圖渲染之前調用;afterCompletion是在視圖渲染之后調用。接下來我們看看他們的不同之處。

1.方法參數不同。WebRequest是Spring定義的接口,它是對HttpServletRequest的封裝。對WebRequest 進行的操作都將同步到HttpServletRequest 中。WebRequest 的set/getAttribute(name, value, scope)比HttpServletRequest 的set/getAttribute多了一個scope參數。它有三個取值:

  • SCOPE_REQUEST:它的值是0,表示request請求作用范圍。
  • SCOPE_SESSION :它的值是1,表示session請求作用范圍。
  • SCOPE_GLOBAL_SESSION :它的值是2 ,表示全局會話作用范圍,即ServletContext上下文作用范圍。

2.preHandle 方法。WebRequestInterceptor的該方法返回值為void,不是boolean。所以該方法不能用於請求阻斷,一般用於資源准備。

3.postHandle 方法。preHandle 中准備的數據都可以通過參數WebRequest訪問。ModelMap 是Controller 處理之后返回的Model 對象,可以通過改變它的屬性來改變Model 對象模型,達到改變視圖渲染效果的目的。

4.afterCompletion方法。Exception 參數表示的是當前請求的異常對象,如果Controller 拋出的異常已經被處理過,則Exception對象為null 。

1.2.1、 WebRequestInterceptorAdapter抽象類

在 Spring 框架之中,還提供了一個和WebRequestInterceptor接口長的很像的抽象類,那就是:WebRequestInterceptorAdapter,其實現了AsyncHandlerInterceptor接口,並在內部調用了WebRequestInterceptor接口。
afterConcurrentHandlingStarted()執行時機:???

1.3、HandlerInterceptorAdapter和WebRequestInterceptor相同點:

兩個接口都可用於Contrller層請求攔截,接口中定義的方法作用也是一樣的。

1.4、HandlerInterceptorAdapter和WebRequestInterceptor不同點:

  1. WebRequestInterceptor的入參WebRequest是包裝了HttpServletRequest 和HttpServletResponse的,通過WebRequest獲取Request中的信息更簡便。
  2. WebRequestInterceptor的preHandle是沒有返回值的,說明該方法中的邏輯並不影響后續的方法執行,所以這個接口實現就是為了獲取Request中的信息,或者預設一些參數供后續流程使用。
  3. HandlerInterceptor的功能更強大也更基礎,可以在preHandle方法中就直接拒絕請求進入controller方法。

二、自定義攔截器配置方法

  1. 在sping的xml配置中可以用<mvc:interceptors>和<mvc:interceptor>來配置攔截器類(實現HandlerInterceptorAdapter)
  2. 在javaConfig中配置通過WebMvcConfiguration的實現類配置攔截器類(實現HandlerInterceptorAdapter)

2.1、javaconfig中配置SpringMVC示例

1、新建一個springboot項目auth-demo2

2、權限校驗相關的注解

package com.dxz.authdemo2.web.auth;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
    /** 檢查項枚舉 */
    PermissionEnum[] permissionTypes() default {};

    /** 檢查項關系 */
    RelationEnum relation() default RelationEnum.OR;
}

package com.dxz.authdemo2.web.auth;

import java.io.PrintWriter;
import java.lang.annotation.Annotation;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

/**
 * 權限檢查攔截器 
 */
@Component
public class PermissionCheckInterceptor extends HandlerInterceptorAdapter {  
    /** 權限檢查服務 */
    @Autowired
    private PermissionCheckProcessor permissionCheckProcessor;  
    @Override  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
        //Class<?> clazz = handler.getClass();  
        Class<?> clazz = ((HandlerMethod)handler).getBeanType();
        System.out.println("PermissionCheckInterceptor.preHandle()" + clazz);
        for(Annotation a : clazz.getAnnotations()){
            System.out.println(a);
        }
        if (clazz.isAnnotationPresent(Permission.class)) { 
            Permission permission = (Permission) clazz.getAnnotation(Permission.class);  
            return permissionCheckProcessor.process(permission, request, response);  
        }  
        return true;  
    }  
    
    public boolean preHandle2(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 
        System.out.println("SecurityInterceptor:"+request.getContextPath()+","+request.getRequestURI()+","+request.getMethod());
        HttpSession session = request.getSession();
        if (session.getAttribute("uid") == null) {
            System.out.println("AuthorizationException:未登錄!"+request.getMethod());
            if("POST".equalsIgnoreCase(request.getMethod())){
                response.setContentType("text/html; charset=utf-8");  
                PrintWriter out = response.getWriter();   
                out.write("未登錄!");
                out.flush();
                out.close();
            }else{
                response.sendRedirect(request.getContextPath()+"/login"); 
            }
            return false;
        } else {
            return true;
        } 
    }  
}  

package com.dxz.authdemo2.web.auth;

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

import org.springframework.stereotype.Component;
@Component
public class PermissionCheckProcessor {
    public boolean process(Permission permission, HttpServletRequest request, HttpServletResponse response) {
        PermissionEnum[] permissionTypes = permission.permissionTypes();
        try {
            String uid = request.getParameter("uid");
            if ("duanxz".equals(uid)) {
                System.out.println("認證成功");
                return true;
            } else {
                System.out.println("認證失敗");
                return false;    
            }
        } catch (Exception e) {
            return false;
        }
    }
}

package com.dxz.authdemo2.web.auth;

public enum PermissionEnum {
    DEVELOPER_VALID, DEVELOPER_FREEZE;
}

package com.dxz.authdemo2.web.auth;

public enum RelationEnum {
    OR, AND;
}

3、SpringMVC攔截器配置

package com.dxz.authdemo2.web.auth;

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.WebMvcConfigurerAdapter;

@Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {

    @Autowired
    PermissionCheckInterceptor permissionCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // addPathPatterns 用於添加攔截規則
        // excludePathPatterns 用戶排除攔截
        // 映射為 user 的控制器下的所有映射
        registry.addInterceptor(permissionCheckInterceptor).addPathPatterns("/admin/*").excludePathPatterns("/index", "/");
        super.addInterceptors(registry);
    }
}

4、測試controller

package com.dxz.authdemo2.web;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

import com.dxz.authdemo2.web.auth.Permission;
import com.dxz.authdemo2.web.auth.PermissionEnum;

@Controller  
@RequestMapping("/admin")  
@Permission(permissionTypes = { PermissionEnum.DEVELOPER_VALID })  
public class AppDetailController {  
    @RequestMapping(value="/appDetail", method = RequestMethod.GET)  
    public String doGet(ModelMap modelMap, HttpServletRequest httpServletRequest) {  
        //1. 業務操作,此處省略  
        System.out.println("appDetail.htm 處理中...");
        return "appDetail";
    }
}  
  
package com.dxz.authdemo2.web;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.dxz.authdemo2.web.auth.Permission;
import com.dxz.authdemo2.web.auth.PermissionEnum;

@Controller  
@RequestMapping("index")  
public class IndexController {  
    @RequestMapping(method = RequestMethod.GET)  
    public void doGet(ModelMap modelMap, HttpServletRequest httpServletRequest) {  
        System.out.println("index");
    }  
}  

cotroller中的jsp文件appDetail.jsp

<html>
<h1>appDetail</h1>
</html>

啟動類:

package com.dxz.authdemo2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@EnableWebMvc
@EnableAutoConfiguration
@SpringBootApplication
public class AuthDemo2Application {

    public static void main(String[] args) {
        SpringApplication.run(AuthDemo2Application.class, args);
    }

    // 配置JSP視圖解析器
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver; }
}

結果:

訪問:http://localhost:8080/admin/appDetail?uid=duanxz2

訪問:http://localhost:8080/admin/appDetail?uid=duanxz

 

2.2、xml中配置SpringMVC示例

首先在springmvc.xml中加入自己定義的攔截器我的實現邏輯PermissionCheckInterceptor,如下:

<!--配置攔截器, 多個攔截器,順序執行 -->  
<mvc:interceptors>    
    <mvc:interceptor>    
        <!-- 匹配的是url路徑, 如果不配置或/**,將攔截所有的Controller -->  
        <mvc:mapping path="/" />  
        <mvc:mapping path="/user/**" />  
        <mvc:mapping path="/test/**" />  
        <bean class="com.dxz.authdemo2.web.auth.PermissionCheckInterceptor"></bean>    
    </mvc:interceptor>  
    <!-- 當設置多個攔截器時,先按順序調用preHandle方法,然后逆序調用每個攔截器的postHandle和afterCompletion方法 -->  
</mvc:interceptors> 

 

三、體驗Spring MVC的異步模式(Callable、WebAsyncTask、DeferredResult)

Spring MVC的同步模式

要知道什么是異步模式,就先要知道什么是同步模式。

瀏覽器發起請求,Web服務器開一個線程處理(請求處理線程),處理完把處理結果返回瀏覽器。這就是同步模式。絕大多數Web服務器都如此般處理。這里面有幾個關鍵的點:簡單示例圖如下

此處需要明晰一個概念:比如tomcat,它既是一個web服務器,同時它也是個servlet后端容器(調java后端服務),所以要區分清楚這兩個概念。請求處理線程是有限的,寶貴的資源~(注意它和處理線程的區別)

  1. 請求發起者發起一個request,然后會一直等待一個response,這期間它是阻塞的
  2. 請求處理線程會在Call了之后等待Return,自身處於阻塞狀態(這個很關鍵)
  3. 然后都等待return,知道處理線程全部完事后返回了,然后把response反給調用者就算全部結束了
問題在哪里?

Tomcat等應用服務器的連接線程池實際上是有限制的;每一個連接請求都會耗掉線程池的一個連接數;如果某些耗時很長的操作,如對大量數據的查詢操作、調用外部系統提供的服務以及一些IO密集型操作等,會占用連接很長時間,這個時候這個連接就無法被釋放而被其它請求重用。如果連接占用過多,服務器就很可能無法及時響應每個請求;極端情況下如果將線程池中的所有連接耗盡,服務器將長時間無法向外提供服務!

Spring MVC異步模式Demo Show

Spring MVC3.2之后支持異步請求,能夠在controller中返回一個Callable或者DeferredResult。由於Spring MVC的良好封裝,異步功能使用起來出奇的簡單。

Callable案例:
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/async/controller")
public class AsyncHelloController {

    @ResponseBody
    @GetMapping("/hello")
    public Callable<String> helloGet() throws Exception {
        System.out.println(Thread.currentThread().getName() + " 主線程start");

        Callable<String> callable = () -> {
            System.out.println(Thread.currentThread().getName() + " 子線程start");
            TimeUnit.SECONDS.sleep(5); // 模擬處理業務邏輯,花費了5秒鍾
            System.out.println(Thread.currentThread().getName() + " 子線程end");

            // 這里稍微小細節一下:最終返回的不是Callable對象,而是它里面的內容
            return "hello world";
        };

        System.out.println(Thread.currentThread().getName() + " 主線程end");
        return callable;
    }
}

 

輸出:

http-apr-8080-exec-3 主線程start http-apr-8080-exec-3 主線程end MvcAsync1 子線程start MvcAsync1 子線程end

先明細兩個概念:

  1. 請求處理線程:處理線程 屬於 web 服務器線程,負責 處理用戶請求,采用 線程池 管理。
  2. 異步線程:異步線程 屬於 用戶自定義的線程,也可采用 線程池管理。

前端頁面等待5秒出現結果,如下:

 

注意:異步模式對前端來說,是無感知的,這是后端的一種技術。所以這個和我們自己開啟一個線程處理,立馬返回給前端是有非常大的不同的,需要注意~

由此我們可以看出,主線程早早就結束了(需要注意,此時還並沒有把response返回的,此處一定要注意),真正干事的是子線程(交給TaskExecutor去處理的,后續分析過程中可以看到),它的大致的一個處理流程圖可以如下:

 

這里能夠很直接的看出:我們很大程度上提高了我們請求處理線程的利用率,從而肯定就提高了我們系統的吞吐量。

異步模式處理步驟概述如下:
  1. 當Controller返回值是Callable的時候
  2. Spring就會將Callable交給TaskExecutor去處理(一個隔離的線程池)
  3. 與此同時將DispatcherServlet里的攔截器、Filter等等都馬上退出主線程,但是response仍然保持打開的狀態
  4. Callable線程處理完成后,Spring MVC將請求重新派發給容器**(注意這里的重新派發,和后面講的攔截器密切相關)**
  5. 根據Callabel返回結果,繼續處理(比如參數綁定、視圖解析等等就和之前一樣了)~~~

Spring官方解釋如下截圖:

WebAsyncTask案例:

官方有這么一句話,截圖給你:

如果我們需要超時處理的回調或者錯誤處理的回調,我們可以使用WebAsyncTask代替Callable

實際使用中,我並不建議直接使用Callable ,而是使用Spring提供的WebAsyncTask 代替,它包裝了Callable,功能更強大些

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.WebAsyncTask;

@Controller
@RequestMapping("/async/controller")
public class AsyncHelloController {

    @ResponseBody
    @GetMapping("/hello")
    public WebAsyncTask<String> helloGet() throws Exception {
        System.out.println(Thread.currentThread().getName() + " 主線程start");

        Callable<String> callable = () -> {
            System.out.println(Thread.currentThread().getName() + " 子線程start");
            TimeUnit.SECONDS.sleep(5); // 模擬處理業務邏輯,話費了5秒鍾
            System.out.println(Thread.currentThread().getName() + " 子線程end");

            return "hello world";
        };

        // 采用WebAsyncTask 返回 這樣可以處理超時和錯誤 同時也可以指定使用的Excutor名稱
        WebAsyncTask<String> webAsyncTask = new WebAsyncTask<>(3000, callable);
        // 注意:onCompletion表示完成,不管你是否超時、是否拋出異常,這個函數都會執行的
        webAsyncTask.onCompletion(() -> System.out.println("程序[正常執行]完成的回調"));

        // 這兩個返回的內容,最終都會放進response里面去===========
        webAsyncTask.onTimeout(() -> "程序[超時]的回調");
        // 備注:這個是Spring5新增的
        // webAsyncTask.onError(() -> "程序[出現異常]的回調");

        System.out.println(Thread.currentThread().getName() + " 主線程end");
        return webAsyncTask;
    }
}

如上,由於我們設置了超時時間為3000ms,而業務處理是5s,所以會執行onTimeout這個回調函數。因此頁面是會顯示“程序[超時]的回調”這幾個字。其執行的過程同Callback。

下面我們簡單看看WebAsyncTask的源碼,非常簡單,就是個包裝:

public class WebAsyncTask<V> implements BeanFactoryAware {
    
    // 正常執行的函數(通過WebAsyncTask的構造函數可以傳進來)
    private final Callable<V> callable;
    // 處理超時時間(ms),可通過構造函數指定,也可以不指定(不會有超時處理)
    private Long timeout;
    // 執行任務的執行器。可以構造函數設置進來,手動指定。
    private AsyncTaskExecutor executor;
    // 若設置了,會根據此名稱去IoC容器里找這個Bean (和上面二選一)  
    // 若傳了executorName,請務必調用set方法設置beanFactory
    private String executorName;
    private BeanFactory beanFactory;

    // 超時的回調
    private Callable<V> timeoutCallback;
    // 發生錯誤的回調
    private Callable<V> errorCallback;
    // 完成的回調(不管超時還是錯誤都會執行)
    private Runnable completionCallback;

    ...
    
    // 這是獲取執行器的邏輯
    @Nullable
    public AsyncTaskExecutor getExecutor() {
        if (this.executor != null) {
            return this.executor;
        } else if (this.executorName != null) {
            Assert.state(this.beanFactory != null, "BeanFactory is required to look up an executor bean by name");
            return this.beanFactory.getBean(this.executorName, AsyncTaskExecutor.class);
        } else {
            return null;
        }
    }


    public void onTimeout(Callable<V> callback) {
        this.timeoutCallback = callback;
    }
    public void onError(Callable<V> callback) {
        this.errorCallback = callback;
    }
    public void onCompletion(Runnable callback) {
        this.completionCallback = callback;
    }

    // 最終執行超時回調、錯誤回調、完成回調都是通過這個攔截器實現的
    CallableProcessingInterceptor getInterceptor() {
        return new CallableProcessingInterceptor() {
            @Override
            public <T> Object handleTimeout(NativeWebRequest request, Callable<T> task) throws Exception {
                return (timeoutCallback != null ? timeoutCallback.call() : CallableProcessingInterceptor.RESULT_NONE);
            }
            @Override
            public <T> Object handleError(NativeWebRequest request, Callable<T> task, Throwable t) throws Exception {
                return (errorCallback != null ? errorCallback.call() : CallableProcessingInterceptor.RESULT_NONE);
            }
            @Override
            public <T> void afterCompletion(NativeWebRequest request, Callable<T> task) throws Exception {
                if (completionCallback != null) {
                    completionCallback.run();
                }
            }
        };
    }

}

 

WebAsyncTask 的異步編程 API。相比於 @Async 注解,WebAsyncTask 提供更加健全的 超時處理 和 異常處理 支持。但是@Async也有更優秀的地方,就是他不僅僅能用於controller中~~~~(任意地方)

DeferredResult案例:

DeferredResult使用方式與Callable類似,但在返回結果上不一樣,它返回的時候實際結果可能沒有生成,實際的結果可能會在另外的線程里面設置到DeferredResult中去。

這個特性非常非常的重要,對后面實現復雜的功能(比如服務端推技術、訂單過期時間處理、長輪詢、模擬MQ的功能等等高級應用

官方給的Demo如下:

 

 

自己寫個非常粗糙的Demo:

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.DeferredResult;

@Controller
@RequestMapping("/async/controller")
public class AsyncHelloController {

    private List<DeferredResult<String>> deferredResultList = new ArrayList<>();

    @ResponseBody
    @GetMapping("/hello")
    public DeferredResult<String> helloGet() throws Exception {
        DeferredResult<String> deferredResult = new DeferredResult<>();

        //先存起來,等待觸發
        deferredResultList.add(deferredResult);
        return deferredResult;
    }

    @ResponseBody
    @GetMapping("/setHelloToAll")
    public void helloSet() throws Exception {
        // 讓所有hold住的請求給與響應
        deferredResultList.forEach(d -> d.setResult("say hello to all"));
    }
}

我們第一個請求/hello,會先deferredResult存起來,然后前端頁面是一直等待(轉圈狀態)的。知道我發第二個請求:setHelloToAll,所有的相關頁面才會有響應~~

執行過程

官方:

  1. controller 返回一個DeferredResult,我們把它保存到內存里或者List里面(供后續訪問)
  2. Spring MVC調用request.startAsync(),開啟異步處理
  3. 與此同時將DispatcherServlet里的攔截器、Filter等等都馬上退出主線程,但是response仍然保持打開的狀態
  4. 應用通過另外一個線程(可能是MQ消息、定時任務等)給DeferredResult set值。然后Spring MVC會把這個請求再次派發給servlet容器
  5. DispatcherServlet再次被調用,然后處理后續的標准流程

簡單看看源碼:

public class DeferredResult<T> {

    private static final Object RESULT_NONE = new Object()

    
    // 超時時間(ms) 可以不配置
    @Nullable
    private final Long timeout;
    // 相當於超時的話的,傳給回調函數的值
    private final Object timeoutResult;

    // 這三種回調也都是支持的
    private Runnable timeoutCallback;
    private Consumer<Throwable> errorCallback;
    private Runnable completionCallback;


    // 這個比較強大,就是能把我們結果再交給這個自定義的函數處理了 他是個@FunctionalInterface
    private DeferredResultHandler resultHandler;

    private volatile Object result = RESULT_NONE;
    private volatile boolean expired = false;


    // 判斷這個DeferredResult是否已經被set過了(被set過的對象,就可以移除了嘛)
    // 如果expired表示已經過期了你還沒set,也是返回false的
    // Spring4.0之后提供的
    public final boolean isSetOrExpired() {
        return (this.result != RESULT_NONE || this.expired);
    }

    // 沒有isSetOrExpired 強大,建議使用上面那個
    public boolean hasResult() {
        return (this.result != RESULT_NONE);
    }

    // 還可以獲得set進去的結果
    @Nullable
    public Object getResult() {
        Object resultToCheck = this.result;
        return (resultToCheck != RESULT_NONE ? resultToCheck : null);
    }


    public void onTimeout(Runnable callback) {
        this.timeoutCallback = callback;
    }
    public void onError(Consumer<Throwable> callback) {
        this.errorCallback = callback;
    }
    public void onCompletion(Runnable callback) {
        this.completionCallback = callback;
    }

    
    // 如果你的result還需要處理,可以這是一個resultHandler,會對你設置進去的結果進行處理
    public final void setResultHandler(DeferredResultHandler resultHandler) {
        Assert.notNull(resultHandler, "DeferredResultHandler is required");
        // Immediate expiration check outside of the result lock
        if (this.expired) {
            return;
        }
        Object resultToHandle;
        synchronized (this) {
            // Got the lock in the meantime: double-check expiration status
            if (this.expired) {
                return;
            }
            resultToHandle = this.result;
            if (resultToHandle == RESULT_NONE) {
                // No result yet: store handler for processing once it comes in
                this.resultHandler = resultHandler;
                return;
            }
        }
        try {
            resultHandler.handleResult(resultToHandle);
        } catch (Throwable ex) {
            logger.debug("Failed to handle existing result", ex);
        }
    }

    // 我們發現,這里調用是private方法setResultInternal,我們設置進來的結果result,會經過它的處理
    // 而它的處理邏輯也很簡單,如果我們提供了resultHandler,它會把這個值進一步的交給我們的resultHandler處理
    // 若我們沒有提供此resultHandler,那就保存下這個result即可
    public boolean setResult(T result) {
        return setResultInternal(result);
    }

    private boolean setResultInternal(Object result) {
        // Immediate expiration check outside of the result lock
        if (isSetOrExpired()) {
            return false;
        }
        DeferredResultHandler resultHandlerToUse;
        synchronized (this) {
            // Got the lock in the meantime: double-check expiration status
            if (isSetOrExpired()) {
                return false;
            }
            // At this point, we got a new result to process
            this.result = result;
            resultHandlerToUse = this.resultHandler;
            if (resultHandlerToUse == null) {
                this.resultHandler = null;
            }
        }
        resultHandlerToUse.handleResult(result);
        return true;
    }

    // 發生錯誤了,也可以設置一個值。這個result會被記下來,當作result
    // 注意這個和setResult的唯一區別,這里入參是Object類型,而setResult只能set規定的指定類型
    // 定義成Obj是有原因的:因為我們一般會把Exception等異常對象放進來。。。
    public boolean setErrorResult(Object result) {
        return setResultInternal(result);
    }

    // 攔截器 注意最終finally里面,都可能會調用我們的自己的處理器resultHandler(若存在的話)
    // afterCompletion不會調用resultHandler~~~~~~~~~~~~~
    final DeferredResultProcessingInterceptor getInterceptor() {
        return new DeferredResultProcessingInterceptor() {
            @Override
            public <S> boolean handleTimeout(NativeWebRequest request, DeferredResult<S> deferredResult) {
                boolean continueProcessing = true;
                try {
                    if (timeoutCallback != null) {
                        timeoutCallback.run();
                    }
                } finally {
                    if (timeoutResult != RESULT_NONE) {
                        continueProcessing = false;
                        try {
                            setResultInternal(timeoutResult);
                        } catch (Throwable ex) {
                            logger.debug("Failed to handle timeout result", ex);
                        }
                    }
                }
                return continueProcessing;
            }
            @Override
            public <S> boolean handleError(NativeWebRequest request, DeferredResult<S> deferredResult, Throwable t) {
                try {
                    if (errorCallback != null) {
                        errorCallback.accept(t);
                    }
                } finally {
                    try {
                        setResultInternal(t);
                    } catch (Throwable ex) {
                        logger.debug("Failed to handle error result", ex);
                    }
                }
                return false;
            }
            @Override
            public <S> void afterCompletion(NativeWebRequest request, DeferredResult<S> deferredResult) {
                expired = true;
                if (completionCallback != null) {
                    completionCallback.run();
                }
            }
        };
    }

    // 內部函數式接口 DeferredResultHandler
    @FunctionalInterface
    public interface DeferredResultHandler {
        void handleResult(Object result);
    }

}

DeferredResult的超時處理,采用委托機制,也就是在實例DeferredResult時給予一個超時時長(毫秒),同時在onTimeout中委托(傳入)一個新的處理線程(我們可以認為是超時線程);當超時時間到來,DeferredResult啟動超時線程,超時線程處理業務,封裝返回數據,給DeferredResult賦值(正確返回的或錯誤返回的)

Spring MVC異步模式中使用Filter和HandlerInterceptor

看到上面的異步訪問,不免我們會新生懷疑,若是普通的攔截器HandlerInterceptor,還生效嗎?若生效,效果是怎么樣的,現在我們直接看一下吧:(備注:我以上面Callable的Demo為示例)

Filter
// 注意,這里必須開啟異步支持asyncSupported = true,否則報錯:Async support must be enabled on a servlet and for all filters involved in async request processing
@WebFilter(urlPatterns = "/*", asyncSupported = true)
public class HelloFilter extends OncePerRequestFilter {

    @Override
    protected void initFilterBean() throws ServletException {
        System.out.println("Filter初始化...");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        System.out.println(Thread.currentThread().getName() + "--->" + request.getRequestURI());
        filterChain.doFilter(request, response);
    }

}

 

輸出:

http-apr-8080-exec-3--->/demowar_war/async/controller/hello http-apr-8080-exec-3 主線程start http-apr-8080-exec-3 主線程end MvcAsync1 子子子線程start MvcAsync1 子子子線程end

由此可以看出,異步上下文,Filter還是只會被執行一次攔截的,符合我們的預期,所以沒什么毛病。

HandlerInterceptor
public class HelloInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(Thread.currentThread().getName() + "---preHandle-->" + request.getRequestURI());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println(Thread.currentThread().getName() + "---postHandle-->" + request.getRequestURI());
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println(Thread.currentThread().getName() + "---postHandle-->" + request.getRequestURI());
    }
}

// 注冊攔截器
@Configuration
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // /**攔截所有請求
        registry.addInterceptor(new HelloInterceptor()).addPathPatterns("/**");
    }
}

輸出:

http-apr-8080-exec-3--->/demowar_war/async/controller/hello http-apr-8080-exec-3---preHandle-->/demowar_war/async/controller/hello http-apr-8080-exec-3 主線程start http-apr-8080-exec-3 主線程end MvcAsync1 子子子線程start MvcAsync1 子子子線程end // 注意 子子子線程處理結束后,再一次觸發了preHandle===== // 此處還要一個細節:這里面的線程既不是子線程,也不是上面的線程 而是新開了一個線程~~~ http-apr-8080-exec-5---preHandle-->/demowar_war/async/controller/hello http-apr-8080-exec-5---postHandle-->/demowar_war/async/controller/hello http-apr-8080-exec-5---afterCompletion-->/demowar_war/async/controller/hello

從上面可以看出,如果我們就是普通的Spring MVC的攔截器,preHandler會執行兩次,這也符合我們上面分析的處理步驟。所以我們在書寫preHandler的時候,一定要特別的注意,要讓preHandler即使執行多次,也不要受到影響(冪等)

異步攔截器 AsyncHandlerInterceptor、CallableProcessingInterceptor、DeferredResultProcessingInterceptor

Spring MVC給提供了異步攔截器,能讓我們更深入的參與進去異步request的生命周期里面去。其中最為常用的為:AsyncHandlerInterceptor

public class AsyncHelloInterceptor implements AsyncHandlerInterceptor {

    // 這是Spring3.2提供的方法,專門攔截異步請求的方式
    @Override
    public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(Thread.currentThread().getName() + "---afterConcurrentHandlingStarted-->" + request.getRequestURI());
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(Thread.currentThread().getName() + "---preHandle-->" + request.getRequestURI());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println(Thread.currentThread().getName() + "---postHandle-->" + request.getRequestURI());
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println(Thread.currentThread().getName() + "---afterCompletion-->" + request.getRequestURI());
    }
}

輸出:

http-apr-8080-exec-3---preHandle-->/demowar_war/async/controller/hello http-apr-8080-exec-3 主線程start http-apr-8080-exec-3 主線程end // 這里發現,它在主線程結束后,子線程開始之前執行的(線程號還是同一個哦~) http-apr-8080-exec-3---afterConcurrentHandlingStarted-->/demowar_war/async/controller/hello MvcAsync1 子子子線程start MvcAsync1 子子子線程end http-apr-8080-exec-6---preHandle-->/demowar_war/async/controller/hello http-apr-8080-exec-6---postHandle-->/demowar_war/async/controller/hello http-apr-8080-exec-6---afterCompletion-->/demowar_war/async/controller/hello

AsyncHandlerInterceptor提供了一個afterConcurrentHandlingStarted()方法, 這個方法會在Controller方法異步執行時開始執行, 而Interceptor的postHandle方法則是需要等到Controller的異步執行完才能執行

(比如我們用DeferredResult的話,afterConcurrentHandlingStarted是在return的之后執行,而postHandle()是執行.setResult()之后執行)

需要說明的是:如果我們不是異步請求,afterConcurrentHandlingStarted是不會執行的。所以我們可以把它當做加強版的HandlerInterceptor來用。平時我們若要使用攔截器,建議使用它。(Spring5,JDK8以后,很多的xxxAdapter都沒啥用了,直接implements接口就成~)

同樣可以注冊CallableProcessingInterceptor或者一個DeferredResultProcessingInterceptor用於更深度的集成異步request的生命周期

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // 注冊異步的攔截器、默認的超時時間、任務處理器TaskExecutor等等
        //configurer.registerCallableInterceptors();
        //configurer.registerDeferredResultInterceptors();
        //configurer.setDefaultTimeout();
        //configurer.setTaskExecutor();
    }

 

只是一般來說,我們並不需要注冊這種精細的攔截器,絕大多數情況下,使用AsyncHandlerInterceptor是夠了的。 (Spring MVC的很多默認設置,請參考WebMvcConfigurationSupport

區別使用

我覺得最主要的區別是:DeferredResult需要自己用線程來處理結果setResult,而Callable的話不需要我們來維護一個結果處理線程。 總體來說,Callable的話更為簡單,同樣的也是因為簡單,靈活性不夠; 相對地,DeferredResult更為復雜一些,但是又極大的靈活性,所以能實現非常多個性化的、復雜的功能,可以設計高級應用。

有些較常見的場景, Callable也並不能解決,比如說:我們訪問A接口,A接口調用三方的服務,服務回調(注意此處指的回調,不是返回值)B接口,這種情況就沒辦法使用Callable了,這個時候可以使用DeferredResult

使用原則:基本上在可以用Callable的時候,直接用Callable;而遇到Callable沒法解決的場景的時候,可以嘗試使用DeferredResult

這里所指的Callable包括WebAsyncTask

總結

在Reactive編程模型越來越流行的今天,多一點對異步編程模型(Spring MVC異步模式)的了解,可以更容易去接觸Spring5帶來的新特性—響應式編程。 同時,異步編程是我們高效利用系統資源,提高系統吞吐量,編寫高性能應用的必備技能。希望此篇文章能幫助到大家,運用到工作中~

然后,關於DeferredResult的高級使用場景,見下一篇博文:高級應用和源碼分析篇

 

四、spring boot 加入攔截器后swagger不能訪問問題

網上找的資料中大部分只說添加這個

// 注冊攔截器
@Configuration
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/user/login")
.excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
}

@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
//...

}

 參考:https://cloud.tencent.com/developer/article/1497804

 

  


免責聲明!

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



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