微服務專題05-Spring WebFlux 運用

前言

前面的章節我們講了REST。本節,繼續微服務專題的內容分享,共計16小節,分別是:

本節內容重點爲:

  • WebFlux 核心組件:包括 HandlerMappingHandlerAdapter 以及 HandlerResultHandler
  • WebFlux 編程模型運用:介紹 Annotation 驅動以及函數聲明是編程模型的差異以及最佳實踐

回顧 Spring Web MVC

Spring Web MVC是基於Servlet API構建的原始Web框架,並從一開始就包含在Spring Framework中。正式名稱“ Spring Web MVC”來自其源模塊spring-webmvc的名稱, 但它通常被稱爲“ Spring MVC”。

DispatcherServlet特殊bean 的委託來處理請求並提供適當的響應。“特殊bean”是指實現WebFlux框架協定的Spring管理的Object實例。這些通常帶有內置合同,但是您可以自定義它們的屬性,擴展或替換它們。

實際上SpringMVC的很多特性與webflux相同,比如下表是 Spring 官網上列出了通過檢測到的特殊 bean DispatcherHandler:

Bean 的類型 說明
HandlerMapping 將請求與攔截器列表一起映射到處理程序,以 進行預處理和後期處理。映射基於某些標準,具體細節因HandlerMapping 實現而異。這兩個主要的HandlerMapping實現是RequestMappingHandlerMapping支持帶@RequestMapping註釋的方法,SimpleUrlHandlerMapping並且維護URI路徑模式向處理程序的顯式註冊。
HandlerAdapter 幫助DispatcherServlet調用映射到請求的處理程序,而不管實際如何調用該處理程序。例如,調用帶註釋的控制器需要解析註釋。主要目的HandlerAdapter是保護DispatcherServlet這些細節。
HandlerExceptionResolver 解決異常的策略,可能將異常映射到處理程序,HTML錯誤視圖或其他。
ViewResolver 解析從處理程序返回到實際的基於邏輯字符串的視圖名稱,View 以呈現給響應。
LocaleResolver, LocaleContextResolver 解決Locale一個客戶正在使用的並且可能是其時區的問題,以便能夠提供國際化的視圖。
ThemeResolver 解決您的Web應用程序可以使用的主題,例如,提供個性化的佈局。
MultipartResolver 藉助多部分解析庫來解析多部分請求的抽象(例如,瀏覽器表單文件上傳)。
FlashMapManager 存儲和檢索“輸入”和“輸出” FlashMap,它們通常用於通過重定向將屬性從一個請求傳遞到另一個請求。

除了上述處理類之外,我們再看類 HandlerInterceptor 的源碼,共計三個方法 :

在這裏插入圖片描述
從源碼上不難看出,分爲 前置、後置處理、完成階段(異常處理):

  • preHandle 前置
  • postHandle 後置
  • afterCompletion 完成:類似於 finally 效果。這和 java.util.concurrent.CompletableFuture#whenComplete方法是不是有異曲同工之妙。

org.springframework.web.servlet.HandlerMapping

接下來說一下 HandlerMapping 相關問題:
在這裏插入圖片描述
(上面截圖的的spring版本爲spring-5.0.7-RELEASE) 首先我們看HandlerMapping 的實現類 AbstractHandlerMapping
通常來講 HandlerMapping 的實現類是包含 HandlerInterceptor 集合的。那麼在 AbstractHandlerMapping 是否也存在呢,答案是肯定的,請見源碼:
在這裏插入圖片描述
包含 HandlerInterceptor 集合 的意義主要在於:

  • 攔截鏈條
  • 各司其職

既然是各司其職,就會引發順序問題,誰先執行?誰後執行?

這裏會引出一個概念------- 責任鏈模式

責任鏈通常有兩種類型,即過濾類型和攔截類型。

Q:責任鏈和filter有什麼區別呢?

