Spring5新特性WebFlux学习

WebFlux 简介
你好! 这是你第一次使用 **Markdown编辑器** 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

Spring WebFlux is a non-blocking web framework built from the ground up to take advantage of multi-core,next-generation processors and handle massive numbers of concurrent connections.

   Spring WebFlux 是一个自底向上构建的非阻塞 Web 框架,用于利用多核、下一代处理器并处理大量并发连接。

在这里插入图片描述
原始的 web 框架包含在 Spring 框架中,即 Spring Web MVC,是专为 Servlet API 与 Servlet容器构建的。reactive stack, web framework, Spring WebFlux 最新被添加到了 Spring 5.0 版本中。它是完全地非阻塞的,支持 Reactive Streams 背压,运行在诸如 Netty、Undertow 与 Server3.1+容器中。

   两个 Web 框架都反映了它们源模块的名称:spring-webmvc 与 spring-webflux,并且它们共存于 Spring 框架之中。每一个模块都是可选的。应用程序可以选择一个或另一个模块,或者在某些情况下两个同时使用。例如,SpringMVC 使用反应式 Web 客户端进行控制。

第一个 WebFlux 程序

创建工程
创建一个webflux,Spring Boot 的版本要求最低为 2.0.0。不要添加原来的web 依赖,而是要添加 Reactive Web,即 flux 依赖,并添加上 Lombok
定义处理器
在这里插入图片描述
访问
通过浏览器地址栏访问,并没有感觉有任何的区别。

修改处理器

//定义耗时操作
private String doThing(String msg) {
    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return msg;
}

修改两个处理器方法

@GetMapping("/common")
public String commonHandle(){
    log.info("common-start");
    //执行耗时操作
    String result = doThing("common handler");
    log.info("common-end");
    return result;
}
@GetMapping("/mono")
public Mono<String> monoHandle(){
    log.info("mono-start");
    //执行耗时操作
    Mono<String> mono = Mono.fromSupplier(() -> doThing("mono handle"));
    log.info("mono-end");
    //Mono表示包含0或1个元素的异步序列
    return mono;
}

再访问
通过浏览器地址栏访问,对于客户端来说感觉是一样的,都是在等待了大约 5 秒后才获得了响应。但查看控制台,其输出内容是不同的。

   **访问普通处理器**

在这里插入图片描述
在这里插入图片描述
仔细查看这两个时间差,发现其差值仅为 2 毫秒。即耗时操作没有阻塞了处理器的执行。这样,处理器就可以处理更多的用户请求了。虽然对于每一个客户端来说好像并没有增加用户体验,但对于处理器来说,其吞吐量却大大提高了。

修改处理器-Flux

   Mono 表示,处理器返回的数据为 0-1 个;而 Flux 表示,处理器返回的数据为 0-多个。
@RequestMapping("/flux")
public Flux<String> fluxHandle(){
    //Flux表示包含0-n个元素的异步序列
    return Flux.just("beijing", "shanghai", "gunagzhou", "shenzhen");
}

访问方式
以下测试均使用如下的访问URL,且不要使用火狐浏览器,而要使用谷歌或360浏览器。
否则,有些效果看不到

数组转 Flux

@RequestMapping("/array")
public Flux<String> fluxHandle(String[] cities){
    //Flux表示包含0-n个元素的异步序列
    return Flux.fromArray(cities);
}

集合转 Flux

@RequestMapping("/list")
public Flux<String> fluxHandle(List<String> cities){
    //将List转为Stream,再将Stream转为Flux
    return Flux.fromStream(cities.stream());
}

Flux 底层不会阻塞处理器执行

@RequestMapping("/time")
public Flux<String> timeHandle(@RequestParam List<String> cities){
    log.info("flux-start");
    //将Flux的每个元素映射为一个doThing耗时操作
    Flux<String> flux = Flux.fromStream(cities.stream().map(i -> doThing("elem-" + i)));
    log.info("flux-end");
    return flux;
}

SSE
SSE,Server-Sent Event,服务端推送事件

异步 Servlet

定义同步 Servlet

@WebServlet("/syn")
public class SynServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        long startTime = System.currentTimeMillis();
        doThing(request, response);
        long endTime =System.currentTimeMillis();
        System.out.println("同步操作Web服务器耗时" + (endTime - startTime));
    }
 
    private void doThing(HttpServletRequest request, HttpServletResponse response)throws IOException {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        response.getWriter().print("Done!");
    }
}

在浏览器访问同步 Servlet 时,浏览器会出现等待 5 秒时间后才可以看到“Done!”字样的情况。但关键是控制台的输出,业务逻辑耗时为 5 秒左右。

在这里插入图片描述
异步 Servlet

import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
 
@WebServlet(value = "/asyn", asyncSupported = true)
public class AsynServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        long startTime = System.currentTimeMillis();
 
        //获取异步上下文,开启异步操作(完成异步线程间的通讯)
        AsyncContext asyncContext = request.startAsync();
 
        //获取NIO的异步请求与响应
        ServletRequest asyncContextRequest = asyncContext.getRequest();
        ServletResponse asyncContextResponse = asyncContext.getResponse();
 
        CompletableFuture.runAsync(()->doThing(asyncContext,asyncContextRequest, asyncContextResponse));
 
        long endTime =System.currentTimeMillis();
        System.out.println("异步操作Web服务器耗时" + (endTime - startTime));
    }
 
    private void doThing(AsyncContext asyncContext, ServletRequest request, ServletResponse response){
        try {
            TimeUnit.SECONDS.sleep(5);
            response.getWriter().print("Done!");
        } catch (Exception e) {
            e.printStackTrace();
        }
        //耗时业务代码通知异步操作,任务完成
        asyncContext.complete();
    }
}

浏览器访问异步 Servlet 时,浏览器同样会出现等待 5 秒时间后才可以看到“Done!”字样的情况。但关键是控制台的输出,业务逻辑耗时为 1毫秒左右
同步 Servlet阻塞了什么

   当请求到达 Tomcat 后,Tomcat 会为其找到与该请求相匹配的 Servlet,并分配一个该Servlet 的线程处理该请求。当 Servlet 中需要处理耗时操作时,当前 Servlet 线程会被阻塞。所以当前 Servlet 中的业务逻辑阻塞了当前 Servlet 线程的执行。

