Reactive programming with Reactor

Reactive programming (Reactive Programming) this new programming paradigm is increasingly popular with developers. RxJava and RxJava 2 are more popular in the Java community. This article will introduce another new reactive programming library Reactor.

Introduction to reactive programming

Reactive programming comes from the flow of data and the propagation of changes, meaning that the underlying execution model is responsible for automatically propagating changes through the data flow. For example, to evaluate a simple expression c = a + b, when the value of a or b changes, the traditional programming paradigm needs to recalculate a + b to get the value of c. If reactive programming is used, the value of c will be automatically updated when the value of a or b changes. Reactive programming was first implemented by the Reactive Extensions (Rx) library on the .NET platform. Later, after the migration to the Java platform, the famous RxJava library was produced, and many corresponding implementations in other programming languages ​​were produced. Based on these implementations, the later Reactive Streams specification was produced. The specification defines the relevant interfaces for reactive streams and will be integrated into Java 9.

In the traditional programming paradigm, we generally traverse a sequence through the Iterator mode. This traversal method is controlled by the caller, using the pull method. Each time the caller uses the next () method to get the next value in the sequence. When using reactive streaming, the push method is used, which is the common publisher-subscriber model. When new data is generated by the publisher, the data will be pushed to the subscriber for processing. A variety of different operations can be added to the reactive flow to process the data, forming a data processing chain. This processing chain added in a declarative manner is only actually executed when the subscriber performs the subscription operation.

The first important concept in reactive flow is backpressure. In the basic message push mode, when the rate of data generated by the message publisher is too fast, the processing speed of the message subscribers cannot keep up with the generated speed, thereby causing great pressure on the subscribers. When the pressure is too great, it may cause the subscribers to collapse, and the resulting cascading effect may even cause the entire system to be paralyzed. The role of negative pressure is to provide a feedback channel from subscribers to producers. Subscribers can declare the number of messages they can process at one time through the request () method, and the producer will only generate a corresponding number of messages until the next request () method call. This has actually become a combination of push and pull.

Introduction to Reactor

The aforementioned RxJava library is the pioneer of reactive programming on the JVM and the foundation of the reactive flow specification. RxJava 2 has made many updates based on RxJava. However, the RxJava library also has its shortcomings. RxJava was created before the reactive flow specification. Although it can be converted with the reactive flow interface, it is not very intuitive to use because of the underlying implementation. RxJava 2 is designed and implemented with the integration of specifications in mind, but in order to maintain compatibility with RxJava, many places are not intuitive to use. Reactor is a library that is completely designed and implemented based on the reactive flow specification. There is no historical burden like RxJava, and it is more intuitive and easy to use. Reactor is also the foundation of reactive programming in Spring 5. Learning and mastering Reactor can better understand the related concepts in Spring 5.

Using the Reactor library in a Java program is very simple. You only need to add a dependency on io.projectreactor: reactor-core through Maven or Gradle. The current version is 3.0.5.RELEASE.

Flux sum Mono

Flux and Mono are two basic concepts in Reactor. Flux represents an asynchronous sequence of 0 to N elements. Three different types of message notifications can be included in this sequence: a normal element-containing message, a sequence end message, and a sequence error message. When the message notification is generated, the corresponding methods onNext (), onComplete () and onError () in the subscriber will be called. Mono represents an asynchronous sequence containing 0 or 1 elements. This sequence can also contain the same three types of message notifications as Flux. Conversion between Flux and Mono is possible. Counting a Flux sequence, the result is a Mono object. Combining the two Mono sequences together results in a Flux object.

Create Flux

There are many different ways to create Flux sequences.

Flux static methods

