52. 関数型メソッドに基づいた Spring WebFlux アプリケーションの開発

★ Spring WebFluxの2つの開発方法

1. 采用类似于Spring MVC的注解的方式来开发。
   此时开发时感觉Spring MVC差异不大,但底层依然是反应式API。

2. 使用函数式编程来开发

★ 機能的な方法で Web Flux を開発する

関数型スタイルを使用して WebFlux を開発する場合、次の 2 つのコンポーネントを開発する必要があります。

▲ Handler:作用:该处理器组件相当于控制器,它负责处理客户端的请求、并对客户端生成响应。

       该Handler组件的每个方法都只带一个ServerRequest参数(不是Servlet API)——代表客户端请求对象,

       且每个方法的返回值类型都是Mono<ServerResponse>,代表作为服务器响应的消息发布者。
       
       mono 代表一个消息发布者

▲ Router:作用:该组件通过函数式的编程方式来定义URL与Handler处理方法之间的映射关系。

★ WebFlux が ServerRequest を通じてリクエスト データを取得するには 2 つの方法があります。

这两种方式并不是可以自由选择的,而是根据数据的来源不同,需要采用对应的获取策略。

 - 对于以请求体提交的数据,通常会通过formData()(表单数据)或bodyToFlux()或bodyToMono()(RESTful)方法来获取,

   由于这种方式都需要通过网络IO读取数据,可能会造成阻塞,

   因此它们都采用了订阅-发布的异步方式,这三个方法的返回值都是Mono或Flux(消息发布者)。

 - 对于URL中的数据(包括传统请求参数和路径参数),由于它们只要直接解析URL字符串即可读取数据,

   不会造成阻塞,因此没有采用订阅-发布的异步方式。直接用pathVariable()或queryParam()方法即可读取数据。

★ハンドラーメソッドの戻り値

ハンドラー機能:プロセッサー コンポーネントはコントローラーに相当し、クライアントの要求を処理し、クライアントへの応答を生成する責任を負います。

Handler处理方法的返回值类型是Mono<ServerResponse>,
调用ServerResponse的ok()(相当于将响应状态码设为200)、
contentType()方法返回ServerResponse.BodyBuilder对象。
有了ServerResponse.BodyBuilder对象之后,根据响应类型不同,
可调用如下两个方法来生成Mono<ServerResponse>作为返回值:

▲ render(String name, Map<String,?> model):使用模板引擎来生成响应,

   其中第一个参数代表逻辑视图名,第二个参数代表传给模板的model数据。render()方法还有其他重载形式,功能类似。

▲ body(P publisher, Class<T> elementClass):直接设置响应体类生成响应,同样用于生成RESTful响应。

   body()方法还有其他重载形式,功能类似。

★ URLとHandlerメソッドの対応関係をRouterで定義する

ルーター機能:このコンポーネントは関数型プログラミングを使用して、URL とハンドラー処理メソッドの間のマッピング関係を定義します。

 ▲ Router就是容器中RouterFunctions类型的Bean。
    ——通常来说,就是使用@Configuration修饰的配置类来配置该Bean即可。

 return RouterFunctions
   // 定义映射地址和处理器方法之间的对应关系
   .route(RequestPredicates.POST("/login")
      .and(RequestPredicates.accept(MediaType.TEXT_HTML)), handler::login)
   .andRoute(RequestPredicates.GET("/viewBook/{id}")
      .and(RequestPredicates.accept(MediaType.TEXT_HTML)), handler::viewBook);

コードデモ:

同じリクエストで、spring mvc とは異なる実装方法を示します。

要求されるデータは単純な URL データであり、フロントエンドから送信されたデータ (id) が URL 内に記述されます。

概要: 前のコントローラに相当する Handler クラスを追加し、Router 構成クラスを作成し、その構成クラスに Router Bean を構成することで、クライアントが要求した URL と Handler の処理メソッドとのマッピング関係が確立されます。実現される。最終的な応答は、json 形式のデータまたは html ページです。

ハンドラー: プロセッサー コンポーネントはコントローラーと同等であり、クライアントの要求を処理し、クライアントへの応答を生成する責任を負います。このクラスはハンドラー コンポーネントです。

次に、クライアントのリクエストを処理するデータを処理するクラス (コントローラーに相当) を作成します。

