Practical case of responsive operation

Project Reactor Framework

Add dependency management in Spring Boot project Maven.

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

<dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-test</artifactId>
    <scope>test</scope>
</dependency>

If you want to use Reactor in your Spring Boot project, then you need to set Reactor's BOM (bill of materials) in the build. The following dependency management entry adds Reactor's Bismuth-RELEASE to the build:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-bom</artifactId>
            <version>Bismuth-RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Reactor asynchronous data sequence

       The basic component of the reactive stream specification is an asynchronous data sequence. In the Reactor framework, we can express this asynchronous data sequence as follows:

The above asynchronous data sequence can be expressed by the following formula:

onNext x 0..N [onError | onComplete]
  • onNext: Indicates normal message notifications containing elements

  • onComplete: Message notification indicating the end of the sequence

  • onError: message notification indicating sequence error

        When these message notifications are triggered, the corresponding three methods with the same names in the subscribers of the asynchronous sequence will be called. Under normal circumstances, both onNext() and onComplete() methods should be called to consume data normally and end the sequence. If the onComplete() method is not called, an unbounded data sequence will be generated, which is usually unreasonable in business systems. The onError() method will only be called when an exception occurs in the sequence. 

       Based on the above asynchronous data sequence, the Reactor framework provides two core components to publish data, namely Flux and Mono components. These two components can be said to be the most basic programming objects in the application development process. These two components are very important. Understanding them clearly is the entry threshold for Reactor responsive programming.

 Flux and Mono components

Flux represents an asynchronous sequence containing 0 to n elements, as follows:

  • Flux is a standard  Publisherrepresenting an asynchronous sequence of 0 to N emitted items, optionally terminated with a completion signal or an error. As in the Reactive Streams specification, these three types of signals translate into calls to the downstream subscriber's onNext, onComplete, or onError methods.

  • In this large range of possible signals, Flux is the generic reactive type. Note that all events, even termination events, are optional: no onNext event, but onComplete event represents an empty finite sequence, but remove onComplete and you have an infinite empty sequence (except for the test about cancellation, not particularly useful). Likewise, an infinite sequence is not necessarily empty. For example, Flux.interval(Duration) produces a  Flux, which is infinite, regular data emitted from the clock.

Mono represents an asynchronous sequence containing 0 to 1 elements, as follows:

  • Mono is a special  Publisherthat emits at most one item and then optionally ends with an onComplete signal or an onError signal.

  • It only provides a subset of the operators available with Flux, and some operators (notably those that combine a Mono with another Publisher) switch to Flux.

  • For example, Mono#concatWith(Publisher) returns a Flux, while Mono#then(Mono) returns another Mono.

  • Note that Mono can be used to represent a valueless asynchronous process with only the notion of completion (similar to Runnable). To create one, use  Mono.

Responsive operation practice 

Create reactive flows through Flux objects

There are two main categories:

  • Static creation methods based on various factory patterns;

  • Dynamically create Flux programmatically;

 Static creation methods based on various factory patterns 

Common methods for statically creating Flux in Reactor include just(), range(), interval(), and various method groups prefixed with from-.

  • just() method

It can specify all elements contained in the sequence, and the created Flux sequence will automatically end after publishing these elements. In general, using the just() method is the most straightforward way to create a Flux when the number and content of elements is known. 

Flux.just("Apple", "Orange", "Grape", "Banana","Strawberry")
    .subscribe(System.out::println);

 Console output:

Apple
Orange
Grape
Banana
Strawberry

 If you want the console to have output here, you must call the subscribe method. If Flux has no subscribers, the data will not flow. To use the garden hose analogy, you have connected the hose to the water outlet, and the other end is the water from the water company. But the water won't flow unless you turn on the tap. Subscribing to a reactive type is how you open a stream of data.

The lambda expression in subscribe() is actually a java.util.Consumer, which is used to create a Subscriber for reactive streams. Since the subscribe() method is called, the data starts to flow. In this example, there are no intermediate operations, so data flows directly from the Flux to the Subscriber.

  • fromXXX() method group

