Project Reactor responsive programming

introduce

"In computers, Reactive Programming or Reactive Programming is a programming paradigm for data flow and change propagation. This means that static or dynamic data flow can be easily expressed in programming language, and the relevant calculation model will automatically propagate the changed values through the data flow. "
——Overview of Wikipedia work

Take a chestnut

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 is 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 migrating to the Java platform, the famous RxJava library was produced, and many corresponding implementations in other programming languages were produced. Based on these implementations, a later Reactive Streams specification was produced. The specification defines the interfaces related to reactive flows and integrates them into Java 9.

Core concepts

The three most important concepts are: observable, subscriber, and subscribe. The relationship between them is expressed in code

Obervable.create().subscribe(new Subscriber())

Advantages:
Thread switching, no need to request and result in different places like handler
Chain programming, complex logic form a chain, eliminate callback hell, fan indentation, and can do a variety of thread switching during
inferiority:
Inconvenient debugging

Flux and Mono

Flux and mono are two basic concepts in Reactor. Flux represents an asynchronous sequence of 0 to N elements. There are three different types of message notifications that can be included in the sequence: normal messages that contain elements, messages that end the sequence, and messages that have sequence errors. When a message notification is generated, the corresponding methods onNext(), onComplete(), and onError() in the subscriber are called. Mono represents an asynchronous sequence of 0 or 1 elements. The sequence can also contain the same three types of message notifications as flux.

How to create Flux

just(): you can specify all elements contained in the sequence. The created Flux sequence will automatically end after these elements are published.
fromArray(), * * fromIterable() *, and fromStream(): you can create a Flux object from an array, Iterable object, or Stream object.
empty(): creates a sequence that does not contain any elements and only publishes the end message.
error(Throwable error): creates a sequence containing only error messages.
never(): creates a sequence that does not contain any message notifications.
range(int start, int count): creates a sequence containing the number of Integer objects starting from start.
interval(Duration period) and interval(Duration delay, Duration period): creates a sequence of Long objects that are incremented from 0. The contained elements are published at the specified interval. In addition to the interval time, you can specify the delay time before the start element is published.

When complex logic is required for sequence generation, the generate() or create() methods should be used.

generate() method
The generate () method generates Flux sequences in a synchronous and one by one fashion. The sequence is generated by calling the next(), complete() and error(Throwable) methods of the provided synchronosink object. Generation by generation means that in the specific generation logic, the next () method can only be called once at most. In some cases, the generation of sequences may be stateful, and some state objects need to be used. At this time, you can use another form of the generate() method, generate (callable < s > stateSupplier, bifunction < s, synchronosink < T >, s > generator), where stateSupplier is used to provide the initial state object. During sequence generation, the state object is passed in as the first parameter used by the generator, and can be modified in the corresponding logic for the next generation.
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 one call.

        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.never().subscribe(System.out::println);
        Flux.range(1, 10).subscribe(System.out::println);

        //Asynchronous execution
        Flux.interval(Duration.of(3, ChronoUnit.SECONDS)).subscribe(e -> System.out.println(Thread.currentThread().getName() + " ===  " + System.currentTimeMillis()));
        Flux.interval(Duration.of(5, ChronoUnit.SECONDS), Duration.of(2, ChronoUnit.SECONDS)).subscribe(e -> System.out.println(Thread.currentThread().getName() + " ===  " + System.currentTimeMillis()));



        //The generate() method generates Flux sequences in a synchronous and one by one fashion.
        // The sequence is generated by calling the next(), complete() and error(Throwable) methods of the provided synchronosink object.
        // Generation by generation means that in the specific generation logic, the next() method can only be called once at most.
        //
        // In some cases, the generation of sequences may be stateful, and some state objects need to be used.
        // At this time, you can use another form of the generate() method, generate (callable < s > statesupplier, bifunction < s, synchronosink < T >, s > generator),
        // stateSupplier is used to provide the initial state object.
        // During sequence generation, the state object is passed in as the first parameter used by the generator, which can be modified in the corresponding logic for use in the next generation.
        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);

        //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 one call. As shown below, all 10 elements are generated in one call.
        Flux.create(sink -> {
            for (int i = 0; i < 10; i++) {
                sink.next(i);
            }
            sink.complete();
        }).subscribe(System.out::println);
        //Because it's asynchronous, I let the main thread sleep for a while
        Thread.sleep(1000000L);