このメソッドは、Json 応答である RESTful 応答を生成します。
ここに画像の説明を挿入します
このメソッドは、HTML 応答を生成します。
ここに画像の説明を挿入します

ルーター: 関数: このコンポーネントは、関数プログラミングを通じて URL とハンドラー処理メソッドの間のマッピング関係を定義します。

Router Bean を構成します。Router Bean は、リクエスト URL とハンドラー処理メソッドの間のマッピングを完了する役割を果たします。

設定メソッドのリクエストパスは「/viewBookabc/{id}」となっており、このパスを辿るとhandler::viewBookメソッドにアクセスします。
そして、handler::viewBooks はラムダのメソッド参照であり、BookHandler クラスに viewBook メソッドがあります。

Beanメソッド内のパラメータはBookHandlerとなっているので、lambdaのメソッド参照関数を利用してこのクラスのviewBookメソッドを参照することができます。

[リクエスト URL] と [ハンドラー処理メソッド] の間のマッピングを完了する責任を負います。

ハンドラー処理メソッド:BookHandlerのviewBookメソッドです。
ここに画像の説明を挿入します

HTMLページにレスポンスを返す
ここに画像の説明を挿入します

この Bean はプロジェクトの開始時にロードされます。
ここに画像の説明を挿入します

メソッドを呼び出してプロセスを確認します。
メソッドにアクセスするときは、BookHandler のこのメソッドが使用されます。
ここに画像の説明を挿入します

試験結果:

ここに画像の説明を挿入します

完全なコード:

ブックハンドラー

// Handler:该处理器组件相当于控制器,它负责处理客户端的请求、并对客户端生成响应,这个类就是handler组件
@Component
public class BookHandler
{
    
    
    private BookService bookService;

    //有参构造器完成依赖注入
    public BookHandler(BookService bookService)
    {
    
    
        this.bookService = bookService;
    }


    // Handler:该处理器组件相当于控制器,它负责处理客户端的请求、并对客户端生成响应,这个类就是handler组件

    //这个方法是生成 RESTful 响应的
    public Mono<ServerResponse> viewBook(ServerRequest request)
    {
    
    
        //如果请求参数是通过 URL 字符串即可解析,可用 pathVariable()或queryParam()方法获取参数
        Integer id = Integer.parseInt(request.pathVariable("id"));
        Book book = bookService.getBook(id);
        //ok()  表示服务器响应正常
        Mono<ServerResponse> body = ServerResponse.ok()
                //选择生成 JSON 响应类型
                .contentType(MediaType.APPLICATION_JSON)
                //如果要生成 JSON 响应,直接用 body 方法
                //参数1:代表数据发布者(Publisher),参数2:指定 Mono 中每个数据项的类型
                //Mono 的 justOrEmpty 将单个及可能为null的数据包装成 Mono
                //如果是设计良好的应用(就是底层数据库的访问也是用 reactor api ,
                // 这样此处从数据库返回的数据就是 Mono 或者 Flux,根本不需要包装)
                .body(Mono.justOrEmpty(book), Book.class);
        return body;
    }

    //这个方法是生成 HTML 响应的
    public Mono<ServerResponse> viewBookHtml(ServerRequest request)
    {
    
    
        //如果请求参数是通过 URL 字符串即可解析,可用 pathVariable()或queryParam()方法获取参数
        Integer id = Integer.parseInt(request.pathVariable("id"));
        Book book = bookService.getBook(id);
        //ok()  表示服务器响应正常
        Mono<ServerResponse> render = ServerResponse.ok()
                //选择生成 HTML 响应类型
                .contentType(MediaType.TEXT_HTML)
                //参数1:逻辑视图名   参数2:相当于 spring mvc 的 model,用于向视图页面传输数据
                .render("viewBook", Map.of("book", book));
        return render;
    }
}

RouterConfig

package cn.ljh.FunctionalFlux.router;

import cn.ljh.FunctionalFlux.handler.BookHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

@Configuration //配置类
public class RouterConfig
{
    
    
    //配置 Router Bean ,负责完成请求 URL 和 Handler 处理方法之间的映射。

