Reactor响应式编程系列(一)- 初识Reactor3

Reactor响应式编程系列导航

前言

临近毕业,去新公司学习,公司主要以Reactor3来做响应式编程,因此准备借此机会对响应式编程进行一个系统的学习。也希望大家能多多交流,毕竟网上对这一块内容的相关博客还是比较少的。

目前准备对Reactor的这么几个方面进行归纳,后期的博客内容也以这些内容为方向来展开,请大家多多关注:

  1. Reactor的基本介绍和使用,Flux和Mono的基本操作。
  2. Publisher,Subscriber的相关内容以及涉及到的队列。
  3. Reactor中的重要角色–>调度器Schedulers。publishOn和subscribeOn方法的解读。
  4. Reactor中的操作符:buffer、filter等用法
  5. Reactor中另一个大类:Processor。
  6. Reactor中的StepVerifier测试类。
  7. 对以上涉及到的类、方法的源码解读。

本文是该系列的第一篇,主要讲一些基本的概念和用法。

一. 响应式编程

什么是响应式编程?先来看下百度百科是这么说的:

响应式编程是一种面向数据流和变化传播的编程范式。 这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。

而我个人理解是这样的:假设我们有个数据监控系统,负责实时的监控某路口人流量的变化。
那么在项目中,一个传统的数据获取方式是这样的:

  1. 假设我们需要的数据都存放在数据库中。那么数据库的数据会经过持久化层(Dao),服务层、Controller层。
  2. 前端则通过不断轮询地方式来调对应的接口,然后页面静态刷新,让页面达到实时展示数据的效果。

说白了,在某种意义上是不是很像Pull模式?也就是消费端去拉取数据。那如果是响应式编程,会变成什么样呢?

后台感知到数据的变化,主动将数据推送给(Push)对应的订阅者,这种模型就是Publisher-Subscriber模型,两者之前存在一条通道,流通的数据叫做响应式流。而响应式流最大的两个特性就是:

  • 异步非堵塞。
  • 流量控制。

而目前实现响应式流规范的Java框架有:RxJava和Reactor。本文主要说Reactor。首先Reactor中,涉及到了上文提及的流量控制这么一个问题:

Reactor 引入观察者模式的设计思想,当所需数据出现或者更新的时候,由Publisher来Push给Subscriber。但是想一想,如果上游的数据更新的过于频繁,而下游的处理数据速度远不及上游数据的推送速度, 那怎么办?

Reactor支持背压操作。 即下游可以向上游发送信号,来控制上游的输出。这就是响应式,除此之外,其还具有如下特点:

  • 可组合性,即可以协调多个异步任务。
  • 提供丰富的操作符。
  • 订阅之前不会产生任何的动作。(即只有订阅的时候才执行操作)
  • 冷热响应。

冷响应:对于每个订阅者,包括在数据源处,数据推送都会从头开始。例如,如果源包装了HTTP调用,则会为每个订阅发出一个新的HTTP请求。
热响应:并不是每个订阅者都可以从头开始接收数据。后面的订阅者会在订阅后接收已推送的信号。

接下来正式开始讲Reactor。

二. Flux 和 Mono

Reactor中,Publisher有两种类型:

  • Flux:表示0…N项的反应异步序列。
  • Mono:表示单值或者空的结果。

这里我们可以看出来,响应式支持无值、一个值或者n个值(包括无限),一个Publisher,即数据发布者可以发出3种信号:

  • 元素值(对应onNext方法)
  • 错误信号(对应onError方法)
  • 完成信号(对应onComplete方法)

并且这里需要大家注意三点:

  1. 若发布者中没有任何元素,那么会直接发送错误或者完成信号,代表这是一个空序列。
  2. 若没有错误或者完成信号,那么这个是一个无限数据流。
  3. 错误信号和完成信号不可共存,但是有个共同点:他们是序列终止的信号。

Flux的转换图(来自官网):
在这里插入图片描述
很直观:

  • Flux序列中可以有n个元素,每个元素都会经过operator操作进行数据的转换然后输出。
  • 如果由于某些原因发生了错误,那么会终止该序列。

