Spring Reactor教程

Spring Reactor Tutorial

总览

在本文中,我们将介绍Spring Reactor项目及其重要性。 这个想法是利用Reactive Streams规范在JVM上构建非阻塞的反应式应用程序。

利用这些知识,我们将构建一个简单的反应式应用程序,并将其与传统的阻止应用程序进行比较。

响应式应用程序是"热门新事物",许多应用程序都切换到该模型。 您可以在The Reactive Manifesto中阅读有关此内容的更多信息。

动机

常规API正在阻止

现代应用程序处理大量并发用户和数据。 摩尔定律不再像以前那样成立。 硬件功能虽然在增加,但是却无法跟上性能非常重要的现代应用程序的步伐。

Java开发人员默认情况下编写阻止代码。 这就是API的设置方式。 另一个示例是传统的servlet(Tomcat)方法。 每个请求都保证有一个新线程,该线程等待整个后台进程完成以将响应发送回去。

这意味着我们的数据层逻辑默认情况下会阻止应用程序,因为线程闲置地等待响应。 在我们等待响应返回之前,不将这些线程用于其他目的是浪费的。

Reactor motivation 信用:http://projectreactor.io/learn

注意:如果我们的资源有限或某个过程需要太多时间来执行,则可能会出现问题。

异步静止块

在Java中,您可以使用Callbacks and Futures异步编写代码。 然后,您可以在以后的某个时间点获取并加入线程并处理结果。 Java 8为我们引入了一个新类CompletableFuture,它使协调这些事情变得更加容易。

它以一种简单的方式工作-当一个进程结束时,另一个进程开始。 在第二个步骤结束之后,将结果合并到第三个过程中。

这使协调您的应用程序变得容易得多,但是在创建线程并等待调用.join()方法时,它最终仍会阻塞。

Reactor motivation 信用:http://projectreactor.io/learn

反应式编程

我们想要的是异步和非阻塞。 来自Netflix,Pivotal,RedHat等公司的一组开发人员聚集在一起,并商定了一个名为"反应流规范"的项目。

Project Reactor是Spring的The Reactive Specification的实现,它特别受Spring Webflux模块的青睐,尽管您可以将其与RxJava等其他模块一起使用。

这个想法是使用发布者和订阅者与Backpressure异步操作。