异步 Servlet 是怎样工作的

   异步 Servlet 没有阻塞 Servlet 线程,而是很快结束了 Servlet 线程的调用,将其又放入到了 Servlet 线程池,供其它请求使用。这样就增加了 Tomcat 服务器的吞吐量了。
   不过需要注意一点,虽然没有阻塞 Servlet 线程的运行,但服务端向客户端的响应是需要业务逻辑线程执行完毕后才会向客户端响应的。即对于客户端的用户体验来说,并没有感觉到服务器运算速度的加快,但服务器的吞吐量增加了。

SSE

   老的 http 协议是请求-响应式的,即客户端提交一个请求,服务端会只会给出一个响应。但对于某些实时性要求比较高的需求(例如微博消息推送),若使用这种原始的请求-响应方式实现起来是比较麻烦的。HTML5 标准中新增了一个 SSE(Server-Sent Event,服务端推送事件),可以方便地做到消息实时推送,即一次请求后会不断的获得多个响应。由于这是官方特性,主流浏览器对其支持是较好的(除了火狐)。

SSE 简介

  理论上, SSE 和 WebSocket 做的是同一件事情。当你需要用新数据局部更新网络应用时,SSE 可以做到不需要用户执行任何操作便可以完成。例如要做一个统计系统的管理后台,我们想知道统计数据的实时情况。类似这种更新频繁、低延迟的场景,SSE 可以完全满足。再比如,邮箱服务的新邮件提醒,微博的新消息推送、在线聊天记录的显示等,SSE 都是不错的选择。

SSE 对比 WebSocket

   SSE 是单向通道,只能服务器向客户端发送消息,如果客户端需要向服务器发送消息,则需要一个新的 HTTP 请求。这对比 WebSocket 的双工通道来说,会有更大的开销。这么一来的话就会存在一个“什么时候才需要关心这个差异?”的问题。如果客户端平均每秒会向服务器发送一次消息的话,那应该选择 WebSocket。如果一分钟仅 5 - 6 次的话,其实这个差异并不大。

SSE 技术规范

   SSE 规范是 HTML 5 规范的一个组成部分。该规范比较简单,主要由两个部分组成:

   服务端与浏览器之间的通讯协议

   浏览器中可供 JavaScript 使用的 EventSource 对象

通讯协议

   这个通讯协议是基于纯文本的简单协议。服务器端的响应内容类型必须是“text/event-stream”。响应文本的内容是一个事件流,事件流是一个简单的文本流,仅支持UTF-8 格式的编码。

   事件流由不同的事件组成。不同事件间通过仅包含回车符和换行符的空行(“\r\n”)来分隔。

   每个事件可以由多行构成,每行由类型和数据两部分组成。类型与数据通过冒号(“:”)进行分隔,冒号前的为类型,冒号后的为其对应的值。每个事件可以包含如下类型的行:

   类型为空白,表示该行是注释,会在处理时被忽略。

   类型为 data,表示该行是事件所包含的数据。以 data 开头的行可以出现多次。所有这些行都是该事件的数据。

   类型为 event,表示该行用来声明事件的类型,即事件名称。浏览器在收到数据时,会产生对应名称的事件。

   类型为 id,表示该行用来声明事件的标识符。

   类型为 retry,表示该行用来声明浏览器在连接断开之后进行重连的等待时间。

   data: china // 该事件仅包含数据
   data: Beijing // 该事件包含数据与事件标识
   id: 100
   event: myevent // 该事件指定了名称
   data:shanghai
   id: 101
   : this is a comment // 该事件具有注释、名称,且包含两行数据
   event:city
   data: guangzhou
   data: shenzhen
   

   事件标识 id 作用
   如果服务端发送的事件中包含事件标识 id,那么浏览器会将最近一次接收到的事件标识id 记录到 HTTP 头的 Last-Event-ID 属性中。如果浏览器与服务端的连接中断,当浏览器再次连接时,会将 Last-Event-ID 记录的事件标识 id 发送给服务端。服务器端通过浏览器端发送的事件标识 id 来确定将继续连接哪个事件。

EventSource 对象

   对于服务端发送的带有事件的响应,浏览器需要在 JavaScript 中使用 EventSource 对象进行处理。EventSource 使用的是标准的事件监听器方式(注意,这里的事件并不是响应中所带的事件,而是浏览器上所发生的事件)。当相应的事件发生时,只需使 EventSource 对象调用相应的事件处理方法即可。EventSource 提供了三个标准事件。

SSE举例

定义一个普通 Servlet