The first way is through static methods in the Flux class.

  • just (): You can specify all the elements contained in the sequence. The created Flux sequence will automatically end after publishing these elements.
  • fromArray (), fromIterable () and fromStream (): Flux objects can be created from an array, Iterable object or Stream object.
  • empty (): Create a sequence that contains no elements and only publishes the end message.
  • error (Throwable error): Create a sequence containing only error messages.
  • never (): Create a sequence that does not contain any message notifications.
  • range (int start, int count): Create a sequence of count Integer objects starting from start.
  • interval (Duration period) and interval (Duration delay, Duration period): Create a sequence containing Long objects that increase from 0. The contained elements are published at specified intervals. In addition to the interval time, you can also specify the delay time before the start element is released.
  • intervalMillis (long period) and intervalMillis (long delay, long period): Same as interval () method, except that this method specifies the time interval and delay time in milliseconds.

Code Listing 1 shows examples of using these methods.

Listing 1. Create Flux sequence by static method of Flux class

Flux.just("Hello", "World").subscribe(System.out::println);
Flux.fromArray(new Integer[] {1, 2, 3}).subscribe(System.out::println);
Flux.empty().subscribe(System.out::println);
Flux.range(1, 10).subscribe(System.out::println);
Flux.interval(Duration.of(10, ChronoUnit.SECONDS)).subscribe(System.out::println);
Flux.intervalMillis(1000).subscribe(System.out::println);

The above static methods are suitable for simple sequence generation. When the sequence generation requires complex logic, you should use the generate () or create () method.

generate () method

The generate () method generates Flux sequences in a synchronized and one-by-one manner. The generation of the sequence is accomplished by calling the next (), complete () and error (Throwable) methods of the provided SynchronousSink object. The meaning of one-by-one generation is that in the specific generation logic, the next () method can only be called at most once. In some cases, the generation of sequences may be stateful, and some state objects are needed. At this time, you can use another form of generate () method generate (Callable  stateSupplier, BiFunction <S, SynchronousSink, S> generator), where stateSupplier is used to provide the initial state object. During sequence generation, the state object will be passed in as the first parameter used by the generator, and the state object can be modified in the corresponding logic for use in the next generation.

In Listing 2, the first sequence generation logic generates a simple value by the next () method, and then ends the sequence by the complete () method. If you don't call the complete () method, the result is an infinite sequence. The state object in the second sequence's generation logic is an ArrayList object. The actual value generated is a random number. The generated random number is added to the ArrayList. When 10 numbers are generated, complete the sequence to end the sequence.

Listing 2. Generate () method to generate Flux sequence

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


final Random random = new Random();
Flux.generate(ArrayList::new, (list, sink) -> {
    int value = random.nextInt(100);
    list.add(value);
    sink.next(value);
    if (list.size() == 10) {
        sink.complete();
    }
    return list;
}).subscribe(System.out::println);

create () method

The difference between the create () method and the generate () method is that the FluxSink object is used. FluxSink supports synchronous and asynchronous message generation, and can generate multiple elements in a single call. In Listing 3, all 10 elements are generated in a single call.

Listing 3. Using the create () method to generate a Flux sequence

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

Soken Mono

Mono is created in a similar way to Flux introduced earlier. The Mono class also contains the same static methods as the Flux class. These methods include just (), empty (), error () and never (). In addition to these methods, Mono also has some unique static methods.

  • fromCallable (), fromCompletionStage (), fromFuture (), fromRunnable (), and fromSupplier (): Create Mono from Callable, CompletionStage, CompletableFuture, Runnable, and Supplier respectively.
  • delay (Duration duration) and delayMillis (long duration): Create a Mono sequence, after the specified delay time, generate the number 0 as the unique value.
  • ignoreElements (Publisher source): Create a Mono sequence, ignore all elements in Publisher as the source, and only generate an end message.
  • justOrEmpty (Optional <? extends T> data) and justOrEmpty (T data): Create Mono from an Optional object or possibly null object. Only when the Optional object contains a value or the object is not null, the Mono sequence will produce the corresponding element.

You can also use MonoSink to create Mono through the create () method. An example of creating a Mono sequence is given in Listing 4.