A:實際上 HandlerInterceptor 就是採用返回值進行流程處理,而 Filter 採用 FilterChain 進行流程處理。 Filter 優先級天生比servlet高,但是 Filter 的最終節點 Servlet。Filter通過filter.chain進入鏈條的下一個環節,在服務器啓動階段動態組合鏈條,符合責任鏈設計模式(動態調用,組合依賴於配置)。

迴歸主線,繼續分析 HandlerMapping的實現類 - AbstractHandlerMapping

在SpringMVC中,我們知道 DispatcherServlet 關聯多個 HandlerMapping,類似於笛卡爾積,即:

  • DispatcherServletHandlerMapping = 1 : N
  • HandlerMappingHandlerInterceptor = 1 : N

Q: DispatcherServlet 作爲 HandlerMapping 一種,那麼問題來了,多個 HandlerMapping 誰能被 DispatcherServlet 選擇?

重點內容

這裏我先猜想,上面截圖的源碼 AbstractHandlerMapping 實現了 Ordered 接口參考順序,這是一種可以排序的可能,另外呢,如果 HandlerMapping 被請求規則匹配了是否可以呢?

猜想驗證過程:
首先看一下 org.springframework.web.servlet.DispatcherServlet#doDispatch 方法:
在這裏插入圖片描述
進去看 org.springframework.web.servlet.DispatcherServlet#getHandler:
這裏很明顯是通過循環將所有的handlerMappings 返回出去
在這裏插入圖片描述
所以爲了看到handlerMappings 賦值的過程,我這裏直接通過idea工具右鍵選中handlerMappings選擇find usages菜單,查看讀寫的位置:
在這裏插入圖片描述
通過工具,發現handlerMappings的寫入都集中在類 DispatcherServlet 上,所以直接跟進:
在這裏插入圖片描述
來到方法:org.springframework.web.servlet.DispatcherServlet#initHandlerMappings

initHandlerMappings 方法,主要是初始化HandlerMappings的結果,這裏首先將所有匹配的handlerMapping放入了Map<String,HandlerMapping> 。

注意 initHandlerMappings 方法中這段代碼:

	this.handlerMappings = new ArrayList<>(matchingBeans.values());

爲什麼不能將匹配的 handlerMappings直接賦值給當前this變量裏呢,而是新創建一個集合再賦值?

重點內容

  • 首先,Map的values()方法返回的就是Collection

我們看說明,返回值是:a collection view of the values contained in this map。注意是View,也就是說,你只能看看values(這裏實際上是HandlerMappings),數據是不可變的。
在這裏插入圖片描述

  • 其次,通過new了一個list數組以後,就可以動態的去寫數據,而能寫數據就可以對HandlerMappings進行排序了。所以緊接着代碼如下:
	// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
			Map<String, HandlerMapping> matchingBeans =
					BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
			if (!matchingBeans.isEmpty()) {
				this.handlerMappings = new ArrayList<>(matchingBeans.values());
				// We keep HandlerMappings in sorted order.
				AnnotationAwareOrderComparator.sort(this.handlerMappings);
			}

接下來看看 sort 方法如何進行排序:
在這裏插入圖片描述

我們看 AnnotationAwareOrderComparator 的 API 發現如何排序的呢,有兩種方式,分別是:

  • 通過某個Bean實現Ordered接口,我們這裏明顯是 AbstractHandlerMapping 去實現 Ordered接口,前面已經兩次提到了!

在這裏插入圖片描述
當實現了Ordered以後,可以通過 getOrder方法返回順序,注意源碼所示:將HIGHEST_PRECEDENCE賦值給Integer的最小值;將LOWEST_PRECEDENCE賦值給Integer的最大值。所以總結:在類Order的規則裏,順序越大,優先級越小,順序越小,優先級越大

  • 通過實現 @Order 註解

在Bean上注入 @Order 註解,這裏沒有應用,略講。

Tips:看源碼就是要考慮很多細節性的問題,一個優秀的架構師,往往能夠做到通用性好,並且思考的更深刻

