Spring 5 responsive programming


main points

  • Reactor is a responsive flow framework running on top of java8, which provides a set of responsive style APIs
  • Except for the differences in individual APIs, its principle is very similar to rxjava
  • It is the fourth generation of responsive framework, supporting operation fusion, similar to rxjava 2
  • Spring 5’s responsive programming model mainly relies on reactor

Rxjava review

ReactorIt is the fourth generation responsive framework, similar to rxjava 2. The reactor project, launched by pivot, is based on the responsive flow specification, java8, and the reactivex glossary. Its design is the result of a joint effort by reactor 2 (the last major release) and the core contributors to rxjava.

In the previous article rxjava instance parsing and testing rxjava, we have learned the basics of responsive programming: the concept of data flow, observable classes and its various operations, and the creation of static and dynamic observable objects through factory methods.

Observable is the source of events. Observer provides a set of simple interfaces and consumes observable events by subscribing to the event source. Observable notifies the observer of the arrival of the event through onnext. It may be followed by onerror or oncomplete to indicate the end of the event.

Rxjava provides testsubscriber to test observable. Testsubscriber is a special observer that can be used to assert stream events.

In this article, we will focus onReactorandRxJavaCompare them, including their similarities and differences.

Type of reactor

There are two types of reactor,Flux<T>andMono<T>

  • FluxLike raxjava’s observable, it can trigger zero to multiple events, and end processing or trigger errors according to the actual situation.
  • MonoOnly one event is triggered at most, which is similar to rxjava’s single and may, so mono < void > can be used to notify when an asynchronous task is completed.

Because of the simple difference between the two types, we can easily distinguish the types of responsive APIs: from the returned types, we can know that a method willLaunch and forgetorRequest and wait(mono), or processing a stream containing multiple data items (flux).

FluxandMonoSome of these operations take advantage of this feature to convert between the two types. For example, calling the single() method of flux < T > will return a mono < T >, while using the concatwith() method to string two mono’s together can get a flux. Similarly, some operations are meaningless to mono (for example, take (n) will get a result of N > 1), while some operations only make sense on mono (for example, or (othermono)).

ReactorOne of the principles of design isKeep the API simpleThe separation of these two types of response is a compromise between expressiveness and API usability.

Build on RX using responsive flow

just asAnalysis of rxjava instanceIn terms of design concept,RxJavaIt’s kind of similarJava 8 Streams API。 andReactorIt looks like a little bitRxJavaBut it’s not just a coincidence. The purpose of this design is to provide a set of native response flow API with RX operation style for complex asynchronous logic. Therefore, reactor is rooted in responsive flow and is as close to rxjava as possible in terms of API.

The use of responsive class library and responsive flow

Reactive streams (hereinafter referred to as RS) is a specification, which provides a standard for asynchronous flow processing based on non blocking back pressure. It is a set of specifications including TCK tool suite and four simple interfaces (publisher, subscriber, subscription and processor), which will be integrated into Java 9

RS is mainly concerned with reactive back pressure (more on this later) and the interaction between multiple responsive event sources. It does not provide any operation methods, it only focuses on the life cycle of the flow.

The key point that reactor differs from other frameworks is rs. Both flux and mono are the publisher implementations of RS, which have the characteristics of responsive back pressure.

stayRxJava 1RXRs does not support any type of back pressure operation in Java observable. It can be said that rxjava 1 actually appeared earlier than the RS specification, and rxjava 1 acted as a functional worker during the design of RS specification.

So when you use those publisher adapters, they don’t provide you with any operations. In order to be able to do some useful operations, you may need to use observable again, and at this time you need another adapter. This kind of visual confusion will destroy the readability of the code, especially for frameworks like spring 5. If the whole framework is built on such a publisher, it will be even more messy.

The RS specification does not support null values, so pay attention to this when migrating from rxjava 1 to reactor or rxjava 2. If you use null as a special purpose in your code, you should pay more attention.

RxJava 2It comes after the RS specification, so it implements publisher directly in the flowable type. However, in addition to the RS type, rxjava 2 also retains rxjava 1’sLegacyType (observable, completable, and single) and introduces some other optional types — maybe. These types provide different semantics, but they do not implement RS interface, which is their shortcoming. Unlike rxjava 1, rxjava 2’s observable does not support rxjava 2’s back pressure protocol (only flowable has this feature). The reason for this design is to provide a set of rich and fluent APIs for some scenarios, such as the events sent by the user interface. In such scenarios, back pressure is not needed, and it is impossible to use it. Completable, single, and maybe don’t need to support back pressure, but they also provide a rich set of APIs and don’t do anything until they’re subscribed.