Listing 4. Create Mono sequence

Mono.fromSupplier(() -> "Hello").subscribe(System.out::println);
Mono.justOrEmpty(Optional.of("Hello")).subscribe(System.out::println);
Mono.create(sink -> sink.success("Hello")).subscribe(System.out::println);

Operator

Like RxJava, Reactor is powerful in that it can add a variety of different operators in a declarative way to reactive streams. The important operators are classified and introduced below.

buffer 和 bufferTimeout

The purpose of these two operators is to collect the elements in the current stream into a collection, and use the collection object as a new element in the stream. Different conditions can be specified when collecting: the maximum number of elements contained or the collection interval. The method buffer () uses only one condition, while bufferTimeout () can specify both conditions. When specifying the time interval, you can use the Duration object or the number of milliseconds, that is, the two methods of bufferMillis () or bufferTimeoutMillis ().

In addition to the number of elements and the time interval, it can also be collected through the bufferUntil and bufferWhile operators. The parameters of these two operators are Predicate objects that represent the conditions to be met by the elements in each collection. bufferUntil will keep collecting until the Predicate returns true. The element that makes the Predicate return true can be added to the current collection or the next collection; bufferWhile is only collected when the Predicate returns true. Once the value is false, the next collection will start immediately.

Listing 5 shows an example of using buffer related operators. The first line of statements output 5 arrays containing 20 elements; the second line of statements output 2 arrays containing 10 elements; the third line of statements output 5 arrays containing 2 elements. Whenever an even number is encountered, the current collection ends; the fourth line of the statement outputs 5 arrays containing 1 element, and the array contains only even numbers.

It should be noted that in Listing 5, the Flux sequence is first converted into a Stream object in Java 8 by the toStream () method, and then output by the forEach () method. This is because the generation of the sequence is asynchronous, and conversion to a Stream object can ensure that the main thread will not exit before the sequence generation is completed, so that all elements in the sequence can be output correctly.

Listing 5. Example usage of buffer related operators

Flux.range(1, 100).buffer(20).subscribe(System.out::println);
Flux.intervalMillis(100).bufferMillis(1001).take(2).toStream().forEach(System.out::println);
Flux.range(1, 10).bufferUntil(i -> i % 2 == 0).subscribe(System.out::println);
Flux.range(1, 10).bufferWhile(i -> i % 2 == 0).subscribe(System.out::println);

filter

Filter the elements contained in the stream, leaving only the elements that meet the conditions specified by the Predicate. The statement in Listing 6 outputs all the even numbers from 1 to 10.

Listing 6. Example of filter operator usage

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

window

The window operator functions like a buffer, except that the window operator collects the elements in the current stream into another Flux sequence, so the return value type is Flux <Flux>. In Listing 7, the output of the two-line statement is 5 and 2 UnicastProcessor characters, respectively. This is because the stream generated by the window operator contains objects of the UnicastProcessor class, and the toString method of the UnicastProcessor class outputs UnicastProcessor characters.

Listing 7. Example of window operator usage

Flux.range(1, 100).window(20).subscribe(System.out::println);
Flux.intervalMillis(100).windowMillis(1001).take(2).toStream().forEach(System.out::println);

zipWith

The zipWith operator merges elements in the current stream with elements in another stream in a one-to-one manner. There is no need to do any processing during the merge, and the result is a stream with the element type Tuple2; the merged elements can also be processed through a BiFunction function, and the element type of the resulting stream is the return value of the function.

In Listing 8, the elements contained in the two streams are a, b, and c, d. The first zipWith operator does not use a merge function, so the element type in the result stream is Tuple2; the second zipWith operation uses the merge function to change the element type to String.

Listing 8. Example use of zipWith operator

Flux.just("a", "b")
        .zipWith(Flux.just("c", "d"))
        .subscribe(System.out::println);