The corresponding console print result is

Hello
World
1
2
3
1
2
3
4
5
6
7
8
9
10
Hello
54
52
14
41
50
66
25
68
56
5
0
1
2
3
4
5
6
7
8
9
parallel-1 ===  1593240678366
parallel-2 ===  1593240680367
parallel-1 ===  1593240681366
parallel-2 ===  1593240682368
parallel-1 ===  1593240684367
parallel-2 ===  1593240684367
parallel-2 ===  1593240686366

How to create Mono

fromCallable(), fromCompletionStage(), fromFuture(), * * fromrunable() * * and fromSupplier(): create Mono from Callable, CompletionStage, completabilefuture, Runnable and Supplier respectively.
delay(Duration duration) and delayMillis(long duration): create a Mono sequence and generate the number 0 as the unique value after the specified delay time.
Ignore elements (Publisher < T > source): create a Mono sequence, ignore all elements in Publisher as the source, and only generate the end message.
Justorempty (Optional <? Extensions T > data) and justOrEmpty(T data): create Mono from an Optional object or possibly null object. The Mono sequence produces the corresponding element only if the Optional object contains a value or the object is not null.

        Mono.fromSupplier(() -> "Hello").subscribe(System.out::println);
        Mono.fromRunnable(() -> System.out.println(Thread.currentThread().getName() + "=====")).subscribe(System.out::println);

        Mono.fromCallable(() -> {
            System.out.println(Thread.currentThread().getName() + "====111====");
            return 111;
        }).subscribe(System.out::println);

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

Corresponding console print results

Hello
main=====
main====111====
111
Hello
Hello

Operator

buffer and bufferTimeout
The purpose of these two operators is to collect the elements in the current flow into the collection and treat the collection object as a new element in the flow. Different criteria can be specified when collecting: the maximum number of elements contained or the time interval between collections. Method buffer() uses only one condition, while bufferTimeout() can specify two conditions at the same time.
bufferUntil is collected until the Predicate returns true. The element that causes Predicate to return true can be added to the current collection or the next collection;
bufferWhile is only collected when Predicate returns true. Once the value is false, the next collection starts immediately.
filter
Filter the elements contained in the flow, leaving only those elements that meet the conditions specified by Predicate
window
The function of window operator is similar to buffer, but the difference is that window operator collects the elements in the current flow into another Flux sequence, so the return value type is Flux < Flux < T > >

zipWith
The zipWith operator merges elements in the current flow with elements in another flow in a one-to-one fashion. When merging, you can do nothing to get a stream whose element type is Tuple2. You can also process the merged elements through a BiFunction function. The element type of the stream you get is the return value of the function