In the responsive field, reactor has become more and more lean. Its mono and flux types implement publisher, and both support back pressure. Although mono as a publisher needs to pay some extra costs, but other advantages of mono make up for its shortcomings. In the following sections we will see what back pressure means for mono.

Compared with rxjava, the API is similar but different

The terms of reactivex and rxjava operations are sometimes really hard to grasp, because the names of some operations are confusing for historical reasons. Reactor tries its best to design the API compactly and chooses a better name when naming the API. However, generally speaking, the two sets of APIs look very similar. In the latest iteration version of rxjava 2, rxjava 2 draws on some terms of reactor, which indicates that there may be closer cooperation between the two projects. Some operations and concepts always appear in one of the projects, then learn from each other, and finally penetrate into two projects at the same time.

For example, flux also has a common just factory approach (although there are only two variants: accepting a parameter or a variable length parameter). However, there are many variations of the from method, and the most noteworthy one is from iteratable. Of course, flux also includes the normal operations: map, merge, concat, flatmap, take, and so on.

Reactor changed the confusing AMB operation in rxjava into the more pertinent first posting. In addition, to keep the API consistent, tolist is renamed collectlist. In fact, all operations that start with collect aggregate values into a collection of a specific type, but only one mono is generated for each collection. All operations beginning with to are reserved for type conversion, and the converted type can be used for nonresponsive programming, such as tofuture().

In the aspect of class initialization and resource utilization, the reason why reactor can perform so excellently is due to its fusion feature: reactor can merge multiple serial operations (such as calling concatwith twice) into a single operation, so that the internal class of this operation can be initialized only once (that is, macro fusion). This feature includes data source based optimizations to offset some of the additional overhead of mono in implementing publisher. It can also share resources (i.e., micro fusion) between multiple related operations, such as internal queues. These features make reactor a truly responsive fourth generation framework, but this is beyond the scope of this article.

Let’s take a look at the operations of several reactors.

Some operation examples

(this section contains some code snippets. We suggest that you run them and take a deep look at reactor. So you need to open the IDE, create a test project, and add reactor to the dependency.)

For maven, you can add the following dependencies to pom.xml Li:


For gradle, take reactor as a dependency, like this:

dependencies {
    compile "io.projectreactor:reactor-core:3.0.3.RELEASE"

Let’s rewrite the examples in the previous articles in the same series!

The creation of observable is a little similar to rxjava. In reactor, you can use the factory methods of just (t…) and from iterator (iteratable < T >). The just method triggers the list as a whole, while fromiterable triggers each element in the list one by one

public class ReactorSnippets {
  private static List<String> words = Arrays.asList(

  public void simpleCreation() {
     Flux<String> fewWords = Flux.just("Hello", "World");
     Flux<String> manyWords = Flux.fromIterable(words);


As in rxjava, the above code will print out:


In order to print every letter in a sentence, we also need the flatmap method (as in rxjava), but in reactor we use fromarray instead of from. Then we use distinct to filter out duplicate letters and sort them. Finally, we use zipwith and range to output the order of each letter:

public void findingMissingLetter() {
  Flux<String> manyLetters = Flux
        .flatMap(word -> Flux.fromArray(word.split("")))
        .zipWith(Flux.range(1, Integer.MAX_VALUE),
              (string, count) -> String.format("%2d. %s", count, string));


We can easily see that s is missing:

1. a
2. b
18. r
19. t
20. u
25. z

But we can fix the problem by adding the word “concat” and “concat” manually

public void restoringMissingLetter() {
  Mono<String> missing = Mono.just("s");
  Flux<String> allLetters = Flux
        .flatMap(word -> Flux.fromArray(word.split("")))
        .zipWith(Flux.range(1, Integer.MAX_VALUE),
              (string, count) -> String.format("%2d. %s", count, string));


In this way, after de duplication and sorting, the missing s letters are added:

1. a
2. b
18. r
19. s
20. t
26. z

The last article mentioned the similarities between RX and streams API. In fact, when the data is ready, reactor will start to push data events as simply as Java steam does (see the following about back pressure). Only subscribing to the event source in the main thread can not complete more complex asynchronous operations, mainly because after the subscription is completed, the control right will return to the main thread and exit the whole program. For example:

public void shortCircuit() {
  Flux<String> helloPauseWorld = 


This unit test will print outHello, but could not be printedworldBecause the program exits prematurely. When doing simple tests, if you just write a simple main class like this, you usually fall into a trap. As a remedy, you can create a countdownlatch object and call the countdown method in the subscriber (including onerror and oncomplete). But that makes it less responsive, isn’t it? What if you forget to call the countdown method and an error happens

The second way to solve this problem is to use some operations to switch to non responsive mode. Toitetable and tostream generate blocking instances. We use tostream in our example:

public void blocks() {
  Flux<String> helloPauseWorld = 


As you would expect, in printHelloThen there is a short pause, then print out “world” and exit. As we mentioned earlier, the AMB operation of rxjava is renamed as firstemitting in reactor (as its name implies: select the first flux to trigger). In the following example, we will create a mono with a delay of 450 ms and a flux that triggers events at 400 ms intervals. When merging them with firstemitting(), because the first value of flux appears before the value of mono, flux will be adopted in the end

public void firstEmitting() {
  Mono<String> a = Mono.just("oops I'm late")
  Flux<String> b = Flux.just("let's get", "the party", "started")

  Flux.firstEmitting(a, b)

This unit test prints out all parts of the sentence with a 400 millisecond interval between them.

At this point, you might think, what if I wrote a test with a 4000 millisecond interval instead of a 400 millisecond interval? You don’t want to wait four seconds in a unit test! In the following sections, we will see that reactor provides some testing tools to solve this problem.

We’ve compared some common operations of reactor with examples, and now we’ll go back to other aspects of the framework.

Based on Java 8

Reactor chose Java 8 as the running base instead of any previous version, which again coincided with its goal of simplifying the API: rxjava chose Java 6, but Java 6 did not java.util.function Package, rxjava cannot use the functino class and consumer class under this package, so it must create many similar func1, func2, action0, action1 Such a class. Rxjava 2 takes these classes as java.util.function Because it also has to support Java 7.

The reactor API also uses some of the new types introduced in Java 8. Because most time-based operations are related to time periods (such as timeout, interval, delay, etc.), the duration class in Java 8 is used directly.

Java 8 stream API and completable future and flux / mono can be easily converted to each other. In general, do we want to convert stream to flux? not always. Although the overhead associated with the use of the mono stream API is insignificant, there is no significant overhead associated with the use of the flux stream API. For the above situation, observable needs to be used in rxjava 2. Since observable does not support back pressure, once it is subscribed, it becomes the source of event push. Reactor is based on Java 8, so in most cases, the stream API can meet the requirements. Note that although flux and mono’s factory patterns also support simple types, their main purpose is to merge objects into higher-level streams. So, in general, when applying responsive patterns to existing code, you don’t want to convert “long getcount()” to “mono < long > getcount()”.

On back pressure

Back pressure is one of the main concerns of RS specification and reactor (if there are other concerns). The principle of back pressure is that in a push scenario, the producer’s production speed is faster than the consumer’s consumption speed, and the consumer will send a signal to the producer and say, “Hey, slow down, I can’t handle it.” Instead of abandoning data or risking cascading errors, producers can control the speed of data generation.

You might wonder why there is a need for backlash in mono: what kind of consumer is overwhelmed by a single trigger? The answer is “there should be no such consumer.”. However, there is still a key difference between mono and the way complete future works. The latter only has push: if you hold a reference to future, then an asynchronous task is already executing. On the other hand, the backpressure flux or mono will start the delayed pull push iteration

  1. The delay is because nothing happens until the subscribe () method is called
  2. Pull is because when subscribing and sending a request, the subscriber will send a signal to the upstream to pull the next data block
  3. Next, the producer pushes the data to the consumer, which is within the scope of the consumer’s request

For mono, the subscribe () method is equivalent to a button, and pressing it means I’m ready to receive data. Flux has a similar button, but it’s the request (n) method, which is a generic use of subscribe().

As a publisher, mono often represents a resource consuming task (in io, latency, etc.), and realizing this is the key to understanding backpressure: if you don’t subscribe to it, you don’t have to pay any price for it. Because mono is often arranged in a responsive chain with flux with backpressure, the results from multiple asynchronous data sources may be combined together. This ability to trigger on demand is the key to avoid blocking.

We can use backpressure to distinguish different usage scenarios of mono. Compared with the above example, mono has another common usage scenario: asynchronously aggregating flux data into mono. Reduce and haselement can consume every element in flux, and then aggregate these data in some form (the call result of reduce function and a Boolean value respectively) to expose the data as a mono. In this case, use the Long.MAX_ Value sends back pressure signal to upstream, and upstream will work in full push mode.

Another interesting topic about backpressure is how it limits the number of objects in the stream stored in memory. As a publisher, the data source is likely to have the problem of slow data generation, and the request from downstream exceeds the available data items. In this case, the entire flow naturally goes into push mode, and consumers are notified when new data arrives. When the production peak comes, or when the production speed increases, the whole flow returns to pull mode. In both cases, up to n items of data (the amount of data requested by request () will be kept in memory.

In this way, you can calculate the memory consumption of each n * item with the maximum memory consumption. In fact, in most cases, reactor will make optimization based on N: create internal queue according to the situation, and apply prefetch strategy to automatically request 75% of the data volume each time.

Reactor operations sometimes change the back pressure signal according to the semantics they represent and the expectations of the caller. For example, for the operation buffer (10): the downstream requests n items of data, and this operation will request 10N data from the upstream, so as to fill the buffer and provide sufficient data for the subscriber. This is often referred to as “active backpressure,” and developers can take advantage of this feature. For example, in a micro batch scenario, they can explicitly tell reactor how to switch from an input source to an output location.

Relationship with spring

Reactor is the foundation of spring ecosystem, especially spring 5 (through spring web reactive) and spring data “Kay” (corresponding to spring data commons 2.0).

The responsive versions of these two projects are very useful, so we can develop fully responsive web applications: processing requests asynchronously, all the way to the database, and finally returning results asynchronously. Therefore, spring applications can make more efficient use of resources and avoid allocating a single thread for each request and waiting for I / O blocking.

Reactor will be used as the internal responsive core components of future spring applications, as well as the APIs exposed by these spring components. Generally, they can handle RS publisher, but most of the time they have to deal with flux / mono, which requires the rich features of reactor. Of course, you can also choose other responsive frameworks. Reactor provides hook interfaces that can be used to fit other reactor types, rxjava types and even simple RS types.

Currently, you can use the spring boot 2.0.0.build-snapshot and spring boot starter web reactive dependencies (available in the start.spring.io To experience spring web reactive:


You can write your @ controller as usual, but change the bottom layer of spring MVC into responsive, and replace most of spring MVC contracts with responsive non blocking contracts. The reactive layer runs on Tomcat 8.5 by default. You can also choose to use underwow or netty.

{% asset_img 1.jpg %}

In addition, although the spring API is based on the reactor type, there are a variety of responsive types for requests and responses in the spring web reactive module:

  • Mono < T >: as @ requestbody, request entity T will be asynchronously deserialized, and subsequent processing can be associated with mono. As a return type, each time mono issues a value, t is asynchronously serialized and sent back to the client. You can take the request mono as a parameter and return the parameterized Association processing as the result mono.
  • Flux < T >: used in stream scenarios (as input stream for @ requestbody and server sent events containing flux return type).
  • Single / observable: corresponds to mono and flux respectively, but switches back to rxjava.
  • Mono as return type: at the end of mono, the processing of the request is completed.
  • Nonresponsive return types (void and T): your @ controller method is synchronous at this time, but it should be non blocking (transient processing). Request processing ends at the end of the method execution, and the returned t is asynchronously serialized and sent back to the client.

Here is an example of using spring web reactive:

public class ExampleController {

  private final MyReactiveLibrary reactiveLibrary;

  public ExampleController(@Autowired MyReactiveLibrary reactiveLibrary) {
     this.reactiveLibrary = reactiveLibrary;

  public Mono<String> hello(@PathVariable String who) {
       return Mono.just(who)
             .map(w -> "Hello " + w + "!");

  @RequestMapping(value = "heyMister", method = RequestMethod.POST)
  public Flux<String> hey(@RequestBody Mono<Sir> body) {
      return Mono.just("Hey mister ")
                  .flatMap(sir -> Flux.fromArray(sir.getLastName().split("")))
            ).concatWith(Mono.just(". how are you?"));

The first endpoint contains a path variable, which is converted into mono and mapped into a greeting that is returned to the client.

A get request to / hello / Simon gets a text response of “Hello Simon!”.

The second endpoint is a bit more complex: it asynchronously receives a serialized Sir object (a class containing the firstname and LastName attributes) and maps it to an alphabet containing all the letters of LastName using the flatmap method. Then it selects the first letter in the stream, capitalizes it, and strings it with the greeting.

So post a JSON object to / heymaster

    "firstName": "Paul",
    "lastName": "tEsT"

Returns the string “Hello Mister T. how are you?”.

Responsive spring data is also under development as part of the Kay release, and the code is on the spring data commons 2.0. X branch. A milestone version is now available:


Then simply add the dependency of spring data Commons (it will automatically get the version number from the BOM above)


Spring data’s support for responsive is mainly reflected in the new reactivecrudrepository interface, which extends the repository. This interface exposes crud methods that use input and return values of type reactor. There is also a version of rxjava 1, called rxjava1crudrepository. To obtain an entity by ID in crudrepository, you can call the “t findone (ID ID)” method, and call “mono < T > findone (ID ID ID)” and “observable < T > findone (ID ID ID)” in reactivecrudrepository and rxjava1crudrepository, respectively. There are other variants that take mono / single as parameters, provide keys asynchronously, and combine the results on this basis.

Suppose there is a responsive back-end storage (or mock’s reactivecrudrepository bean), the following controllers will be responsive from front to back:

public class DataExampleController {

  private final ReactiveCrudRepository<Sir, String> reactiveRepository;

  public DataExampleController(
                 @Autowired ReactiveCrudRepository<Sir, String> repo) {
     this.reactiveRepository = repo;

  public Mono<ResponseEntity<Sir>> hello(@PathVariable String who) {
     return reactiveRepository.findOne(who)

Notice the whole process: we get the entity asynchronously and wrap it as a responseentity with a map, and get a mono that can be returned immediately. If the spring data repository cannot find the data of this key, it will return an empty mono. We use defaultifempty to explicitly return 404.

Test reactor

Test rxjavaThis article describes how to test observable. As we can see, rxjava provides testscheduler, which we can use together with rxjava operations. These operations accept a scheduler parameter, and testscheduler will start the virtual clock for these operations. Rxjava also provides a testsubscriber class, which can be used to wait for the observable to be executed, or to assert each event (the value of onnext and its number, the onerror triggered, etc.). In rxjava 2, testsubscriber is RS subscriber. You can use it to test flux and mono of reactor!

In reactor, these two widely used features are combined into the stepverifier class. Step verifier can be obtained from reactor test module of reactor addons warehouse. When you create an instance of publisher, call StepVerifier.create Method can initialize a stepverifier. If you want to use a virtual clock, you can call StepVerifier.withVirtualTime Method, which takes a supplier as an argument. The reason for this design is that it first ensures that a virtualtime scheduler object is created and passed to the old operation as the default scheduler. Step verifier will configure the flux / mono created in the supplier to turn the time-based operation into “virtual time operation”. Then you can write all kinds of use cases that you expect: what should be the next element, whether there should be an error, whether it should move forward in time, and so on. With other methods, such as matching events to predict or consuming the next event, you can do some more advanced interaction with those values (as if using an assertion framework). Any assertion error thrown anywhere will be reflected in the final result. Finally, call verify() to test your use case. This method will pass StepVerifier.create Or StepVerifier.withVirtualTime Method to subscribe to a predefined event source.

Let’s give some simple examples to illustrate how step verifier works. First, add dependency to POM



Suppose you have a class called myreactivelibrary. You need to test some fluxes generated by this class:

public class MyReactiveLibrary {

  public Flux<String> alphabet5(char from) {
     return Flux.range((int) from, 5)
           .map(i -> "" + (char) i.intValue());

  public Mono<String> withDelay(String value, int delaySeconds) {
     return Mono.just(value)

The first method returns five letters after the given letter. The second method returns a flux that triggers the given value at a given interval, in seconds. The first test is to ensure that the output of calling alphabet5 with X is limited to x, y, Z. Using step verifier looks like this:

public void testAlphabet5LimitsToZ() {
  MyReactiveLibrary library = new MyReactiveLibrary();
        .expectNext("x", "y", "z")

The second test ensures that every value returned by alphabet5 is a letter. Here we use the assertion framework assertj:

public void testAlphabet5LastItemIsAlphabeticalChar() {
  MyReactiveLibrary library = new MyReactiveLibrary();
              .consumeNextWith(c -> assertThat(c)
                    .as("first is alphabetic").matches("[a-z]"))
              .consumeNextWith(c -> assertThat(c)
                    .as("second is alphabetic").matches("[a-z]"))
              .consumeNextWith(c -> assertThat(c)
                    .as("third is alphabetic").matches("[a-z]"))
              .consumeNextWith(c -> assertThat(c)
                    .as("fourth is alphabetic").matches("[a-z]"))

As a result, these tests failed. Let’s check the output of stepvirifier to see if we can find the bug:

java.lang.AssertionError: expected: onComplete(); actual: onNext({)

java.lang.AssertionError: [fourth is alphabetic] 
to match pattern:

It seems that our method doesn’t stop at z, but continues to emit ASCII characters. We can join. Take( Math.min (5, ‘Z’ – from + 1)) to fix the bug, or Math.min As the second parameter of range.

The last test we’re going to do needs to use the virtual clock: we use the with virtual time constructor to test the method latency without actually waiting for the specified time:

public void testWithDelay() {
  MyReactiveLibrary library = new MyReactiveLibrary();
  Duration testDuration =
     StepVerifier.withVirtualTime(() -> library.withDelay("foo", 30))
  System.out.println(testDuration.toMillis() + "ms");

This test case tests a flux that will be delayed by 30 seconds: nothing happens within 30 seconds after the subscription, and ends with an on next (“foo”) event.

System.out It prints out the time required for validation, which took 8 milliseconds in the last test.

If you call the constructor’s create method, thenawait and expectnoevent methods can still be used, but they block the specified time.

Custom dynamic source

stayAnalysis of rxjava instanceThe dynamic and static observable mentioned in this article are also applicable to reactor.

If you want to create a custom flux, you need to use fluxsink of reactor. This class will consider all asynchronous situations for you. You just need to focus on triggering events.

use Flux.create The fluxsink obtained from the callback can be used for subsequent triggering events. This custom flux is static. In order to make it dynamic, you can use the publish() and connect() methods. Based on the example in the previous article, we can almost translate it into the version of reactor word for word:

SomeFeed<PriceTick> feed = new SomeFeed<>();
Flux<PriceTick> flux =
     Flux.create(emitter ->
        SomeListener listener = new SomeListener() {
           public void priceTick(PriceTick event) {
              if (event.isLast()) {

           public void error(Throwable e) {
     }, FluxSink.OverflowStrategy.BUFFER);

ConnectableFlux<PriceTick> hot = flux.publish();

Before connecting to dynamic flux, you can make two Subscriptions: one will print the details of each tick, and the other will print the instrument:

hot.subscribe(priceTick -> System.out.printf("%s %4s %6.2f%n", priceTick
     .getDate(), priceTick.getInstrument(), priceTick.getPrice()));

hot.subscribe(priceTick -> System.out.println(priceTick.getInstrument()));

Next, we connect to dynamic flux and let it run for 5 seconds before the end of the program:


(note that if pricetick’s islast() method changes, the feed itself will end.).

Fluxsink checks whether the downstream subscription has been cancelled through iscancelled(). You can also use requestedfromdownstream() to get the number of requests, which is useful when following a back pressure strategy. Finally, you can release all used resources through the setcancellation method.

Note that fluxsink uses back pressure, so you have to provide an overflow strategy to handle backpressure explicitly. This is equivalent to using the onbackpressurexxx operation (for example, FluxSink.OverflowStrategy.BUFFER Equivalent to. Onbackpressurebuffer ()), they will cover the back pressure signal from downstream.


In this article, we learned about reactor, a fourth generation responsive framework that runs on Java 8 and is based on the RX specification and the reactive streams specification. We show how the design concepts in rxjava are applied to reactor, although there are some differences in API design between them. We also showed how reactor became the foundation of spring 5 and provided some resources related to testing publisher, flux and mono.

Welcome to my personal blog

Pay attention to the official account:Java class at 9:30Here are a group of excellent program apes. Join us to discuss technology and make progress together! Reply to “information” to get the latest information of 2T industry!