在这里,我们将介绍几个新概念! 让我们一一解释:

  • 发布者-发布者是数量可能不受限制的元素的提供者。

  • 订阅服务器-订阅服务器侦听该发布服务器,询问新数据。 有时,它也被称为消费者。

  • 背压-订阅服务器让发布服务器当时可以处理多少个请求的能力。 因此,负责数据流的是订阅服务器,而不是发布服务器,因为发布服务器仅提供数据。

  • Reactor项目提供两种类型的发布者。 这些被认为是Spring Webflux的主要构建基块:

  • 通量-是产生0N值的发布者。 它可能是无限的。 返回多个元素的操作使用此类型。

  • Mono-是产生01值的发布者。 返回单个元素的操作使用此类型。

  • 开发反应性应用程序

    考虑到以上所有内容,让我们跳入创建一个简单的Web应用程序,并利用这一新的响应式范例的优势!

    与往常一样,从框架式Spring Boot项目开始的最简单方法是使用Spring Initializr。 选择您首选的Spring Boot版本,并添加" Reactive Web"依赖项。 之后,将其生成为Maven项目,一切就绪!

    让我们定义一个简单的POJO-Greeting

    1
    2
    3
    4
    public class Greeting {
        private String msg;
        // Constructors, getters and setters
    }

    定义发布者

    伴随它,让我们定义一个具有适当映射的简单REST控制器:

    1
    2
    3
    4
    5
    6
    7
    8
    @RestController
    public class GreetReactiveController {
        @GetMapping("/greetings")
        public Publisher<Greeting> greetingPublisher() {
            Flux<Greeting> greetingFlux = Flux.<Greeting>generate(sink -> sink.next(new Greeting("Hello"))).take(50);
            return greetingFlux;
        }
    }

    调用Flux < T > .generate()将创建Greeting对象的永不停止的流。

    顾名思义,take()方法将仅从流中获取前50个值。

    重要的是要注意,该方法的返回类型是异步类型Publisher

    要测试此终结点,请将浏览器导航到http:// localhost:8080 / greetings或在命令行上使用curl客户端-curl localhost:8080/greetings

    系统将提示您类似以下内容的响应:

    这看起来没什么大不了的,我们可以简单地返回List以获得相同的视觉效果。

    但是要再次注意,我们返回的是Flux,它是一个异步类型,因为这会改变所有内容。

    假设我们有一个发布者返回了超过一千条甚至更多的记录。 考虑一下框架必须做什么。 它为对象提供了Greeting类型的对象,必须将其转换为最终用户的JSON。

    如果我们在Spring MVC中使用传统方法,则这些对象将继续在您的RAM中累积,并且一旦它收集了所有东西,便会将其返回给客户端。 这可能超出了我们的RAM容量,同时也阻止了其他任何操作的处理。

    当我们使用Spring Webflux时,整个内部动力学都会改变。 框架开始从发布者那里订阅这些记录,并序列化每个项目并将其分块发送回客户端。

    我们异步进行操作,而无需创建太多线程并重用正在等待的线程。 最好的部分是您不必为此做任何额外的事情。 在传统的Spring MVC中,我们可以通过返回AsyncResultDefferedResult等来实现异步,但是在内部,Spring MVC必须创建一个新的Thread,由于必须等待它被阻塞。

    服务器发送的事件

    自服务器到达事件以来一直使用的另一个发布者。

    这些事件使网页可以实时从服务器获取更新。

    让我们定义一个简单的反应式服务器:

    1
    2
    3
    4
    5
    6
    7
    @GetMapping(value ="/greetings/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Publisher<Greeting> sseGreetings() {
        Flux<Greeting> delayElements = Flux
                .<Greeting>generate(sink -> sink.next(new Greeting("Hello @" + Instant.now().toString())))
                .delayElements(Duration.ofSeconds(1));
        return delayElements;
    }

    另外,我们可以定义以下内容:

    1
    2
    3
    4
    5
    6
    @GetMapping(value ="/greetings/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    Flux<Greeting> events() {
        Flux<Greeting> greetingFlux = Flux.fromStream(Stream.generate(() -> new Greeting("Hello @" + Instant.now().toString())));
        Flux<Long> durationFlux = Flux.interval(Duration.ofSeconds(1));
        return Flux.zip(greetingFlux, durationFlux).map(Tuple2::getT1);
    }

    这些方法产生TEXT_EVENT_STREAM_VALUE,从本质上讲,这意味着服务器已发送事件以数据的形式发送。

    请注意,在第一个示例中,我们使用Publisher,在第二个示例中,我们使用Flux。 一个有效的问题是:

    "那我应该使用哪种返回类型?"

    建议在Publisher上使用FluxMono。 这两个类都是源自Reactive Streams的Publisher接口的实现。 虽然可以互换使用它们,但是使用实现更具表达性和描述性。

    这两个示例重点介绍了创建延迟的服务器发送事件的两种方法:

  • .delayElements()-此方法将磁通量的每个元素延迟给定持续时间

  • .zip()-我们正在定义一个Flux来生成事件,并定义一个Flux来每秒生成值。 通过将它们压缩在一起,我们每秒都会得到一个Flux生成事件。

  • 导航到http:// localhost:8080 / greetings / sse或在命令行上使用curl客户端,您将看到类似以下内容的响应:

    定义消费者

    现在让我们看看它的消费者方面。 值得注意的是,您不需要具有反应式发布者即可在消费方使用反应式编程:

    1
    2
    3
    4
    5
    public class Person {
        private int id;
        private String name;
        // Constructor with getters and setters
    }

    然后我们有一个带有单个映射的传统RestController

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @RestController
    public class PersonController {
        private static List<Person> personList = new ArrayList<>();
        static {
            personList.add(new Person(1,"John"));
            personList.add(new Person(2,"Jane"));
            personList.add(new Person(3,"Max"));
            personList.add(new Person(4,"Alex"));
            personList.add(new Person(5,"Aloy"));
            personList.add(new Person(6,"Sarah"));
        }

        @GetMapping("/person/{id}")
        public Person getPerson(@PathVariable int id, @RequestParam(defaultValue ="2") int delay)
                throws InterruptedException {
            Thread.sleep(delay * 1000);
            return personList.stream().filter((person) -> person.getId() == id).findFirst().get();
        }
    }

    我们初始化了类型为Person的列表,并根据传递给我们映射的id,使用流将该人过滤掉。

    尽管Thread.sleep()只是用来模拟2秒的网络延迟,但您可能会对这里使用Thread.sleep()感到震惊。

    如果您有兴趣阅读有关Java Streams的更多信息,请阅读本文!

    让我们继续前进,创造我们的消费者。 就像发布者一样,我们可以使用Spring Initializr轻松做到这一点:

    我们的生产者应用程序正在端口8080上运行。 现在假设我们要调用/person/{id}端点5次。 我们知道,默认情况下,由于"网络滞后",每个响应都会延迟2秒。

    首先让我们使用传统的RestTemplate方法进行此操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class CallPersonUsingRestTemplate {

        private static final Logger logger = LoggerFactory.getLogger(CallPersonUsingRestTemplate.class);
        private static RestTemplate restTemplate = new RestTemplate();

        static {
            String baseUrl ="http://localhost:8080";
            restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(baseUrl));
        }

        public static void main(String[] args) {
            Instant start = Instant.now();

            for (int i = 1; i <= 5; i++) {
                restTemplate.getForObject("/person/{id}", Person.class, i);
            }

            logTime(start);
        }

        private static void logTime(Instant start) {
            logger.debug("Elapsed time:" + Duration.between(start, Instant.now()).toMillis() +"ms");
        }
    }

    让我们运行它:

    正如预期的那样,花了10秒钟多一点,这就是Spring MVC默认工作的方式。

    在这个时代,等待一页结果超过10秒是不可接受的。 这是保留客户/客户与由于等待太久而丢失客户/客户之间的区别。

    Spring Reactor引入了一个新的Web客户端来发出Web请求,称为WebClient。 与RestTemplate相比,此客户端具有更多的功能感,并且具有完全的反应性。 它包含在spring-boot-starter-weblux依赖项中,并且以非阻塞方式替换RestTemplate。

    这次,我们使用WebClient重写同一控制器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class CallPersonUsingWebClient_Step1 {

        private static final Logger logger = LoggerFactory.getLogger(CallPersonUsingWebClient_Step1.class);
        private static String baseUrl ="http://localhost:8080";
        private static WebClient client = WebClient.create(baseUrl);

        public static void main(String[] args) {

            Instant start = Instant.now();

            for (int i = 1; i <= 5; i++) {
                client.get().uri("/person/{id}", i).retrieve().bodyToMono(Person.class);
            }

            logTime(start);
        }

        private static void logTime(Instant start) {
            logger.debug("Elapsed time:" + Duration.between(start, Instant.now()).toMillis() +"ms");
        }

    }

    在这里,我们通过传递baseUrl创建了WebClient。 然后在main方法中,我们仅调用端点。

    get()表示我们正在发出GET请求。 我们知道响应将是单个对象,因此我们使用了Mono,如前所述。

    最终,我们要求Spring将响应映射到Person类:

    并没有发生任何意外。

    这是因为我们没有订阅。 整个事情都推迟了。 它是异步的,但是直到我们调用.subscribe()方法后它才开始。 这是Spring Reactor的新手经常遇到的问题,因此请注意这一点。

    让我们更改主要方法并添加订阅:

    1
    2
    3
    for (int i = 1; i <= 5; i++) {
        client.get().uri("/person/{id}", i).retrieve().bodyToMono(Person.class).subscribe();
    }

    添加方法会提示我们所需的结果:

    发送了请求,但.subscribe()方法没有坐下来等待响应。 由于它不会阻塞,因此它在完全收到响应之前就完成了。

    我们可以通过在方法调用的末尾链接.block()来解决这个问题吗?

    1
    2
    3
    for (int i = 1; i <= 5; i++) {
        client.get().uri("/person/{id}", i).retrieve().bodyToMono(Person.class).block();
    }

    结果:

    这次我们确实收到了每个人的回复,尽管花费了10秒钟以上。 这违反了应用程序具有反应性的目的。

    解决所有这些问题的方法很简单:我们列出一个类型为Mono的列表,然后等待所有它们完成,而不是等待每个:

    1
    2
    3
    4
    5
    List<Mono<Person>> list = Stream.of(1, 2, 3, 4, 5)
        .map(i -> client.get().uri("/person/{id}", i).retrieve().bodyToMono(Person.class))
        .collect(Collectors.toList());

    Mono.when(list).block();

    结果:

    这就是我们的目标。 这次,即使有巨大的网络延迟,也只花了两秒钟多的时间。 这极大地提高了我们应用程序的效率,确实改变了游戏规则。

    如果仔细观察线程,Reactor会重用它们而不是创建新线程。 如果您的应用程序在短时间内处理了许多请求,这真的很重要。

    结论

    在本文中,我们讨论了对反应式编程的需求及其Spring的实现-Spring Reactor。

    之后,我们讨论了内部使用Reactor的Spring Webflux模块以及诸如Publisher和Subscriber之类的涵盖概念。 基于此,我们构建了一个应用程序,该应用程序将数据发布为响应流,并在另一个应用程序中使用它。

    该教程的源代码可以在Github上找到。