Reactive programming (2): code demonstration

The book follows Reactive programming above , and we continue to use real code to explain some concepts. We'll take a closer look at what makes Reactive different and what it can do. These examples are very abstract, but they can give us a better understanding of the API and programming style used, and truly feel the difference. We'll look at the core elements of Reactive, learn how to control data flow, and use background threads for processing if necessary.

create project

We use Reactor library for demonstration. Of course other tools can also be used. If you don't want to copy and paste the code, you can directly use the sample project on github .

Create an empty project via https://start.spring.io and add Reactor Core dependencies.

You can use Maven:

<dependency>
  <groupId>io.projectreactor</groupId>
  <artifactId>reactor-core</artifactId>
  <version>3.0.0.RC2</version>
</dependency>

Gradle can also be used:

compile 'io.projectreactor:reactor-core:3.0.0.RC2'

working principle

Reactive consists of a sequence of events and 2 parties that publish and subscribe to those events. We can also call it stream. If necessary, we use the term streams, but java8 has a java.util.Stream library, which is different from the concept we are going to talk about here, so don't confuse these two concepts. We try to focus on publishers and subscribers (the behavior of Reactive Streams).

We will use Reactorthe library and call the publisher Flux(which implements the Reactive Streams Publisherinterface), the name in the RxJava library is Observable, which represents a similar concept. (The name in Reactor2.0 is Stream, which is easily confused with Java 8's Streams, so we only use the new definition in Reactor 3.0).

create

Fluxis a Publisher for event sequences of type POJO, eg Flux<T>is Ta Publisher of type . FluxThere are a series of static methods that create instances from different sources. For example created from an array Flux:


Flux<String> flux = Flux.just("red", "white", "blue");

We created a Flux, now we start to do some things with it. There are really only 2 things you can do: operations (transformation or combination with other sequences) and subscriptions.

sequence of single values

The sequences we often encounter often have only one element, or have no elements, such as finding records by id. In Reactor, Mono represents a single-valued Flux or an empty Flux. Mono's API is similar to Flux, but more concise, because not all operations make sense on single-valued sequences. The similar type in RxJava is called Single, and the empty sequence is called Completable. In Reactor an empty sequence is Mono<Void>.

operator

Most of Flux's methods are operations. We won't talk about all the methods here (you can read the javadoc), we just need to figure out what the operation is and what it can do.
For example, Flux's internal events can log()be displayed with, or map()transformed with:


Flux<String> flux = Flux.just("red", "white", "blue");

Flux<String> upper = flux
  .log()
  .map(String::toUpperCase);

This code converts the input string to uppercase, which is very simple and clear. At the same time, it is very interesting (always pay attention, although I am not used to it at first), that the data has not started to be processed. Nothing will be displayed, because nothing happened (you can run the code yourself), calling the Flux operator just creates an execution plan. The logic implemented by the operator will only be executed when the data starts to flow, when a party subscribes to this Flux.

Java 8's Streams also have a similar way of handling data streams:


Stream<String> stream = Streams.of("red", "white", "blue");
Stream<String> upper = stream.map(value -> {
    System.out.println(value);
    return value.toUpperCase();
});

But there is a big difference between Flux and Stream, and Stream's API is not suitable for Reactive.

subscription

To make the data flow effective, we need to use the subscribe() method to subscribe to Flux. These methods will go back to the operation chain we defined earlier and request the publisher to generate data. In our simple example, a collection of strings is iterated over for processing. In a more complex scenario, it might be reading a file from the file system, or reading data from a database, or calling an http service.

Start calling subscribe():


Flux.just("red", "white", "blue")
  .log()
  .map(String::toUpperCase)
.subscribe();

output:


09:17:59.665 [main] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxIterable$IterableSubscription@3ffc5af1)
09:17:59.666 [main] INFO reactor.core.publisher.FluxLog -  request(unbounded)
09:17:59.666 [main] INFO reactor.core.publisher.FluxLog -  onNext(red)
09:17:59.667 [main] INFO reactor.core.publisher.FluxLog -  onNext(white)
09:17:59.667 [main] INFO reactor.core.publisher.FluxLog -  onNext(blue)
09:17:59.667 [main] INFO reactor.core.publisher.FluxLog -  onComplete()

You can see that when subscribe() has no parameters, it will request the publisher to send all the data - there is only one request and it is "unbounded". We can also see the callbacks for each item published (onNext()), the callback for completion (onComplete()), and the callback for the original subscription (onSubscribe()). If necessary, we can also use Flux's doOn*() method to listen to the callbacks of these events.

The subscribe() method is overloaded and there are many variants. One of the important and commonly used forms is with callback parameters. The first parameter is a Consumer, a callback for each data item, an optional Consumer for error handling, and a Runnable to execute when the sequence is complete.

For example, to add a callback for each data item:


Flux.just("red", "white", "blue")
    .log()
    .map(String::toUpperCase)