Flux.just("a", "b")
        .zipWith(Flux.just("c", "d"), (s1, s2) -> String.format("%s-%s", s1, s2))
        .subscribe(System.out::println);

take

The take series of operators is used to extract elements from the current stream. There are many ways to extract.

  • take (long n), take (Duration timespan) and takeMillis (long timespan): extract according to the specified quantity or time interval.
  • takeLast (long n): extract the last N elements in the stream.
  • takeUntil (Predicate <? super T> predicate): Extract elements until Predicate returns true.
  • takeWhile (Predicate <? super T> continuePredicate): Only extract when the Predicate returns true.
  • takeUntilOther (Publisher <?> other): Extract elements until another stream begins to produce elements.

In Listing 9, the first line of statements output numbers 1 to 10; the second line of statements output numbers 991 to 1000; the third line of statements output numbers 1 to 9; the fourth line of statements output numbers 1 to 10, the elements that make the Predicate return true are also included.

Listing 9. Example use of the take series operator

Flux.range(1, 1000).take(10).subscribe(System.out::println);
Flux.range(1, 1000).takeLast(10).subscribe(System.out::println);
Flux.range(1, 1000).takeWhile(i -> i < 10).subscribe(System.out::println);
Flux.range(1, 1000).takeUntil(i -> i == 10).subscribe(System.out::println);

reduce 和 reduceWith

The reduce and reduceWith operators perform a cumulative operation on all elements contained in the stream to obtain a Mono sequence containing the calculation results. The accumulation operation is represented by a BiFunction. An initial value can be specified during operation. If there is no initial value, the first element of the sequence is used as the initial value.

In Listing 10, the first line of statement adds elements in the stream, and the result is 5050; the second line of statements also performs the addition operation, but the initial value is given by a Supplier, which is 5150 .

Listing 10. Examples of use of reduce and reduceWith operators

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

merge and mergeSequential

The merge and mergeSequential operators are used to merge multiple streams into a Flux sequence. The difference is that merge is merged according to the actual order in which the elements in all streams are generated, while mergeSequential merges in units of streams according to the order in which all streams are subscribed.

Listing 11 uses the merge and mergeSequential operators, respectively. The merged streams all produce an element every 100 milliseconds, but the generation of each element in the second stream is delayed by 50 milliseconds compared to the first stream. In the result stream using merge, the elements from the two streams are interleaved in chronological order; while the result stream using mergeSequential is to first generate all the elements in the first stream and then all the elements in the second stream element.

Listing 11. Examples of use of merge and mergeSequential operators

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

flatMap and flatMapSequential

The flatMap and flatMapSequential operators convert each element in the stream into a stream, and then merge all the elements in the stream. The difference between flatMapSequential and flatMap is the same as the difference between mergeSequential and merge.

In Listing 12, the elements in the stream are converted into a different number of streams generated every 100 milliseconds, and then merged. Because the number of elements in the first stream is small, the elements of the two streams are interwoven in the resulting stream, and then only the elements of the second stream.

Listing 12. Example usage of flatMap operator

Flux.just(5, 10)
        .flatMap(x -> Flux.intervalMillis(x * 10, 100).take(x))
        .toStream()
        .forEach(System.out::println);

concatMap

The concatMap operator also converts each element in the stream into a stream, and then merges all the streams. Unlike flatMap, concatMap will merge the converted streams in sequence according to the order of elements in the original stream; unlike flatMapSequential, concatMap subscribes to the converted stream dynamically, and flatMapSequential has already been merged before the merge Subscribed to all streams.

Code Listing 13 is similar to Code Listing 12, except that flatMap is replaced with concatMap, and the resulting stream contains all the elements in the first stream and the second stream in sequence.

Listing 13. Example usage of concatMap operator

Flux.just(5, 10)
        .concatMap(x -> Flux.intervalMillis(x * 10, 100).take(x))
        .toStream()
        .forEach(System.out::println);

combineLatest

