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