If we already have an array, an Iterable object or a Stream object, then we can automatically create Flux from these objects through the fromXXX() method group provided by Flux, including fromArray(), fromIterable() and fromStream() methods.

 

// fromArray()
String[] fruits = new String[] {"Apple", "Orange", "Grape", "Banana", "Strawberry"};
Flux.fromArray(fruits).subscribe(System.out::println);

// fromIterable()
List<String> fruitList = new ArrayList<>();
fruitList.add("Apple");
fruitList.add("Orange");
fruitList.add("Grape");
fruitList.add("Banana");
fruitList.add("Strawberry");
Flux.fromIterable(fruitList).subscribe(System.out::println);

// fromStream() 
Stream<String> fruitStream =Stream.of("Apple", "Orange", "Grape", "Banana", "Strawberry");
Flux.fromStream(fruitStream).subscribe(System.out::println);

 All three method consoles are output:

Apple
Orange
Grape
Banana
Strawberry
  • range() method

Sometimes you don't have any data to work with and just need to use Flux as a counter, emitting a number that increments with each new value. To create a counter Flux, the static range() method can be used.

Flux.range(1, 5).subscribe(System.out::println);
  • interval() method

In the Reactor framework, the interval() method can be used to generate a data sequence of Long objects incrementing from 0. Through a set of overloaded methods of interval(), we can specify the delay time before the release of the first element in this data sequence, and the time interval between each element.

When each element in the figure is released, it is equivalent to adding a timer effect. Sample code using the interval() method is as follows:

Flux.interval(Duration.ofSeconds(2), Duration.ofMillis(200)).subscribe(System.out::println);

 The execution effect of this code is equivalent to generating an unbounded data sequence increasing from 0 one by one after waiting for 2 seconds, and pushing data every 200 milliseconds.

  • empty()、error() 和 never()

We can use the three method classes empty(), error() and never() respectively to create some special data sequences. Among them, if you want to create an empty sequence that only contains the end message, you can use the empty() method, and the usage example is shown below. Obviously, there should be no output on the console at this time.

Flux.empty().subscribe(System.out::println);

Then, a sequence containing only error messages can be created with the error() method. If you don't want the created sequence to not emit any similar message notifications, you can also use the never() method to achieve this goal. Of course, these methods are relatively rare and are usually only used for debugging and testing.

It is not difficult to see that the method of statically creating Flux is simple and direct, and is generally used to generate data sequences that have been defined in advance. However, if the data sequence cannot be determined in advance, or the generation process contains complex business logic, then a dynamic creation method is required.

 Dynamically create Flux programmatically

The dynamic creation of Flux is to create data sequences programmatically. The most commonly used methods are the generate() method and the create() method.

  • generate() method

The generate() method generates a Flux sequence that depends on the SynchronousSink component provided by Reactor, which is defined as follows:

public static <T> Flux<T> generate(Consumer<SynchronousSink<T>> generator)

 The SynchronousSink component includes three core methods, next(), complete(), and error(). From the naming of the SynchronousSink component, we can know that it is a synchronous Sink component, which means that the generation process of elements is executed synchronously. It should be noted here that the next() method can only be called at most once. The sample code for creating a Flux using the generate() method is as follows:

Flux.generate(sink -> {
    sink.next("splendor.s");
    sink.complete();
}).subscribe(System.out::println);

Running the code console will print " splendor.s ", we call the next() method once here, and end the data flow through the complete() method, if the complete() method is not called, then all elements will be generated Both are " splendor.s " unbounded data streams.

If you want to introduce state during sequence generation, you can use the generate() method overload as shown below.

Flux.generate(() -> 1, (i, sink) -> {
    sink.next(i);
    if (i == 5) {
        sink.complete();
    }
    return ++i;
}).subscribe(System.out::println);

A variable i representing the intermediate state is introduced here, and then it is judged whether to terminate the sequence according to the value of i. Obviously, the execution effect of the above code will input 5 numbers from 1 to 5 in the console. 

  • create()