    @Bean
    public RouterFunction<ServerResponse> routerFunctions(BookHandler handler)
    {
    
    
        //MediaType.APPLICATION_JSON 设置响应类型 ,  handler::viewBooks  是 lambda 中的方法引用
        RouterFunction<ServerResponse> route =
                RouterFunctions
                        //这里就映射到 BookHandler 类里面的 viewBook 方法,/viewBookabc/{id}这个是我们这边给的访问路径
                        .route(RequestPredicates.GET("/viewBookabc/{id}")
                                .and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::viewBook)

                        //这里就映射到 BookHandler 类里面的 viewBookHtml 方法,/viewBookHtml/{id}这个是我们这边给的访问路径
                        .andRoute(RequestPredicates.GET("/viewBookHtml/{id}")
                                .and(RequestPredicates.accept(MediaType.TEXT_HTML)), handler::viewBookHtml);
        return route;
    }
}

上記のコードは、リクエストされたデータが単純な URL データであること、つまり、フロントエンドから送信されたデータ (ID) が URL に記述されていることを示しています。

今回のデモン​​ストレーションは、フロントエンドがフォームまたは RESTful な方法でデータを送信することです。

デモ: RESTful に送信されたデータの処理

こちら側はフロントエンドからデータを受け取って処理する、コントローラーに相当します。
ここに画像の説明を挿入します

ここでの Bean は、リクエスト URL とハンドラー処理メソッドの間のマッピング関係です。
ここに画像の説明を挿入します

テスト結果:
本を追加するメソッドは正常に処理され、追加された本のデータが postman に実装されました。
ここに画像の説明を挿入します

デモ: フォーム ページからリクエストを送信する

簡単なフォームページを作成する
ここに画像の説明を挿入します

フロントエンドはフォームページを通じてリクエストを送信します。
ここに画像の説明を挿入します

リクエストURLとハンドラー処理メソッド間のマッピングを追加
ここに画像の説明を挿入します

試験結果:

注: ハンドラーの処理メソッドがソース Mono を新しい Mono に変換するためにマップを使用するため、変換結果ではマップが使用されず、追加に失敗することがわかりました。

図に示すように:
Mono 変換後の結果を使用する必要がない場合は、現時点では map() メソッドを使用する必要はありません。map() メソッドは、
ソース Mono を新しい Mono に変換する役割を果たします。
Mono 内のデータのみを使用したい場合は、To Consumer data,
just Consumer this message となります。Mono の結果をビュー ページに返す必要がないため、変換に Map メソッドを使用する必要はありません。
ここに画像の説明を挿入します

テスト成功:
フォーム ページからリクエストが正常に送信されました。
ここに画像の説明を挿入します

フロントエンドの注意を払うためのヒント:

テンプレート パスの下にある静的ページには直接アクセスできません。アクセスするには、コントローラーの処理メソッドを通じて転送する必要があります。
または、ページを静的リソース ディレクトリ (static、public) に配置することで、ページに直接アクセスできます。
注: ページは静的ページである必要があります。動的コンテンツを含めることはできません。また、動的ページにすることもできません。

ここに画像の説明を挿入します

完全なコード:

ドメイン
ここに画像の説明を挿入します

処理クラス: BookHandler、コントローラーに類似

package cn.ljh.FunctionalFlux.handler;


import cn.ljh.FunctionalFlux.domain.Book;
import cn.ljh.FunctionalFlux.service.BookService;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.Collection;
import java.util.Map;


// Handler:该处理器组件相当于控制器,它负责处理客户端的请求、并对客户端生成响应,这个类就是handler组件
@Component
public class BookHandler
{
    
    
    private BookService bookService;

    //有参构造器完成依赖注入
    public BookHandler(BookService bookService)
    {
    
    
        this.bookService = bookService;
    }


    // Handler:该处理器组件相当于控制器,它负责处理客户端的请求、并对客户端生成响应,这个类就是handler组件

    //这个方法是生成 RESTful 响应的 ,就是 Json 响应
    public Mono<ServerResponse> viewBook(ServerRequest request)
    {
    
    
        //如果请求参数是通过 URL 字符串即可解析,可用 pathVariable()或queryParam()方法获取参数
        Integer id = Integer.parseInt(request.pathVariable("id"));
        Book book = bookService.getBook(id);
        //ok()  表示服务器响应正常
        Mono<ServerResponse> body = ServerResponse.ok()
                //选择生成 JSON 响应类型
                .contentType(MediaType.APPLICATION_JSON)
                //如果要生成 JSON 响应,直接用 body 方法
                //参数1:代表数据发布者(Publisher),参数2:指定 Mono 中每个数据项的类型
                //Mono 的 justOrEmpty 将单个及可能为null的数据包装成 Mono
                //如果是设计良好的应用(就是底层数据库的访问也是用 reactor api ,
                // 这样此处从数据库返回的数据就是 Mono 或者 Flux,根本不需要包装)
                .body(Mono.justOrEmpty(book), Book.class);
        return body;
    }

