1. 程式人生 > >Spring Boot 實踐折騰記(17):Spring WebFlux中的函數語言程式設計模型

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.serverServerResponse介面表示伺服器端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 實踐折騰17Spring 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 FPJava8: 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 boot7spring boot 2.0簡單介紹

從這篇文章開始以spring boot2為主要版本進行使用介紹。 Spring boot 2特性 spring boot2在如下的部分有所變化和增強,相關特性在後續逐步展開。 特性增強 基礎元件升級: JDK1.8+ tomcat 8+ Thymeleaf 3

Spring boot集成Redis1—進行增加,更新,查詢,批量刪除等操作

緩存 獲取數據 prope XML ray end 序列 www pin 前言:最近工作中使用到了redis緩存,故分享一點自己總結的東西,這篇文章使用的是StringRedisTemplate進行學習,這裏值的說的是,(1)StringRedisTemplate在進行批量