The journey begins with using Java 8 introduced Optional/Stream/CompletableFuture more functional, after which Reactive Streams is introduced with a homemade implementation that is ultimately made functional to increase usability. Finally Spring Reactor (Project Reactor) is presented and used for building a device simulator periodically reporting data to device controller.
5. Assume we are building a service
on the imaginary banking model
public class User {
private String id;
private String name;
...
}
public class Account {
private String accountNumber;
private String userId;
private double balance;
...
}
public class AccountInfo {
private String userId;
private String userName;
private String accountNumber;
private double balance;
...
}
6. and with Java 8 Optional, we have
the repositories
public class UserRepository {
public Optional<User> get(String userId) {
return Optional.ofNullable(FakeData.users.get(userId));
}
}
public class AccountRepository {
public Optional<Account> get(String accountNumber) {
return FakeData.accounts.get(accountNumber).stream().findFirst();
}
}
7. Now we are building a service for
querying account details combining
user information
public class AccountService {
private UserRepository userRepository;
private AccountRepository accountRepository;
...
public Optional<AccountInfo> getAccountInfo(String accountNumber) {
Optional<Account> optAccount = this.accountRepository.get(accountNumber);
if (!optAccount.isPresent()) {
return Optional.empty();
}
Account account = optAccount.get();
Optional<User> optUser = this.userRepository.get(account.getUserId());
if (!optUser.isPresent()) {
return Optional.empty();
}
User user = optUser.get();
return Optional.of(AccountInfo.create(user, account));
}
}
8. We are treating Optional as a data structure
containing a possible existing value
That's nothing diļ¬erent from using null as
an indication of "absence of value"
Optional<Account> optAccount =
this.accountRepository.get(accountNumber);
if (!optAccount.isPresent()) {
return Optional.empty();
}
Account account = optAccount.get();
Optional<User> optUser =
this.userRepository.get(account.getUserId());
if (!optUser.isPresent()) {
return Optional.empty();
}
User user = optUser.get();
return Optional.of(AccountInfo.create(user, account));
Account account =
this.accountRepository.get(accountNumber);
if (account == null) {
return null;
}
User user =
this.userRepository.get(account.getUserId());
if (user == null) {
return null;
}
return AccountInfo.create(user, account);
9. Can we combine 2 Optional values
in a more functional way?
Optional<Account> optAccount =
this.accountRepository.get(accountNumber);
if (!optAccount.isPresent()) {
return Optional.empty();
}
Account account = optAccount.get();
Optional<User> optUser =
this.userRepository.get(account.getUserId());
if (!optUser.isPresent()) {
return Optional.empty();
}
User user = optUser.get();
return Optional.of(AccountInfo.create(user, account));
public<U> Optional<U> flatMap(
Function<? super T, Optional<U>> mapper) {
requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return requireNonNull(mapper.apply(value));
}
}
public<U> Optional<U> map(
Function<? super T, ? extends U> mapper) {
requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
Let's check out
Optional.ļ¬atMap and Optional.map
10. public class Optional<T> {
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
}
Optional.map takes a function of (T => U) to
transform the value of type "T" into something
else of type "U"
Optional.map creates an Optional<U> out
of Ā an Optional<T>
11. public class Optional<T> {
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return requireNonNull(mapper.apply(value));
}
}
}
Optional.ļ¬atMap takes a mapper function of (T => Optional<U>)
Ā
This mapper function, with the access to the actual value of
Optional<T>, creates another Optional<U>
12. Optional.ļ¬atMap is said to be more powerful than
Optional.map
ļ¬atMap is able to compose other Optional objects and come
out with a new Optional object
Ā
ļ¬atMap has the ability of ļ¬ow control. It can decide whether
or not to create a new Optional object or simply an empty
Optional object depending on the given access to previous
Optional value.
Something about ļ¬atMap
13. Attempt to compose Optional using map
Optional<Account> optAccount = accountRepository.get(accountNumber);
Optional<Optional<AccountInfo>> result = optAccount.map(account -> {
Optional<User> optUser = userRepository.get(account.getUserIdI());
Optional<AccountInfo> optAccountInfo =
optUser.map(user -> AccountInfo.create(user, account);
return optAccountInfo
});
Optional.map is unable to compose 2 Optional objects and peel
oļ¬ or ļ¬atten the outer Optional.
Ā
Optional.map just accumulates the layers of nested Optional.
It is not easy to access the value of multi-layer Optonal, e.g.
Optinoal<Optional<AccountInfo>>Ā
14. Attempt to compose Optional using ļ¬atMap
Optional<Account> optAccount = accountRepository.get(accountNumber);
Optional<AccountInfo> result = optAccount.flatMap(account -> {
Optional<User> optUser = userRepository.get(account.getUserIdI());
Optional<AccountInfo> optAccountInfo =
optUser.flatMap(user -> Optional.of(AccountInfo.create(user, account));
return optAccountInfo
});
With ļ¬atMap, we are capable of composing
Optional<Account> and Optional<User> to
obtain a "ļ¬attened" Optional<AccountInfo>
15. Use ļ¬atMap for Flow Control
Optional<Account> optAccount = accountRepository.get(accountNumber);
Optional<AccountInfo> result = optAccount.flatMap(account -> {
if (account.isCredential()) {
return Optional.empty();
}
Optional<User> optUser = userRepository.get(account.getUserIdI());
Optional<AccountInfo> optAccountInfo =
optUser.flatMap(user -> Optional.of(AccountInfo.create(user, account));
return optAccountInfo
});
16. ļ¬atMap and map work together
Optional<Account> optAccount = accountRepository.get(accountNumber);
Optional<AccountInfo> result = optAccount.flatMap(account -> {
Optional<User> optUser = userRepository.get(account.getUserIdI());
Optional<AccountInfo> optAccountInfo =
optUser.flatMap(user -> Optional.of(AccountInfo.create(user, account));
return optAccountInfo
});
Don't bother to wrap AccountInfo with an Optional in the last ļ¬atMap composition.
We can use "map" to achieve the same result
Optional<Account> optAccount = accountRepository.get(accountNumber);
Optional<AccountInfo> result = optAccount.flatMap(account -> {
Optional<User> optUser = userRepository.get(account.getUserIdI());
Optional<AccountInfo> optAccountInfo =
optUser.map(user -> AccountInfo.create(user, account);
return optAccountInfo
});
17. Composing 2 or more Optionals
Optional<TransactionInfo> result = accountRepository.get(accountNumber).flatMap(account ->
userRepository.get(account.getUserId()).flatMap(user ->
transactionRepository.get(account.getAccountNumber()).map(transaction -> {
// use "account", "user", "transaction" to create TransactionInfo
...
return TransactionInfo.create(user, account, transaction);
})
)
);
Optional<SomeType> result = optional1.flatMap(v1 ->
optional2.flatMap(v2 ->
optional3.flatMap(v3 ->
...
optionaln.map(vn ->
// do something with v1, v2, v3,..., vn
//return some value of some type
return SomeType.create(...);
)
)
)
);
23. Banking Repositories with CompletableFuture
public class UserRepository extends BaseRepository {
public UserRepository() {
super(defaultExecutorService());
}
public CompletableFuture<User> get(String userId) {
return CompletableFuture.supplyAsync(
() -> FakeData.users.get(userId),
this.executorService
);
}
}
public class AccountRepository extends BaseRepository {
public AccountRepository() {
super(defaultExecutorService());
}
public CompletableFuture<Account> get(String accountNumber) {
return CompletableFuture.supplyAsync(
() -> {
List<Account> accounts = FakeData.accounts.get(accountNumber);
return accounts.isEmpty() ? null : accounts.get(0);
},
this.executorService);
}
}
24. AccountService
public class AccountService {
private UserRepository userRepository;
private AccountRepository accountRepository;
...
public CompletableFuture<AccountInfo> getAccountInfo(String accountNumber) {
CompletableFuture<AccountInfo> result = new CompletableFuture<>();
try {
CompletableFuture<Account> accountFuture =
this.accountRepository.get(accountNumber);
Account account = accountFuture.get();
if (account == null) result.complete(null);
CompletableFuture<User> userFuture = this.userRepository.get(account.getUserId());
User user = userFuture.get();
if (user == null) result.complete(null);
else result.complete(AccountInfo.create(user, account));
} catch (Exception e) {
result.completeExceptionally(e);
}
return result;
}
}
25. map and ļ¬atMap in disguise
CompletableFuture does not have map and ļ¬atMap
Ā
CompletableFuture deļ¬nes thenApply and thenCompose
with similar sematics:
thenApply -> map
thenCompose -> ļ¬atMap
public CompletableFuture<T> implements Future<T>, CompletionStage<
// map
public <U> CompletableFuture<U> thenApply(
Function<? super T,? extends U> fn) {
...
}
// flatMap
public <U> CompletableFuture<U> thenCompose(
Function<? super T, ? extends CompletionStage<U>> fn) {
...
}
}
26. Threading Control for
CompletableFuture.map/ļ¬atMap
There are variant thenApply and thenCompose to control
the threading policy for the execution of mapping functions
public CompletableFuture<T> implements Future<T>, CompletionStage<T> {
public <U> CompletableFuture<U> thenApply(
Function<? super T,? extends U> fn) {...}
public <U> CompletableFuture<U> thenApplyAsync(
Function<? super T,? extends U> fn) {...}
public <U> CompletableFuture<U> thenApplyAsync(
Function<? super T,? extends U> fn, Executor executor) {...}
public <U> CompletableFuture<U> thenCompose(
Function<? super T, ? extends CompletionStage<U>> fn) {...}
public <U> CompletableFuture<U> thenComposeAsync(
Function<? super T, ? extends CompletionStage<U>> fn) {...}
public <U> CompletableFuture<U> thenComposeAsync(
Function<? super T, ? extends CompletionStage<U>> fn,
Executor executor) {...}
}
27. AccountService with brighter future
public class FunctionalAccountService {
private UserRepository userRepository;
private AccountRepository accountRepository;
...
public CompletableFuture<AccountInfo> getAccountInfo(String accountNumber) {
return this.accountRepository.get(accountNumber).thenCompose(account ->
this.userRepository.get(account.getUserId()).thenApply(user ->
AccountInfo.create(user, account)));
}
}
28. Why are Optional, Stream, CompletableFuture
able to chain the computations?
Ā
What's the pattern?
public final class Optional<T> {
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper)
public<U> Optional<U> map(Function<? super T, ? extends U> mapper)
}
public interface Stream<T> extends BaseStream<T, Stream<T>> {
public <U> Stream<U> flatMap(Function<? super T, ? extends Stream<? extends U>> mapper)
public <U> Stream<U> map(Function<? super T, ? extends U> mapper)
}
public CompletableFuture<T> implements Future<T>, CompletionStage<T> {
// flatMap
public <U> CompletableFuture<U> thenCompose(
Function<? super T, ? extends CompletionStage<U>> fn)
// map
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
}
29. Monad
// This is psudo code illustrating Monad structure
M[T] {
flatMap(mapper: T => M[U]): M[U]
// unit wraps a value into monad context
//
// in Optional, it is Optional.of(...), Optional.ofNullable(...)
//
// in Stream, it is Stream.of(...)
//
// in CompletableFuture, it is CompletableFuture.supplyAsync(...)
// or {new CompletableFuture().complete(...)}
unit(value: T): M[T]
}
What about map?
map can always be implemented with ļ¬atMap and unit. This
proves again ļ¬atMap is more powerful than map.
map(mapper: T => U) {
return flatMap(v -> unit(mapper(v)))
}
30. Monad
A great article explaining Monad in plain English
http://blog.leichunfeng.com/blog/2015/11/08/functor-applicative-and-monad
A monad is a computational context for some value with a
"unit" method and a "ļ¬atMap" method
Ā
Context value of M[T] Ā is passes as parameter of (T => M[U]).
In the mapper function, you get to access the context value
and decide the new monad value to return.
accountRepository.get(accountNumber).flatMap(account -> {
if (account.isCredential()) {
return Optional.empty();
}
Optional<User> optUser = userRepository.get(account.getUserIdI());
Optional<AccountInfo> optAccountInfo =
optUser.flatMap(user -> Optional.of(AccountInfo.create(user, account));
return optAccountInfo
});
31. Use ļ¬atMap to implement other combinators
JDK implementation for Optional.ļ¬lter
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}
another implementation with ļ¬atMap
public Optional<T> filter(Predicate<? super T> predicate) {
return flatMap(v -> predicate.test(v) ? this : Optional.empty();
}
the emptiness test (isPresent()) has been done in ļ¬atMap
32. Monad Comparision
Type Context Value Composition Eļ¬ect (ļ¬atMap)
Optional a value may or may not exist composition happens if Optional
contains value
composition stops if Optional is
empty and subsequent
compositions are ignored
CompletableFuture a value available in the future composition happens if
CompletableFuture obtains a
value without error ???
composition stops if error
occurred in CompletableFuture
and subsequent compositions
are ignored ???
Stream each value in the stream composition happens if the
stream is not empty
composition stops if Stream is
empty and subsequent
compositions are ignored
The resulting sub-streams are
merged to a joint stream
34. Streams are a series of elements emitted over time. The
series potentially has no beginning and no end.
Kevin Webber, A Journey into Reactive Streams
Reactive Stream goes a step further by being able to
signal demand from downstream thus controlling the
overall speed of data elements ļ¬owing through it
Walter Chang, Reactive Streams in Scala
What is Reactive Streams
35. Who deļ¬nes Reactive Streams?
What is it?
How is it related to Java 9 Flow
https://en.wikipedia.org/wiki/Reactive_Streams
36. The scope of Reactive Streams is to ļ¬nd a minimal set of interfaces, methods and
protocols that will describe the necessary operations and entities to achieve the goalā
asynchronous streams of data with non-blocking back pressure.
www.reactive-streams.org
So there are only 4 interfaces and and 7 methods in total inside reactive-streams-1.0.2.jar
package org.reactivestreams;
public interface Publisher<T> {
public void subscribe(Subscriber<? super T> s);
}
public interface Subscriber<T> {
public void onSubscribe(Subscription s);
public void onNext(T t);
public void onError(Throwable t);
public void onComplete();
}
public interface Subscription {
public void request(long n);
public void cancel();
}
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {}
End-user DSLs or protocol binding APIs have purposefully been left out of the scope to encourage and
enable diļ¬erent implementations that potentially use diļ¬erent programming languages to stay as true as
possible to the idioms of their platform.
www.reactive-streams.org
37. Reactive Streams is a speciļ¬cation for library developers.
Ā
Reactive-stream libraries complying with Reactive Steams speciļ¬cation are
capable of
1. back-pressure control
2. interoperate with other libraries.
Ā
For example, a Reactor Processor can subscribe to RxJava Producer given that
Spring Reactor and RxJava are both Reactive Streams compliant.
39. Dynamic Push / Pull
request n elements
push at most n elements
Subscriber PublisherSubscription
Requests can be made asynchronously.
Multiple requests are accumulated on Publisher and will be served later
Fast subscriber demands more elements; publisher don't need to wait for
requests => push mode
Slow subscriber request less; publish waits for requests => pull mode
The dynamic push/pull makes back-pressure possible and ensure all
participating components are resilient to massive load although this may
degrade the performance
40. Let's fake it till we make it
public interface Publisher<T> {
public void subscribe(Subscriber<? super T> s);
}
abstract public class Flowie<T> implements Publisher<T> {
public static <T> Flowie<T> fromIterable(Iterable<T> iterable) {
...
}
}
"Flowie", a homemade implementation for Reactive Streams
Spring Reactor has "Flux"; RxJava comes with "Flowable".
Why don't we create one of our own?
41. public class FlowieIterable<T> extends Flowie<T> {
private Iterable<T> iterable;
public FlowieIterable(Iterable<T> iterable) {
this.iterable = iterable;
}
@Override
public void subscribe(Subscriber<? super T> subscriber) {
try {
Iterator<T> iterator = this.iterable.iterator();
subscriber.onSubscribe(new IterableSubscription<>(subscriber, iterator));
} catch (Exception e) {
subscriber.onError(e);
}
}
...
static class IterableSubscription<T> implements Subscription {
private Subscriber<? super T> subscriber;
private Iterator<? extends T> iterator;
private boolean cancelled = false;
private AtomicLong requested = new AtomicLong(0L);
@Override
public void request(long n) {
...
}
}
}
We take a similar approach to that for Spring Reactor
Put major logic in Subscription object.
This subscription object gets delivered to users via
onSubscribe()
42. static class IterableSubscription<T> implements Subscription {
private boolean cancelled = false;
/**
* pending request
*
* The updates to the pending request may come from other threads via {@link Subscription#request(lo
*
* Use AtomicLong to make sure the update and comparison of pending request counter is conducted ato
*
*/
private AtomicLong requested = new AtomicLong(0L);
@Override
public void request(long n) {
long updatedRequested = addRequestAndReturnCurrent(n);
if (updatedRequested == 0L) {
doEmit(n);
}
}
private void doEmit(long n) {
long emitted = 0L;
while (true) {
Great care is taken to make sure multiple asynchronous Subscription.request(n) are
accumulated and served atomically
often leverage java.util.concurrent.atomic components
use CAS (compare and set) to ensure counter is updated atomically at the right
time
Sometimes the price to maintain the correctness of request counter is messing up
the code making it hard to read and understand.
43. public class FlowieIterable<T> extends Flowie<T> {
private Iterable<T> iterable;
public FlowieIterable(Iterable<T> iterable) {
this.iterable = iterable;
}
@Override
public void subscribe(Subscriber<? super T> subscriber) {
try {
Iterator<T> iterator = this.iterable.iterator();
subscriber.onSubscribe(new IterableSubscription<>(subscriber, iterator));
} catch (Exception e) {
subscriber.onError(e);
}
}
///////////////////////////////////////////////////////////////////////////
static class IterableSubscription<T> implements Subscription {
private Subscriber<? super T> subscriber;
private Iterator<? extends T> iterator;
private boolean cancelled = false;
/**
* pending request
*
* The updates to the pending request may come from other threads via {@link Subscription#request(lo
*
* Use AtomicLong to make sure the update and comparison of pending request counter is conducted ato
*
*/
44. Ya! We have the ļ¬rst Flowie implementation
Flowie.fromIterable(Arrays.asList("a", "b", "c")).subscribe(new Subscriber<String>() {
@Override
public void onSubscribe(Subscription s) {
s.request(Long.MAX_VALUE);
}
@Override
public void onNext(String s) {
System.out.println(s);
}
@Override
public void onError(Throwable t) {
}
@Override
public void onComplete() {
}
});
After subscribing, Subscription.request() needs to be
invoked to trigger element emission
a request of Long.MAX_VALUE elements, by the rule of
3.17 (https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.2/README.md#speciļ¬cation), is
treated as requesting ALL elements from the publisher
45. Let's pursue the banking service and make it
more "Flowie"Ā
public class UserRepository {
public Flowie<User> get(String userId) {
List<User> user = FakeData.users.containsKey(userId) ?
Collections.singletonList(FakeData.users.get(userId)) :
Collections.emptyList();
return Flowie.fromIterable(user);
}
}
public class AccountRepository {
public Flowie<Account> get(String accountNumber) {
return Flowie.fromIterable(
FakeData.accounts.get(accountNumber)
);
}
}
46. AccountService in callback hell
public class AccountServiceInCallbackHell {
private UserRepository userRepository;
private AccountRepository accountRepository;
...
/**
* NOTE: This only works in a single-thread execution context.
* The implementation does not catch the timing of "completion" in onComplete() or onError().
* In a muti-thread environment, the elements (User, Account) might not be ready
* when this method returns.
*/
public Flowie<AccountInfo> getAccountInfo(String accountNumber) {
final List<AccountInfo> result = new ArrayList<>();
this.accountRepository.get(accountNumber).subscribe(new Subscriber<Account>() {
@Override
public void onSubscribe(Subscription s) {
s.request(Long.MAX_VALUE);
}
@Override
public void onNext(Account account) {
userRepository.get(account.getUserId()).subscribe(new Subscriber<User>() {
@Override
public void onSubscribe(Subscription s) {
s.request(Long.MAX_VALUE);
}
@Override
public void onNext(User user) {
result.add(AccountInfo.create(user, account));
}
...
});
}
...
47. We need a way out of this callback hell.Ā
Ā
Can we make Flowie a monad?
This way, we can do monadic composition
of 2 Flowie values like this.
flowieAccuont.flatMap(account ->
flowieUser.map(user ->
// create AccountInfo out of account, user
accountInfo))
48. Start with the simple one: Flowie.map
abstract public class Flowie<T> implements Publisher<T> {
public <R> Flowie<R> map(Function<? super T, ? extends R> mapper) {
return new FlowieMap<>(this, mapper);
}
}
public class FlowieMap<T, R> extends Flowie<R> {
private Publisher<T> source;
private Function<? super T, ? extends R> mapper;
...
@Override
public void subscribe(Subscriber<? super R> s) {
this.source.subscribe(new MapSubscriber<>(s, this.mapper));
}
static class MapSubscriber<T, R> implements Subscriber<T>, Subscription {
private Subscriber<? super R> actualSubscriber;
private Function<? super T, ? extends R> mapper;
private Subscription upstreamSubscription;
2 levels of decorator pattern adoption
1st level is FlowMap decorating the actual Publisher
2nd level is MapSubscriber decorating the actual
Subscriber
MapSubscriber applies the mapper function in "onNext"
49. Flowie.ļ¬atMap
Compliant ļ¬atMap implementation for Reactive Streams
is complicated and needs to tackle the concurrent sub-
streams emissions and elements queuing.
For quick demonstration of functional programming
beneļ¬t, we rush a non-compliant implementation that
only works in a single-thread execution context.
50. Flowie.ļ¬atMap Implementation FYI
/**
* This implementation DOES NOT comply with Reactive Streams. It does not take care of
* the situation where elements from sub-streams emitted asynchronously.
*
* The compliant implementation is complicated and usually needs one or more queues
* to store un-consumed elements emitted from sub-streams.
*
* This non-compliant implementation only serves the purpose of demonstrating the
* advantage of making reactive streams functional.
*/
public class FlowieNonCompliantSynchronousFlatMap<T, R> extends Flowie<R> {
private Publisher<T> source;
private Function<? super T, ? extends Publisher<? extends R>> mapper;
public FlowieNonCompliantSynchronousFlatMap(Publisher<T> source, Function<? super T, ? extend
this.source = source;
this.mapper = mapper;
}
@Override
public void subscribe(Subscriber<? super R> s) {
this.source.subscribe(new SynchronousNonThreadSafeFlapMapSubscriber<>(s, this.mapper));
}
///////////////////////////////////////////////////////////////////////////
static class SynchronousNonThreadSafeFlapMapSubscriber<T, R> implements Subscriber<T>, Subscr
51. With map/ļ¬atMap in place, here is the
upgraded Flowie AccountService
public class FunctionalAccountService {
private UserRepository userRepository;
private AccountRepository accountRepository;
...
public Flowie<AccountInfo> getAccountInfo(String accountNumber) {
return this.accountRepository.get(accountNumber).flatMap(account ->
this.userRepository.get(account.getUserId()).map(user ->
new AccountInfo(
user.getId(), user.getName(),
account.getAccountNumber(), account.getBalance())));
}
}
53. Project Reactor implements Reactive Streams, inherently with non-
blocking streaming natureĀ
Ā
On top of Reactive Streams, Project Reactor provides its own APIs for
functional streaming handling and adapting other data source.
54. Reactor oļ¬ers 2 Publisher implementations
Flux: a reactive stream of 0-N elements
Mono: a reactive stream of 0-1 elements
Ā
In addition to Reactive Streams, Reactor extensively
implements APIs deļ¬ned by Reactive Extensions (Rx)
(http://reactivex.io)
55. Spring WebFlux runs on top of Reactor non-blocking IO, if
Netty is chosen as the underlying web server among Tomcat,
Jetty, Undertow.
Ā
Spring WebClient is built on top of Reactor non-blocking IO
56. AccountService with Reactor
public class UserRepository {
public Mono<User> get(String userId) {
return Mono.justOrEmpty(FakeData.users.get(userId));
}
}
public class AccountRepository {
public Flux<Account> get(String accountNumber) {
return Flux.fromIterable(
FakeData.accounts.get(accountNumber)
);
}
}
public class FunctionalAccountService {
private UserRepository userRepository;
private AccountRepository accountRepository;
...
public FunctionalAccountService(UserRepository userRepository, AccountRepository accountRepository) {
this.userRepository = userRepository;
this.accountRepository = accountRepository;
}
public Flux<AccountInfo> getAccountInfo(String accountNumber) {
return this.accountRepository.get(accountNumber).flatMap(account ->
this.userRepository.get(account.getUserId()).map(user ->
new AccountInfo(
user.getId(), user.getName(),
account.getAccountNumber(), account.getBalance())));
}
}
57. Mono/Flux.ļ¬atMap
3 ļ¬avours of ļ¬atMap
ļ¬atMap
ļ¬atMapSequential
concatMap
Generation of inners
and subscription
Ordering of the
ļ¬attened values
Interleaving
ļ¬atMap Eagerly subscribing to
inners
No Yes
ļ¬atMapSequential Eagerly subscribing to
inners
Yes (elements from
late inners are
queued)
No
concatMap subscribing to inners
one by one
Yes No
58. Threading Control
In Reactor, the execution model and where the execution happens is
determined by the Scheduler that is used. A Scheduler has scheduling
responsibilities similar to an ExecutorService
Ā
Ā
Default Schedulers
Ā
Schedulers.elastic()
An elastic thread pool (Schedulers.elastic()). It creates new worker pools as needed, and reuse idle
ones.
Ā
Schedulers.parallel()
a ļ¬xed pool of workers that is tuned for parallel work (Schedulers.parallel()). It creates as many
workers as you have CPU cores.
Ā
Schedulers.immediate()
the current thread
Create new instances
Schedulers.newElastic
Schedulers.newParallel
Schedulers.newSingle
Schedulers.fromExecutor
59. Flux/Mono.publishOn
publishOn takes signals from upstream and replays them
downstream while executing the callback on a worker from
the associated Scheduler.
https://projectreactor.io/docs/core/release/reference/#schedulers
61. Getting closer to Project Reactor
Reactor 3 Reference Guide
https://projectreactor.io/docs/core/release/reference/
Reactor 3 Javadoc
https://projectreactor.io/docs/core/release/api/
methods are illustrated with diagrams
62. Put it all together
A device simulator that simulator speciļ¬ed
number of devices.
Ā
Each device periodically reportsĀ
heartbeat
stats
to a device controller
63. First create a stream of device MACs
List<String> deviceMacs = new MacGenerator("AA", "BB").generate(deviceCount);
Flux<String> deviceMacFlux = Flux.fromIterable();
Flux<DeviceRequest> createDeviceStatsStream(String deviceMac) {
...
}
Assume we have a method that creates a stream of reporting
requests for a single device.
Ā
The stream is a sum of heartbeat/stats request stream
Flux<DeviceRequest> requestFlux = deviceMacFlux.flatMap(mac -> createDeviceRequestStream(mac));
How do we turn a stream of device MAC into a stream of device request stream and
combine each single device request stream into a single massive one?
Ā
Yes... ļ¬atMap
64. Dive deeper to implement (deviceMac => Flux<DeviceRequest>)
A device request stream is composed ofĀ
heatbeat request stream
stats request stream
private Flux<DeviceRequest> createDeviceRequestStream(String deviceMac) {
return Flux.merge(
createDeviceHeartbeatStream(deviceMac),
createDeviceStatsStream(deviceMac)
);
}
Now what's left is the terminal streams,
heartbeat/stats request streams
65. heartbeat/stats request streams emit elements in a periodical manner
We need a source stream that generates elements in a ļ¬xed time interval.
Then each element is transformed to a DeviceRequest.
Ā
Flux.interval(Duration) is what we need
private Flux<DeviceRequest> createDeviceHeartbeatStream(String deviceMac) {
return Flux.interval(Duration.ofSeconds(this.heartbeatIntervalInSeconds))
.map(n -> // create a DeviceRequest out of the given deviceMac);
}
private Flux<DeviceRequest> createDeviceStatsStream(String deviceMac) {
return Flux.interval(Duration.ofSeconds(this.statsIntervalInSeconds))
.map(n -> // create a DeviceRequest out of the given deviceMac);,
67. The problem with Flux.ļ¬atMapĀ
ļ¬atMap eagerly subscribes to inners with conļ¬gurable value with the default as 256
public static final int SMALL_BUFFER_SIZE = Math.max(16,
Integer.parseInt(System.getProperty("reactor.bufferSize.small", "256")));
We observed that only 256 devices reported heartbeat/stats
even we speciļ¬ed more than 256 devices.
Why?
Each device request stream is an inļ¬nite stream, and ļ¬atMap
eagerly subscribes ļ¬rst 256 inner streams.
Ā
Each device request steam is an inļ¬nite stream
(Flux.interval()). That means only ļ¬rst 256 device streams get
to be created and emit data.
68. More on ļ¬atMap
ļ¬atMap exposes 2 conļ¬gurable parameters for eager inner
stream subscription
maxConcurrency
prefetch
ļ¬atMap eagerly subscribes "maxConcurrentcy" inner streams when
subscribed (onSubscribe)
Ā
When subscribing inner stream, it will also pre-fetch "prefetch" elements by
invoking Subscription.request(prefetch) of inner stream.
To ļ¬x this problem, we explicitly conļ¬gure "maxConcurrency" when ļ¬atMapping
Flux.fromIterable(deviceMacs).flatMap(mac -> createDeviceRequestStream(mac), deviceMacs.size())