    //这个方法是生成 HTML 响应的
    public Mono<ServerResponse> viewBookHtml(ServerRequest request)
    {
    
    
        //如果请求参数是通过 URL 字符串即可解析,可用 pathVariable()或queryParam()方法获取参数
        Integer id = Integer.parseInt(request.pathVariable("id"));
        Book book = bookService.getBook(id);
        //ok()  表示服务器响应正常
        Mono<ServerResponse> render = ServerResponse.ok()
                //选择生成 HTML 响应类型
                .contentType(MediaType.TEXT_HTML)
                //参数1:逻辑视图名   参数2:相当于 spring mvc 的 model,用于向视图页面传输数据
                .render("viewBook", Map.of("book", book));
        return render;
    }


    //以 RESTful 方式提交的数据的处理
    public Mono<ServerResponse> addBook(ServerRequest request)
    {
    
    
        //假设数据来自 RESTful 的 POST 请求,此时用 bodyToMono() 或 bodyToFlux() 来获取数据
        //bodyToFlux():如果请求的数据中包含多个数据,就用这个。
        //bodyToMono():如果请求的数据只有一个数据,那就用这个
        //这两个方法参数指定了 Mono 或 Flux 中数据的类型

        // 添加一本图书,只是一个对象,所以用.bodyToMono() ,
        // 如果是一个集合,就应该使用 .bodyToFlux()

        Mono<Book> bookMono = request.bodyToMono(Book.class);
        //map() 负责将 Mono 或者 Flux 中的元素,转换成新的 Mono 或 Flux 中的元素
        Mono<Book> resultMono = bookMono.map(book ->
        {
    
    
            //添加 Book 对象
            bookService.addBook(book);
            return book;
        });
        Mono<ServerResponse> body = ServerResponse.ok()
                //选择生成 JSON 响应类型
                .contentType(MediaType.APPLICATION_JSON)
                //如果要生成 JSON 响应,直接用 body 方法
                //参数1:代表数据发布者(Publisher),参数2:指定 Mono 中每个数据项的类型
                //Mono 的 justOrEmpty 将单个及可能为null的数据包装成 Mono
                //如果是设计良好的应用(就是底层数据库的访问也是用 reactor api ,
                // 这样此处从数据库返回的数据就是 Mono 或者 Flux,根本不需要包装)
                .body(resultMono, Book.class);
        return body;
    }


    //通过表单页面提交请求
    public Mono<ServerResponse> addBookHtml(ServerRequest request)
    {
    
    
        //假设数据来自 表单页面 的 POST 请求,通过 formData() 获取表单的数据
        Mono<MultiValueMap<String, String>> formData = request.formData();

        /*
         * 如果不需要使用 Mono 转换之后的结果,此时就不需要使用 map() 方法
         * map() 方法就是负责将 源Mono 转换成新的 Mono
         * 如果只是希望用到 Mono 中的数据,此时成为消费数据
         */
        formData.subscribe(map ->
        {
    
    
            String name = map.get("name").get(0);
            String price = map.get("price").get(0);
            String author = map.get("author").get(0);
            Book book = new Book(null, name, Double.parseDouble(price), author);
            bookService.addBook(book);
        });
        Mono<ServerResponse> render = ServerResponse.ok()
                //选择生成 JSON 响应类型
                .contentType(MediaType.TEXT_HTML)
                .render("addBookResult", Map.of("tip", "添加书籍成功"));
        return render;
    }


}

構成クラス: RouterConfig、URL とハンドラー クラスのメソッド間のマッピング関係を処理する Bean を追加します。

package cn.ljh.FunctionalFlux.router;

import cn.ljh.FunctionalFlux.handler.BookHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

