Spring WebFlux学习记录

Posted AlaGeek

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring WebFlux学习记录相关的知识,希望对你有一定的参考价值。

本文按照如下顺序一步步深入解释WebFlux是个什么东西:

1、Reactive Stream
2、Reactor
3、WebFlux

其中 Reactive Stream 是 Java 9 新增的一个重要特性,而 Reactor 就相当于 Java 8 的 Stream 流 + Java 9 的 Reactive Stream,最后的 WebFlux 的核心就是基于 Reactor 的相关 API 开发的。


1、Reactive Stream

响应式流是 Java 9 引入的一套基于发布/订阅模式的数据处理规范,它的目标是以非阻塞背压方式实现数据的异步流。

这个是网上摘抄的一个定义,其中背压原先是一个工程里的概念,指的是在管道运输中,气流或液流由于管道突然变细、急弯等原因导致由某处出现了下游向上游的逆向压力,而在响应式流中就被引申为了:当数据流传输过程中,发布者生产数据的速度大于订阅者消费数据的速度时,订阅者告诉发布者暂时不需要更多的数据了这么一个交互动作。实际上就是流控。

Java 9 的响应式流精简为了四个接口:

  • Publisher
  • Subscriber
  • Subscription
  • Processor

其中 Publisher 是发布者接口,Subscriber 是订阅者接口,Subscription 接口用于联系发布者和订阅者,你可以理解为“合同”,两者签订了合同后,才能进行数据流传输,Processor 接口既是发布者又是订阅者,用于中间数据处理,承上启下。

以下是用这四个接口写的一个案例:

public class FlowDemo {

    public static void main(String[] args) throws InterruptedException {

        // 定义发布者
        SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>();

        MyProcessor processor = new MyProcessor();

        publisher.subscribe(processor);

        // 定义订阅者
        Flow.Subscriber<String> subscriber = new Flow.Subscriber<>() {

            private Flow.Subscription subscription;

            @Override
            public void onSubscribe(Flow.Subscription subscription) {
                // 保存订阅关系,需要用它来给发布者响应
                this.subscription = subscription;
                // 请求一个数据
                this.subscription.request(1);
            }

            @Override
            public void onNext(String item) {
                // 接收数据并处理
                System.out.println("subscriber 接收数据: " + item);
                // 再请求一个数据
                this.subscription.request(1);
                // 达到目标,调用cancel告诉发布者不再接收数据
                //this.subscription.cancel();
            }

            @Override
            public void onError(Throwable throwable) {
                // 出现异常
                throwable.printStackTrace();
                // 停止接收数据
                this.subscription.cancel();
            }

            @Override
            public void onComplete() {
                System.out.println("subscriber 处理完毕");
            }
        };

        // 发布者和订阅者建立订阅关系
        processor.subscribe(subscriber);

        // 生产数据,并发布
        for (int i = 0, j = 1; i < 4; i++, j *= -1) {
            System.out.println("publisher 生成数据: " + i * j);
            publisher.submit(i * j);
        }

        // 关闭发布者
        publisher.close();

        // 主线程延时
        Thread.currentThread().join(10000);

        // 关闭中转
        processor.close();
    }

}

class MyProcessor 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(1);
    }

    @Override
    public void onNext(Integer item) {
        System.out.println("processor 接收数据: " + item);
        item = item < 0 ? -item : item;
        this.submit(item + "(转换后)");
        this.subscription.request(1);
    }

    @Override
    public void onError(Throwable throwable) {
        throwable.printStackTrace();
        this.subscription.cancel();
    }

    @Override
    public void onComplete() {
        System.out.println("processor 中转完成");
    }
}

这个案例中用 Publisher 的实现类 SubmissionPublisher 定义了一个发布者,用 Subscriber 定义了一个订阅者,其中实现的方法中,onSubscribe 用于和发布者建立联系,onNext 用于接收数据,onError 用于异常处理,onComplete 在发布者关闭后执行,中转者通过继承 SubmissionPublisher 以及实现接口 Processor 来执行发布者和订阅者的职责,如下是案例执行结果:

发布者与中转者建立订阅关系,中转者与订阅者建立订阅关系,发布者将循环生成的数据发送给中转者,中转者将负数全部处理为正数,然后发送给订阅者。

2、Reactor

Reactor与Spring是兄弟项目,侧重于Server端的响应式编程,是一个基于 Java 8 的实现了响应式流规范的响应式库,虽然说是基于 Java 8 实现的,但是简单来说 Reactor 可以理解为是 Java 8 stream + Java 9 reactive stream。

在 Reactor 中发布者由 Flux 和 Mono 两个类定义:一个 Flux 对象代表了一个包含0-N个元素的序列,而一个 Mono 对象代表了一个包含0-1个元素的序列。

代码样例如下:

public class ReactorDemo {

    public static void main(String[] args) {

        // 定义订阅者
        Subscriber<Integer> subscriber = new Subscriber<Integer>() {

            private Subscription subscription;

            @Override
            public void onSubscribe(Subscription subscription) {
                // 保存订阅关系,需要用它来给发布者响应
                this.subscription = subscription;
                // 请求一个数据
                this.subscription.request(1);
            }

            @Override
            public void onNext(Integer item) {
                // 接收数据并处理
                System.out.println("接收到数据: " + item);
                // 再请求一个数据
                this.subscription.request(1);
                // 达到目标,调用cancel告诉发布者不再接收数据
                //this.subscription.cancel();
            }

            @Override
            public void onError(Throwable throwable) {
                // 出现异常
                throwable.printStackTrace();
                // 停止接收数据
                this.subscription.cancel();
            }

            @Override
            public void onComplete() {
                System.out.println("处理完毕");
            }
        };

        String[] strings = {"1","2","3"};

        Flux.fromArray(strings)
                // jdk8 stream
                .map(Integer::parseInt)
                // jdk9 reactive stream
                .subscribe(subscriber);
    }

}