@WebServlet("/common")
public class CommonServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        PrintWriter out = response.getWriter();
        for (int i = 0; i < 10; i++) {
            out.print("data:"+ i + "\n");
            out.print("\r\n");
            out.flush();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
定义默认事件SSE Servlet  DefaultEventServlet

@WebServlet("/sse/default")
public class DefaultEventServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/event-stream;charset=utf-8");
        PrintWriter out = response.getWriter();
        for (int i = 0; i < 10; i++) {
            out.print("data:"+ i + "\n");
            out.print("\r\n");
            out.flush();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

访问 Servlet

   分别在浏览器访问两个 Servlet,可以看到两种不同的效果:对 CommonServlet 的访问是,浏览器会被阻塞,然后一下将所有数据显示;对 DefaultEventServlet的访问是,浏览器会逐条显示数据,未出现阻塞现象。这个逐条显示的内容即为事件流中的内容。

再定义并访问 Servlet

@WebServlet("/sse/custome")
public class CustomeEventServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/event-stream;charset=utf-8");
        PrintWriter out = response.getWriter();
        for (int i = 0; i < 10; i++) {
            out.print("event:zjut\n");
            out.print("data:"+ i + "\n");
            out.print("\r\n");
            out.flush();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

访问 Servlet

   对 CustomeEventServlet 的访问是,浏览器会逐条显示数据,未出现阻塞现象。与DefaultServlet 的访问效果相同,但事件流内容多了事件名称。

定义default.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
    <script type="text/javascript">
        //创建EventSource对象,指定事件源为sse/default请求
        var es = new EventSource("sse/default");
        //当接收到服务端发送来的事件时触发该方法执行
        es.onmessage = function (evt) {
            //参数一:日志名称,随意
            //参数二:事件中携带的数据
            //参数三:事件本身
            console.log("sse-msg", evt.data, evt)
            if (evt.data == 9){
                es.close();
            }
        }
    </script>
</head>
<body>
 
</body>
</html>

访问

   打开浏览器后,F12 打开开发者工具栏,在其中找到 Console 控制台,可看到接收到的服务端发送来的事件内容。

定义custome.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
    <script type="text/javascript">
        //创建EventSource对象,指定事件源为sse/custome请求
        var es = new EventSource("sse/custome");
        //当接收到服务端发送来的事件时触发该方法执行
 
        es.addEventListener("zjut", function (evt) {
            console.log("zjut-msg", evt.data, evt);
            if (evt.data == 9){
                es.close();
            }
        });
    </script>
</head>
<body>
 
</body>
</html>

访问
在这里插入图片描述

Reactive Stream

   RxJava 是一种响应式编程库,产生于响应式流规范之前,使用比较麻烦。

RxJava2 是对 RxJava 的改进,产生于响应式流规范之后,其兼顾了响应式流规范,同时又兼容了 RxJava,所以其使用起来仍不是很方便。
Reactor 是完全基于响应式流规范的、全新的响应式编程库,使用更为直观易懂。是Spring5 中响应式编程的基础。

Reactive Stream

推拉模型与背压

   在流处理机制中,push(推送)模型和 pull(拉取)模型是最常见的。push 模型中,发布者将元素主动推送给订阅者。而 pull 模式中,订阅者会向发布者主动索要。在同步式系统中发布者与订阅者的工作效率相当,发布者发布一个消息后阻塞,等待订阅者消费。订阅者消费完后,订阅者阻塞,等待发布者发布。这种同步式处理方式效率很低。一般使用的是异步消息处理机制。即发布者发布消息,与消费者消费消息的速度是不一样的。那么它们间是如何协调工作的。

   当订阅者比发布者快时,会出现订阅者无消息可消费的情况。在同步数据处理机制中订阅者需无限期等待,直到有消息可用。但在异步处理机制中,订阅者无需阻塞,其继续处理其他任务即可。当出现了准备就绪的消息时,发布者会将它们异步发送给订阅者。所以,在异步处理机制中,这种情况并不会对系统性能产生负面影响。

   当发布者比订阅者快时,有两大类解决方案。

   一类解决方案是改变订阅者。要么使订阅者拥有一个无边界缓冲区来保存快速传入的消息,要么让订阅者将它无法处理的消息丢弃。

   另一类解决方案是改变发布者。这类解决方案采用的策略称为背压(Back Pressure)策略。订阅者告诉发布者让其减慢发布速度并保持消息,直到订阅者准备好处理更多消息。使用背压策略可确保较快的发布者不会压制较慢的订阅者。但该解决方案要求发布者要拥有无限制缓冲区,以确保发布者可以一直生产和保存消息。当然,发布者也可以实现有界缓冲区以保存有限数量的消息。但若缓冲区满,则需要放弃这些消息。不过,可以让发布者将放弃的消息再发布,直到订阅将其消费。

响应式流

   响应式流从 2013 年开始,作为提供非阻塞背压的异步流处理标准的倡议,旨在解决处理元素流(即消息流、数据流)的问题——如何将元素流从发布者传递到订阅者,而不需要发布者阻塞,或订阅者有无限制的缓冲区或丢弃。

   响应式流模型非常简单:订阅者向发布者发送多个元素的异步请求,发布者向订阅者异步发送多个或稍少的元素。响应式流会在 pull 模型和 push 模型流处理机制之间动态切换。当发布者快、订阅者慢时,它使用 pull 模型;当发布者慢、订阅者快时,它使用 push 模型。即谁慢谁占主动。

   2015 年出版了用于处理响应式流的规范和 Java API。

JDK 与响应式流的关系

   Reactive Steam 是一种数据处理机制,是一种思想,一套解决方案。JDK 使用 Flow 类定义了这种规范,该规范中仅包含四个接口。这套规范最初是定义在 JDK9 中的。不过,JDK9 昙花一现,不到一年时间 Oracle 官方已经不提供 JDK9 的支持与下载了,并建议 JDK9的用户使用 JDK10,所以可以说,Reactive Stream 是 JDK10 的特性。

响应式流接口

   在 JDK 中 Flow 类中声明了四个响应式流接口。

Publisher 接口

  Publisher,即发布者,是有序消息的生产者。它根据收到的请求向订阅者发布消息。

Subscriber接口

   Subscriber,即订阅者,从发布者那里订阅并接收消息。发布者向订阅者发送订阅令牌(Subscription)。使用订阅令牌,订阅者可以从发布者那里请求多个消息。当消息元素准备就绪时,发布者向订阅者发送多个或更少的元素。然后订阅者可以再次请求更多的消息元素,或取消订阅。一个发布者可能需要处理来自多个订阅者的请求。

Subscription接口

   Subscription(订阅费),订阅令牌。当订阅请求成功时,发布者将其传递给订阅者。订阅者使用订阅令牌与发布者进行交互,例如请求更多的消息元素或取消订阅。

三个接口的关系
在这里插入图片描述

Processor<T, R> 接口

  Processor,即处理器,充当订阅者和发布者的处理阶段。Processor 接口继承了 Publisher和 Subscriber 接口。它用于转换发布者/订阅者管道中的元素。Processor<T, R>会将来自于发布者的 T 类型的消息数据,接收并转换为 R 类型的数据,并将转换后的 R 类型数据发布给订阅者。一个发布者可以拥有多个处理者。

在这里插入图片描述

“发布- 订阅”模式响应式流编程

定义订阅者

public class Subscriber implements Flow.Subscriber {
    //声明订阅令牌
    private Flow.Subscription subscription;
    //当发布者第一次发布消息时会自动调用该方法
    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        this.subscription = subscription;
        //设置订阅者向发布者(令牌)订阅消息的数量
        this.subscription.request(10);
    }
 
    //订阅者每接收一次消息数据,就会自动调用一次该方法
    //订阅者对数据的消费发生在这里
    @Override
    public void onNext(Object item) {
        System.out.println("当前订阅者正在消费消息为:" + item);
        try {
            TimeUnit.MILLISECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //设置订阅者向发布者再次订阅消息数量,即每消费一条消息,则向发布者订阅多条消息
        this.subscription.request(10);
        //当满足某条件时取消订阅
//        if (){
//            this.subscription.cancel();
//        }
    }
    //当订阅过程中出现异常会自动调用该方法
    @Override
    public void onError(Throwable throwable) {
        throwable.printStackTrace();
        //取消订阅关系
        this.subscription.cancel();
    }
    //当令牌中所有消息全部消费完毕时会自动调用该方法
    @Override
    public void onComplete() {
        System.out.println("所有消息消费完毕");
    }
}
定义测试类

public class SubcriberTest {
    public static void main(String[] args) {
        //创建发布者
        SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();
        //创建订阅者
        Subscriber subscriber = new Subscriber();
        //建立订阅关系
        publisher.subscribe(subscriber);
        //发布者生产并发送消息
        for (int i = 0; i < 300; i++) {
            int item = new Random().nextInt(100);
            //发布消息,发布者缓存满时submit()方法阻塞
            System.out.println("生产出第" + i + "条消息" + item);
            //因为发布者不具有无限缓冲区
            publisher.submit(item);
        }
        //关闭发布者
        publisher.close();
        //为了防止消息没有消费完毕主线程完毕的情况发生
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

“ 发布- 处理- 订阅” 模式响应流编程

   这里要使用处理器 Processor。该处理器的处理逻辑是,将发布者发布的大于 50 的消息过滤掉,并将小于 50 的 Integer 消息转换为 String 后发布给订阅者。

修改测试类

   该类中定义了发布者、处理器与订阅者,并使发布者与处理器间建立了订阅关系,使处理器与订阅者间建立了订阅关系。
public class SubcriberTest {
    public static void main(String[] args) {
        //创建发布者
        SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();
        //创建订阅者
        Subscriber subscriber = new Subscriber();
        //创建处理器
        Processor processor = new Processor();
        //建立订阅关系
        publisher.subscribe(processor);
        processor.subscribe(subscriber);
        //发布者生产并发送消息
        for (int i = 0; i < 300; i++) {
            int item = new Random().nextInt(100);
            //发布消息,发布者缓存满时submit()方法阻塞
            System.out.println("生产出第" + i + "条消息" + item);
            //因为发布者不具有无限缓冲区
            publisher.submit(item);
        }
        //关闭发布者
        publisher.close();
        //为了防止消息没有消费完毕主线程完毕的情况发生
        try {
            TimeUnit.SECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

定义处理器类

public class Processor extends SubmissionPublisher<String> implements Flow.Processor<Integer, String> {
    private Flow.Subscription subscription;
 
    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        this.subscription = subscription;
        //设置订阅者向发布者(令牌)订阅消息的数量
        this.subscription.request(10);
    }
 
    //将发布者发布大于50的消息过滤掉
    //并将小于50的Integer消息转换为String后发布给订阅者
    @Override
    public void onNext(Integer item) {
       if (item < 50){
           this.submit("该消息处理完毕" + item);
       }
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.subscription.request(10);
    }
    @Override
    public void onError(Throwable throwable) {
        throwable.printStackTrace();
        this.subscription.cancel();
    }
    //当令牌中的所有消息全部处理完毕时会自动调用该方法
    @Override
    public void onComplete() {
        System.out.println("所有消息处理完毕");
    }
}

修改订阅者类

public class Subscriber implements Flow.Subscriber<String> {
    //声明订阅令牌
    private Flow.Subscription subscription;
    //当发布者第一次发布消息时会自动调用该方法
    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        this.subscription = subscription;
        //设置订阅者向发布者(令牌)订阅消息的数量
        this.subscription.request(10);
    }
 
    //订阅者每接收一次消息数据,就会自动调用一次该方法
    //订阅者对数据的消费发生在这里
    @Override
    public void onNext(String item) {
        System.out.println("当前订阅者正在消费消息为:" + item);
        try {
            TimeUnit.MILLISECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //设置订阅者向发布者再次订阅消息数量,即每消费一条消息,则向发布者订阅多条消息
        this.subscription.request(10);
        //当满足某条件时取消订阅
//        if (){
//            this.subscription.cancel();
//        }
    }
    //当订阅过程中出现异常会自动调用该方法
    @Override
    public void onError(Throwable throwable) {
        throwable.printStackTrace();
        //取消订阅关系
        this.subscription.cancel();
    }
    //当令牌中所有消息全部消费完毕时会自动调用该方法
    @Override
    public void onComplete() {
        System.out.println("所有消息消费完毕");
    }
}

WebFlux 开发

   需求:通过 WebFlux 实现对 MongoDB 的 CRUD 操作。因为 WebFlux 不支持 RDBMS(关系型数据库管理系统),所以只能从 NoSQL 中选择。Redis 是键值对型的内存数据库,而MongoDB 则与关系型数据库更为相近,其也存在表的概念。所以这里选择使用 MongoDB。对于该功能的实现,采用了两种实现方式:使用传统处理器开发,与使用 Router Functions开发。

使用传统处理器开发

   使用传统处理器开发,指的是使用@Controller 注解的类作为处理器类,使用@RequestMapping 进行请求与处理器方法映射,来开发 WebFlux 的方式。

基本结构搭建

创建工程

   创建一个 webflux-common,Spring Boot 的版本要求最低为 2.1.5。不要添加原来的web 依赖,而是要添加Reactive Web,即 flux 依赖,并添加上 Lombok依赖。另外,再添加上 Reactive MongoDB 依赖。

修改启动类

@EnableReactiveMongoRepositories//开启MongoDB的spring-data-jpa
@SpringBootApplication
public class WebfluxCommonApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(WebfluxCommonApplication.class, args);
    }
 
}

定义实体类

@Data
//指定在MogoDB中生成的表
@Document(collection = "t_student")
public class Student {
 
    @Id//会在生成的表中设置id为主键
    private String id; //MongoDB中的主键一般为String类型
    private String name;
    private Integer age;
}

定义处理器

   当处理器方法返回 Flux 时,一般都会定义两种数据返回形式:一次性返回,与 SSE实时性返回
@RestController
@RequestMapping("/student")
public class StudentController {
    @Autowired
    private StudentRepository studentRepository;
 
    //一次性返回数据
    @GetMapping("/all")
    public Flux<Student> getAll(){
        return studentRepository.findAll();
    }
 
    //以SSE形式实时返回数据
    @GetMapping(value = "/sse/all", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<Student> getSseAll(){
        return studentRepository.findAll();
    }
}

定义 Repository 接口

//第一个泛型是该Repository操作的对象类型
//第二个泛型是操作对象主键的类型
public interface StudentRepository extends ReactiveMongoRepository<Student, String> {
}

数据库连接配置

spring:
  data:
    mongodb:
      uri: mongodb://localhost:27017/test
   这是访问 mongoDB 的数据库连接,其中 test 为要连接的数据库名。

在这里插入图片描述

CURD 的实现

添加对象

改处理器 修改处理器交 (提交 Form表单)

   定义处理器方法时首先要考虑该方法的返回值。对于执行添加操作的处理器方法,可以让其返回添加成功的这个对象本身。所以,可以在处理器中添加如下处理器方法。需要注意,在 REST 协议中,添加操作需要使用 POST 请求提交方式,所以这里要使用@PostMapping 注解。
//添加数据 表单方式提交数据
//save()可以完成插入与修改操作,执行不同操作差别是其id是否为null
//id为null执行插入操作,否则执行修改操作
@PostMapping("/save")
public Mono<Student> saveStudent(Student student){
    return studentRepository.save(student);
}
   对于 Spring-Data-JPA 中的 save()方法需要注意,若参数对象的 id 属性为 Null,则 save()为添加操作,底层执行 insert into 语句;若参数对象的 id 属性不为 Null,则 save()为修改操作,底层执行 update 语句。 

测试

   选择POST请求方式,填写请求地址,使用表单提交参数

在这里插入图片描述

修改处理器(提交JSON数据)

   在处理器中添加如下处理器方法。由于数据来自于JSON,所以参数中需要添加@RequestBody。
//添加数据
@PostMapping("/save2")
public Mono<Student> saveStudent2(@RequestBody Student student){
    return studentRepository.save(student);
}

测试

   使用JSON提交数据

在这里插入图片描述

无状态数据 删除

   所谓无状态删除,即指定的要删除的对象无论是否存在,其响应码均为 200,无法知道是否真正删除了数据。

修改处理器

   对于执行删除操作的处理器方法,其可以是没有返回值的。Spring-Data-JPA 的 deleteById()方法是没有返回值,但在 WebFlux 编程中需要将其包装为 Mono,但泛型为 Void。

   需要注意,在 REST 协议中,删除操作需要使用 DELETE 请求提交方式,所以这里要使用@DeleteMapping 注解。
//无状态删除
@DeleteMapping("/delecomm/{id}")
public Mono<Void> deleteStudent(@PathVariable("id") String id){
    return studentRepository.deleteById(id);
}

测试

在这里插入图片描述

有状态数据删除

   所谓有状态删除,即指若删除的对象存在,且删除成功,则返回响应码 200,否则返回响应码 404。通过响应码就可以判断删除操作是否成功。

修改处理器

   编写该处理器的思路是:首先根据 id 对该对象进行查询,若存在则先将该查询结果删除,然后再将其映射为状态码 200;若不存在,则将查询结果映射为状态码 404。

   对于该处理器,需要注意以下几点:

   ResponseEntity 可以封装响应体中所携带的数据及响应码,其泛型用于指定携带数据的类型。若响应体中没有携带数据,则泛型为 Void。本例中要返回的 ResponseEntity 中仅封装了响应码不携带任何数据,所以泛型为 Void。响应码只能采用 HttpStatus 枚举类型常量表示,这是 ResponseEntity 的构造器所要求的。

   为什么做映射时使用 flatMap(),不使用 map()?首先这两个方法都是 Mono 的方法,不是 Stream 的方法,与 Stream 的两个同名方法无关,但均是做映射的。若需要对对象数据先执行操作后再做映射,则使用 flatMap();若纯粹是一种数据映射,没有数据操作,则使用 map()。

   在 Mono 的访求中,对于没有返回值的方法,若想为其添加返回值,则可链式调用 then()方法,由 then()方法返回想返回的值。对于本例,由于 Spring-Data-JPA 的 delete()方法没有返回值,所以这里使用 then()为其添加返回值。
//有状态删除,删除成功则返回响应码200,否则返回响应404
//ResponseEntity可以封装相应体中的数据及响应码
//Mono的map()与flatMap()方法均可用于做元素映射,选择的标准是:
//一般情况下映射过程中需要再执行一些操作时,需要选择使用flatMap();若仅仅是元素映射,则无需执行一些操作,则选择map()
//若一个方法没有返回值,但是又需要让其具有返回值,则可为其添加then方法,该方法的返回值将作为这个方法的返回值
//defaultIfEmpty()执行的条件是,其Mono中没有元素则执行
@DeleteMapping("/del/{id}")
public Mono<ResponseEntity<Void>> deleteStatusStudent(@PathVariable("id") String id){
    return studentRepository.findById(id)
            .flatMap(stu->studentRepository.delete(stu).
                    then(Mono.just(new ResponseEntity<Void>(HttpStatus.OK))))
            .defaultIfEmpty(new ResponseEntity<Void>(HttpStatus.NOT_FOUND));
}

测试(删除存在的数据)

在这里插入图片描述

测试(删除不存在的数据)

   再次提交前面的删除请求即可,因为该对象已经在上次请求时被删除了。

在这里插入图片描述

修改数据

   需要注意,在 REST 协议中,修改操作需要使用 PUT 请求提交方式,所以这里要使用@PutMapping 注解。

修改处理器

   对于执行修改操作的处理器方法,可以这定义其返回值:若修改成功,则返回修改后的对象数据;若指定的 id 对象不存在,则返回 404。
//修改操作,若修改成功,则返回修改后对象数据及200,若指定的id对象不存在,则返回404
@PutMapping("/update/{id}")
public Mono<ResponseEntity<Student>> updateStudent(@PathVariable("id") String id, @RequestBody Student student){
 
    return studentRepository.findById(id)
            .flatMap(stu->{
                stu.setName(student.getName());
                stu.setAge(student.getAge());
                return studentRepository.save(stu);
            })
            .map(stu->new ResponseEntity<Student>(stu, HttpStatus.OK))
            .defaultIfEmpty(new ResponseEntity<Student>(HttpStatus.NOT_FOUND));
}

测试

在这里插入图片描述

根据年龄上下限查询

  前面的查询,要么是查询所有,与字段无关,要么是根据 id 查询,而 id 属性属于业务无关属性。这些查询在 Spring-Data-JPA 的 Repository 接口中已经定义好了,可以直接使用。但就像根据姓名、年龄等字段进行查询,Repository 接口中就不可能事先定义好,因为这些字段属性业务相关字段,Repository 接口不可能事先知道当前业务中都具有哪些字段。所以,这些业务相关字段的查询,需要程序员在自定义的 Repository 接口中自己声明,然后Spring-Data-JPA 会根据指定的字段名称自动实现该方法。

修改 StudentRepository 接口

/**
 * 根据年龄上下限查询
 * @param below 年龄上限(不包含此边界)
 * @param top 年龄下限(不包含此边界)
 * @return
 */
Flux<Student> findByAgeBetween(int below, int top);

修改处理器

//根据年龄的上下限进行查询--一次性返回
@GetMapping("/age/{below}/{top}")
public Flux<Student>  findStudentByAge(@PathVariable("below") int below, @PathVariable("top") int top){
    return studentRepository.findByAgeBetween(below, top);
}
 
//根据年龄的上下限进行查询
@GetMapping(value = "/sse/age/{below}/{top}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Student>  findStudentByAgeSSE(@PathVariable("below") int below, @PathVariable("top") int top){
    return studentRepository.findByAgeBetween(below, top);
}

测试

在这里插入图片描述

使用 MongoDB 的原始查询语句

回顾 MongoDB 原始操作

   在命令行窗口中何意目录下键入 mongo 命令,打开 Mongo Shell 窗口。

在这里插入图片描述
在这里插入图片描述

   查看指定表中的所有数据 

在这里插入图片描述

   查寻年龄大于等于 21 且小于 25 的所有学生。 

在这里插入图片描述

修改 StudentRepository 接口

   在接口中添加如下抽象方法。
/**
 * 使用MongoDB原始查询实现根据年龄上下限查询
 * @param below
 * @param top
 * @return
 */
@Query("{'age':{'$gte':?0, '$lt':?1}}")
Flux<Student> queryByAge(int below, int top);

修改处理器

@GetMapping("/query/age/{below}/{top}")
public Flux<Student> queryStudentByAge(@PathVariable("below") int below, @PathVariable("top") int top){
    return studentRepository.queryByAge(below, top);
}
 
@GetMapping(value = "/sse/query/age/{below}/{top}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Student> queryStudentByAgeSSE(@PathVariable("below") int below, @PathVariable("top") int top){
    return studentRepository.queryByAge(below, top);
}

测试

在这里插入图片描述

参数校验

   为了保证数据在进入到业务运算时的正确性,一般会对参数首先进行校验。

使用 Hibernate 注解校验

   Hibernate Validator 中已经定义好了很多通用的校验注解,可以直接使用。

代码中添加注解

修改实体类 Student

   在要验证的属性上添加相应注解。

**@Data
//指定在MogoDB中生成的表
@Document(collection = “t_student”)
public class Student {

@Id//会在生成的表中设置id为主键
private String id; //MongoDB中的主键一般为String类型
@NotBlank(message = "姓名不能为空")
private String name;
@Range(min = 10, max = 50, message = "年龄必须在{min} - {max}范围")
private Integer age;

}**
修改处理器

   修改具有 Student 类型参数的处理器方法,即两个添加数据方法,及修改数据方法的参数上添加@Valide 注解。
//添加数据
//save()可以完成插入与修改操作,执行不同操作差别是其id是否为null
//id为null执行插入操作,否则执行修改操作
@PostMapping("/save")
public Mono<Student> saveStudent(@Valid Student student){
    return studentRepository.save(student);
}
 
//添加数据
@PostMapping("/save2")
public Mono<Student> saveStudent2(@Valid @RequestBody Student student){
    return studentRepository.save(student);
}

测试

   测试添加与修改。提交正常数据,运行成功,返回码 200。

在这里插入图片描述

添加校验切面

   前面验证失败的结果是 400 异常,但并没有指出是哪个属性出现了异常,出现了什么异常。即前面校验注解中的校验异常信息并没有显示。可以通过添加切面的方式给出校验异常详情。

添加切面类

   对于该切面类的定义,需要注意以下几点:

   由于 getFieldErrors()方法获取到的是一个 List 集合,所以 stream()后的流就是一个集合数据流,且集合元素为异常对象。

   map()方法用于将集合数据流中的每一个元素,即异常对象映射为字符串形式:发生异常的属性名 加 指定的异常信息。

   map()的最终结果仍为一个集合流,只不过集合流中的元素由异常对象变为了字符串。但这里需要的最终结果不是字符串集合,而是一个字符串,所以需要使用 reduce()将集合流缩减为一个字符串。

   reduce()的第一个参数为默认值,即在流中元素为空时所使用的值,可以避免异常的抛出。第二个参数为BinaryOperator,两个输入一个输出,且类型相同。由两个输入最终变为了一个输出,就达到了缩减 reduce 的效果了。
//验证失败,则返回失败信息及400
//表示当前类为通知(切面),其连接点为处理器方法
@ControllerAdvice
public class ParameterValidAdvice {
    @ExceptionHandler
    public ResponseEntity<String> validHandle(WebExchangeBindException ex){
        return new ResponseEntity<String>(exToStr(ex), HttpStatus.BAD_REQUEST);
    }
 
    private String exToStr(WebExchangeBindException ex) {
        return ex.getFieldErrors()
                .stream()
                .map(e -> e.getField() + ":"  + e.getDefaultMessage())
                .reduce("", (s1, s2) -> s1 + "\n" + s2);
    }
}

测试

   无论测试添加功能还是修改功能,只要提交的数据不合法,则响应就会给出具体的异常详情。

在这里插入图片描述

自定义校验逻辑

   需要使 Student 的 name 值不能是 admin 或 administrator,则可自己定义校验逻辑

定义异常类

@Getter
@Setter
@NoArgsConstructor
public class StudentException extends RuntimeException {
    private String errField;
    private String errValue;
    public StudentException(String message, String errField, String errValue){
        super(message);
        this.errField = errField;
        this.errValue = errValue;
    }
}
定义校验工具类

public class NameValidateUtil {
    //无效姓名列表
    private static final String[] INVALID_NAMES = {"admin", "administrator"};
 
    public static void validateName(String name){
        Stream.of(INVALID_NAMES)
                .filter(invalidName-> name.equalsIgnoreCase(invalidName))
        .findAny()
        .ifPresent(invalidName->{
            throw new StudentException("name", invalidName, "使用了非法姓名");
        });
    }
}

修改校验切面

   在校验切面类中添加如下异常处理方法。
@ExceptionHandler
public ResponseEntity<String> validHandle(StudentException ex){
    String msg = ex.getMessage() + "[" + ex.getErrValue() + ex.getErrField() + "]";
    return new ResponseEntity<String>(msg, HttpStatus.BAD_REQUEST);
}

修改处理器

   修改具有 Student 类型参数的处理器方法,即两个添加数据方法,及修改数据方法,在其中添加校验代码。
//添加数据
//save()可以完成插入与修改操作,执行不同操作差别是其id是否为null
//id为null执行插入操作,否则执行修改操作
@PostMapping("/save")
public Mono<Student> saveStudent(@Valid Student student){
    //验证姓名合法性
    NameValidateUtil.validateName(student.getName());
    return studentRepository.save(student);
}
 
//添加数据
@PostMapping("/save2")
public Mono<Student> saveStudent2(@Valid @RequestBody Student student){
    //验证姓名合法性
    NameValidateUtil.validateName(student.getName());
    return studentRepository.save(student);
}
 

//修改操作,若修改成功,则返回修改后对象数据及200,若指定的id对象不存在,则返回404
@PutMapping("/update/{id}")
public Mono<ResponseEntity<Student>> updateStudent(@PathVariable("id") String id, @Valid @RequestBody Student student){
    //验证姓名合法性
    NameValidateUtil.validateName(student.getName());
    return studentRepository.findById(id)
            .flatMap(stu->{
                stu.setName(student.getName());
                stu.setAge(student.getAge());
                return studentRepository.save(stu);
            })
            .map(stu->new ResponseEntity<Student>(stu, HttpStatus.OK))
            .defaultIfEmpty(new ResponseEntity<Student>(HttpStatus.NOT_FOUND));
}

测试

   无论测试添加功能还是修改功能,只要提交的姓名不合法,则响应就会给出具体的异常详情。

在这里插入图片描述
在这里插入图片描述

使用 Router Functions

   使用 Router Functions 开发,指的是使用由@Component 注解的普通类作为处理器类,使用 Router 进行请求与处理器方法映射,来开发 WebFlux 的方式。

   该开发方式所开发的 WebFlux 不仅可以运行在 Servlet 3.1 容器上(例如 Tomcat),还可以运行在 Netty、Jetty 等容器中。不过,在开发过程中会出现一个问题:Servlet 中的HttpServletRequest、HttpServletResponse,在好多 Web 容器中并不支持,例如 Netty。为了能够统一请求与响应,Router Functions 专门定义了所有容器都技术的 ServerRequest 与ServerResponse 作为请求与响应对象。

基本结构搭建

创建工程

复制代码

   将前面工程中的实体类 Student、仓库接口 StudentRepository,及 application.yml 文件复制到当前工程。

定义处理器

   这里的处理器并不是之前使用@Controller 注解的处理器类,而是一个使用@Component注解的普通的类。其中的方法要求必须是处理器函数,即 HandlerFunction 接口中定义的方法:输入一个 ServerRequest,返回一个使用 Mono 封装的 ServerResponse。



   不过,需要注意的是,该处理器类并不需要实现这个 HandlerFunction 接口。HandlerFunction 接口只要是用于定义处理器函数的,而非让实现的。
@Component
public class StudentHandler {
    @Autowired
    private StudentRepository repository;
 
    public Mono<ServerResponse> findAllHandle(ServerRequest request){
        return ServerResponse
                //指定响应码(返回BodyBuilder的方法称为相应体设置中间方法)
                .ok()
                //指定响应体中的内容类型
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                //响应体设置终止方法,构建响应体
                .body(repository.findAll(), Student.class);
    }
}

定义路由器

   Router 的作用是将用户请求 URI 映射到处理器中的处理器函数,即做映射关系,相当于SpringMVC 的处理器中的@RequestMaping。而处理器中的处理器函数则相当于SpringMVC 的处理器中的处理器。所以可以这样简单地理解 Router Function 编程与 SpringMVC 的原始编程间的关系:处理器函数 + Router = @Controller 处理器。
@Configuration
public class StudentRouter {
    @Bean
    public RouterFunction<ServerResponse> customRouter(StudentHandler handler){
        return RouterFunctions
                .nest(RequestPredicates.path("/student"),
                        RouterFunctions.route(RequestPredicates.GET("/all"), handler::findAllHandle));
    }
}

测试
在这里插入图片描述

CURD的实现

添加对象

修改处理器

在处理器中添加如下处理器函数。

public Mono<ServerResponse> saveHandle(ServerRequest request){
    //从请求中获取添加数据,并将其封装为指定类型的对象,存放到Mono中
    Mono<Student> studentMono = request.bodyToMono(Student.class);
    return ServerResponse
            .ok()
            .contentType(MediaType.APPLICATION_JSON_UTF8)
            .body(repository.saveAll(studentMono), Student.class);
}

修改路由器

   在路由方法中添加如下路由规则。
@Bean
public RouterFunction<ServerResponse> customRouter(StudentHandler handler){
    return RouterFunctions
            .nest(RequestPredicates.path("/student"),
                    RouterFunctions.route(RequestPredicates.GET("/all"), handler::findAllHandle)
                    .andRoute(RequestPredicates.POST("/save")
                            .and(RequestPredicates.accept(MediaType.APPLICATION_JSON_UTF8)), handler::saveHandle));
}

测试
在这里插入图片描述

删除对象

修改处理器

   在处理器中添加如下处理器函数。
public Mono<ServerResponse> delHandle(ServerRequest request){
    //从请求路径中获取id
    String id = request.pathVariable("id");
    return repository.findById(id)
            .flatMap(stu -> repository.delete(stu)
                    .then(ServerResponse.ok().build()))
            .switchIfEmpty(ServerResponse.notFound().build());
    /*
        1)defaultIfEmpty()中的参数是Mono中的值
        若其调用者Mono中没有任何元素,则会将此方法中的参数作为该调用者Mono中的元素
        2)switchIfEmpty()中徐额参数是一个Mono
        若其调用者Mono中没有任何元素,则直接将参数作为返回值,摒弃了原掉用者Mono
        即调用者与返回值Mono不是同一个对象
 
     */
}

修改路由器

   在路由方法中添加如下路由规则。
@Bean
public RouterFunction<ServerResponse> customRouter(StudentHandler handler){
    return RouterFunctions
            .nest(RequestPredicates.path("/student"),
                    RouterFunctions.route(RequestPredicates.GET("/all"), handler::findAllHandle)
                    .andRoute(RequestPredicates.POST("/save")
                            .and(RequestPredicates.accept(MediaType.APPLICATION_JSON_UTF8)), handler::saveHandle)
                    .andRoute(RequestPredicates.DELETE("/del/{id}"), handler::delHandle));
}

测试

在这里插入图片描述

修改对象

修改处理器

   这里实现的逻辑是:若指定的 id 对象不存在,则指定 id 作为新的 id 完成插入;否则完成修改。在处理器中添加如下处理器函数。
public Mono<ServerResponse> updateHandle(ServerRequest request){
    //从请求路径中获取id
    String id = request.pathVariable("id");
    //从请求中获取添加数据,并将其封装为指定类型的对象,存放到Mono中
    Mono<Student> studentMono = request.bodyToMono(Student.class);
 
    return studentMono
            .flatMap(stu -> {
                stu.setId(id);
                return ServerResponse
                        .ok()
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .body(repository.save(stu), Student.class);
            });

修改路由器

   在路由方法中添加如下路由规则。
@Bean
public RouterFunction<ServerResponse> customRouter(StudentHandler handler){
    return RouterFunctions
            .nest(RequestPredicates.path("/student"),
                    RouterFunctions.route(RequestPredicates.GET("/all"), handler::findAllHandle)
                    .andRoute(RequestPredicates.POST("/save")
                            .and(RequestPredicates.accept(MediaType.APPLICATION_JSON_UTF8)), handler::saveHandle)
                    .andRoute(RequestPredicates.DELETE("/del/{id}"), handler::delHandle)
                    .andRoute(RequestPredicates.PUT("/update/{id}")
                            .and(RequestPredicates.accept(MediaType.APPLICATION_JSON_UTF8)), handler::updateHandle));
}

测试

   没有指定 id 对象的,完成插入。

在这里插入图片描述

   存在指定 id 对象的完成修改。将 JSON 数据修改一下后直接提交即可将上一次刚插入的数据进行修改。 

在这里插入图片描述

参数校验

   由于这里的处理器函数只有 ServerRequest 一个参数,所以无法使用注解方式的参数校验,即无法使用 Hibernate Validator。但可以使用自定义的参数校验。

   将原来工程中的 NameUtil 类与 StudentException 类复制到当前工程中。

   修改添加处理函数。

在使用@Controller处理器时,处理器方法可以直接获取到请求参数封装的Student对象,但在这里,从请求中获取到的是由 Student 对象封装的 Mono 对象。若要验证 name 属性,则需要获取到Mono中封装的 Student 对象,如何获取呢?Mono 中有个 block()方法可以获取,但其会堵塞请求,且从 spring boot 2.0.1 版本开始其会报错。可能通过从 Mon 对象流中操作 Student 的方式对 name 属性进行验证。

//添加(对name属性进行遍历)
public Mono<ServerResponse> saveHandleValid(ServerRequest request){
    //从请求中获取添加数据,并将其封装为指定类型的对象,存放到Mono中
    Mono<Student> studentMono = request.bodyToMono(Student.class);

    return studentMono
            .flatMap(stu -> {
                //对name合法性进行验证
                NameValidateUtil.validateName(stu.getName());
                return ServerResponse
                        .ok()
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .body(repository.save(stu), Student.class);
            });
}
   **修改“修改处理器函数”。**
public Mono<ServerResponse> updateHandle(ServerRequest request){
    //从请求路径中获取id
    String id = request.pathVariable("id");
    //从请求中获取添加数据,并将其封装为指定类型的对象,存放到Mono中
    Mono<Student> studentMono = request.bodyToMono(Student.class);

    return studentMono
            .flatMap(stu -> {
                //验证姓名的合法性
                NameValidateUtil.validateName(stu.getName());
                stu.setId(id);
                return ServerResponse
                        .ok()
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .body(repository.save(stu), Student.class);
            });
}

定义异常处理器

@Component
//默认情况下系统内部定义的异常处理器的优先级要高于自定义异常处理器。可通过@Order指定优先级级别
//指定的数值越小,优先级越高,支持负数
@Order(-100)
public class CustomeExceptionHandler implements WebExceptionHandler {
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        //获取Http响应对象
        ServerHttpResponse response = exchange.getResponse();
        //设置响应码
        response.setStatusCode(HttpStatus.BAD_REQUEST);
        //设置响应普通文本类型
        response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
        //格式化异常方法
        String message = this.formatExceptionMessage(ex);
        //获取数据缓存对象
        DataBuffer buffer = response.bufferFactory().wrap(message.getBytes());
        //给出响应
        return response.writeWith(Mono.just(buffer));
    }

    private String formatExceptionMessage(Throwable ex) {
        String msg = ex.getMessage();
        if (ex instanceof StudentException){
            StudentException e = (StudentException) ex;
            msg = msg + "[" + e.getErrField() + ":" + e.getErrValue() + "]";
        }
        return msg;
    }
}

添加(正常添加)

在这里插入图片描述

测试(异常添加)

在这里插入图片描述

测试(异常修改)

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值