1. About the Documentation
This section provides a brief overview of Reactor reference documentation. You do not need to read this guide in a linear fashion. Each piece stands on its own, though they often refer to other pieces.
1.1. Latest Version & Copyright Notice
The Reactor reference guide is available as HTML documents. The latest copy is available at https://projectreactor.io/docs/core/release/reference/index.html
Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.
1.2. Contributing to the Documentation
The reference guide is written in Asciidoc, and you can find its sources at https://github.com/reactor/reactor-core/tree/main/docs/asciidoc.
If you have an improvement or a suggestion, we will be happy to get a pull request from you!
We recommend that you check out a local copy of the repository so that you can
generate the documentation by running the asciidoctor gradle task and checking the
rendering. Some of the sections rely on included files, so GitHub rendering is
not always complete.
| To facilitate documentation edits, most sections have a link at the end that opens an edit UI directly on GitHub for the main source file for that section. These links are only present in the HTML5 version of this reference guide. They look like the following: Suggest Edit to About the Documentation. |
1.3. Getting Help
You can reach out for help in several ways with Reactor:
-
Get in touch with the community on Gitter.
-
Ask a question on stackoverflow.com at
project-reactor. -
Report bugs in Github issues. We closely monitor the following repositories: reactor-core (which covers the essential features) and reactor-addons (which covers reactor-test and adapters issues).
| All of Reactor is open source, including this documentation. If you find problems with the docs or if you want to improve them, please get involved. |
1.4. Where to Go from Here
-
Head to Getting Started if you feel like jumping straight into the code.
-
If you are new to reactive programming, though, you should probably start with the Introduction to Reactive Programming.
-
If you are familiar with Reactor concepts and are just looking for the right tool for the job but cannot think of a relevant operator, try the Which operator do I need? Appendix.
-
In order to dig deeper into the core features of Reactor, head to Reactor Core Features to learn:
-
More about Reactor’s reactive types in the
Flux, an Asynchronous Sequence of 0-N Items andMono, an Asynchronous 0-1 Result sections. -
How to switch threading contexts using a scheduler.
-
How to handle errors in the Handling Errors section.
-
-
Unit testing? Yes it is possible with the
reactor-testproject! See Testing. -
Programmatically creating a sequence offers a more advanced way of creating reactive sources.
-
Other advanced topics are covered in Advanced Features and Concepts.
2. Getting Started
This section contains information that should help you get going with Reactor. It includes the following sections:
2.1. Introducing Reactor
Reactor is a fully non-blocking reactive programming foundation for the JVM, with
efficient demand management (in the form of managing “backpressure”). It integrates
directly with the Java 8 functional APIs, notably CompletableFuture, Stream, and
Duration. It offers composable asynchronous sequence APIs — Flux (for [N] elements) and
Mono (for [0|1] elements) — and extensively implements the
Reactive Streams specification.
Reactor also supports non-blocking inter-process communication with the
reactor-netty project. Suited for Microservices Architecture, Reactor Netty offers
backpressure-ready network engines for HTTP (including Websockets), TCP, and UDP.
Reactive encoding and decoding are fully supported.
2.2. Prerequisites
Reactor Core runs on Java 8 and above.
It has a transitive dependency on org.reactivestreams:reactive-streams:1.0.3.
|
Android Support
|
2.3. Understanding the BOM and versioning scheme
Reactor 3 uses a BOM (Bill of Materials) model (since reactor-core 3.0.4, with the Aluminium release train).
This curated list groups artifacts that are meant to work well together, providing
the relevant versions despite potentially divergent versioning schemes in these artifacts.
Note the versioning scheme has changed between 3.3.x and 3.4.x (Dysprosium and Europium).
Artifacts follow a versioning scheme of MAJOR.MINOR.PATCH-QUALIFIER while the BOM is versioned using a CalVer inspired scheme of YYYY.MINOR.PATCH-QUALIFIER, where:
-
MAJORis the current generation of Reactor, where each new generation can bring fundamental changes to the structure of the project (which might imply a more significant migration effort) -
YYYYis the year of the first GA release in a given release cycle (like 3.4.0 for 3.4.x) -
.MINORis a 0-based number incrementing with each new release cycle-
in the case of projects, it generally reflects wider changes and can indicate a moderate migration effort
-
in the case of the BOM it allows discerning between release cycles in case two get first released the same year
-
-
.PATCHis a 0-based number incrementing with each service release -
-QUALIFIERis a textual qualifier, which is omitted in the case of GA releases (see below)
The first release cycle to follow that convention is thus 2020.0.x, codename Europium.
The scheme uses the following qualifiers (note the use of dash separator), in order:
-
-M1..-M9: milestones (we don’t expect more than 9 per service release) -
-RC1..-RC9: release candidates (we don’t expect more than 9 per service release) -
-SNAPSHOT: snapshots -
no qualifier for GA releases
| snapshots appear higher in the order above because, conceptually, they’re always "the freshest pre-release" of any given PATCH. Even though the first deployed artifact of a PATCH cycle will always be a -SNAPSHOT, a similarly named but more up-to-date snapshot would also get released after eg. a milestone or between release candidates. |
Each release cycle is also given a codename, in continuity with the previous codename-based scheme, which can be used to reference it more informally (like in discussions, blog posts, etc…). The codenames represent what would traditionally be the MAJOR.MINOR number. They (mostly) come from the Periodic Table of Elements, in increasing alphabetical order.
| Up until Dysprosium, the BOM was versioned using a release train scheme with a codename followed by a qualifier, and the qualifiers were slightly different. For example: Aluminium-RELEASE (first GA release, would now be something like YYYY.0.0), Bismuth-M1, Californium-SR1 (service release would now be something like YYYY.0.1), Dysprosium-RC1, Dysprosium-BUILD-SNAPSHOT (after each patch, we’d go back to the same snapshot version. would now be something like YYYY.0.X-SNAPSHOT so we get 1 snapshot per PATCH) |
2.4. Getting Reactor
As mentioned earlier, the easiest way to use Reactor in your core is to use the BOM and add the relevant dependencies to your project. Note that, when you add such a dependency, you must omit the version so that the version gets picked up from the BOM.
However, if you want to force the use of a specific artifact’s version, you can specify it when adding your dependency, as you usually would. You can also forgo the BOM entirely and specify dependencies by their artifact versions.
As of this version (reactor-core 3.5.13), the latest stable BOM in the associated
release train line is 2022.0.14, which is what is used in snippets below.
There might be newer versions since then (including snapshots, milestones and new release train lines),
see https://projectreactor.io/docs for the latest artifacts and BOMs.
|
2.4.1. Maven Installation
Maven natively supports the BOM concept. First, you need to import the BOM by
adding the following snippet to your pom.xml:
<dependencyManagement> (1)
<dependencies>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-bom</artifactId>
<version>2022.0.14</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
| 1 | Notice the dependencyManagement tag. This is in addition to the regular
dependencies section. |
If the top section (dependencyManagement) already exists in your pom, add only the contents.
Next, add your dependencies to the relevant reactor projects, as usual, except without a
<version>, as follows:
<dependencies>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId> (1)
(2)
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId> (3)
<scope>test</scope>
</dependency>
</dependencies>
| 1 | Dependency on the core library. |
| 2 | No version tag here. |
| 3 | reactor-test provides facilities to unit test reactive streams. |
2.4.2. Gradle Installation
Prior to version 5.0, Gradle has no core support for Maven BOMs, but you can use Spring’s gradle-dependency-management plugin.
First, apply the plugin from the Gradle Plugin Portal, as follows:
plugins {
id "io.spring.dependency-management" version "1.0.7.RELEASE" (1)
}
| 1 | as of this writing, 1.0.7.RELEASE is the latest version of the plugin. Check for updates. |
Then use it to import the BOM, as follows:
dependencyManagement {
imports {
mavenBom "io.projectreactor:reactor-bom:2022.0.14"
}
}
Finally add a dependency to your project, without a version number, as follows:
dependencies {
implementation 'io.projectreactor:reactor-core' (1)
}
| 1 | There is no third : separated section for the version. It is taken from
the BOM. |
Since Gradle 5.0, you can use the native Gradle support for BOMs:
dependencies {
implementation platform('io.projectreactor:reactor-bom:2022.0.14')
implementation 'io.projectreactor:reactor-core' (1)
}
| 1 | There is no third : separated section for the version. It is taken from
the BOM. |
2.4.3. Milestones and Snapshots
Milestones and developer previews are distributed through the Spring Milestones repository rather than Maven Central. To add it to your build configuration file, use the following snippet:
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
For Gradle, use the following snippet:
repositories {
maven { url 'https://repo.spring.io/milestone' }
mavenCentral()
}
Similarly, snapshots are also available in a separate dedicated repository, as the following example show:
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshot Repository</name>
<url>https://repo.spring.io/snapshot</url>
</repository>
</repositories>
repositories {
maven { url 'https://repo.spring.io/snapshot' }
mavenCentral()
}
2.5. Support and policies
The entries below are mirroring https://github.com/reactor/.github/blob/main/SUPPORT.adoc
2.5.1. Do you have a question?
| Search Stack Overflow first; discuss if necessary |
If you’re unsure why something isn’t working or wondering if there is a better way of doing it please check on Stack Overflow first and if necessary start a discussion. Use relevant tags among the ones we monitor for that purpose:
-
reactor-nettyfor specific reactor-netty questions -
project-reactorfor generic reactor questions
If you prefer real-time discussion, we also have a few Gitter channels:
-
reactoris the historic most active one, where most of the community can help -
reactor-coreis intended for more advanced pinpointed discussions around the inner workings of the library -
reactor-nettyis intended for netty-specific questions
Refer to each project’s README for potential other sources of information.
We generally discourage opening GitHub issues for questions, in favor of the two channels above.
2.5.2. Our policy on deprecations
When dealing with deprecations, given a version A.B.C, we’ll ensure that:
-
deprecations introduced in version
A.B.0will be removed no sooner than versionA.B+1.0 -
deprecations introduced in version
A.B.1+will be removed no sooner than versionA.B+2.0 -
we’ll strive to mention the following in the deprecation javadoc:
-
target minimum version for removal
-
pointers to replacements for the deprecated method
-
version in which method was deprecated
-
This policy is officially in effect as of January 2021, for all modules in 2020.0 BOMs and newer release trains, as well as Dysprosium releases after Dysprosium-SR15.
|
| Deprecation removal targets are not a hard commitment, and the deprecated methods could live on further than these minimum target GA versions (ie. only the most problematic deprecated methods will be removed aggressively). |
| That said, deprecated code that has outlived its minimum removal target version may be removed in any subsequent release (including patch releases, aka service releases) without further notice. So users should still strive to update their code as early as possible. |
2.5.3. Active Development
The following table summarises the development status of the various Reactor release trains:
| Version | Supported |
|---|---|
2022.0.x (core 3.5.x, netty 1.1.x) |
|
2020.0.x (codename Europium) (core 3.4.x, netty 1.0.x) |
|
Dysprosium Train (core 3.3.x, netty 0.9.x) |
|
Califonium and below (core < 3.3, netty < 0.9) |
|
Reactor 1.x and 2.x Generations |
3. Introduction to Reactive Programming
Reactor is an implementation of the Reactive Programming paradigm, which can be summed up as follows:
Reactive programming is an asynchronous programming paradigm concerned with data streams and the propagation of change. This means that it becomes possible to express static (e.g. arrays) or dynamic (e.g. event emitters) data streams with ease via the employed programming language(s).
As a first step in the direction of reactive programming, Microsoft created the Reactive
Extensions (Rx) library in the .NET ecosystem. Then RxJava implemented reactive
programming on the JVM. As time went on, a standardization for Java emerged through the
Reactive Streams effort, a specification that defines a set of interfaces and
interaction rules for reactive libraries on the JVM. Its interfaces have been
integrated into Java 9 under the Flow class.
The reactive programming paradigm is often presented in object-oriented languages as an
extension of the Observer design pattern. You can also compare the main reactive streams
pattern with the familiar Iterator design pattern, as there is a duality to the
Iterable-Iterator pair in all of these libraries. One major difference is that, while
an Iterator is pull-based, reactive streams are push-based.
Using an iterator is an imperative programming pattern, even though the method of
accessing values is solely the responsibility of the Iterable. Indeed, it is up to the
developer to choose when to access the next() item in the sequence. In reactive
streams, the equivalent of the above pair is Publisher-Subscriber. But it is the
Publisher that notifies the Subscriber of newly available values as they come, and
this push aspect is the key to being reactive. Also, operations applied to pushed values
are expressed declaratively rather than imperatively: The programmer expresses the logic
of the computation rather than describing its exact control flow.
In addition to pushing values, the error-handling and completion aspects are also covered
in a well defined manner. A Publisher can push new values to its Subscriber (by
calling onNext) but can also signal an error (by calling onError) or completion (by
calling onComplete). Both errors and completion terminate the sequence. This can
be summed up as follows:
onNext x 0..N [onError | onComplete]
This approach is very flexible. The pattern supports use cases where there is no value, one value, or n values (including an infinite sequence of values, such as the continuing ticks of a clock).
But why do we need such an asynchronous reactive library in the first place?
3.1. Blocking Can Be Wasteful
Modern applications can reach huge numbers of concurrent users, and, even though the capabilities of modern hardware have continued to improve, performance of modern software is still a key concern.
There are, broadly, two ways one can improve a program’s performance:
-
parallelize to use more threads and more hardware resources.
-
seek more efficiency in how current resources are used.
Usually, Java developers write programs by using blocking code. This practice is fine until there is a performance bottleneck. Then it is time to introduce additional threads, running similar blocking code. But this scaling in resource utilization can quickly introduce contention and concurrency problems.
Worse still, blocking wastes resources. If you look closely, as soon as a program involves some latency (notably I/O, such as a database request or a network call), resources are wasted because threads (possibly many threads) now sit idle, waiting for data.
So the parallelization approach is not a silver bullet. It is necessary to access the full power of the hardware, but it is also complex to reason about and susceptible to resource wasting.
3.2. Asynchronicity to the Rescue?
The second approach mentioned earlier, seeking more efficiency, can be a solution to the resource wasting problem. By writing asynchronous, non-blocking code, you let the execution switch to another active task that uses the same underlying resources and later comes back to the current process when the asynchronous processing has finished.
But how can you produce asynchronous code on the JVM? Java offers two models of asynchronous programming:
-
Callbacks: Asynchronous methods do not have a return value but take an extra
callbackparameter (a lambda or anonymous class) that gets called when the result is available. A well known example is Swing’sEventListenerhierarchy. -
Futures: Asynchronous methods immediately return a
Future<T>. The asynchronous process computes aTvalue, but theFutureobject wraps access to it. The value is not immediately available, and the object can be polled until the value is available. For instance, anExecutorServicerunningCallable<T>tasks useFutureobjects.
Are these techniques good enough? Not for every use case, and both approaches have limitations.
Callbacks are hard to compose together, quickly leading to code that is difficult to read and maintain (known as “Callback Hell”).
Consider an example: showing the top five favorites from a user on the UI or suggestions if she does not have a favorite. This goes through three services (one gives favorite IDs, the second fetches favorite details, and the third offers suggestions with details), as follows:
userService.getFavorites(userId, new Callback<List<String>>() { (1)
public void onSuccess(List<String> list) { (2)
if (list.isEmpty()) { (3)
suggestionService.getSuggestions(new Callback<List<Favorite>>() {
public void onSuccess(List<Favorite> list) { (4)
UiUtils.submitOnUiThread(() -> { (5)
list.stream()
.limit(5)
.forEach(uiList::show); (6)
});
}
public void onError(Throwable error) { (7)
UiUtils.errorPopup(error);
}
});
} else {
list.stream() (8)
.limit(5)
.forEach(favId -> favoriteService.getDetails(favId, (9)
new Callback<Favorite>() {
public void onSuccess(Favorite details) {
UiUtils.submitOnUiThread(() -> uiList.show(details));
}
public void onError(Throwable error) {
UiUtils.errorPopup(error);
}
}
));
}
}
public void onError(Throwable error) {
UiUtils.errorPopup(error);
}
});
| 1 | We have callback-based services: a Callback interface with a method invoked when
the asynchronous process was successful and one invoked when an error occurs. |
| 2 | The first service invokes its callback with the list of favorite IDs. |
| 3 | If the list is empty, we must go to the suggestionService. |
| 4 | The suggestionService gives a List<Favorite> to a second callback. |
| 5 | Since we deal with a UI, we need to ensure our consuming code runs in the UI thread. |
| 6 | We use a Java 8 Stream to limit the number of suggestions processed to five, and we
show them in a graphical list in the UI. |
| 7 | At each level, we deal with errors the same way: We show them in a popup. |
| 8 | Back to the favorite ID level. If the service returned a full list, we need to
go to the favoriteService to get detailed Favorite objects. Since we want only five,
we first stream the list of IDs to limit it to five. |
| 9 | Once again, a callback. This time we get a fully-fledged Favorite object that we
push to the UI inside the UI thread. |
That is a lot of code, and it is a bit hard to follow and has repetitive parts. Consider its equivalent in Reactor:
userService.getFavorites(userId) (1)
.flatMap(favoriteService::getDetails) (2)
.switchIfEmpty(suggestionService.getSuggestions()) (3)
.take(5) (4)
.publishOn(UiUtils.uiThreadScheduler()) (5)
.subscribe(uiList::show, UiUtils::errorPopup); (6)
| 1 | We start with a flow of favorite IDs. |
| 2 | We asynchronously transform these into detailed Favorite objects
(flatMap). We now have a flow of Favorite. |
| 3 | If the flow of Favorite is empty, we switch to a fallback through the
suggestionService. |
| 4 | We are only interested in, at most, five elements from the resulting flow. |
| 5 | At the end, we want to process each piece of data in the UI thread. |
| 6 | We trigger the flow by describing what to do with the final form of the data (show it in a UI list) and what to do in case of an error (show a popup). |
What if you want to ensure the favorite IDs are retrieved in less than 800ms or, if it
takes longer, get them from a cache? In the callback-based code, that is a complicated
task. In Reactor it becomes as easy as adding a timeout operator in the chain, as follows:
userService.getFavorites(userId)
.timeout(Duration.ofMillis(800)) (1)
.onErrorResume(cacheService.cachedFavoritesFor(userId)) (2)
.flatMap(favoriteService::getDetails) (3)
.switchIfEmpty(suggestionService.getSuggestions())
.take(5)
.publishOn(UiUtils.uiThreadScheduler())
.subscribe(uiList::show, UiUtils::errorPopup);
| 1 | If the part above emits nothing for more than 800ms, propagate an error. |
| 2 | In case of an error, fall back to the cacheService. |
| 3 | The rest of the chain is similar to the previous example. |
Future objects are a bit better than callbacks, but they still do not do well at composition,
despite the improvements brought in Java 8 by CompletableFuture. Orchestrating multiple
Future objects together is doable but not easy. Also, Future has other problems:
-
It is easy to end up with another blocking situation with
Futureobjects by calling theget()method. -
They do not support lazy computation.
-
They lack support for multiple values and advanced error handling.
Consider another example: We get a list of IDs from which we want to fetch a name and a
statistic and combine these pair-wise, all of it asynchronously. The following example
does so with a list of type CompletableFuture:
CompletableFuture combinationCompletableFuture<List<String>> ids = ifhIds(); (1)
CompletableFuture<List<String>> result = ids.thenComposeAsync(l -> { (2)
Stream<CompletableFuture<String>> zip =
l.stream().map(i -> { (3)
CompletableFuture<String> nameTask = ifhName(i); (4)
CompletableFuture<Integer> statTask = ifhStat(i); (5)
return nameTask.thenCombineAsync(statTask, (name, stat) -> "Name " + name + " has stats " + stat); (6)
});
List<CompletableFuture<String>> combinationList = zip.collect(Collectors.toList()); (7)
CompletableFuture<String>[] combinationArray = combinationList.toArray(new CompletableFuture[combinationList.size()]);
CompletableFuture<Void> allDone = CompletableFuture.allOf(combinationArray); (8)
return allDone.thenApply(v -> combinationList.stream()
.map(CompletableFuture::join) (9)
.collect(Collectors.toList()));
});
List<String> results = result.join(); (10)
assertThat(results).contains(
"Name NameJoe has stats 103",
"Name NameBart has stats 104",
"Name NameHenry has stats 105",
"Name NameNicole has stats 106",
"Name NameABSLAJNFOAJNFOANFANSF has stats 121");
| 1 | We start off with a future that gives us a list of id values to process. |
| 2 | We want to start some deeper asynchronous processing once we get the list. |
| 3 | For each element in the list: |
| 4 | Asynchronously get the associated name. |
| 5 | Asynchronously get the associated task. |
| 6 | Combine both results. |
| 7 | We now have a list of futures that represent all the combination tasks. To execute these tasks, we need to convert the list to an array. |
| 8 | Pass the array to CompletableFuture.allOf, which outputs a Future that completes
when all tasks have completed. |
| 9 | The tricky bit is that allOf returns CompletableFuture<Void>, so we
reiterate over the list of futures, collecting their results by using join()
(which, here, does not block, since allOf ensures the futures are all done). |
| 10 | Once the whole asynchronous pipeline has been triggered, we wait for it to be processed and return the list of results that we can assert. |
Since Reactor has more combination operators out of the box, this process can be simplified, as follows:
Flux<String> ids = ifhrIds(); (1)
Flux<String> combinations =
ids.flatMap(id -> { (2)
Mono<String> nameTask = ifhrName(id); (3)
Mono<Integer> statTask = ifhrStat(id); (4)
return nameTask.zipWith(statTask, (5)
(name, stat) -> "Name " + name + " has stats " + stat);
});
Mono<List<String>> result = combinations.collectList(); (6)
List<String> results = result.block(); (7)
assertThat(results).containsExactly( (8)
"Name NameJoe has stats 103",
"Name NameBart has stats 104",
"Name NameHenry has stats 105",
"Name NameNicole has stats 106",
"Name NameABSLAJNFOAJNFOANFANSF has stats 121"
);
| 1 | This time, we start from an asynchronously provided sequence of ids (a
Flux<String>). |
| 2 | For each element in the sequence, we asynchronously process it (inside the function
that is the body flatMap call) twice. |
| 3 | Get the associated name. |
| 4 | Get the associated statistic. |
| 5 | Asynchronously combine the two values. |
| 6 | Aggregate the values into a List as they become available. |
| 7 | In production, we would continue working with the Flux asynchronously by further
combining it or subscribing to it. Most probably, we would return the result Mono.
Since we are in a test, we instead block, waiting for the processing to finish, and then
directly return the aggregated list of values. |
| 8 | Assert the result. |
The perils of using callbacks and Future objects are similar and are what reactive programming
addresses with the Publisher-Subscriber pair.
3.3. From Imperative to Reactive Programming
Reactive libraries, such as Reactor, aim to address these drawbacks of “classic” asynchronous approaches on the JVM while also focusing on a few additional aspects:
-
Composability and readability
-
Data as a flow manipulated with a rich vocabulary of operators
-
Nothing happens until you subscribe
-
Backpressure or the ability for the consumer to signal the producer that the rate of emission is too high
-
High level but high value abstraction that is concurrency-agnostic
3.3.1. Composability and Readability
By “composability”, we mean the ability to orchestrate multiple asynchronous tasks, in which we use results from previous tasks to feed input to subsequent ones. Alternatively, we can run several tasks in a fork-join style. In addition, we can reuse asynchronous tasks as discrete components in a higher-level system.
The ability to orchestrate tasks is tightly coupled to the readability and maintainability of code. As the layers of asynchronous processes increase in both number and complexity, being able to compose and read code becomes increasingly difficult. As we saw, the callback model is simple, but one of its main drawbacks is that, for complex processes, you need to have a callback executed from a callback, itself nested inside another callback, and so on. That mess is known as “Callback Hell”. As you can guess (or know from experience), such code is pretty hard to go back to and reason about.
Reactor offers rich composition options, wherein code mirrors the organization of the abstract process, and everything is generally kept at the same level (nesting is minimized).
3.3.2. The Assembly Line Analogy
You can think of data processed by a reactive application as moving through an assembly
line. Reactor is both the conveyor belt and the workstations. The raw material pours from
a source (the original Publisher) and ends up as a finished product ready to be pushed
to the consumer (or Subscriber).
The raw material can go through various transformations and other intermediary steps or be part of a larger assembly line that aggregates intermediate pieces together. If there is a glitch or clogging at one point (perhaps boxing the products takes a disproportionately long time), the afflicted workstation can signal upstream to limit the flow of raw material.
3.3.3. Operators
In Reactor, operators are the workstations in our assembly analogy. Each operator adds
behavior to a Publisher and wraps the previous step’s Publisher into a new instance.
The whole chain is thus linked, such that data originates from the first Publisher and
moves down the chain, transformed by each link. Eventually, a Subscriber finishes the
process. Remember that nothing happens until a Subscriber subscribes to a Publisher,
as we will see shortly.
| Understanding that operators create new instances can help you avoid a common mistake that would lead you to believe that an operator you used in your chain is not being applied. See this item in the FAQ. |
While the Reactive Streams specification does not specify operators at all, one of the best added values of reactive libraries, such as Reactor, is the rich vocabulary of operators that they provide. These cover a lot of ground, from simple transformation and filtering to complex orchestration and error handling.
3.3.4. Nothing Happens Until You subscribe()
In Reactor, when you write a Publisher chain, data does not start pumping into it by
default. Instead, you create an abstract description of your asynchronous process (which
can help with reusability and composition).
By the act of subscribing, you tie the Publisher to a Subscriber, which triggers
the flow of data in the whole chain. This is achieved internally by a single request
signal from the Subscriber that is propagated upstream, all the way back to the source
Publisher.
3.3.5. Backpressure
Propagating signals upstream is also used to implement backpressure, which we described in the assembly line analogy as a feedback signal sent up the line when a workstation processes more slowly than an upstream workstation.
The real mechanism defined by the Reactive Streams specification is pretty close to the
analogy: A subscriber can work in unbounded mode and let the source push all the data
at its fastest achievable rate or it can use the request mechanism to signal the source
that it is ready to process at most n elements.
Intermediate operators can also change the request in-transit. Imagine a buffer
operator that groups elements in batches of ten. If the subscriber requests one buffer, it
is acceptable for the source to produce ten elements. Some operators also implement
prefetching strategies, which avoid request(1) round-trips and is beneficial
if producing the elements before they are requested is not too costly.
This transforms the push model into a push-pull hybrid, where the downstream can pull n elements from upstream if they are readily available. But if the elements are not ready, they get pushed by the upstream whenever they are produced.
3.3.6. Hot vs Cold
The Rx family of reactive libraries distinguishes two broad categories of reactive sequences: hot and cold. This distinction mainly has to do with how the reactive stream reacts to subscribers:
-
A Cold sequence starts anew for each
Subscriber, including at the source of data. For example, if the source wraps an HTTP call, a new HTTP request is made for each subscription. -
A Hot sequence does not start from scratch for each
Subscriber. Rather, late subscribers receive signals emitted after they subscribed. Note, however, that some hot reactive streams can cache or replay the history of emissions totally or partially. From a general perspective, a hot sequence can even emit when no subscriber is listening (an exception to the “nothing happens before you subscribe” rule).
For more information on hot vs cold in the context of Reactor, see this reactor-specific section.
4. Reactor Core Features
The Reactor project main artifact is reactor-core, a reactive library that focuses on
the Reactive Streams specification and targets Java 8.
Reactor introduces composable reactive types that implement Publisher but also provide
a rich vocabulary of operators: Flux and Mono. A Flux object
represents a reactive sequence of 0..N items, while a Mono object represents a
single-value-or-empty (0..1) result.
This distinction carries a bit of semantic information into the type, indicating the
rough cardinality of the asynchronous processing. For instance, an HTTP request produces
only one response, so there is not much sense in doing a count operation. Expressing
the result of such an HTTP call as a Mono<HttpResponse> thus makes more sense than
expressing it as a Flux<HttpResponse>, as it offers only operators that are relevant to
a context of zero items or one item.
Operators that change the maximum cardinality of the processing also switch to the
relevant type. For instance, the count operator exists in Flux, but it returns a
Mono<Long>.
4.1. Flux, an Asynchronous Sequence of 0-N Items
The following image shows how a Flux transforms items:
A Flux<T> is a standard Publisher<T> that represents an asynchronous sequence of 0 to N
emitted items, optionally terminated by either a completion signal or an error.
As in the Reactive Streams spec, these three types of signal translate to calls to a downstream
Subscriber’s onNext, onComplete, and onError methods.
With this large scope of possible signals, Flux is the general-purpose reactive type.
Note that all events, even terminating ones, are optional: no onNext event but an
onComplete event represents an empty finite sequence, but remove the onComplete and
you have an infinite empty sequence (not particularly useful, except for tests around cancellation).
Similarly, infinite sequences are not necessarily empty. For example, Flux.interval(Duration)
produces a Flux<Long> that is infinite and emits regular ticks from a clock.
4.2. Mono, an Asynchronous 0-1 Result
The following image shows how a Mono transforms an item:
A Mono<T> is a specialized Publisher<T> that emits at most one item via the
onNext signal then terminates with an onComplete signal (successful Mono,
with or without value), or only emits a single onError signal (failed Mono).
Most Mono implementations are expected to immediately call onComplete on their
Subscriber after having called onNext. Mono.never() is an outlier: it doesn’t
emit any signal, which is not technically forbidden although not terribly useful outside
of tests. On the other hand, a combination of onNext and onError is explicitly forbidden.
Mono offers only a subset of the operators that are available for a Flux, and
some operators (notably those that combine the Mono with another Publisher)
switch to a Flux.
For example, Mono#concatWith(Publisher) returns a Flux while Mono#then(Mono)
returns another Mono.
Note that you can use a Mono to represent no-value asynchronous processes that only
have the concept of completion (similar to a Runnable). To create one, you can use an empty
Mono<Void>.
4.3. Simple Ways to Create a Flux or Mono and Subscribe to It
The easiest way to get started with Flux and Mono is to use one of the numerous
factory methods found in their respective classes.
For instance, to create a sequence of String, you can either enumerate them or put them
in a collection and create the Flux from it, as follows:
Flux<String> seq1 = Flux.just("foo", "bar", "foobar");
List<String> iterable = Arrays.asList("foo", "bar", "foobar");
Flux<String> seq2 = Flux.fromIterable(iterable);
Other examples of factory methods include the following:
Mono<String> noData = Mono.empty(); (1)
Mono<String> data = Mono.just("foo");
Flux<Integer> numbersFromFiveToSeven = Flux.range(5, 3); (2)
| 1 | Notice the factory method honors the generic type even though it has no value. |
| 2 | The first parameter is the start of the range, while the second parameter is the number of items to produce. |
When it comes to subscribing, Flux and Mono make use of Java 8 lambdas. You
have a wide choice of .subscribe() variants that take lambdas for different
combinations of callbacks, as shown in the following method signatures:
Fluxsubscribe(); (1)
subscribe(Consumer<? super T> consumer); (2)
subscribe(Consumer<? super T> consumer,
Consumer<? super Throwable> errorConsumer); (3)
subscribe(Consumer<? super T> consumer,
Consumer<? super Throwable> errorConsumer,
Runnable completeConsumer); (4)
subscribe(Consumer<? super T> consumer,
Consumer<? super Throwable> errorConsumer,
Runnable completeConsumer,
Consumer<? super Subscription> subscriptionConsumer); (5)
| 1 | Subscribe and trigger the sequence. |
| 2 | Do something with each produced value. |
| 3 | Deal with values but also react to an error. |
| 4 | Deal with values and errors but also run some code when the sequence successfully completes. |
| 5 | Deal with values and errors and successful completion but also do something with the
Subscription produced by this subscribe call. |
These variants return a reference to the subscription that you can use to cancel the
subscription when no more data is needed. Upon cancellation, the source should stop
producing values and clean up any resources it created. This cancel-and-clean-up behavior
is represented in Reactor by the general-purpose Disposable interface.
|
4.3.1. subscribe Method Examples
This section contains minimal examples of each of the five signatures for the subscribe
method. The following code shows an example of the basic method with no arguments:
Flux<Integer> ints = Flux.range(1, 3); (1)
ints.subscribe(); (2)
| 1 | Set up a Flux that produces three values when a subscriber attaches. |
| 2 | Subscribe in the simplest way. |
The preceding code produces no visible output, but it does work. The Flux produces
three values. If we provide a lambda, we can make the values visible. The next example
for the subscribe method shows one way to make the values appear:
Flux<Integer> ints = Flux.range(1, 3); (1)
ints.subscribe(i -> System.out.println(i)); (2)
| 1 | Set up a Flux that produces three values when a subscriber attaches. |
| 2 | Subscribe with a subscriber that will print the values. |
The preceding code produces the following output:
1
2
3
To demonstrate the next signature, we intentionally introduce an error, as shown in the following example:
Flux<Integer> ints = Flux.range(1, 4) (1)
.map(i -> { (2)
if (i <= 3) return i; (3)
throw new RuntimeException("Got to 4"); (4)
});
ints.subscribe(i -> System.out.println(i), (5)
error -> System.err.println("Error: " + error));
| 1 | Set up a Flux that produces four values when a subscriber attaches. |
| 2 | We need a map so that we can handle some values differently. |
| 3 | For most values, return the value. |
| 4 | For one value, force an error. |
| 5 | Subscribe with a subscriber that includes an error handler. |
We now have two lambda expressions: one for the content we expect and one for errors. The preceding code produces the following output:
1
2
3
Error: java.lang.RuntimeException: Got to 4
The next signature of the subscribe method includes both an error handler and
a handler for completion events, as shown in the following example:
Flux<Integer> ints = Flux.range(1, 4); (1)
ints.subscribe(i -> System.out.println(i),
error -> System.err.println("Error " + error),
() -> System.out.println("Done")); (2)
| 1 | Set up a Flux that produces four values when a subscriber attaches. |
| 2 | Subscribe with a Subscriber that includes a handler for completion events. |
Error signals and completion signals are both terminal events and are exclusive of one another (you never get both). To make the completion consumer work, we must take care not to trigger an error.
The completion callback has no input, as represented by an empty pair of
parentheses: It matches the run method in the Runnable interface. The preceding code
produces the following output:
1
2
3
4
Done
4.3.2. Cancelling a subscribe() with Its Disposable
All these lambda-based variants of subscribe() have a Disposable return type.
In this case, the Disposable interface represents the fact that the subscription
can be cancelled, by calling its dispose() method.
For a Flux or Mono, cancellation is a signal that the source should stop
producing elements. However, it is NOT guaranteed to be immediate: Some sources
might produce elements so fast that they could complete even before receiving the
cancel instruction.
Some utilities around Disposable are available in the Disposables class.
Among these, Disposables.swap() creates a Disposable wrapper that lets
you atomically cancel and replace a concrete Disposable. This can be useful,
for instance, in a UI scenario where you want to cancel a request and replace it
with a new one whenever the user clicks on a button. Disposing the wrapper itself
closes it. Doing so disposes the current concrete value and all future attempted replacements.
Another interesting utility is Disposables.composite(…). This composite
lets you collect several Disposable — for instance, multiple in-flight requests
associated with a service call — and dispose all of them at once later on.
Once the composite’s dispose() method has been called, any attempt to add
another Disposable immediately disposes it.
4.3.3. An Alternative to Lambdas: BaseSubscriber
There is an additional subscribe method that is more generic and takes a full-blown
Subscriber rather than composing one out of lambdas. In order to help with writing
such a Subscriber, we provide an extendable class called BaseSubscriber.
Instances of BaseSubscriber (or subclasses of it) are single-use,
meaning that a BaseSubscriber cancels its subscription to the first Publisher if it
is subscribed to a second Publisher.
That is because using an instance twice |