The combineLatest operator merges the most recently generated elements in all streams into a new element as the elements in the returned result stream. As long as a new element is generated in any of these streams, the merge operation will be performed once, and a new element will be generated in the resulting stream. In Listing 14, the newly generated elements in the stream are collected into an array, and the array is converted to a String by the Arrays.toString method.

Listing 14. Example use of combineLatest operator

Flux.combineLatest(
        Arrays::toString,
        Flux.intervalMillis(100).take(5),
        Flux.intervalMillis(50, 100).take(5)
).toStream().forEach(System.out::println);

Message processing

When you need to process messages in Flux or Mono, as shown in the previous code listing, you can add the corresponding subscription logic through the subscribe method. When calling the subscribe method, you can specify the type of message to be processed. You can process only the normal messages contained in it, or you can process error messages and completion messages at the same time. Listing 15 handles both normal and error messages through the subscribe () method.

Listing 15. Handling normal and error messages through the subscribe () method

Flux.just(1, 2)
        .concatWith(Mono.error(new IllegalStateException()))
        .subscribe(System.out::println, System.err::println);

Normal message processing is relatively simple. When an error occurs, there are many different processing strategies. The first strategy is to return a default value through the onErrorReturn () method. In Listing 16, when an error occurs, the stream produces a default value of 0.

Listing 16. Return to default value when an error occurs

Flux.just(1, 2)
        .concatWith(Mono.error(new IllegalStateException()))
        .onErrorReturn(0)
        .subscribe(System.out::println);

The second strategy is to use another stream to generate elements through the switchOnError () method. In Listing 17, when an error occurs, the stream corresponding to Mono.just (0) is generated, which is the number 0.

Listing 17. Use another stream when an error occurs

Flux.just(1, 2)
        .concatWith(Mono.error(new IllegalStateException()))
        .switchOnError(Mono.just(0))
        .subscribe(System.out::println);

The third strategy is to use the onErrorResumeWith () method to select the stream of generated elements to use according to different exception types. In Listing 18, according to the exception type, different streams are returned as the data source when an error occurs. Because the type of the exception is IllegalArgumentException, the resulting element is -1.

Listing 18. Selecting streams based on exception type when an error occurs

Flux.just(1, 2)
        .concatWith(Mono.error(new IllegalArgumentException()))
        .onErrorResumeWith(e -> {
            if (e instanceof IllegalStateException) {
                return Mono.just(0);
            } else if (e instanceof IllegalArgumentException) {
                return Mono.just(-1);
            }
            return Mono.empty();
        })
        .subscribe(System.out::println);

When an error occurs, you can also use the retry operator to retry. The retry action is achieved by re-subscribing to the sequence. You can specify the number of retries when using the retry operator. Code Listing 19 specifies the number of retries as 1, and the output results are 1, 2, 1, 2 and error message.

Listing 19. Retry using the retry operator

Flux.just(1, 2)
        .concatWith(Mono.error(new IllegalStateException()))
        .retry(1)
        .subscribe(System.out::println);

scheduler

The reactive flow and the various operations that can be performed on it are described above. The scheduler (Scheduler) can specify the manner in which these operations are performed and the thread where they are located. There are several different scheduler implementations.

  • The current thread is created by the Schedulers.immediate () method.
  • A single reusable thread is created by the Schedulers.single () method.
  • Use an elastic thread pool, created by the Schedulers.elastic () method. The threads in the thread pool can be reused. When needed, new threads are created. If a thread is idle for too long, it will be destroyed. The scheduler is suitable for the processing of streams related to I / O operations.
  • The thread pool optimized for parallel operations is created by the Schedulers.parallel () method. The number of threads depends on the number of CPU cores. The scheduler is suitable for processing of computationally intensive streams.
  • Use a scheduler that supports task scheduling, created by the Schedulers.timer () method.
  • Create a scheduler from an existing ExecutorService object, through Schedulers.fromExecutorService () method.