Mono的转换图:
在这里插入图片描述

  • 最多发出一项元素,并且以成功信号或者失败信号为终止信号。

2.1 创建序列

首先跟大家说明一点:Mono可以说是一种特殊的Flux。 接下来来看下他们怎么用。

1.创建一个maven项目,并且在pom文件中导入依赖:

<!--reactor的核心包,提供一些核心组件,如Flux和Mono-->
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
    <version>3.1.4.RELEASE</version>
</dependency>
<!--提供Reactor相关测试类的包,如StepVerifierAPI-->
<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-test</artifactId>
    <version>3.1.4.RELEASE</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

2.直接看类:

@org.junit.Test
public void testCreate() {
    // 1.just():可以指定序列中包含的全部元素,创建出来的Flux序列会在发布这些元素之后自动结束
    Flux<String> f1 = Flux.just("str1", "str2", "str3");
    // 这里若输入俩参数,则会报错,Mono是一个或者空序列
    Mono<String> m1 = Mono.just("1");

    // 2.fromIterable()从一个Iterable对象中创建一个Flux对象
    Flux<String> f2 = Flux.fromIterable(Arrays.asList("str1", "str2", "str3"));

    // 3.fromStream()从一个Stream对象中创建一个Flux对象
    ArrayList<Integer> numList = new ArrayList<>();
    numList.add(1);
    Flux<Integer> f3 = Flux.fromStream(numList.stream());

    // 4.fromArray()从一个数组对象中创建一个Flux对象
    Integer[] arr = new Integer[]{1, 2, 3};
    Flux<Integer> f4 = Flux.fromArray(arr);

    // 5.range(start,count) 表示从start开始,递增的生成count个数字,都是int类型的参数
    Flux<Integer> f5 = Flux.range(2, 5);

    // 6.创建一个不包含任何元素,只发布结束消息的序列。
    // 并且这种方式不会进行后续传递,需要switchIfEmpty()方法来进行处理。
    // 因为响应式编程中,流的处理是基于元素的,而empty()是没有元素的!
    Flux<Object> empty = Flux.empty();
    
    // 7.创建一个只包含错误消息的序列,里面的参数类型是Throwable
    Flux<Object> error = Flux.error(new Exception("error!"));
    
    // 8.创建一个不包含任何消息通知的序列,注意区别empty(),empty还是会发布结束消息的。
    Flux<Object> never = Flux.never();
}

2.2 subscribe订阅

其实上面提到的几种创建序列的方式是最基本的,大家可能有疑问,我用Reactor,肯定是要进行数据处理或者调用方法的,而不是像上面一样将数据写死🤣🤣。别急,高级的在后面,也就是可编程式的创建一个序列。

不过在此之前,必须先介绍下如何进行订阅,因为至少我得把上面的数据进行一个输出吧。大家还记得上文提到过,发布者可能会发出3种信号吗?其实这3个信号对应了subscribe()方法的3个参数。先展示一个最基本的代码:

@org.junit.Test
public void testOut() {
    Flux<String> f1 = Flux.just("str1", "str2", "str3");
    f1.subscribe(System.out::println);// lambda表达式
}

结果如下:
在这里插入图片描述

我在这直接一步到位,将参数最多的subscribe()方法展示出来:

/**
  * consumer:即我们要对正常的数据元素如何处理,比如我们可以打印输出print。
  * errorConsumer:若发生错误,我们如何处理,即错误信号(同时终止序列)。
  * completeConsumer:即完成信号(同时终止序列)。
  * subscriptionConsumer:订阅发生时候的处理逻辑
  */
public final Disposable subscribe(@Nullable Consumer<? super T> consumer, 
								  @Nullable Consumer<? super Throwable> errorConsumer, 
								  @Nullable Runnable completeConsumer, 
								  @Nullable Consumer<? super Subscription> subscriptionConsumer) {
  return (Disposable)this.subscribeWith(new LambdaSubscriber(consumer, errorConsumer, completeConsumer, subscriptionConsumer));
}

可见,返回类型是Disposable,该接口可以提供一些取消订阅、清理数据的一些操作。

