Spring Boot 實踐折騰記(17):Spring WebFlux中的函數語言程式設計模型
楊絳先生說:大部分人的問題是,做得不多而想得太多。
今天要講的函數語言程式設計可能和Spring Boot本身的關係不太大,但是它很重要!不僅是因為從Java 7升級到Java 8多了一種新程式設計語法的支援,更因為這是一種不同的思維模式。同時,今天的內容可能會偏多一點,希望愛學習的你能耐心看完。
Spring 5中的引入了對響應式程式設計的支援——WebFlux,它基於Reactor類庫的基礎實現,之前的三篇文章:
已經詳細講述過Reactor和WebFlux註解模型以及用法,可以點選複習。目前在Spring Boot中不支援兩種正規化混用。
本文我們將實現一個使用Java 8 lambda表示式來定義,使用Spring WebFlux的請求處理方法HandlerFunctions的例子。我們會使用Java 8 lambda程式碼風格來編寫程式碼,如下所示:
HandlerFunction<ServerResponse> echoHandlerFn = (request) -> ServerResponse.ok().body(fromObject(request.queryParam("name")));
RequestPredicate predicate = RequestPredicates.GET("/echo");
RouterFunction <ServerResponse> routerFunction = RouterFunctions.route(GET("/echo"), echoHandler::echo);
Mono<ServerResponse> echo = ServerResponse.ok().body(fromObject(request.queryParam("name" )));
接下來,我們先看幾個關鍵元件。
HandlerFunction
簡單來說,HandlerFunction
是一個接受ServletRequest
並返回ServletResponse
的函式介面。在使用註解模型時,我們會直接使用@RequestMapping("/")
註解來對映請求路徑,而HandlerFunction就是起到這個同樣的作用。介面定義如下程式碼:
package org.springframework.web.reactive.function.server;
import reactor.core.publisher.Mono;
@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
Mono<T> handle(ServerRequest var1);
}
這裡,我們又看到了@FunctionalInterface
的身影,標明是一個函式式介面,ServerRequest和ServerResponse是在Reactor型別之上構建的介面。我們可以將請求主體body轉換為Reactor的Mono或Flux型別,並且可以還發送任何響應的例項流給釋出者作為響應主體。
ServerRequest
在包org.springframework.web.reactive.function.server
中的ServerRequest
介面是表示的伺服器端HTTP請求。 我們可以在HTTP請求中通過各種方法來使用它,這裡要注意和註解模型下的ServerHttpRequest區別開,使用的方法如下:
HttpMethod method = request.method();
String path = request.path();
String id = request.pathVariable("id");
Map<String, String> pathVariables = request.pathVariables();
Optional<String> email = request.queryParam("email");
URI uri = request.uri();
由於需要轉換為Reactor的響應型別,我們就需要使用bodyToMono()
或bodyToFlux()
方法來將請求body轉換為Mono<T>
或Flux<T>
型別,如下:
Mono<User> manMono = request.bodyToMono(Man.class);
Flux<User> mansFlux = request.bodyToFlux(Man.class)
bodyToMono()和bodyToFlux()方法實際上是BodyExtractor
物件的例項,它主要用於提取請求主體內容並將其反序列化為POJO物件。這也就意味著,我們可以使用BodyExtractor類來將請求主體body內容轉化為Mono或Flux型別,如下所示:
Mono<User> manMono = request.body(BodyExtractors.toMono(Man.class));
Flux<User> mansFlux = request.body(BodyExtractors.toFlux(Man.class));
如果要將請求主體轉換為泛型型別時,還可以使用ParameterizedTypeReference
。
ParameterizedTypeReference<Map<String, List<User>>> typeReference = new Parameterized
TypeReference<Map<String, List<User>>>() {};
Mono<Map<String, List<User>>> mapMono = request.body(BodyExtractors.toMono(typeReference));
ServerResponse
同樣,在包中org.springframework.web.reactive.function.server
的ServerResponse
介面表示伺服器端HTTP的響應式響應。 ServerResponse也是一個唯一的介面,並提供了許多靜態構建器方法來構建響應,包括status, contentType, cookies, headers和body等。
以下是如何使用構造器方法,來構建ServerResponse的幾個例子:
ServerResponse.ok().contentType(APPLICATION_JSON).body(userMono, User.class);
ServerResponse.ok().contentType(APPLICATION_JSON).body(BodyInserters.fromObject(user));
ServerResponse.created(uri).build();
ServerResponse.notFound().build();
我們還可以使用render()
方法渲染檢視模板,如下所示:
Map<String,?> modelAttributes = new HashMap<>();
modelAttributes.put("man",man);
ServerResponse.ok().render("home", modelAttributes);
因此,使用這些ServerResponse的構造器方法,便可以構造HandlerFunction.handle
法的返回值了。
RouterFunction
RouterFunction
使用RequestPredicate將傳入請求對映到HandlerFunction。我們可以使用RouterFunctions的類靜態方法來構建RouterFunction,如下所示:
RouterFunctions.route(GET("/echo"), request -> ok().body(fromObject(request.queryParam("name"))));
還可以將多個路由定義合併到一個新的路由定義中,以便路由到與謂詞相匹配的第一個處理函式。
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
RouterFunctions.route(GET("/echo"), request -> ok().body(fromObject(request.queryParam("name"))))
.and(route(GET("/home"), request -> ok().render("home")))
.andRoute(POST("/mans"), request -> ServerResponse.ok().build());
上面,我們將三個路由合成為一個傳入給請求,並分別對映處理Handler。假設我們需要編寫多個具有相同父級字首的路由,這時,並不用在每個路由中重複URL路徑,使用RouterFunctions.nest()
方法即可實現父級目錄和子級目錄的對映,如下所示:
RouterFunctions.nest(path("/api/mans"),
nest(accept(APPLICATION_JSON),
route(GET("/{id}"), request -> ServerResponse.ok().build())
.andRoute(method(HttpMethod.GET), request -> ServerResponse.ok().build())));
說明一下,程式碼中將兩個URL對映到其處理函式。一種是GET /api/mans
,它返回所有使用者,另一種是GET /api/mans/{id}
返回給定id的使用者詳細資訊。
我們還可以使用RequestPredicates靜態方法建立RequestPredicate,以及使用RequestPredicate.and
組合請求達到同樣的效果,如下所示:
RouterFunctions.route(path("/api/mans").and(method(HttpMethod.GET)),
request -> ServerResponse.ok().build());
RouterFunctions.route(GET("/api/mans").or(GET("/api/mans/list")),
request -> ServerResponse.ok().build());
HandlerFilterFunction
我們如果將基於註解的方法與函式方法進行比較,則RouterFunction與@RequestMapping
註解類似,而HandlerFunction與使用@RequestMapping("/")註解的方法
類似。 WebFlux框架還提供了HandlerFilterFunction
介面,它更類似於Servlet Filter或@ControllerAdvice
方法,介面定義如下:
package org.springframework.web.reactive.function.server;
import java.util.function.Function;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
@FunctionalInterface
public interface HandlerFilterFunction<T extends ServerResponse, R extends ServerResponse> {
Mono<R> filter(ServerRequest var1, HandlerFunction<T> var2);
//其它預設方法
}
比如,我們可以使用HandlerFilterFunction根據使用者角色過濾路由,如下示例:
RouterFunction<ServerResponse> route = route(DELETE("/api/mans/{id}"), request -> ok().build());
RouterFunction<ServerResponse> filteredRoute = route.filter((request, next) -> {
if (hasAdminRole()) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
});
private boolean hasAdminRole()
{
//判斷是否有管理全許可權的邏輯程式碼
}
當我們向/api/mans/{id}
的URL發出請求時,篩選器將檢查使用者是否具有管理員角色,並決定執行處理函式,還是返回UNAUTHORIZED響應。
將HandlerFunctions註冊為方法引用
我們也可以不使用內聯lambda來定義HandlerFunctions,而是將它們定義為方法引用並在路由配置中使用方法引用,如下所示:
@Component
public class EchoHandler {
public Mono<ServerResponse> echo(ServerRequest request) {
return ServerResponse.ok().body(fromObject(request.queryParam("name")));
}
}
@Configuration
public class ManControllerFunc {
@Autowired
private com.hjf.boot.demo.flux.func.EchoHandler echoHandler;
@Bean
public RouterFunction<ServerResponse> echoRouterFunction() {
return RouterFunctions.route(GET("/echo"), echoHandler::echo);
}
}
實戰:使用RouterFunction的查詢API
現在,有了前面的基礎知識受,我們就可以使用功函數語言程式設計模型來構建應用程式了。 我們將建立一個UserHandler來作為HandlerFunctions的操作定義,然後配置一個RouterFunctions的路由Bean來對映路徑以處理請求。
第一步,建立示例Bean:ManEntity.class,如下所示:
@Entity
@Table(name="Man")
public class ManEntity {
@Id @GeneratedValue(strategy= GenerationType.AUTO)
private int id;
// @Column(nullable=false)
private String name;
// @Column(nullable=false, unique=true)
private int age;
// 省略get、set
第二步,建立服務元件類:ManHandler.class
@Component
public class ManHandler {
// private ManReactiveRepository manReactiveRepository;//目前Spring-data-jpa並不支援,會報錯,要麼使用mongodb
//
// @Autowired
// public void UserHandlerFunctions(ManReactiveRepository manReactiveRepository) {
// this.manReactiveRepository = manReactiveRepository;
// }
//
public Mono<ServerResponse> getAllUsers(ServerRequest request)
{
// Flux<ManEntity> allMans = manReactiveRepository.findAll();
Flux<ManEntity> allMans = Flux.fromArray(mockMan(10).toArray(new ManEntity[10]));
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8)
.body(allMans, ManEntity.class);
}
//
public Mono<ServerResponse> getUserById(ServerRequest request) {
// Mono<ManEntity> manMono = manReactiveRepository.findById(Integer.valueOf(request.pathVariable("id")));
Mono<ManEntity> manMono = Mono.just(mockMan(1).get(0));
Mono<ServerResponse> notFount = ServerResponse.notFound().build();
return manMono.flatMap(user -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(fromObject(user)))
.switchIfEmpty(notFount);
}
//
public Mono<ServerResponse> saveUser(ServerRequest request) {
Mono<ManEntity> manMono = request.bodyToMono(ManEntity.class);
// Mono<ManEntity> mono = manMono.flatMap(man -> manReactiveRepository.save(man));
return ServerResponse.ok().body(manMono, ManEntity.class);
}
public Mono<ServerResponse> deleteUser(ServerRequest request) {
Integer id = Integer.valueOf(request.pathVariable("id"));
// Mono<Void> mono = manReactiveRepository.deleteById(id); 刪除返回void的空
return ServerResponse.ok().build(Mono.empty());
}
static public List<ManEntity> mockMan(int num){
List<ManEntity> manEntityList = new ArrayList<>();
for (int i = 0; i < num; i++) {
ManEntity man = new ManEntity();
man.setId(i);
man.setName("testname_"+i);
man.setAge(18+i);
manEntityList.add(man);
}
return manEntityList;
}
}
細心的同學已經發現,這裡我們應該是要使用JPA的,但是通過實踐並查閱官方文件發現,Spring-data-jpa目前暫時還未支援響應式,所以會在啟動時報錯——
No property saveAll found for type
不過相信在未來,應該是會逐漸受到支援的。這裡我們手動寫一個mockMan方法來模擬讀取資料庫資料。這裡還可以使用已經受支援的Redis或MongoDB替代也行。
第三步,建立啟動類:RunAppFunc .class,並註冊RouterFunction,如下
@SpringBootApplication
public class RunAppFunc {
public static void main(String[] args) {
SpringApplication.run(RunAppFunc.class,args);
}
@Autowired
ManHandler manHandler;
@Bean
public RouterFunction<ServerResponse> routerFunctions() {
return nest(path("/api/mans"),
nest(accept(APPLICATION_JSON),
route(GET("/{id}"), manHandler::getUserById)
.andRoute(method(HttpMethod.GET), manHandler::getAllUsers)
.andRoute(DELETE("/{id}"), manHandler::deleteUser)
.andRoute(POST("/"), manHandler::saveUser)));
}
}
程式碼中,我們對應對映服務類ManHandler的CRUD操作方法,並使用字首的方式來統一建立路由。再次提醒,SpringBoot目前是不支援兩種程式設計模型的混用的。
第四步,啟動應用,再次報錯!發現路由沒有生效,查詢了StackOverflow後發現,是內嵌的tomcat影響,我們只需要排除掉即可,更新pom如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
再次啟動,成功!這時,控制檯列印會多出以下內容:
Mapped /api/mans => {
Accept: [application/json] => {
(GET && /{id}) -> com.hjf.boot.demo.flux.func.RunAppFunc$$Lambda$236/[email protected]
GET -> com.hjf.boot.demo.flux.func.RunAppFunc$$Lambda$238/[email protected]
(DELETE && /{id}) -> com.hjf.boot.demo.flux.func.RunAppFunc$$Lambda$239/[email protected]
(POST && /) -> com.hjf.boot.demo.flux.func.RunAppFunc$$\Lambda\$240/[email protected]
}
}
[{“id”:0,”name”:”testname_0”,”age”:18},{“id”:1,”name”:”testname_1”,”age”:19},{“id”:2,”name”:”testname_2”,”age”:20},{“id”:3,”name”:”testname_3”,”age”:21},{“id”:4,”name”:”testname_4”,”age”:22},{“id”:5,”name”:”testname_5”,”age”:23},{“id”:6,”name”:”testname_6”,”age”:24},{“id”:7,”name”:”testname_7”,”age”:25},{“id”:8,”name”:”testname_8”,”age”:26},{“id”:9,”name”:”testname_9”,”age”:27}]
其實,預設情況下,spring-boot-starter-webflux使用reactor-netty作為執行時引擎。 我們可以排除reactor-netty,並使用其他支援反應式非阻塞I/O的伺服器,比如,Undertow,Jetty或Tomcat。
小結
本文詳細介紹了Spring WebFlux中的函數語言程式設計模型中的各個關鍵元件,並通過一個實際的例子來講函數語言程式設計與SpringBoot進行了集合。雖然我們可能已經習慣了註解模型的程式設計方式,但瞭解一個新的思維模式同樣對我們的學習進步有幫助。
參考資源
1、Spring Boot官方文件
2、Spring 5 WebFlux
3、注意有關WebFlux自動配置的更多詳細,請檢視org.springframework.boot.autoconfigure.web.reactive包中的配置類
相關推薦
Spring Boot 實踐折騰記(17):Spring WebFlux中的函數語言程式設計模型
楊絳先生說:大部分人的問題是,做得不多而想得太多。 今天要講的函數語言程式設計可能和Spring Boot本身的關係不太大,但是它很重要!不僅是因為從Java 7升級到Java 8多了一種新程式設計語法的支援,更因為這是一種不同的思維模式。同時,今天的內容可能
Spring Boot 實踐折騰記(一):快速,Hello World例子
規劃人生旅程是你自己責無旁貸的責任,沒有其他人可以替你規劃。即便是你的老闆,也沒有這個義務。要實現自我發展,必須同時做好兩件事:精益求精和嘗試新鮮。——《非營利性組織的管理》 開始前… 最近閒下來,看了些Spring Boot和Docker的相關資料
Spring Boot 實踐折騰記(四):配置即使用,常用配置
生活不可能像你想象的那麼好,但也不會像你想象的那麼糟。我覺得人的脆弱和堅強都超乎自己的想象。有時,可能脆弱得一句話就淚流滿面;有時,也發現自己咬著牙走了很長的路。——源自 莫泊桑 開始前… 本章內容主要介紹的是Spring Boot常用的配置,相對前
Spring Boot 實踐折騰記(三):三板斧,Spring Boot下使用Mybatis
你要搞清楚自己人生的劇本:不是你父母的續集,不是你子女的前傳,更不是你朋友的外篇。對待生命你不妨大膽冒險一點,因為好歹你要失去它。——源自尼采 開始前… 上面的金句是被轉載很多的一句話,Spring Boot也有自己的舞臺,只是這個舞臺還沒有大量展開
Spring Boot 實踐折騰記(14):使用Kotlin
博爾赫斯說,沒有比思考更復雜的思考了,因此我們樂此不疲。 從Spring Boot 2開始,Boot也開始正式支援Kotlin程式設計,我們可以在建立Spring Boot應用時程式時使用Spring初始化Kotlin,不過Kotlin要在新的Spring 5
Spring Boot 實踐折騰記(11):使用 Spring 5的WebFlux快速構建效響應式REST API
關於Spring 5中的反應式程式設計支援Reactor類庫,上一篇文章《 Spring Boot 實踐折騰記(10):2.0+版本中的反應式程式設計支援——Reactor》已經簡要介紹過,Spring 5 框架所包含的內容很多,本文還會繼續介紹其中新增的 W
Python學習筆記(十二):lambda表示式與函數語言程式設計
以Mark Lutz著的《Python學習手冊》為教程,每天花1個小時左右時間學習,爭取兩週完成。 --- 寫在前面的話 2013-7-22 21:00 學習筆記 1,lambda的一般形式是關鍵字lambda後面跟一個或多個引數,緊跟一個冒號,以後是一個表示
機器學習折騰記(1):先成功執行一個Python例子
最近,聽很多朋友都在說人工智慧越來越火,想要了解其中究竟,於是我就推薦了幾本書,但結果卻是,除了工程師朋友能夠勉強看下去外,其餘大部分人到最後都不得放棄了,原因是太多數學公式,太難理解了。 比如,《深度學習》這本書,算得上一本科普書了,是專門寫給一般人看的,其
Spring核心探索與總結(二):Spring容器初始化原始碼探索
Spring容器概述 容器是spring的核心,Spring容器使用DI管理構成應用的元件,它會建立相互協作的元件之間的關聯,負責建立物件,裝配它們,配置它們並管理它們的生命週期,從生存到死亡(在這裡,可能就是new 到 finalize())。 Sprin
Java FP(Java8): Java中函數語言程式設計的Map和Fold(Reduce)
public double totalAmount(List<Double> amounts) { double sum = 0; for(double amount : amounts) { sum += amount; } return sum
從零一起學Spring Boot之LayIM項目長成記(一) 初見 Spring Boot
部分 基礎 依賴 com stat boot.s 情況下 比較 tar 項目背景 之前寫過LayIM的.NET版後端實現,後來又寫過一版Java的。當時用的是servlet,websocket和jdbc。雖然時間過去很久了,但是仍有些同學在關註。偶然間我聽說了Sprin
spring boot 監控與管理(actuator)
依賴 操作 上下 -1 star oms blog start 技術分享 Spring POMs 中提供了一個特殊的依賴模塊,即spring-boot-starter-actuator,我們只需要在我們的POM中添加依賴即可 <!-- 監控 管理 --> &l
Java框架spring Boot學習筆記(八):Spring相關概念
擴展 靜態 輕量級 想要 spring配置 核心 使用 oot 調用方法 Spring是開源、輕量級、一站式框架。 Spring核心主要兩部分 aop:面向切面編程,擴展功能不是修改源代碼實現 ioc:控制反轉,比如一個類,在類裏面有方法(不是靜態的方法),想要調用類
Spring Boot自動配置原理(轉)
腳本 bst file ade hazelcast oauth dbd 參考 b-s 第3章 Spring Boot自動配置原理3.1 SpringBoot的核心組件模塊首先,我們來簡單統計一下SpringBoot核心工程的源碼java文件數量:我們
Sping Boot入門到實戰之入門篇(三):Spring Boot屬性配置
git 測試 add 禁用 rop fix ron org set 該篇為Sping Boot入門到實戰系列入門篇的第三篇。介紹Spring Boot的屬性配置。 傳統的Spring Web應用自定義屬性一般是通過添加一個demo.properties配置文件(
類加載(四):spring-boot-loader 模塊
sys png out gpo 技術 jar getc spa 依賴 1. spring-boot jar包結構 2、 正常情況下,java -jar的類加載器是AppClassLoader 但是spring 使用自定義的URLClassLoader加載我們寫的cl
spring boot之入門配置(一)
麻煩 config src 符號 pos files 分享圖片 PE strong yml、properties配置文件 yml相比properties配置文件,yml可以省略不必要的前綴,並且看起來更加的有層次感。推薦使用yml文件。 @Value 根據
linux 子系統折騰記 (三)
style 簡單 翻譯軟件 linux目錄 mage visual 繼續 .com logs 所以說,英文真是個好東西,很多資料都只有英文版本,要是不懂英文,甚至你不知道這個資料的存在,更別提用蹩腳的翻譯軟件去翻譯了。wsl 的資料:https://docs.microso
Spring基礎:快速入門spring boot(7):spring boot 2.0簡單介紹
從這篇文章開始以spring boot2為主要版本進行使用介紹。 Spring boot 2特性 spring boot2在如下的部分有所變化和增強,相關特性在後續逐步展開。 特性增強 基礎元件升級: JDK1.8+ tomcat 8+ Thymeleaf 3
Spring boot集成Redis(1)—進行增加,更新,查詢,批量刪除等操作
緩存 獲取數據 prope XML ray end 序列 www pin 前言:最近工作中使用到了redis緩存,故分享一點自己總結的東西,這篇文章使用的是StringRedisTemplate進行學習,這裏值的說的是,(1)StringRedisTemplate在進行批量