Some operators already use a specific type of scheduler by default. For example, the stream created by the intervalMillis () method uses the scheduler created by Schedulers.timer (). Through the publishOn () and subscribeOn () methods, you can switch the scheduler that performs the operation. The publishOn () method switches the execution mode of the operator, and the subscribeOn () method switches the execution mode when generating elements in the stream.

In Listing 20, use the create () method to create a new Flux object, which contains the only element that is the name of the current thread. Then there are two pairs of publishOn () and map () methods, whose function is to first switch the scheduler during execution, and then add the current thread name as a prefix. Finally, the subscribeOn () method is used to change the execution method when the stream is generated. The result after running is [elastic-2] [single-1] parallel-1. The innermost thread name parallel-1 comes from the Schedulers.parallel () scheduler used when generating elements in the stream, and the middle thread name single-1 comes from the Schedulers.single () scheduler before the first map operation, the outermost The layer's thread name elastic-2 comes from the Schedulers.elastic () scheduler before the second map operation.

Listing 20. Use scheduler to switch operator execution mode

Flux.create(sink -> {
    sink.next(Thread.currentThread().getName());
    sink.complete();
})
.publishOn(Schedulers.single())
.map(x -> String.format("[%s] %s", Thread.currentThread().getName(), x))
.publishOn(Schedulers.elastic())
.map(x -> String.format("[%s] %s", Thread.currentThread().getName(), x))
.subscribeOn(Schedulers.parallel())
.toStream()
.forEach(System.out::println);

test

When testing code using Reactor, you need to use the io.projectreactor.addons: reactor-test library.

Use StepVerifier

A typical scenario when testing is for a sequence to verify that the elements it contains are as expected. The role of StepVerifier is to verify the elements contained in the sequence one by one. In Listing 21, the stream to be verified contains two elements, a and b. Use StepVerifier.create () to wrap a stream and then verify it. The expectNext () method is used to declare the value of the next element in the stream expected during the test, and the verifyComplete () method verifies whether the stream ends normally. A similar method has verifyError () to verify that the stream was terminated due to an error.

Listing 21. Using StepVerifier to verify elements in the stream

StepVerifier.create(Flux.just("a", "b"))
        .expectNext("a")
        .expectNext("b")
        .verifyComplete();

Operation test time

Some sequences are generated with time requirements, for example, a new element is generated every 1 minute. In conducting tests, it is impossible to spend actual time waiting for each element to be generated. In this case, you need to use the virtual time function provided by StepVerifier. StepVerifier using virtual clock can be created by StepVerifier.withVirtualTime () method. The virtual clock can be advanced through the thenAwait (Duration) method.

In Listing 22, the stream to be verified contains two elements with a generation interval of one day, and the generation delay of the first element is 4 hours. After wrapping the stream through the StepVerifier.withVirtualTime () method, the expectNoEvent () method is used to verify that no message is generated within 4 hours, and then the first element 0 is generated; then the thenAwait () method advances the virtual clock by one day , And then verify that the second element 1 is generated; finally verify that the flow ends normally.

Listing 22. Operation test time

StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofHours(4), Duration.ofDays(1)).take(2))
        .expectSubscription()
        .expectNoEvent(Duration.ofHours(4))
        .expectNext(0L)
        .thenAwait(Duration.ofDays(1))
        .expectNext(1L)
        .verifyComplete();

使用 TestPublisher

TestPublisher's role is to control the production of elements in the flow, even in violation of the reaction flow specification. In Listing 23, create a new TestPublisher object through the create () method, then use the next () method to generate the element, and use the complete () method to end the flow. TestPublisher is mainly used to test operators created by developers themselves.

Listing 23. Using TestPublisher to create the flow for testing

final TestPublisher<String> testPublisher = TestPublisher.create();
testPublisher.next("a");
testPublisher.next("b");
testPublisher.complete();