Let's look at the create() method again, defined as follows:

public static <T> Flux<T> create(Consumer<? super FluxSink<T>> emitter)

In addition to the three core methods of next(), complete() and error(), FluxSink also defines a backpressure strategy and can generate multiple elements in one call. The sample code for creating a Flux using the create() method is as follows:

Flux.create(sink -> {
    for (int i = 0; i < 5; i++) {
        sink.next("splendor.s" + i);
    }
    sink.complete();
}).subscribe(System.out::println);

Running the code console will print 5 data from "micro-splendor.s 0" to "splendor.s 4". The way to create a Flux object through the create() method is very flexible. 

Create reactive streams through Mono objects

For Mono, it can be considered a special case of Flux, so many methods of creating Flux are also applicable.

For the scenario of creating Mono statically, the methods of just(), empty(), error() and never() given above are also applicable. In addition to these methods, methods such as justOrEmpty() are more commonly used.

The justOrEmpty() method will first determine whether the passed-in object contains a value. Only when the passed-in object is not empty, the Mono sequence will generate the corresponding element. The sample code of this method is as follows:

Mono.justOrEmpty(Optional.of("splendor.s")).subscribe(System.out::println);

If we want to create Mono dynamically, we can also use the create() method and use the MonoSink component. The sample code is as follows:

Mono.create(sink -> sink.success("splendor.s")).subscribe(System.out::println);

 Operator classification

The API in the Reactor library provides a rich set of operators that provide a great convenience for reactive stream specification. However, there are a large number of operators provided in Reactor, and only a few representative types of operators are discussed here.

I divide the Flux and Mono operators into the following six types:

  • Transforming operators are responsible for transforming elements in a sequence into another element;

  • Filtering (Filtering) operator is responsible for removing unnecessary data from the sequence;

  • The Combining operator is responsible for merging, connecting and integrating elements in the sequence;

  • The Conditional operator is responsible for processing the elements in the sequence according to specific conditions;

  • The Reducing operator is responsible for performing various custom cutting operations on the elements in the sequence;

  • The Utility operator is responsible for some auxiliary operations for stream processing.

 Among them, the first three types of operators are collectively referred to as "conversion type" operators, and the remaining three types are collectively referred to as "cutting type" operators.

conversion class operator 

    Conversion operators are the most common when we code, such as buffer, window, map, and flatMap.

  • buffer operator

The function of the buffer operator is equivalent to collecting the elements in the current stream into a collection, and using this collection object as a new data stream. When using the buffer operator to collect elements, you can specify the maximum number of elements contained in the collection object.

 Given a Flux of String values, each of which contains the name of a fruit, you can create a new Flux of List collections, where each List has no more than the specified number of elements.

Flux<String> fruitFlux = Flux.just("Apple", "Orange", "Grape", "Banana", "Strawberry");
fruitFlux.buffer(3).subscribe(System.out::println);

Running the code console will print:

["Apple", "Orange", "Grape"]
["Banana", "Strawberry"]
  • window operator

The function of the window operator is similar to buffer, the difference is that the window operator collects the elements in the current stream into another Flux sequence instead of a collection. So the return type of the operator becomes  Flux<Flux>. The window operator is relatively complex, as shown in the following figure:

 The above figure is more complicated, representing an operation of windowing the sequence. Let's look at the code for easy understanding:

Flux.range(1, 5).window(2).toIterable().forEach(w -> {
    w.subscribe(System.out::println);
    System.out.println("-------");
});

Here we generate 5 elements, and then convert these 5 elements into 3 Flux objects through the window operator. After converting these Flux objects into Iterable objects, print them out through the forEach() loop, and the running code console will print:

1
2
-------
3
4
-------
5
  • map operator

The map operator is equivalent to a mapping operation. It applies a mapping function to each element in the stream to achieve the conversion effect . It is relatively simple. Let's take a look at an example.

Flux.just(1, 2).map(i -> "number-" + i).subscribe(System.out::println);

Running the code console will print:

number-1
number-2