基于此,在这里再写案例:
1.创建类MySubscriber

import org.reactivestreams.Subscription;
import reactor.core.publisher.BaseSubscriber;

public class MySubscriber extends BaseSubscriber {
    @Override
    protected void hookOnSubscribe(Subscription subscription) {
        System.out.println("订阅开始!");
        request(1);
    }

    @Override
    protected void hookOnNext(Object value) {
        System.out.println("获取当前元素成功!" + value);
        request(1);
    }
}

2.测试代码:

@org.junit.Test
public void testOut() {
    // 1.创建一个序列
    Flux<Integer> flux = Flux.range(1, 6)
            .map(i -> {
                if (i <= 4) {
                    return i * 2;
                }
                throw new RuntimeException("数据超过了5!");
            });
    // 2.订阅
    MySubscriber ss = new MySubscriber();
    flux.subscribe(System.out::println,
            error -> System.out.println("Error:" + error),
            () -> System.out.println("Complete!"),
            s -> ss.request(2));
    flux.subscribe(ss);
}

3.结果:
在这里插入图片描述
注意:

  1. subscribe()方法的第四个参数只能接收一个自定义的Subscriber,需要重写BaseSubscriber
  2. BaseSubscriber是一个抽象类,定义了多种用于处理不同信号的hook方法。
  3. 并且至少要重写hookOnSubscribe()方法和hookOnNext()方法。
  4. 若存在第四个参数,那么在对应的自定义订阅类中,对应重写的方法代替对应位置的lambda表达式。 比如我重写了onNext()方法,替换了原有lambda表达式中单纯的输出结果(还加了几个中文字)。那么再简单点,说白了,BaseSubscriber是Lambda的替代品。

2.3 背压和重塑请求

1.代码如下:让订阅者每次处理消息都需要睡眠1秒钟,并每次向上游请求1个元素。

@org.junit.Test
public void testOut() {
    Flux.range(1, 20)
            .doOnRequest(n -> System.out.println("Request请求" + n + "个值"))
            .subscribe(new MySubscriber());
}

public class MySubscriber extends BaseSubscriber<Integer> {
    @Override
    protected void hookOnSubscribe(Subscription subscription) {
        System.out.println("Subscribed and make a request...");
        // 订阅时首先向上游请求1个元素
        request(1);
    }

    @Override
    protected void hookOnNext(Integer value) {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Get value [" + value + "]");
        // 每次处理完后再去请求1个元素
        request(1);
    }
}

结果:
在这里插入图片描述
要知道,我们的发布者的数据推送是非常快的,而我们的订阅者每次处理数据还需要睡眠1秒,但是依旧将所有数据都获取到了,这是为什么呢?

这是因为由rang()方法生成的Flux采用的背压策略是缓存,即能够缓存订阅者暂时来不及处理的元素。

当然,关于背压策略以及策略的更改会在后面内容中提及,到时候对于这个request()方法也就更加直观了。

三. 可编程式方式创建序列

我们通过对应的事件来创建相应的Flux和Mono,而所有这些方法通过API来触发,叫做sink(池)事件。

3.1 Generate

这是一个同步、逐个产生值的方法,并且可以同时记录一个状态值。意思是:

  • 其sink为一个SynchronousSink。
  • onNext()方法每次回调的时候最多调用一次。

案例如下:

@org.junit.Test
public void testGenerate() {
    Flux.generate(
    		// 1.生成一个对象,用来作为状态,即初始化操作。
            AtomicInteger::new,
            // 2.逻辑处理
            (state, sink) -> {
            	// 改变状态值
                int i = state.getAndIncrement();
                sink.next("2*" + i + "=" + 2 * i);
                if (i == 5) {
                	// sink事件结束。
                    sink.complete();
                }
                return state;
            },
            // 3.序列终止时,输出状态值。
            (state) -> System.out.println("state:" + state)

    ).subscribe(System.out::println);
}

输出结果如下:
在这里插入图片描述

来看下generate()方法的结构:

generate(Callable<S> stateSupplier, // 用于初始化值
		BiFunction<S, SynchronousSink<T>, S> generator, // 生成器,也就是所谓的落脚处理的函数
		Consumer<? super S> stateConsumer) // consumer,用于在序列终止的时候调用执行,比如关闭数据源

简单来看,该方法的调用就是三步走:

  1. 初始化。
  2. 逻辑处理。
  3. 终止处理(fiinally)

再看看上文的代码:
在这里插入图片描述

3.2 Create

Create()方法跟Generate()比较来看,有这么几个不同:

  • Create()可以同步、也可以异步。而Generate()是同步的。
  • Create()可以每次发出多个元素,而Generate()每次发出一个。
  • Create()不需要状态值。
  • Create()可以在回调中触发多个事件。
  • Create()多线程的。

并且, Create()最常用的场景是:将现有的API转化为响应式,比如监听器的异步方法。

案例1:create可以发出多个元素,generate不行。

// 可以发出多个元素然后结束,这里发出了5个
Flux.create(sink -> {
    for (int i = 0; i < 5; i++) {
        sink.next(i);
    }
    sink.complete();
}).subscribe(System.out::println);

但是如果把create改成generate,那么会报错:
在这里插入图片描述

案例2:create支持异步,而generate不支持。
1.自定义一个事件源类EventSource

public class EventSource {

    private List<MyListener> listeners;

    public EventSource() {
        this.listeners = new ArrayList<>();
    }

    public void register(MyListener listener){
        listeners.add(listener);
    }

    public void newEvent(Event event){
        for (MyListener listener : listeners) {
            listener.newEvent(event);
        }
    }

    public void eventStopped(){
        for (MyListener listener : listeners) {
            listener.eventSourceStopped();
        }
    }

    @Data
    @AllArgsConstructor
    public static class Event{
        private Date time;
        private String msg;
    }
}

2.定义一个监听器接口MyListener

public interface MyListener<T> {
    // 监听新事件
    void newEvent(EventSource.Event event);
    // 监听事件源的终止
    void eventSourceStopped();
}

3.测试方法:

// 1.构建一个事件源
EventSource eventSource = new EventSource();
Flux.create(sink -> {
    // 2.向事件源中添加一个监听器,负责监听事件的产生和事件源的停止。
    // 相当于将事件转换成异步的事件流
    eventSource.register(new MyListener() {
        @Override
        public void newEvent(EventSource.Event event) {
            // 3.监听器收到事件回调的时候,会通过sink将事件发出
            sink.next(event);
        }

        @Override
        public void eventSourceStopped() {
            // 4.监听器收到事件源停止的回调后,由sink发出完成信号,停止sink
            sink.complete();
        }
    });
}).subscribe(System.out::println);

// 5.循环产生订阅
for (int i = 0; i < 5; i++) {
    TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
    eventSource.newEvent(new EventSource.Event(new Date(), "事件" + i + "注册!"));
}
// 6.停止事件源
eventSource.eventStopped();

4.执行结果:
在这里插入图片描述
相当于将每个元素都转换成异步的事件流FluxSink。

那如果将创建Flux的create方法改成generate会怎样呢?结果如下:证明generate不支持异步
在这里插入图片描述
tips:

Create是Pull和Push模式的混合,意思是:
1.上游在数据准备完成后可以将其推送给下游。
2.下游可以主动从上游中拉取数据。

Create有一个变体模式:push()方法,和create()方法不同的是,他是一个单线程的,即只有一个线程可以调用next()complete()方法。

3.3 包装同步阻塞

给出伪代码:

Mono<List<User>> listMono = Mono.fromCallable(() -> userService.getUsers())
                        .subscribeOn(Schedulers.elastic());
  1. 即用Mono.fromCallable()方法将目标方法进行包装。
  2. subscribeOn(Schedulers.elastic())创建了一个专用线程来等待阻塞资源而不影响其他非阻塞处理。

本篇文章当这里就结束了,回顾下讲了什么:

  1. 介绍了响应式编程。
  2. 介绍了Flux和Mono几种常规的创建方式。(比如从数组、集合中转换数据)
  3. 介绍了创建序列的两个常用方法,create()generate()注意区分他们的区别。
  • 17
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值