@Configuration //配置类
public class RouterConfig
{
    
    
    //配置 Router Bean ,负责完成请求 URL 和 Handler 处理方法之间的映射。
    @Bean
    public RouterFunction<ServerResponse> routerFunctions(BookHandler handler)
    {
    
    
        //MediaType.APPLICATION_JSON 设置响应类型 ,  handler::viewBooks  是 lambda 中的方法引用
        RouterFunction<ServerResponse> route =
                RouterFunctions
                        //这里就映射到 BookHandler 类里面的 viewBook 方法,/viewBookabc/{id}这个是我们这边给的访问路径
                        .route(RequestPredicates.GET("/viewBookabc/{id}")
                                .and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::viewBook)

                        //这里就映射到 BookHandler 类里面的 viewBookHtml 方法,/viewBookHtml/{id}这个是我们这边给的访问路径
                        .andRoute(RequestPredicates.GET("/viewBookHtml/{id}")
                                .and(RequestPredicates.accept(MediaType.TEXT_HTML)), handler::viewBookHtml)

                        //这里就映射到 BookHandler 类里面的 addBook 方法,/addBook 这个是我们这边给的访问路径
                        .andRoute(RequestPredicates.POST("/addBook")
                                .and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::addBook)

                        //这里就映射到 BookHandler 类里面的 addBookHtml 方法,/addBookHtml/{id}这个是我们这边给的访问路径
                        .andRoute(RequestPredicates.POST("/addBookHtml")
                                .and(RequestPredicates.accept(MediaType.TEXT_HTML)), handler::addBookHtml);
        return route;
    }
}

ブックサービス

package cn.ljh.FunctionalFlux.service;


import cn.ljh.FunctionalFlux.domain.Book;
import java.util.Collection;

public interface BookService
{
    
    
    Book getBook(Integer id);

    Integer addBook(Book book);

    Collection<Book> getAllBooks();
}

BookServiceImpl

package cn.ljh.FunctionalFlux.service.impl;


import cn.ljh.FunctionalFlux.domain.Book;
import cn.ljh.FunctionalFlux.service.BookService;
import org.springframework.stereotype.Service;

import java.util.*;

//添加这个@Service注解,springboot就可以自动扫描这个Service组件的实现类,然后把这个类部署成容器中的bean。
@Service
public class BookServiceImpl implements BookService
{
    
    
    //添加一个 Map 集合,假设为数据库
    public static final Map<Integer, Book> bookDB = new LinkedHashMap<>();

    //创建一个自增id
    static int nextId = 4;

    //初始化这个数据库
    static
    {
    
    
        bookDB.put(1, new Book(1, "火影忍者", 100.0, "岸本"));
        bookDB.put(2, new Book(2, "家庭教师", 110.0, "天野明"));
        bookDB.put(3, new Book(3, "七龙珠Z", 120.0, "鸟山明"));
    }


    //查看图书
    @Override
    public Book getBook(Integer id)
    {
    
    
        Book book = bookDB.get(id);
        if (book == null){
    
    
            throw new RuntimeException("没有此图书信息!");
        }
        return book;
    }

    //添加图书
    @Override
    public Integer addBook(Book book)
    {
    
    
        book.setId(nextId);
        bookDB.put(nextId,book);
        //返回id,先返回在自增。
        return nextId++;
    }

    //查看所有的图书
    @Override
    public Collection<Book> getAllBooks()
    {
    
    
        //获取集合中的所有元素
        Collection<Book> values = bookDB.values();
        return values;
    }
}

書籍ページの追加: addBook.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>添加图书页面</title>
</head>
<body>
<h2>添加图书页面</h2>

<form method="post" action="/addBookHtml">
    书名:<input name="name"  id="name" type="text"><br>
    价格:<input name="price"  id="price" type="text"><br>
    作者:<input name="author"  id="author" type="text"><br>
    <input type="submit" value="提交"/>
    <input type="reset" value="重设"/>
</form>
</body>
</html>

書籍結果ページの追加: addBookResult.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>添加图书结果</title>
</head>
<body>
<h2>添加图书结果</h2>
<div th:text="${tip}">
</div>
</body>
</html>

ID に基づいて書籍をクエリします: viewBook.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>查看图书</title>
</head>
<body>
<h2>查看图书</h2>
<div th:text="${book.name}"></div>
<div th:text="${book.price}"></div>
<div th:text="${book.author}"></div>

</div>
</body>
</html>

おすすめ

転載: blog.csdn.net/weixin_44411039/article/details/132686771