The important thing to understand about map() is that the mapping is performed synchronously because each item is published by the source Flux. If you want to perform the map asynchronously, you should consider using the flatMap() operation. 

  • flatMap operator

The flatMap operator also performs a mapping operation, but unlike map, the operator maps each element in the stream to a stream rather than an element . flatMap() does not simply map one object to another , instead mapping each object to a new Mono or Flux. The result of a Mono or Flux is squashed into a new Flux. When used with subscribeOn(), flatMap() unleashes the asynchronous capabilities of Reactor types. Then merge the elements in all the streams obtained. Please refer to the following figure for the flow chart of the whole process:

Flux.just(1, 5)
   .flatMap(x -> Mono.just(x * x))
   .subscribe(System.out::println);

The effect is as follows:

1
25

 In fact, flatMap can transform any operation you are interested in. For example, in the process of system development, we often encounter the scene of processing the data items obtained from the database query one by one. At this time, we can make full use of the characteristics of the flatMap operator to carry out related operations.

The code shown below demonstrates how to use this operator to query the order information generated by User one by one for the User data obtained from the database.

Flux<User> users = userRepository.getUsers();
users.flatMap(u -> getOrdersByUser(u))

filter operator

  • filter operator 

The filter operator is actually similar to the filter method in Java8, which filters the elements in the stream, and the filter conditions are generally specified through assertions. 

Flux.range(1, 10).filter(i -> i % 2 == 0).subscribe(System.out::println);

 For example, filter out 10 elements from 1 to 10 that are even numbers, and "i % 2 == 0" represents an assertion.

  • first/last operator

The execution effect of the first operator is to return the first element in the stream, and the execution effect of the last operator is to return the last element in the stream.

  • skip/skipLast

If the skip operator is used, the first n elements of the data stream will be ignored. Similarly, if the skipLast operator is used, the last n elements of the stream will be ignored.

  • take/takeLast

The take family of operators is used to extract elements from the current stream. We can extract elements according to the specified number, or we can extract elements according to the specified time interval. Similarly, the takeLast family of operators is used to extract elements from the end of the current stream.

combination operator 

Combination operators commonly used in Reactor include then/when, merge, startWith, and zip, etc. Combination operators are a little more complicated than filter operators.

  • then/when operator

The meaning of the then operator is to wait until the previous operation is completed before proceeding to the next one.

Flux.just(1, 2, 3)
    .then()
    .subscribe(System.out::println);

 Here, although a Flux flow containing three elements 1, 2, and 3 is generated, the then operator will not trigger a new data flow until the upstream elements are executed, that is to say, the incoming elements will be ignored, so the above The code doesn't actually produce any output on the console.

Along with then there is a thenMany operation service, which has the same meaning, but can initiate a new Flux flow. The sample code is shown below, this time we see two elements 4 and 5 printed on the console.

Flux.just(1, 2, 3)
    .thenMany(Flux.just(4, 5))
    .subscribe(System.out::println);

 Correspondingly, the meaning of the when operator is to wait until multiple operations are completed together. The following code is a good example of when operator in action.

public Mono<Void> updateOrders(Flux<Order> orders) {
    return orders
        .flatMap(file -> {
            Mono<Void> saveOrderToDatabase = ...;
            Mono<Void> sendMessage = ...;
            return Mono.when(saveOrderToDatabase, sendMessage);
   });
}

 Suppose we batch update the order list, first persist the order data to the database, and then send a notification message. We need to ensure that the method returns after both operations are complete, so the when operator is used.

  • merge operator

The merge operator is used to merge multiple Flux streams into a Flux sequence, and the merge rule is to follow the order in which the elements in the stream are actually generated.

 We create two Flux sequences respectively through the Flux.intervalMillis() method, and then print them out after merging them.

Flux.merge(Flux.intervalMillis(0, 100).take(2), Flux.intervalMillis(50, 100).take(2))
    .toStream().forEach(System.out::println);

 Please note that the first intervalMillis method here has no delay and generates an element every 100 milliseconds, while the second intervalMillis method sends the first element after a delay of 50 milliseconds, and the time interval is also 100 milliseconds. It is equivalent to two data sequences generating data interleavedly and merging them together. So the execution effect of the above code is as follows:

0
0
1
1

 Similar to merge, there is also a mergeSequential method. Unlike the merge operator, the mergeSequential operator merges streams in units of streams in the order in which all streams are subscribed. Now let's take a look at this code, where only the merge operation is replaced with a mergeSequential operation.

Flux.mergeSequential (Flux.intervalMillis(0, 100).take(2), Flux.intervalMillis(50, 100).take(2))
    .toStream().forEach(System.out::println);

Executing the above code, we will get different results as follows:

0
1
0
1

Obviously from the results, the mergeSequential operation waits for the end of the previous stream to merge the newly generated stream elements.

  • zip operator

The above merge operation conforms to the order of the data sent by the merged Flux, which is consistent with the time order of the data sent by the source. Since both Fluxes are set to send data at a constant frequency, the values ​​alternate through the merged Flux - a…b…a…b on and on. If the sending time of any of these Fluxes is modified, you may see 2 a's followed by 1 b's or 2 b's after 1 a's.

Because merge does not guarantee perfect alternation between sources, you may want to consider using a zip() operation. When two Flux objects are zipped together, a new Flux is produced that produces a tuple containing one item from each source Flux. The following diagram illustrates how two Flux objects can be zipped together:

 Use the zip operator to do nothing when merging. The result is a stream whose element type is Tuple2. The sample code is as follows:

Flux flux1 = Flux.just(1, 2);
Flux flux2 = Flux.just(3, 4);
Flux.zip(flux1, flux2).subscribe(System.out::println);

The execution effect is as follows:

[1,3]
[2,4]

We can use the zipWith operator to achieve the same effect, the sample code is as follows:

Flux.just(1, 2).zipWith(Flux.just(3, 4))
    .subscribe(System.out::println);

conditional operator

  • defaultIfEmpty operator

The defaultIfEmpty operator provides a simple and useful handling method for empty streams. This operator is used to return an element from the original data stream, or a default element if there is no element in the original data stream.

@GetMapping("/orders/{id}")
public Mono<ResponseEntity<Order>> findOrderById(@PathVariable String id) {
     return orderService.findOrderById(id)
         .map(ResponseEntity::ok)
         .defaultIfEmpty(ResponseEntity.status(404).body(null));
}

 As you can see, the defaultIfEmpty operator is used here to implement the default return value. In the HTTP endpoint shown in the sample code, when the specified data cannot be found, we can return an empty object and a 404 status code through the defaultIfEmpty method.

  • takeUntil/takeWhile operators

The basic usage of the takeUntil operator is takeUntil (Predicate predicate), where Predicate represents a predicate condition, and the operator will extract elements from the data stream until the predicate condition returns true.

The sample code of takeUntil is shown below, we want to get 1~10 elements from a sequence containing 100 consecutive elements.

Flux.range(1, 100).takeUntil(i -> i == 10)
    .subscribe(System.out::println);

Similarly, the basic usage of the takeWhile operator is takeWhile (Predicate continuePredicate), where continuePredicate represents an assertion condition. Unlike takeUntil, takeWhile will extract elements only when the continuePredicate condition returns true. The sample code of takeWhile is shown below, and the execution effect of this code is consistent with the sample code of takeUntil.

Flux.range(1, 100).takeWhile(i -> i <= 10)
    .subscribe(System.out::println);
  • skipUntil/skipWhile operators

Corresponding to takeUntil, the basic usage of the skipUntil operator is skipUntil (Predicate predicate). skipUntil will discard elements in the original data stream until the Predicate returns true.

Similarly, corresponding to takeWhile, the basic usage of the skipWhile operator is skipWhile (Predicate continuePredicate), and elements are discarded only when continuePredicate returns true.

clipping operator 

Clipping operators are often used to count the number of elements in a stream, or to check whether an element has a certain attribute. In Reactor, commonly used clipping operators are any, concat, count, and reduce.

  • any operator