take
The take series operator is used to extract elements from the current flow. There are many ways to extract.
Take (long), take(Duration timespan) and take millis (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.
Take while (Predicate <? Super T > continuepredicate): extract only when Predicate returns true.
Take until other (publisher <? > other): extract elements until another stream begins to produce them.

reduce and reduceWith
The reduce and reduceWith operators accumulate all the elements contained in the convection to get a Mono sequence containing the calculation results. The cumulative operation is represented by a BiFunction. An initial value can be specified during the operation. If there is no initial value, the first element of the sequence is the initial value
merge and mergeSequential
The merge and mergeSequential operators are used to merge multiple streams into a single Flux sequence. The difference is that merge merges in the order in which the elements in all flows are actually generated, while mergeSequential merges in the order in which all flows are subscribed
flatMap and flatMapSequential
The flatMap and flatMapSequential operators convert each element in the flow into a flow, and then merge all the elements in the flow. The difference between flatMapSequential and flatMap is the same as that between mergeSequential and merge

concatMap
The concatMap operator is also used to convert each element in the flow into a flow, and then merge all flows. Different from flatMap, concatMap will merge the transformed flows in turn according to the order of the elements in the original flow. Different from flatMapSequential, the subscription of concatMap to the transformed flows is dynamic, and flatMapSequential has subscribed to all flows before merging
combineLatest
The combine latest operator merges the newly generated elements from all streams into a new element, which returns the elements in the result stream. As long as new elements are generated in any one of the flows, the merge operation will be performed once, and new elements will be generated in the result flow

		System.out.println("----------------------------------buffer----------------------------------------------");
        //buffer
        Flux.range(1, 100).buffer(20).subscribe(System.out::println);
        Flux.interval(Duration.of(100, ChronoUnit.MILLIS)).bufferTimeout(10, Duration.of(1001, ChronoUnit.MILLIS)).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);

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

        System.out.println("-----------------------------window------------------------------------------------------");
        //window
        Flux.range(1, 100).window(20).subscribe(System.out::println);
        Flux.interval(Duration.of(100, ChronoUnit.MILLIS)).windowTimeout(10, Duration.of(1001, ChronoUnit.MILLIS)).take(2).toStream().forEach(System.out::println);

        System.out.println("-------------------------------------zipWith---------------------------------------------");
        //zipWith
        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);


        System.out.println("--------------------------------------take------------------------------------------------");
        //take
        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);

        System.out.println("-------------------------------reduce and reduceWith-----------------------------------------------");
        //reduce and reduceWith
        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);


        System.out.println("---------------------------------merge and mergeSequential----------------------------------------");
        //merge and mergeSequential
        Flux.merge(Flux.interval(Duration.of(100, ChronoUnit.MILLIS)).take(5), Flux.interval(Duration.of(300, ChronoUnit.MILLIS), Duration.of(100, ChronoUnit.MILLIS)).take(5))
                .toStream()
                .forEach(System.out::println);

        Flux.mergeSequential(Flux.interval(Duration.of(100, ChronoUnit.MILLIS)).take(5), Flux.interval(Duration.of(300, ChronoUnit.MILLIS), Duration.of(100, ChronoUnit.MILLIS)).take(5))
                .toStream()
                .forEach(System.out::println);


        System.out.println("---------------------------------flatMap and flatMapSequential----------------------------------------");
        Flux.just(5, 10)
                .flatMap(x -> Flux.interval(Duration.of(10 * x, ChronoUnit.MILLIS)).take(x))
                .toStream()
                .forEach(System.out::println);


        System.out.println("---------------------------------concatMap----------------------------------------");
        Flux.just(5, 10)
                .concatMap(x -> Flux.interval(Duration.of(10 * x, ChronoUnit.MILLIS)).take(x))
                .toStream()
                .forEach(System.out::println);

        System.out.println("---------------------------------combineLatest----------------------------------------");
        Flux.combineLatest(
                Arrays::toString,
                Flux.interval(Duration.of(100, ChronoUnit.MILLIS)).take(5),
                Flux.interval(Duration.of(50, ChronoUnit.MILLIS), Duration.of(100, ChronoUnit.MILLIS)).take(5)
        ).toStream().forEach(System.out::println);