.subscribe(System.out::println);

The output is:


09:56:12.680 [main] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@59f99ea)
09:56:12.682 [main] INFO reactor.core.publisher.FluxLog -  request(unbounded)
09:56:12.682 [main] INFO reactor.core.publisher.FluxLog -  onNext(red)
RED
09:56:12.682 [main] INFO reactor.core.publisher.FluxLog -  onNext(white)
WHITE
09:56:12.682 [main] INFO reactor.core.publisher.FluxLog -  onNext(blue)
BLUE
09:56:12.682 [main] INFO reactor.core.publisher.FluxLog -  onComplete()

We can control the flow of data to make it "bounded" in a number of ways. The internal interface for control is Subscription obtained from Subscriber. The complex form equivalent to the previous simple call to subscribe() is:


.subscribe(new Subscriber<String>() {

    @Override
    public void onSubscribe(Subscription s) {
        s.request(Long.MAX_VALUE);
    }
    @Override
    public void onNext(String t) {
        System.out.println(t);
    }
    @Override
    public void onError(Throwable t) {
    }
    @Override
    public void onComplete() {
    }

});

If you want to control the data flow to consume 2 data items at a time, you can use Subscription more intelligently:


.subscribe(new Subscriber<String>() {

    private long count = 0;
    private Subscription subscription;

    @Override
    public void onSubscribe(Subscription subscription) {
        this.subscription = subscription;
        subscription.request(2);
    }

    @Override
    public void onNext(String t) {
        count++;
        if (count>=2) {
            count = 0;
            subscription.request(2);
        }
     }
...

This Subscriber will pack 2 data items each time. This scenario is very common, so we will consider extracting the implementation into a dedicated class for convenience. The output is as follows:


09:47:13.562 [main] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@61832929)
09:47:13.564 [main] INFO reactor.core.publisher.FluxLog -  request(2)
09:47:13.564 [main] INFO reactor.core.publisher.FluxLog -  onNext(red)
09:47:13.565 [main] INFO reactor.core.publisher.FluxLog -  onNext(white)
09:47:13.565 [main] INFO reactor.core.publisher.FluxLog -  request(2)
09:47:13.565 [main] INFO reactor.core.publisher.FluxLog -  onNext(blue)
09:47:13.565 [main] INFO reactor.core.publisher.FluxLog -  onComplete()

In fact, batch subscription is a very common scenario, so Flux already includes related methods. The above example could be implemented as:


Flux.just("red", "white", "blue")
  .log()
  .map(String::toUpperCase)
.subscribe(null, 2);

(Note that the subscribe method takes a request limit parameter) The output is:


10:25:43.739 [main] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@4667ae56)
10:25:43.740 [main] INFO reactor.core.publisher.FluxLog -  request(2)
10:25:43.740 [main] INFO reactor.core.publisher.FluxLog -  onNext(red)
10:25:43.741 [main] INFO reactor.core.publisher.FluxLog -  onNext(white)
10:25:43.741 [main] INFO reactor.core.publisher.FluxLog -  request(2)
10:25:43.741 [main] INFO reactor.core.publisher.FluxLog -  onNext(blue)
10:25:43.741 [main] INFO reactor.core.publisher.FluxLog -  onComplete()

Threads, Scheduling, and Background Processing

An interesting feature of the above example is that all log methods are executed on the main thread, the thread of the subscribe() caller. This is a key point: Reactor achieves high performance with as few threads as possible. In the past 5 years, we have been accustomed to using multi-threading, thread pool and asynchronous processing to improve system performance. You might be surprised by this new approach. But the fact is: Even with a technology optimized for thread processing like JVM, the cost of thread switching is very high. It is always much faster to do calculations on a single thread. Reactor gives us the means to do asynchronous programming and assume we know what we're doing.

Flux provides methods to control thread boundaries. For example, a subscription can be Flux.subscribeOn()configured :


Flux.just("red", "white", "blue")
  .log()
  .map(String::toUpperCase)
  .subscribeOn(Schedulers.parallel())
.subscribe(null, 2);

Output result:


13:43:41.279 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@58663fc3)
13:43:41.280 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  request(2)
13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onNext(red)
13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onNext(white)
13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  request(2)
13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onNext(blue)
13:43:41.281 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onComplete()

You can see that the subscription and all processing are in the "parallel-1-1" background thread. Single threading is fine for CPU intensive processing. However, if it is IO-intensive processing, it may block. In this scenario, we want the processing to complete as far as possible without blocking the caller. A thread pool would still help a lot, we can Schedulers.parallel()get . To split the processing of a single data item into an independent thread for processing, we need to put it in an independent publisher, and each publisher requests the execution result in a background thread. One way is to call the flatMap() operation, which maps data items to a Publisher and returns a new type of sequence:


Flux.just("red", "white", "blue")
  .log()
  .flatMap(value ->
     Mono.just(value.toUpperCase())
       .subscribeOn(Schedulers.parallel()),
     2)
.subscribe(value -> {
  log.info("Consumed: " + value);
})

Note flatMap()Putting data items into a sub-publisher allows you to control the subscription of each sub-item rather than the entire sequence. Reactor's internal default behavior is to hang on a thread for as long as possible, so if you need specific data items to be processed in a background thread, you must explicitly specify it. In fact, this is one of a series of methods to force parallel computing.

output:


15:24:36.596 [main] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxIterable$IterableSubscription@6f1fba17)
15:24:36.610 [main] INFO reactor.core.publisher.FluxLog -  request(2)
15:24:36.610 [main] INFO reactor.core.publisher.FluxLog -  onNext(red)
15:24:36.613 [main] INFO reactor.core.publisher.FluxLog -  onNext(white)
15:24:36.613 [parallel-1-1] INFO com.example.FluxFeaturesTests - Consumed: RED
15:24:36.613 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  request(1)
15:24:36.613 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onNext(blue)
15:24:36.613 [parallel-1-1] INFO reactor.core.publisher.FluxLog -  onComplete()
15:24:36.614 [parallel-3-1] INFO com.example.FluxFeaturesTests - Consumed: BLUE
15:24:36.617 [parallel-2-1] INFO com.example.FluxFeaturesTests - Consumed: WHITE

Now multiple threads are doing the processing, and the batch parameter in flatMap() guarantees that 2 data items will be processed at a time whenever possible. Reactor will make itself as smart as possible, prefetching data items from Publisher and estimating the waiting time of subscribers.

Flux also has a publishOn() method that works similarly, except that it controls the behavior of the publisher:


Flux.just("red", "white", "blue")
  .log()
  .map(String::toUpperCase)
  .subscribeOn(Schedulers.newParallel("sub"))
  .publishOn(Schedulers.newParallel("pub"), 2)
.subscribe(value -> {
    log.info("Consumed: " + value);
});

output:


15:12:09.750 [sub-1-1] INFO reactor.core.publisher.FluxLog -  onSubscribe(reactor.core.publisher.FluxIterable$IterableSubscription@172ed57)
15:12:09.758 [sub-1-1] INFO reactor.core.publisher.FluxLog -  request(2)
15:12:09.759 [sub-1-1] INFO reactor.core.publisher.FluxLog -  onNext(red)
15:12:09.759 [sub-1-1] INFO reactor.core.publisher.FluxLog -  onNext(white)
15:12:09.770 [pub-1-1] INFO com.example.FluxFeaturesTests - Consumed: RED
15:12:09.771 [pub-1-1] INFO com.example.FluxFeaturesTests - Consumed: WHITE
15:12:09.777 [sub-1-1] INFO reactor.core.publisher.FluxLog -  request(2)
15:12:09.777 [sub-1-1] INFO reactor.core.publisher.FluxLog -  onNext(blue)
15:12:09.777 [sub-1-1] INFO reactor.core.publisher.FluxLog -  onComplete()
15:12:09.783 [pub-1-1] INFO com.example.FluxFeaturesTests - Consumed: BLUE

Note that the subscriber's callback (with content "Consumed: ...​") is executed pub-1-1on . If you remove the subscribeOn() method, you will find that all data items are processed pub-1-1on . This again means that Reactor uses as few threads as possible - if no thread switching is explicitly specified, the next call will be executed on the current calling thread.

Extractors: Subscribers with side effects

Another way to subscribe to a sequence is to call Mono.block()or Mono.toFuture()or Flux.toStream()(these are extractor methods that convert reactive types to blocking types). Flux also has collectList() and collectMap() to convert Flux to Mono. They don't really have a sequence of subscriptions, but they do throw away the ability to control subscriptions to individual data items.

Warning:
A golden rule is "never call an extractor". There are exceptions of course, such as in a test program that needs to be able to aggregate results by blocking.

These methods are used to convert Reactive to blocking mode, when we need to adapt an old-fashioned API, such as Spring MVC. When calling Mono.block(), we give up all the advantages of Reactive Streams. This is the key difference between Reactive Streams and Java 8 Streams - Java Stream has only "all or nothing" subscription mode, which is equivalent to Mono.block(). Of course, subscribe() will also block the calling thread, so it is as dangerous as the conversion method, but there are enough controls - you can use subscribeOn() to prevent blocking, or you can use backpressure to overflow data items and decide whether to continue processing at regular intervals .

Summarize

In this article we covered the basic concepts of Reactive Streams and Reactor API. You can learn more through the sample code on GitHub or the Lite RX Hands On experimental project. In the next article we'll dig deeper into the blocking, dispatching, asynchronous aspects of the Reactive model, and show opportunities where you can really benefit.

Guess you like

Origin blog.csdn.net/haiyan_qi/article/details/79606204