The any operator is used to check whether at least one element has the specified attribute, the code is as follows:

Flux.just(3, 5, 7, 9, 11, 15, 16, 17)
    .any(e -> e % 2 == 0)
    .subscribe(isExisted -> System.out.println(isExisted));

There is an element 16 divisible by 2 in this Flux stream, so the console will output "true".

  • concat operator

There is an element 16 divisible by 2 in this Flux stream, so the console will output "true".

  • concat operator

The concat operator is used to combine data from different Fluxes. Unlike the merge operator described above, this merge is performed in a sequential manner, so it is not strictly a merge operation, so we classify it into the clipping operator category.

Flux.concat(
    Flux.range(1, 3),
    Flux.range(4, 2),
    Flux.range(6, 5)
).subscribe(System.out::println);

We will see the 10 numbers 1 through 10 sequentially in the console.

  • reduce operator

The most classic cut operator is the reduce operator. The reduce operator accumulates all elements from the Flux sequence and obtains a Mono sequence that contains the final calculation result. The schematic diagram of the reduce operator is as follows:

The BiFunction here is a summation function, which is used to sum the numbers from 1 to 10, and the result is 55.

Flux.range(1, 10).reduce((x, y) -> x + y)
    .subscribe(System.out::println);

Similar to the reduce operator, there is also a reduceWith operator, which is used to specify an initial value during the reduce operation. The code example for the reduceWith operator is shown below, we initialize the summation process with 5 and obviously the result will be 60.

Flux.range(1, 10).reduceWith(() -> 5, (x, y) -> x + y)
    .subscribe(System.out::println);

 tool operator

Common tool operators in Reactor include subscribe, timeout, block, log, and debug.

  • subscribe operator

 The subscribe operator is the most used, so let's look at its API. 

/订阅流的最简单方法,忽略所有消息通知
subscribe();

//对每个来自 onNext 通知的值调用 dataConsumer,但不处理 onError 和 onComplete 通知
subscribe(Consumer<T> dataConsumer);

//在前一个重载方法的基础上添加对 onError 通知的处理
subscribe(Consumer<T> dataConsumer, Consumer<Throwable> errorConsumer);

//在前一个重载方法的基础上添加对 onComplete 通知的处理
subscribe(Consumer<T> dataConsumer, Consumer<Throwable> errorConsumer, Runnable completeConsumer);

//这种重载方法允许通过请求足够数量的数据来控制订阅过程
subscribe(Consumer<T> dataConsumer, Consumer<Throwable> errorConsumer, Runnable completeConsumer, Consumer<Subscription> subscriptionConsumer);

//订阅序列的最通用方式,可以为我们的 Subscriber 实现提供所需的任意行为
subscribe(Subscriber<T> subscriber);
  • timeout operator

The timeout operator is very simple, keeping the original stream publisher and generating an exception when no events are produced for a certain period of time.

  • block operator

 As the name implies, the block operator blocks until the next element is received. The block operator is often used to transform reactive data streams into traditional data streams. For example, use the following method to convert the Flux data flow and Mono data flow into a common List object and a single Order object respectively, and we can also set the waiting time of the block operation.

public List<Order> getAllOrders() {
    return orderservice.getAllOrders().block(Duration.ofSecond(5));
}

public Order getOrderById(Long orderId) {
    return orderservice.getOrderById(orderId).block(Duration.ofSecond(2));
}
  • log operator

The tool operator log for logs is specially provided in Reactor, which will observe all data and use log tools for tracking. We can demonstrate the use of the log operator through the following code, adding the log() function directly after the Flux.just() method.

Flux.just(1, 2).log().subscribe(System.out::println);

 The execution result of the above code is as follows (for the sake of brevity, some content and format have been adjusted). Usually, we can also add parameters to the log() method to specify the name of the log category.

Info: | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
Info: | request(unbounded)
Info: | onNext(1)
1
Info: | onNext(2)
2
Info: | onComplete()

Guess you like

Origin blog.csdn.net/SHYLOGO/article/details/129447238