代码中的订阅者实际是从第一步中的代码拷贝过来的,可以看到两个代码在订阅者的包引入上有所差异,原因是第一步中的案例是基于 Java 9 开发的,而第二步中的案例是基于 Java 8 + Reactor相关jar包 开发的。

代码中首先用 Flux 的 fromArray 方法将字符串数组转化为了一个数据流,然后因为订阅者消费的数据是Integer类型,所以用 map 将 String 转为 Integer,最后调用 subscribe 方法对流进行消费,这个方法的传参可以传入一个订阅者。

样例执行结果如下:

3、Spring WebFlux

讲 Reactor 主要是为了引出 Flux 和 Mono,实际上这部分看懂已经足够学习WebFLux(没看懂可能也能学)。

Spring WebFlux 是随着 Spring5 推出的响应式编程框架,与 Spring MVC 相比,WebFlux 最大的特点就是非阻塞,这部分直接有代码来讲解。

3.1 类MVC开发模式

首先创建一个测试项目,在pom文件中添加如下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

然后创建一个controller,写入以下测试代码:

@GetMapping("/1")
public String get1() {
    long start = System.currentTimeMillis();
    log.info("get1 start");
    String str = createStr("1");
    log.info("get1 end");
    long end = System.currentTimeMillis();
    log.info("get1 use time: {}", end - start);
    return str;
}

@GetMapping("/2")
public Mono<String> get2() {
    long start = System.currentTimeMillis();
    log.info("get2 start");
    Mono<String> mono = Mono.fromSupplier(() -> createStr("2"));
    log.info("get2 end");
    long end = System.currentTimeMillis();
    log.info("get2 use time: {}", end - start);
    return mono;
}

private String createStr(String str) {
    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return str;
}

其中 get1 方法是典型的 MVC 写法,在方法中调用 createStr 方法,为了模拟阻塞,在 createStr 方法中加了5秒延时,而 get2 方法就是 WebFlux 的写法,在方法体中通过语句 Mono.fromSupplier(() -> createStr(“2”)) 来构造一个数据流,这里需要注意的是,流是具有惰性处理的特性的,如果方法体内没有消费这个流,那么实际上 createStr是不会执行的,那么阻塞的5秒也就不存在了。如下是代码执行结果:

可以看到,get2方法确实没有产生延迟,因为它只是返回了一个流,而这个流是在方法体执行完后,被 Spring WebFlux 框架消费的,以运行在 Tomcat 上为例,这样的写法就不会导致 Servlet线程 处于堵塞状态,从而提升吞吐量。

上面展示的是返回值为 Mono 的情况,下面演示种返回值为 Flux 的案例:

@GetMapping(value = "/3", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> flux() {
    long start = System.currentTimeMillis();
    log.info("get3 start");
    Flux<String> stringFlux = Flux.fromStream(IntStream.range(1, 5).mapToObj(
        i -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "flux data " + i + "\\n";
        }
    ));
    log.info("get3 end");
    long end = System.currentTimeMillis();
    log.info("get3 use time: {}", end - start);
    return stringFlux;
}

Flux 和 Mono最大的区别就在于 Flux 产生的数据流可能会有多个元素,那么对于返回给调用方就有两种方式,一种是所有元素都消费完一起返回,另一种是消费一个返回一个,上述代码中的 MediaType.TEXT_EVENT_STREAM_VALUE 就是告诉浏览器一个个返回,如下是运行结果:
第一次返回:

第四次返回:

同样,方法体内消耗的时间如下:

3.2 Router Function开发模式

上面代码的开发模式其实跟Spring MVC的差别不大,也是写一个controller,里面的方法用@RequestMapping注解,而WebFlux还有一种开发,叫做 Router Function。

对应controller中的方法,Router Function开发模式有 HandlerFunction 类:

Mono<T extends ServerResponse> handle(ServerRequest request);

而请求路由由类 RouterFunction 实现:

Mono<HandlerFunction<T>> route(ServerRequest request);

就比如将上面的 get2 方法改写如下:

@Component
public class TestHandler {

    public Mono<ServerResponse> get2(ServerRequest serverRequest) {
        return ok().contentType(MediaType.TEXT_PLAIN).body(Mono.fromSupplier(() -> createStr("2")), String.class);
    }

    private String createStr(String str) {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return str;
    }

}

@Configuration
public class RouterConfig {

    @Resource
    private TestHandler testHandler;

    @Bean
    public RouterFunction<ServerResponse> timerRouter() {
        return route(GET("/get2"), testHandler::get2);
    }
}

启动服务后,访问 /get 就会调用 testHandler 的 get2 方法来处理请求。


参考文献:

1、https://blog.csdn.net/get_set/article/details/79466657

2、https://www.bilibili.com/video/BV1y44y117pp?share_source=copy_web

以上是关于Spring WebFlux学习记录的主要内容,如果未能解决你的问题,请参考以下文章

Spring WebFlux学习记录

如何在 Spring Webflux Java 中记录请求正文

使用 Spring boot webflux reactive 记录未持久化到 R2DB

如何在 spring-mvc 中将日志记录添加到 webflux 端点?

如何在Spring WebFlux中记录请求和响应主体

Log Spring webflux 类型 - Mono 和 Flux