回過頭來,繼續看主線內容:
前面提到: DispatcherServlet 可以關聯多個 HandlerMappingHandlerMapping 也可以關聯多個 HandlerInterceptor,這種 1 對 N 的特性其實是有條件的!是需要經過篩選的,請看AbstractHandlerMapping源碼:

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		
		Object handler = getHandlerInternal(request);
		if (handler == null) {
			handler = getDefaultHandler();
		}
		if (handler == null) {
			return null;
		}
		// Bean name or resolved handler?
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = obtainApplicationContext().getBean(handlerName);
		}
		//核心代碼
		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
		if (CorsUtils.isCorsRequest(request)) {
			CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}
		return executionChain;
	}

此方法前面進行參數判斷,尾部適配瀏覽器標準(跨域訪問),重要看中間代碼

直接看方法getHandlerExecutionChain:
在這裏插入圖片描述
注意這裏:

String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);

此方法主要對HTTP請求的 URL 地址做處理,舉個栗子:請求地址是 http://www.baidu.com/abc/def,那麼 轉化後剩下 /abc/def,其實就是相對路徑。

接下來對於HandlerInterceptor 集合遍歷,然後篩選,在本節開頭就提到的HandlerInterceptor,它和MappedInterceptor有什麼區別呢?

HandlerInterceptor可以匹配請求,而MappedInterceptor則不可以。

正因爲HandlerInterceptor可以匹配請求,源碼接着往下看,經過chain添加Interceptor纔是被過濾的!

那麼chain是怎麼添加的Interceptor的呢?在回答這個問題之前,我們播放一個小插曲!

在前面的劇情裏,AbstractHandlerMapping 重寫了HandlerMapping接口的 getHandler, 返回值是 HandlerExecutionChain,現在我們看此返回值類:
在這裏插入圖片描述

這裏面有兩個集合:

  • HandlerInterceptor[]
  • List< HandlerInterceptor >

爲什麼要用兩個屬性相同的集合來表示?

插曲播放完畢,讓我們回到剛纔的問題上,剛纔說到的1對N問題,我們說是要經過篩選的!是通過chain去添加Interceptor的,那麼怎麼添加的呢?

我們將

chain.addInterceptor(mappedInterceptor.getInterceptor());

這段代碼點擊實現裏看看:

	private List<HandlerInterceptor> initInterceptorList() {
		if (this.interceptorList == null) {
			this.interceptorList = new ArrayList<>();
			if (this.interceptors != null) {
				// An interceptor array specified through the constructor
				CollectionUtils.mergeArrayIntoCollection(this.interceptors, this.interceptorList);
			}
		}
		this.interceptors = null;
		return this.interceptorList;
	}

這裏的實現是通過interceptors數組作爲臨時變量存儲後merge到interceptorList集合裏,然後清空interceptors數組。interceptors數組起到臨時變量的作用。

問題來了,爲什麼要臨時存儲interceptor 集合呢?我們可以看看此集合在哪裏被用到了便一目瞭然

老辦法,對準臨時變量interceptors就是操作find usages:
在這裏插入圖片描述
我們找到這裏:
在這裏插入圖片描述
list是可以變化的,而數組是沒法變化的,寫兩種集合,好處就在於:

  • 爲了防止串數據,改動一方不影響另一方。
  • 如果不改的話,考慮到框架前後版本不一致問題,某些版本訪問是數組,也可能是集合,一旦寫死就很麻煩~

最後我們再看類HandlerMapping 的 getHandler方法進行分析,getHandler的返回值是HandlerExecutionChain:

public class HandlerExecutionChain {

	private final Object handler;

	@Nullable
	private HandlerInterceptor[] interceptors;

