一、SpringMVC定義interceptor方式
在SpringMVC 中定義一個Interceptor是比較非常簡單,主要有兩種方式:
第一種:實現HandlerInterceptor 接口,或者是繼承實現了HandlerInterceptor 接口的類,例如HandlerInterceptorAdapter;
第二種:實現Spring的WebRequestInterceptor接口,或者是繼承實現了WebRequestInterceptor的類。
1.1、HandlerInterceptorAdapter
1.1.1、 HandlerInterceptor接口
SpringMVC的攔截器HandlerInterceptor對應提供了三個preHandle,postHandle,afterCompletion方法:
- boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handle)方法:該方法將在請求處理之前進行調用,只有該方法返回true,才會繼續執行后續的Interceptor和Controller,當返回值為true 時就會繼續調用下一個Interceptor的preHandle 方法,如果已經是最后一個Interceptor的時候就會是調用當前請求的Controller方法;
- void postHandle (HttpServletRequest request, HttpServletResponse response, Object handle, ModelAndView modelAndView)方法:該方法將在請求處理之后,DispatcherServlet進行視圖返回渲染之前進行調用,可以在這個方法中對Controller 處理之后的ModelAndView 對象進行操作。
- 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不同點:
- WebRequestInterceptor的入參WebRequest是包裝了HttpServletRequest 和HttpServletResponse的,通過WebRequest獲取Request中的信息更簡便。
- WebRequestInterceptor的preHandle是沒有返回值的,說明該方法中的邏輯並不影響后續的方法執行,所以這個接口實現就是為了獲取Request中的信息,或者預設一些參數供后續流程使用。
- HandlerInterceptor的功能更強大也更基礎,可以在preHandle方法中就直接拒絕請求進入controller方法。
二、自定義攔截器配置方法
- 在sping的xml配置中可以用<mvc:interceptors>和<mvc:interceptor>來配置攔截器類(實現HandlerInterceptorAdapter)
- 在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,如下:
三、體驗Spring MVC的異步模式(Callable、WebAsyncTask、DeferredResult)
Spring MVC的同步模式
要知道什么是異步模式,就先要知道什么是同步模式。
瀏覽器發起請求,Web服務器開一個線程處理(請求處理線程
),處理完把處理結果返回瀏覽器。這就是同步模式。絕大多數Web服務器都如此般處理。這里面有幾個關鍵的點:簡單示例圖如下

此處需要明晰一個概念:比如tomcat,它既是一個web服務器,同時它也是個servlet后端容器(調java后端服務),所以要區分清楚這兩個概念。請求處理線程是有限的,寶貴的資源~(注意它和處理線程的區別)
- 請求發起者發起一個request,然后會一直等待一個response,這期間它是阻塞的
- 請求處理線程會在Call了之后等待Return,
自身處於阻塞狀態
(這個很關鍵) - 然后都等待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
先明細兩個概念:
- 請求處理線程:處理線程 屬於 web 服務器線程,負責 處理用戶請求,采用 線程池 管理。
- 異步線程:異步線程 屬於 用戶自定義的線程,也可采用 線程池管理。
前端頁面等待5秒出現結果,如下:
注意:異步模式對前端來說,是無感知的,這是后端的一種技術。所以這個和我們自己開啟一個線程處理,立馬返回給前端是有非常大的不同的,需要注意~
由此我們可以看出,主線程早早就結束了(需要注意,此時還並沒有把response返回的,此處一定要注意),真正干事的是子線程(交給TaskExecutor
去處理的,后續分析過程中可以看到),它的大致的一個處理流程圖可以如下:

這里能夠很直接的看出:我們很大程度上提高了我們請求處理線程
的利用率,從而肯定就提高了我們系統的吞吐量。
異步模式處理步驟概述如下:
- 當Controller返回值是Callable的時候
- Spring就會將Callable交給TaskExecutor去處理(一個隔離的線程池)
- 與此同時將
DispatcherServlet
里的攔截器、Filter等等都馬上退出主線程,但是response仍然保持打開的狀態 - Callable線程處理完成后,Spring MVC將請求重新派發給容器**(注意這里的重新派發,和后面講的攔截器密切相關)**
- 根據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
,所有的相關頁面才會有響應~~
執行過程
官方:

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