StepVerifier.create(testPublisher)
        .expectNext("a")
        .expectNext("b")
        .expectComplete();

debugging

Due to the difference between the reactive programming paradigm and the traditional programming paradigm, code written using Reactor is more difficult to debug when problems occur. In order to better assist developers in debugging, Reactor provides corresponding auxiliary functions.

Enable debug mode

When you need to get more execution information related to the stream, you can add the code in Listing 24 at the beginning of the program to enable debug mode. After the debug mode is enabled, all operators will save additional information related to the execution chain when they are executed. When an error occurs, this information is output as part of the exception stack information. Through this information, it can be analyzed which operator has a problem in the execution.

Listing 24. Enable debug mode

Hooks.onOperator(providedHook -> providedHook.operatorStacktrace());

However, when the debug mode is enabled, recording this additional information comes at a price. Generally, only after an error occurs, consider enabling debug mode. But when debugging mode is enabled to find the problem, the previous error may not be easily reproducible. In order to reduce the possible overhead, you can restrict the debugging mode to only be enabled for certain types of operators.

Use checkpoint

Another approach is to enable debug mode for a specific stream processing chain through the checkpoint operator. In Listing 25, a checkpoint named test is added after the map operator. When an error occurs, the checkpoint name will appear in the exception stack information. For important or complex stream processing chains in the program, checkpoints can be enabled at critical locations to help locate possible problems.

Listing 25. Using the checkpoint operator

Flux.just(1, 0).map(x -> 1 / x).checkpoint("test").subscribe(System.out::println);

Logging

Another practical function in development and debugging is to record stream-related events in the log. This can be achieved by adding the log operator. In Listing 26, the log operator is added and the name of the log classification is specified.

Listing 26. Logging events using the log operator

Flux.range(1, 2).log("Range").subscribe(System.out::println);

In actual operation, the output generated is shown in Listing 27.

Listing 27. The log generated by the log operator

13:07:56.735 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
13:07:56.751 [main] INFO Range - | onSubscribe([Synchronous Fuseable] FluxRange.RangeSubscription)
13:07:56.753 [main] INFO Range - | request(unbounded)
13:07:56.754 [main] INFO Range - | onNext(1)
1
13:07:56.754 [main] INFO Range - | onNext(2)
2
13:07:56.754 [main] INFO Range - | onComplete()

"Cold" and "Hot" Sequence

The cold sequences were created in the previous code listing. The meaning of the cold sequence is that no matter when the subscriber subscribes to the sequence, it can always receive all the messages generated in the sequence. The corresponding hot sequence is to generate messages continuously, and subscribers can only obtain messages generated after their subscription.

In Listing 28, the original sequence contains 10 elements at intervals of 1 second. Convert a Flux object to a ConnectableFlux object through the publish () method. The method autoConnect () is to start generating messages when the ConnectableFlux object has a subscriber. The source.subscribe () code is to subscribe to the ConnectableFlux object and let it start generating data. Then the current thread sleeps for 5 seconds, and the second subscriber can only get the last 5 elements in the sequence at this time, so the output is the numbers 5 to 9.

Listing 28. Hot sequence

final Flux<Long> source = Flux.intervalMillis(1000)
        .take(10)
        .publish()
        .autoConnect();
source.subscribe();
Thread.sleep(5000);
source
        .toStream()
        .forEach(System.out::println);

Conclusion

The reactive programming paradigm is not only a challenge that requires a transformation of thinking for developers who are accustomed to the traditional programming paradigm, but also an opportunity full of more possibilities. Reactor as a new Java library based on the reactive flow specification can be used as the basis for reactive applications. This article gives a detailed introduction to the Reactor library, including the creation of Flux and Mono sequences, the use of common operators, schedulers, error handling, and testing and debugging skills.

Published 203 original articles · praised 6 · visits 4484

Guess you like

Origin blog.csdn.net/weixin_42073629/article/details/105523721