	@Nullable
	private List<HandlerInterceptor> interceptorList;
    ...

前面對於兩個集合分析了一遍,那麼看看這個Object類型的handler指的又是什麼呢?

請參考本節開始的截圖的HandlerMapping類圖:我們根據類圖層級調用關係可以猜測到:

當一個request請求來臨時,一定是通過handlerMapping抽象實現類AbstractHandlerMapping或者CompositeHandlerMapping去處理請求返回handler的,那麼CompositeHandlerMapping實爲透傳,我們將重點放在AbstractHandlerMapping類的getHandler:

在這裏插入圖片描述
我們看着一段代碼:

Object handler = getHandlerInternal(request);

在這裏插入圖片描述
接着看getHandlerInternal:
在這裏插入圖片描述
後兩個方法實爲透傳,我們分別看前兩個實現類:

  • AbstractHandlerMethodMapping

在這裏插入圖片描述
AbstractHandlerMethodMapping 通過其實現類, RequestMappingInfoHandlerMapping,再調用實現類 RequestMappingHandlerMapping 返回 HandlerMethod

  • AbstractUrlHandlerMapping

在這裏插入圖片描述
AbstractUrlHandlerMapping 獲取 Handler(注意返回類型),在通過其實現類 SimpleUrlHandlerMapping 返回 Object

Tips: 通過這一小段分析,我們知道handler的類型是不固定的,通過SimpleUrlHandlerMapping 處理後返回的handler類型是Object,而通過RequestMappingHandlerMapping 返回的的handelr類型是 HandlerMethod

那麼 HandlerMethod 與 執行方法有什麼聯繫呢?

HandlerMethod 初始化過程

在這裏插入圖片描述

如果當 @Controller 方法上面標註了 @RequestMapping,而@RequestMapping 主要可以標識URI。比如@RequestMapping(“/cache”)。所以我們就可以大膽猜測是通過RequestMappingHandlerMapping 處理的。

Q:那麼源碼上對於 HandlerMethod 與 執行方法是怎麼處理的呢?

A:我們從 HandlerMethod 初始化的地方入手看 org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping#afterPropertiesSet:

在這裏插入圖片描述
afterPropertiesSet方法會在父類的Bean(RequestMappingHandlerMapping)初始化的時候調用。
我們看這裏:

if (beanType != null && isHandler(beanType)) {
					detectHandlerMethods(beanName);
				}

如果beanType 不爲空,並且符合isHandler條件的,才能夠進入裏面的方法,所以我們先看看,什麼是所謂的Handler呢?
在這裏插入圖片描述
就是說當前bean被標註爲@Controller或者@RequestMapping註解的纔會被掃描到。

那麼@RestController 以及@PostMapping等註解會被掃描到麼?答案是肯定的,還記得我們前面的小節講到的註解的派生性麼?可以簡單的理解爲子類與父類的關係,只是功能上等同哦,所以這裏掃描@Controller或者@RequestMapping的父類是可以將子類掃描進去的,框架本身也更好的在註解層面實現了擴展。

然後看方法detectHandlerMethods,根據字面意思是,探測所有的HandlerMethods,所以進一步查看:
在這裏插入圖片描述
此方法首先求Bean,然後把Bean裏的Method都執行一遍,註冊HandlerMethod,進去看註冊的過程:
在這裏插入圖片描述
透傳參數,繼續調用查看register方法:
在這裏插入圖片描述

由上述過程,可以總結一下HandlerMethod 初始化過程:

  1. Spring 應用上下文獲取所有的 Bean
  2. 篩選出標註 @Controller 或者 @RequestMapping 的 Bean
  3. 再次篩選標準 @RequestMapping 方法
  4. 將該 Bean 和對應 Method 合併成 HandlerMethod
  5. 存入HandlerMethod 集合

HandlerMethod 定位過程

我們知道當一個Request請求進來時,要先經過 DispatchServlet 的doDispatch方法:
在這裏插入圖片描述
如何獲取到的Handler呢?接着看源碼的調用:
在這裏插入圖片描述
這裏基本上相當於透傳,接着往裏看:
在這裏插入圖片描述
這不又是回到了最初的起點麼?
我們通過前面的分析已經知道,實際從Request裏解析的是:AbstractHandlerMapping,而AbstractHandlerMapping又是通過另外兩個子類實現不同的返回值類型的Handler,這裏我們前面已經提到。

這裏以AbstractHandlerMehtodMapping 爲切入點:我們看獲取HandlerMethod時這段代碼:
在這裏插入圖片描述
基本上分爲三個部分,前面判斷如果沒有MatchingMappings,則從所有的mapping集合裏取出來:

private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
		for (T mapping : mappings) {
			T match = getMatchingMapping(mapping, request);
			if (match != null) {
				matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
			}
		}
	}

