情景引入 很早之前,Java就火起來了,是因為它善於開發和處理網路方面的應用。 Java有一個愛好,就是喜歡制定規範標準,但自己又不善於去實現。 反倒是一些服務提供商使用它的規範標準來製造應用伺服器而賺的盆滿缽滿。 企業用戶因要使用這些應用伺服器而向提供商支付高額費用,而且也不是特別好用。 一個青年 ...
情景引入
很早之前,Java就火起來了,是因為它善於開發和處理網路方面的應用。
Java有一個愛好,就是喜歡制定規範標準,但自己又不善於去實現。
反倒是一些服務提供商使用它的規範標準來製造應用伺服器而賺的盆滿缽滿。
企業用戶因要使用這些應用伺服器而向提供商支付高額費用,而且也不是特別好用。
一個青年才俊為了打破這種局面而奔走呼號、奮發圖強。
自我介紹
顯然,這個青年才俊就是後來的Spring。
因企業應用大都和web相關,而Java的web標準中較核心的一部分其實就是JavaEE里的Servlet。
Spring和Servlet“相親相愛”一番後,我就來到了這個世界。我的全名叫Spring MVC,這裡的Spring既是我的姓也是我的“爸爸”,那Servlet就是我的“媽媽”了,大家叫我MVC就行了。
那個年代社會很落後,條件也不好,好歹我們要求也不高,求個溫飽就行了。
所以我的媽媽Servlet和她的閨蜜Filter天生就是同步阻塞的,包括她們同事HttpServletRequest的getParameter,getPart等這些方法也都是阻塞的。
雖然我的爸爸Spring給了我23條染色體來進行改良,但不要忘了我還從Servlet媽媽那裡繼承了23條,所以我也是同步阻塞的。不過我的“長相”已經好看很多了,因為Spring爸爸知道,在以後的日子里,除了拼實力之外,顏值也是非常重要的。
因為我媽媽Servlet是一個規範,我爸爸Spring是一個框架,所以我跟他們一樣,都是無法自己獨立運行的。
所以在我們要運行的時候,必須要尋找一個特殊的“家”,通常稱它為Servlet容器,比如tomcat就算非常知名的一個。
Servlet容器熟知我極有可能阻塞當前執行線程,所以專門量身打造。它給我準備了一個非常大的線程池,裡面有好多線程。每過來一個請求,它就扔給我一個線程,說自己玩去吧,隨便“折騰”。
好在那時美國那個叫喬布斯的家伙被自己的公司趕出去在外面“流浪”,Servlet容器為我量身打造的這種方法完全能夠勝任日常,關鍵還非常的簡單。
這種小富即安的日子就這樣往前過著。
兄弟出生
生命不息,變化不止。隨著喬布斯推出iphone,智能機瞬間大火,全民進入移動互聯網時代。激增的網民數量,給現有軟體架構帶來極大的挑戰。
一般來說,社會越發達,分工越精細,對單一工種的要求就越高。
軟體也是如此,在傳統“大塊頭”軟體表現的越來越格格不入的時候,微服務就如一絲春風吹了進來。
按它的指導原則,將大軟體按某種方式拆分為一個個小工程。小工程規模小,便於管理,而且機動性也好,功能聚合性更好。它承受的併發應該更高。
有人覺得與微服務比起來,過去使用的web伺服器如tomcat略顯笨重,不夠輕量級。也有人說tomcat內部一個請求一個線程這種阻塞執行方式消耗太多線程,不太容易支撐超高併發。
無論怎麼說,簡而言之一句話,一個全新的時代已經到來。
此時我們需要一個更加輕量級web應用,它使用更少的硬體資源和線程,反而更容易處理高併發。那麼它一定是非同步非阻塞的。
這樣的使命自然落到了響應式編程的範疇上了。所以我的爸爸Spring審時度勢,在5.0之後就趕緊把我推出來了。
沒錯,我就是Spring WebFlux,這裡的Spring既是我的姓也是我爸爸。大家可以叫我WebFlux。初來乍到,好多人都對我不熟悉,請容許我介紹一番。
首先這個響應式究竟是什麼意思呢?響應式這個術語,指的是一個編程模型,它是圍繞著對變化的反映來構建的。
如網路組件用來響應I/O事件,UI控制器用來響應滑鼠事件等等。按照這種意識的話,非阻塞就是響應式的,對操作完成或數據可用通知事件的響應方式。
另外一個關於響應式的機制是非阻塞後壓。在命令式代碼中,同步阻塞調用帶有自然的後壓迫使調用者等待。
在非同步代碼中,它變得非常重要,用來控制事件的速率,以至於不讓一個快速的事件源壓垮它的響應者。就是響應者能夠控制事件源發射事件的快慢。
因為響應式編程是非阻塞的,所以我也是非阻塞的,因此我通常運行在非阻塞web伺服器上,如Netty,Undertow等。
因為我不會阻塞線程的執行,所以使用一個小的固定數量的線程池(event loop workers)來處理請求。典型地,線程數與CPU的核數相同。
這裡還要感謝我的姥爺Java 8,他老人家引入了lambda表達式造就了函數式編程API。這對於非阻塞應用和連續式API來說是一個非常棒的東西,允許以聲明的方式把非同步邏輯組合起來。
我感覺我的爸爸Spring已經超越了一個框架,成為一個平臺了。所以他自己並沒有親自去實現響應式處理,而是為我選擇Reactor作為響應式庫。
Reactor提供Flux和Mono類型,擁有豐富的操作符,支持非阻塞後壓,使用函數式API來組合非同步邏輯。並且Reactor強烈聚焦於Java伺服器端。它在開發時就已經與爸爸Spring親密協作了。
爸爸說,我也支持其它的庫如RxJava,但看樣子似乎讓我更愛Reactor一些。
這就是我,WebFlux,一個集天時地利於一身的幸運兒。但你是不是已經暈暈的啦,沒關係,慢慢來。
包羅萬象
我想,大家都看出了我爸爸Spring的野心,他不僅要成為一個平臺,還要建起自己的生態系統,豎起壁壘。
所以他的核心事業就是進行抽象,組合和裝配,進而包羅萬象。說的掉渣一些,就是哪個技術好,就給它整合進來。
為了抹平底層不同web伺服器的差異,我爸爸抽象了一個最低級別的契約介面,HttpHandler,用於響應式HTTP請求的處理。
Mono<java.lang.Void> handle(ServerHttpRequest request, ServerHttpResponse response);
它是一個通用的介面,要橫跨不同的運行時。它是有意設計成最小化的,只有一個方法,主要唯一目的就是在不同的HTTP伺服器API上面成為一個最小化的抽象。
如果想用Netty伺服器的話,就基於Netty實現一下,同理也可以基於Undertow實現一下,等等,只要以後有了新的伺服器,都可以加進來的。
顯而易見,HttpHandler的目標是抽象出來對不同HTTP伺服器的使用,說白了就是為了和底層伺服器對接。但由於太偏底層,不利用上層代碼使用。
為此,我的爸爸又抽象出一個稍微高一點級別的契約介面,WebHandler,用於Web請求處理。很明顯,WebHandler的目標是提供web應用中廣泛使用的通用特性,如Session、表單數據和附件等等,也是為了更容易和上層代碼對接。
很自然的,WebHandler是構建於HttpHandler之上的,換句話說WebHander的處理會通過一個適配器HttpWebHandlerAdapter最終代理給HttpHandler來執行。
WebHandler介面也只有一個方法:
Mono<java.lang.Void> handle(ServerWebExchange exchange);
參數類型是ServerWebExchange,可以這樣理解,你發一個請求,給你一個響應,相當於用請求交換了一個響應,而且是在伺服器端交換的。
其實,整個web請求的處理過程是一個鏈式的,最後才是一個WebHandler,它前面可以插入多個錯誤處理器,WebExceptionHandler,多個過濾器,WebFilter。
這是錯誤處理器介面:
Mono<java.lang.Void> handle(ServerWebExchange exchange, java.lang.Throwable ex);
這是過濾器介面:
Mono<java.lang.Void> filter(ServerWebExchange exchange, WebFilterChain chain);
可見,我的爸爸Spring的抽象能力非常強,對下抽象一個介面,抹平了不同伺服器的差異。對上抽象一個介面,可以用於支撐不同的編程模型。
都有哪些編程模型呢,請繼續往下看吧。
皮囊之下
上面我在介紹自己的時候使用了美顏,所以諸位很難看清我的“真面目”,下麵就來進行一下自我剖析,看看真實的我。
我包含一個輕量級函數式編程模型,函數被用來參與處理請求,它是相對於基於註解編程模型的另一種選擇,這種編程模型叫做函數式端點,functional endpoints,是構建於上面提到的WebHandler之上的。
我是使用HandlerFunction來處理一個HTTP請求的,這是一個函數式介面,也稱處理函數:
@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
reactor.core.publisher.Mono<T> handle(ServerRequest request);
}
帶有一個ServerRequest參數,返回一個Mono<ServerResponse>,其中request和response對象都是不可變的,HandlerFunction就等價於Controller中的@RequestMapping標記的方法。
實際當中,請求很多,處理函數也很多,如何知道一個請求過來後,該由哪個處理函數去處理呢?
這自然要用到我的另一個函數式介面RouterFunction來搞定,稱為路由函數:
@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {
reactor.core.publisher.Mono<HandlerFunction<T>> route(ServerRequest request);
}
帶有一個ServerRequest參數,返回一個Mono<HandlerFunction>。就是它把一個請求路由到一個HandlerFunction的,當路由函數匹配時,就返回一個處理函數,否則返回一個空的Mono。RouterFunction等價於@RequestMapping註解,但主要不同的是路由函數提供的不僅是數據,還有行為。
下麵通過一些示例,來更加直觀的幫助大家認識這兩個函數式介面。
因處理函數是函數式介面,所以可以直接用一個lambda表達式來處理請求,如下:
HandlerFunction<ServerResponse> handler = request -> Response.ok().body("Hello World");
這就表示當任何一個請求過來時,都返回Hello World作為響應。
在實際應用中,處理邏輯一般都很複雜,肯定不是一個lambda表達式能搞定的,此時希望把處理方法專門寫到一個類里,就叫處理器類,和MVC里的Controller差不多一回事。
下麵就是一個Person的處理器類:
public class PersonHandler {
public Mono<ServerResponse> listPeople(ServerRequest request) {
// ...
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
// ...
}
}
此時就可以通過處理函數,引用這些處理器方法了,如下:
PersonHandler handler = new PersonHandler();
HandlerFunction<ServerResponse> list = handler::listPeople;
HandlerFunction<ServerResponse> create = handler::createPerson;
HandlerFunction<ServerResponse> get = handler::getPerson;
要想使請求能夠正確被路由,首先要定義好路由函數,如下:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/person/{id}", get)
.GET("/person", list)
.POST("/person", create)
.build();
它表示當以GET方法請求/person/{id}時,最終會由getPerson方法處理。當以GET方法請求/person時,最後會由listPeople方法處理。同理,以POST方法請求/person時,會由createPerson方法處理。
可見,一個路由函數可以包含多個路由規則,實際當中,可以定義多個路由函數,這些路由函數可以組合在一起。
路由函數是按順序計算的,如果第一個路由不匹配,計算第二個,等等。因此,把更加具體的路由放到通用路由前面是非常有意義的。註意這和基於註解的不同。
怎麼樣,關掉濾鏡的我是不是更加真實了。我相信你也看明白了,至少要記住,這是基於函數式的一種編程模型,叫做函數式端點。
雨露均沾
像我這樣的幸運兒,你們一定以為Spring爸爸對我非常溺愛吧,告訴你,確實是這樣的。不過考慮到大家伙一路走來對Spring的不離不棄,爸爸也設身處地為你們著想。
為此,我除了支持函數式端點這種編程模型之外,還支持一種編程模型叫基於註解的控制器,annotated controllers,沒錯,就是MVC里的那個。
話說的再白一些,就是大家已經非常熟悉的Spring MVC那套東西,我百分之百的完全支持,妥妥的,放心使用。
但是,並不是所有的控制器方法參數都支持響應式類型,只有一些支持,如WebSession,java.security.Principal,@RequestBody,HttpEntity<B>,@RequestPart等。
下麵看一個示例:
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) {
// ...
}
@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) {
// ...
}
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
// ...
}
不過對於控制器方法的所有返回值,都是支持響應式類型的。
各有千秋
Spring MVC和Spring WebFlux可以一起使用,從設計上講,它們互為繼續、互為一致。
它們的關係,請看下圖,既有共同的部分,也有互相獨立的部分。
Spring MVC的特點就是,它是命令式編程,代碼非常容易寫,也好理解和調試。但是它是同步的,會有人覺得它性能不好。
但是我要說的是,響應式和非阻塞通常來講也不會使應用運行的更快。相反,非阻塞方式要求做更多的事情,而且還會稍微增加一些必要的處理時間。也就是說,還可能稍稍變慢一點,what,那為啥還要用它呢?
響應式和非阻塞的關鍵好處是,在使用很少固定數目的線程和較少的記憶體情況下的擴展能力。
這使應用在負載下更有適應能力,因為它們以一個更加具有可預見性的方式在擴展。
為了能夠觀察到這些好處,你需要有一些延遲才行,比如一個既不可靠且速度又慢的網路I/O,這才是響應式開始展示它強勁的地方,帶來的差異(驚喜)可能是巨大的哦。
其實技術無好壞,各有各的適用場景罷了。
(完)
編程新說
用獨特的視角說技術