拋棄Servlet API和Postman開發(fā)RESTful

Spring WebFlux由Spring 5.0框架首次引入。它具有無需Servlet、異步兩大特征,從而更好地提高Web應用的可伸縮性。
Spring WebFlux簡介
Spring WebFlux由Spring 5.0框架首次引入。與傳統(tǒng)Spring MVC相比,主要提供了如下兩個優(yōu)勢:
完全脫離了Servlet API。使用Spring WebFlux開發(fā)Web應用時,Servlet容器都成了可選項,默認使用Reactor Netty作為服務器。
Spring WebFlux實現了完全的異步非阻塞,可以很好地支持反應式流(Reactive Stream)編程范式,也能支持背壓(back pressure)等特征。
Reactor框架采用Mono和Flux兩個類代表消息發(fā)布者,因此它們都實現了CorePublisher
Mono代表0~1個非阻塞數據;而Flux則代表0~個非阻塞序列。
Mono相當于只是一個Optional值;而Flux才是Stream。
簡單來說,Mono包含多個數據項,而Flux能包含多個數據項。Spring WebFlux一樣也要用Mono和Flux這兩個類。
Spring WebFlux就是基于Reactor實現的,其中Flux名稱就是來自Reactor中的Flux類,WebFlux包括了對反應式HTTP、服務器推送事件(SSE:Server Send Event)及WebSocket的支持。
Spring WebFlux提供了兩種開發(fā)方式:
使用類似Spring MVC的注解方式。在這種方式下,依然使用@Controller、@RequestMapping等注解修飾類、方法即可。
使用函數式編程模型的方式。在這種方式下,程序使用RouterFunction來注冊映射地址和處理器方法之間路由關系。
上面這兩種編程模型只是形式上有所不同(代碼編寫方式上存在不同),它們本質上完全是一樣的,它們都運行在相同的反應式流的基礎之上。
使用注解開發(fā)WebFlux
下面先使用@Controller、@RequestMapping等注解來開發(fā)Spring WebFlux應用。依然按慣例創(chuàng)建一個基于maven-archetype-quickstart的Maven項目,并讓其pom.xml文件繼承spring-boot-starter-parent,并添加spring-boot-starter-webflux.jar的依賴。
接下來定義如下控制器類:
程序清單:Annotation\src\main\java\org\crazyit\app\controller\ItemController.javapublic class ItemController{public Monohello() {return Mono.just("Hello WebFlux");}}
查看該類代碼,不難發(fā)現該控制器類與Spring MVC應用的控制器類非常相似,它們同樣使用@Controller或@RestController注解來修飾控制器類、同樣使用@RequestMapping或其變體注解修飾處理方法;區(qū)別只是處理方法的返回值,WebFlux應用的控制器的返回值類型是Mono或Flux(此處是Mono)。
Mono和Flux正是Reactor框架中消息發(fā)布者API,它們都實現了CorePublisher
本應用的主類并沒有任何改變,依然通過SpringApplication的靜態(tài)run()方法來運行由@SpringBootApplication注解修飾的類即可。
運行該應用的主類來啟動應用,將會在控制臺看到如下輸出:
Netty started on port(s): 8080從上面輸出可以看出,WebFlux應用默認使用Netty作為嵌入式服務器,不再使用Tomcat作為服務器。
然后使用瀏覽器或Postman向http://localhost:8080/item/hello發(fā)送GET請求,即可看到服務器生成如下響應:
Hello WebFlux上面處理方法只是返回的Mono對象只是包含一個簡單的String數據,下面定義的處理方法返回的Mono對象將會包含復合對象。在ItemController類中添加如下方法:
程序清單:Annotation\src\main\java\org\crazyit\app\controller\ItemController.javaprivate ItemService itemService;public Mono- getByItemId( Integer id)
{return Mono.justOrEmpty(this.itemService.getItemById(id)).switchIfEmpty(Mono.error(new ItemNotFoundException("商品找不到")));}public Mono- create( Item item)
{return Mono.just(this.itemService.createOrUpdate(item));}public Mono- update( Item item)
{Objects.requireNonNull(item);return Mono.just(this.itemService.createOrUpdate(item));}public Mono- delete( Integer id)
{return Mono.justOrEmpty(this.itemService.delete(id));}
上面這些處理方法同樣很簡單,它們調用itemService組件來執(zhí)行CRUD操作,由于itemService的這4個CRUD方法的返回值只是單個Item對象或null,因此程序只要將該返回值放入Mono對象,這樣這些處理方法的返回值就變成了消息發(fā)布者。
上面控制器類所依賴的ItemService組件實現類代碼如下:
程序清單:Annotation\src\main\java\org\crazyit\app\service\impl\ItemService.javapublic class ItemServiceImpl implements ItemService{private final Mapdata = new ConcurrentHashMap<>(); private static final AtomicInteger idGenerator = new AtomicInteger(0);public Collection- list()
{return this.data.values();}public Item getItemById(Integer id){return this.data.get(id);}public Item createOrUpdate(Item item){// 修改用戶if (item.getId() != null && data.containsKey(item.getId())){this.data.put(item.getId(), item);}else{Integer id = idGenerator.incrementAndGet();item.setId(id);this.data.put(id, item);}return item;}public Item delete(Integer id){return this.data.remove(id);}}
正如上面代碼所看到的,本Service組件并未依賴DAO組件來訪問真正的數據庫,而是使用內存中Map來模擬內存數據庫:當程序需要添加記錄時就向Map中添加一個key-value對;當程序需要刪除記錄時就刪除一個key-value對。
使用Map模擬內存中的數據庫在學習控制器層和Service層開發(fā)時很有用,因為這樣可以避免涉及數據庫開發(fā),從而更好地聚焦正在學習的內容。
運行該應用的主類來啟動應用,然后可使用Postman來發(fā)送GET、POST、PUT、DELETE請求來測試上面這些處理方法。
使用curl代替Postman
本節(jié)打算教讀者使用curl來測試它們。
curl是一個Linux和windows系統(tǒng)都支持的命令行工具,如果能熟練地使用curl工具,你會發(fā)發(fā)現它非常強大,而且用起來非常方便——唯一的缺點是要記幾條命令。讀者可登錄https://curl.haxx.se下載和安裝curl工具,并可參考https://curl.haxx.se/docs/manpage.html快速掌握該工具的用法。當你熟練掌握它之后,你會發(fā)現它比Postman更高效、更好用。
curl工具的基本用法如下:
curl 選項 URL啟動命令行工具,執(zhí)行如下命令:
curl?-H?"Content-Type:?application/json"?-X?POST?-d?@item.json?http://localhost:8080/item上面命令涉及如下幾個選項:
-H:該選項用于指定請求頭。
-X:該選項用于指定請求方法,可指定為GET、POST、PUT、DELETE等。
-d:該選項用于指定請求數據。請求數據即可直接給出,也可通過讀取文件,帶@符號就表示讀取文件內容來作為請求數據。
讀者可能會把某個字符之間的間距當成空格。在這里可以告訴大家關于計算機命令格式的一個常識:空格是命令格式中非常敏感的字符。基本常識是:每個選項名(如-H、-X、-d等)與選項值之間有空格;選項值整體不能有空格,否則計算機會嘗試將它空格后面的內容解釋成下一個選項,因此如果選項值之間有空格或特殊字符,需要用雙引號括起來,比如上面"Content-Type: application/json"就是-H選項的選項值,它需要用引號括起來;第二個選項名與前一個選擇值之間有空格,例如-X選項與前面的"Content-Type: application/json"之間有空格,-d選項與前面的POST之間有空格。
如果在Windows平臺上使用curl命令,最好使用讀取文件的方式來提交請求數據——因為Windows平臺的命令行窗口默認采用GBK字符集,因此處理起來比較煩人。
上面命令中指定了-d @item.json選項,這意味著curl命令要讀取當前目錄下的item.json文件內容作為請求數據。因此還需在當前目錄(當你在Windows命令行窗口中執(zhí)行curl命令時,命令行窗口中>符號前的字符串就是當前目錄)下使用UTF-8字符集創(chuàng)建如下item.json文件。
{"name": "瘋狂Java講義","price": 128}
執(zhí)行上面命令,將會在命令行窗口看到如下輸出:
curl -H "Content-Type: application/json" -X POST -d .json http://localhost:8080/item{"id":1,"name":"瘋狂Java講義","price":128.0}
上面第二行輸出就是服務器響應,這就表明向服務器發(fā)送POST請求添加數據成功。
將item.json的數據略作修改(只能修改name屬性或price屬性的值),再次發(fā)送上面POST請求即可向服務器添加新的Item。
執(zhí)行如下命令來發(fā)送GET請求:
curl http://localhost:8080/item/1上面命令沒有指定任何選項,這意味著發(fā)送默認的GET請求,沒有請求數據,沒有指定額外的請求頭。執(zhí)行上面命令將會看到如下輸出:
curl http://localhost:8080/item/1{"id":1,"name":"瘋狂Java講義","price":128.0}
在當前目錄下使用UTF-8字符集創(chuàng)建如下item_update.json文件。
{"id": 1,"name": "瘋狂Android講義","price": 128}
上面JSON字符串定義的Item對象指定了id屬性,該字符串可用于更新id為1的Item對象。然后執(zhí)行如下命令來發(fā)送PUT請求:
curl -H "Content-Type: application/json" -X PUT -d .json http://localhost:8080/item上面命令與前面的執(zhí)行POST請求的命令基本相同,只是將-X選項改成了PUT,并改為讀取當前目錄下item_update.json文件的內容作為請求數據。
執(zhí)行上面命令將會看到如下輸出:
curl -H "Content-Type: application/json" -X PUT -d .json http://localhost:8080/item{"id":1,"name":"瘋狂Android講義","price":128.0}
這樣就服務端id為1的Item進行了修改,再次執(zhí)行curl http://localhost:8080/item/1命令來查看id為1的Item對象,即可看到它的name屬性值是修改后的屬性值了。
執(zhí)行如下命令來發(fā)送DELETE請求:
curl -X DELETE http://localhost:8080/item/1上面命令使用-X選項指定了發(fā)送DELETE請求,執(zhí)行上面命令將會看到如下輸出:
curl -X DELETE http://localhost:8080/item/1{"id":1,"name":"瘋狂Android講義","price":128.0}
上面命令執(zhí)行完成后,服務端id為1的Item對象就被刪除了。如果再次執(zhí)行curl http://localhost:8080/item/1命令來查看id為1的Item對象,即可看到如下輸出:
curl http://localhost:8080/item/1{"timestamp":"2020-10-14T23:37:31.472+00:00","path":"/item/1","status":500,"error":"Internal Server Error","message":"商品找不到",...
從服務器響應即可看出,id為1的Item對象不再存在。
上面4個處理方法返回的都是包含單個數據的Mono對象,當服務器相應是多項數據時,可使用Flux返回值來定義發(fā)布者。在ItemController中添加如下處理方法:
程序清單:Annotation\src\main\java\org\crazyit\app\controller\ItemController.javapublic Flux- list(Integer size)
{if (size == null || size == 0){size = 5;}return Flux.fromIterable(this.itemService.list()).take(size);}
上面代碼調用Flux的fromIterable()方法來將整個序列包含的數據變成消息發(fā)布者,然后調用Flux的take()方法來取出指定數量的數據項——本例將會根據size請求參數(如果該參數不存在,則使用默認值5)來取出數據項。
再次運行主程序來啟動應用,先使用curl發(fā)送POST請求添加幾條數據,,然后使用curl執(zhí)行如下命令:
curl http://localhost:8080/item?size=3上面命令沒有指定任何選項,這意味著它依然是發(fā)送GET請求,但發(fā)送請求時指定了size參數,運行該命令將會看到如下輸出:
curl http://localhost:8080/item?size=3[]
到此為止,可能有讀者會對WebFlux感到有點失望,好像WebFlux與Spring MVC并沒有什么區(qū)別,不僅開發(fā)方式差不多,連服務器生成的響應也差不多——實際上前面已經說過,WebFlux的變化主要是兩點:①、徹底拋棄Servlet API;②、基于訂閱-發(fā)布的異步機制。而這兩點的區(qū)別主要體現在底層服務器能以較小的線程池處理更高的并發(fā),從而提高應用的可伸縮性,它的區(qū)別往往并不體現在表面上。
當然異步響應也還是略有不同的,在ItemController中再次添加如下處理方法:
程序清單:Annotation\src\main\java\org\crazyit\app\controller\ItemController.java@GetMapping(value = "", produces = "application/stream+json")public Flux- list()
{// 需要周期生成數據,使用 Flux.intervalreturn Flux.interval(Duration.ofMillis(2000)).onBackpressureDrop()// 每隔interval,執(zhí)行一次itemService.list()的方法.map((interval) -> itemService.list())// 將List- 轉換成Flux
.flatMapIterable(item -> item).log("生成信息");}
上面@GetMapping注解中指定了produces = "application/stream+json"),這意味著該處理方法將負責處理Accept請求頭為“application/stream+json”的GET請求。
上面list()方法中使用了Flux的interval()方法來周期性地生成數據,而且由于客戶端可接受“流式”JSON響應,這樣該方法將可每隔2秒向客戶端發(fā)送一次響應。
再次運行主程序來啟動應用,先使用curl發(fā)送POST請求添加2條數據,,然后使用curl執(zhí)行如下命令:
curl http://localhost:8080/item -i -H "Accept: application/stream+json"上面命令使用-H選項指定了Accept請求頭,還使用了一個 -i選項,該選項無需選項值,它的作用是控制輸出服務器響應的響應頭。
運行上面命令將可看到如下輸出:
curl http://localhost:8080/item -i -H "Accept: application/stream+jsonHTTP/1.1 200 OKtransfer-encoding: chunkedContent-Type: application/stream+json{"id":1,"name":"瘋狂Python講義","price":118.0}{"id":2,"name":"瘋狂Java講義","price":128.0}{"id":1,"name":"瘋狂Python講義","price":118.0}{"id":2,"name":"瘋狂Java講義","price":128.0}...
此時將會看到服務器響應不斷地“跳出”,每次生成兩項數據——這是因為Flux訂閱者每次獲取的都只有兩條數據(itemService.list()方法只返回兩條數據)。
啟動另一個命令行窗口,再次使用curl執(zhí)行POST請求添加一個Item對象,再次切換回原來的命令行窗口,此時由于系統(tǒng)中包含了3個Item對象(itemService.list()方法返三條數據),此時將可看到服務器每次會生成三條數據的響應。
關于更多Spring編程的深入技巧可參考李剛老師的《輕量級Java Web企業(yè)應用實戰(zhàn)》

喜歡請分享到朋友圈
長按二維碼輕松關注