接下來是匹配合適的Handler最後將其返回。

由上述過程,可以總結一下HandlerMethod 定位過程:

  1. HandlerMethod 集合查找 @RequestMapping 定義的 URI
  2. 返回 HandlerMethod

HandlerMappingHandlerAdapter 區別

從DispatchServlet類的屬性上我們看到,實際上1對多的關係有很多:
在這裏插入圖片描述
這裏講一下 HandlerMappingHandlerAdapter 區別:
HandlerMapping 主要負責映射,通過一次 HTTP 請求找到對應(最佳匹配規則)的 HandlerMethod 以及多個 HandlerInterceptor ,而 HandlerInterceptor 也等同與 HandlerExecutionChain,看一下此類的基本屬性:
在這裏插入圖片描述
HandlerExecutionChain這熟悉的三件套和我們之前分析AbstractHandlerMapping不也是異曲同工麼!

HandlerAdapter 主要負責 Handler 執行後處理,詳細邏輯見org.springframework.web.servlet.DispatcherServlet#getHandler:

	@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			for (HandlerMapping hm : this.handlerMappings) {
				if (logger.isTraceEnabled()) {
					logger.trace(
							"Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
				}
				HandlerExecutionChain handler = hm.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

HandlerMapping 的getHandler處理後返回HandlerExecutionChain。

HandlerInterceptor : 沒有匹配請求,而MappedInterceptor : 能夠匹配請求。

Spring MVC 2.5 之前面向接口編程

在老版本的頁面渲染處理中, HTML頁面渲染採用 JstlView, JSON渲染則採用 MappingJackson2JsonView

並且我們看Controller 接口定義:

 public interface Controller {

 	ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
 
 }

其方法返回值 ModelAndView,方法參數 HttpServletRequestHttpServletResponse

  • HandlerMapping 老實現:

Spring MVC 2.5+ 面向註解驅動編程

通常我們說 View 是來做頁面渲染,同樣的JstlView也是對其做的實現,但是,實際場景中。@Controller 處理方法可能不返回 ModelAndView,這種情況下怎麼辦呢?

這就是SpringMVC新版本提出的特性:就是通過 HandlerMethod 對於 ModelAndView 進行適配。

到了Spring Web MVC 2.5+ 以後,則引入了適配器(Adapter)與HandlerMethod的概念。前面我們也分析了HandlerMethod流轉過程,那麼HandlerMethod作爲形參有什麼樣的優勢呢?

  • HandlerMethod返回值:
    • ModelAndView
    • String
    • ResponseEntity
    • void
  • HandlerMethod參數:
    • @RequestParam
    • @RequestHeader
    • @PathVariable
    • @RequestBody
    • Model
  • HandlerMapping 新實現:RequestMappingHandlerMapping

這些參數的擴展相對於老版本不是更加靈活了麼!

理解 WebFlux 實現

org.springframework.web.reactive.HandlerMapping

對比參考 org.springframework.web.servlet.HandlerMapping

org.springframework.web.reactive.HandlerAdapter

對比參考 org.springframework.web.servlet.HandlerAdapter

org.springframework.web.reactive.DispatcherHandler

對比參考 org.springframework.web.servlet.DispatcherServlet

Java 微服務實現方案

Vert.x

Vert.x 入門以及與SpringBoot的一些聯動

Spring Boot / Spring Cloud

https://spring.io/projects/spring-cloud

後記

更多架構知識,歡迎關注本套Java系列文章Java架構師成長之路

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章