Corresponding console print results

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
[21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40]
[41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60]
[61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80]
[81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
[1, 2]
[3, 4]
[5, 6]
[7, 8]
[9, 10]
[2]
[4]
[6]
[8]
[10]
----------------------------------filter----------------------------------------------
2
4
6
8
10
-----------------------------window------------------------------------------------------
UnicastProcessor
UnicastProcessor
UnicastProcessor
UnicastProcessor
UnicastProcessor
UnicastProcessor
UnicastProcessor
-------------------------------------zipWith---------------------------------------------
15:06:31.918 [parallel-4] DEBUG reactor.core.publisher.Operators - onNextDropped: UnicastProcessor
[a,c]
[b,d]
a-c
b-d
--------------------------------------take------------------------------------------------
1
2
3
4
5
6
7
8
9
10
991
992
993
994
995
996
997
998
999
1000
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9
10
-------------------------------reduce and reduceWith-----------------------------------------------
5050
5150
---------------------------------merge and mergeSequential----------------------------------------
0
1
2
0
3
1
4
2
3
4
0
1
15:06:32.916 [parallel-4] DEBUG reactor.core.publisher.Operators - onNextDropped: UnicastProcessor
2
3
4
0
1
2
3
4
---------------------------------flatMap and flatMapSequential----------------------------------------
0
1
0
2
3
1
4
2
3
4
15:06:33.917 [parallel-4] DEBUG reactor.core.publisher.Operators - onNextDropped: UnicastProcessor
5
6
7
8
9
---------------------------------concatMap----------------------------------------
0
1
2
3
4
0
1
2
3
4
5
6
7
8
9
---------------------------------combineLatest----------------------------------------
[0, 0]
[0, 1]
[1, 1]
[1, 2]
[2, 2]
[2, 3]
[3, 3]
[3, 4]
[4, 4]

Message processing

subscribe
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 you call the subscribe method, you can specify the type of message that needs to be processed. You can process only the normal messages contained in it, or you can process both error messages and completion messages at the same time

        //Handle normal and error messages through the subscribe() method
        Flux.just(1, 2)
                .concatWith(Mono.error(new IllegalStateException()))
                .subscribe(System.out::println, System.err::println);

        System.out.println("-------------------------------------------------------------------------");
        //Return default value when error occurs
        Flux.just(1, 2)
                .concatWith(Mono.error(new IllegalStateException()))
                .onErrorReturn(0)
                .subscribe(System.out::println);

        System.out.println("-------------------------------------------------------------------------");

        //Select flow based on exception type when an error occurs
        Flux.just(1, 2)
                .concatWith(Mono.error(new IllegalArgumentException()))
                .onErrorResume(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);

        System.out.println("-------------------------------------------------------------------------");

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

Corresponding console print results

1
2
java.lang.IllegalStateException
-------------------------------------------------------------------------
1
2
0
-------------------------------------------------------------------------
1
2
-1
-------------------------------------------------------------------------
1
2
1
2
Exception in thread "main" reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalStateException
Caused by: java.lang.IllegalStateException
	at geektime.spring.reactor.simple.ReactorSubscribeDemo.main(ReactorSubscribeDemo.java:44)

publishOn and subscribeOn

The difference between the two lies in the scope of influence. publishOn affects the thread pool executed by the operator after it, while subscribeOn affects the whole execution process from the source. Therefore, the influence range of publishOn is related to its location, while that of subscribeOn is not.

        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);


        System.out.println("-------------------------------------------------------------------------");
        Flux.just("tom")
                .map(s -> {
                    System.out.println("[map] Thread name: " + Thread.currentThread().getName());
                    return s.concat("@mail.com");
                })
                .publishOn(Schedulers.newElastic("thread-publishOn"))
                .filter(s -> {
                    System.out.println("[filter] Thread name: " + Thread.currentThread().getName());
                    return s.startsWith("t");
                })
                .subscribeOn(Schedulers.newElastic("thread-subscribeOn"))
                .subscribe(s -> {
                    System.out.println("[subscribe] Thread name: " + Thread.currentThread().getName());
                    System.out.println(s);
                });

Corresponding console print results

[elastic-2] [single-1] parallel-1
-------------------------------------------------------------------------
[map] Thread name: thread-subscribeOn-5
[filter] Thread name: thread-publishOn-6
[subscribe] Thread name: thread-publishOn-6
tom@mail.com

Scheduler

The above describes the reactive flow and the various operations that can be performed on it. Through the Scheduler, you can specify the execution mode and the thread of these operations

  • Current thread, via Schedulers.immediate() method to create
  • Single reusable thread, through Schedulers.single() method to create
  • Use elastic thread pool, through Schedulers.elastic() method. Threads in the thread pool are reusable. New threads are created when needed. If a thread is idle for too long, it will be destroyed. The scheduler is suitable for the processing of I/O operation related streams
  • Using a pool of threads optimized for parallel operations Schedulers.parallel() method. The number of threads depends on the number of cores in the CPU. The scheduler is suitable for the processing of computationally intensive flows
  • Using a scheduler that supports task scheduling through Schedulers.timer() method to create
  • Create a scheduler from an existing ExecutorService object by Schedulers.fromExecutorService() method to create
        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);


        System.out.println("-------------------------------------------------------------------------");
        Flux.just("tom")
                .map(s -> {
                    System.out.println("[map] Thread name: " + Thread.currentThread().getName());
                    return s.concat("@mail.com");
                })
                .publishOn(Schedulers.newElastic("thread-publishOn"))
                .filter(s -> {
                    System.out.println("[filter] Thread name: " + Thread.currentThread().getName());
                    return s.startsWith("t");
                })
                .subscribeOn(Schedulers.newElastic("thread-subscribeOn"))
                .subscribe(s -> {
                    System.out.println("[subscribe] Thread name: " + Thread.currentThread().getName());
                    System.out.println(s);
                });

Corresponding console print results

[elastic-2] [single-1] parallel-1
-------------------------------------------------------------------------
[map] Thread name: thread-subscribeOn-5
[filter] Thread name: thread-publishOn-6
[subscribe] Thread name: thread-publishOn-6
tom@mail.com

Tags: Programming Java Spring

Posted on Sat, 27 Jun 2020 03:53:49 -0400 by Iokina