Reactive Programming with Spring
- Pulkit Pushkarna
What is Reactive Programming ?
Reactive Programming manages asynchronous data flows between producers of data and consumers that need to react to that data in a non-blocking manner.
Reactive Programming is all about non-blocking applications that are asynchronous and event-driven
Core features of reactive programming
- Asynchronous and non blocking
- Data flow as event driven streams
- Backpressure on Data streams
Spring 5 Framework introduced Reactor as an implementation for the Reactive Streams specification.
Reactor is a next-gen Reactive library for building non-blocking applications on the JVM.
Reactor extends the basic Reactive Streams Publisher contract and defines the Flux and Mono API.
Spring Web Reactive makes use of the Servlet 3.1 offering for non-blocking I/O and runs on Servlet 3.1 containers.
Spring 5 Offering for reactive programming
Producing Stream in Reactor
There are two ways of creating a Stream in Reactor core:
- Flux : Capable of emitting 0 or more elements
Flux.just(1,2,3,4,5);
- Mono : Can emit at most one element
Mono.just(1);
Flux and Mono are implementations of the Reactive Streams Pubisher interface
Gradle Dependency for Reactor
implementation group: 'io.projectreactor', name: 'reactor-core', version: '3.5.0'
Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).map(e -> {
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
return e;
})
.filter(e -> e % 2 == 0)
.forEach(e -> System.out.println("even>>>"+e));
Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).map(e -> {
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
return e;
})
.filter(e -> e % 2 == 1)
.forEach(e -> System.out.println("odd>>>"+e));
Synchronous blocking code
Flux.just(1,2,3,4,5,6,7,8,9,10)
.delayElements(Duration.ofSeconds(1))
.filter(e->e%2==0)
.subscribe(e-> System.out.println("even>>"+e));
Flux.just(1,2,3,4,5,6,7,8,9,10)
.delayElements(Duration.ofSeconds(1))
.filter(e->e%2==1)
.subscribe(e-> System.out.println("odd>>"+e));
System.in.read();
Asynchronous non blocking code
Flux.just(1,2,3,4).log() .subscribe();
Output
06:56:22.288 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
06:56:22.304 [main] INFO reactor.Flux.Array.1 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
06:56:22.308 [main] INFO reactor.Flux.Array.1 - | request(unbounded)
06:56:22.309 [main] INFO reactor.Flux.Array.1 - | onNext(1)
06:56:22.309 [main] INFO reactor.Flux.Array.1 - | onNext(2)
06:56:22.309 [main] INFO reactor.Flux.Array.1 - | onNext(3)
06:56:22.309 [main] INFO reactor.Flux.Array.1 - | onNext(4)
06:56:22.310 [main] INFO reactor.Flux.Array.1 - | onComplete()
Subscribing to Stream
Consuming elements
Flux.just(1,2,3,4) .subscribe(System.out::println);
Different ways of creating a flux
-
Flux.range(1,10)
-
Flux.just(1,2,3,4)
-
Flux.fromIterable(Arrays.asList(1,2,3,4))
-
Flux.fromStream(Stream.of(1,2,3,4)) .subscribe( System.out::println, System.out::println, ()->System.out.println("Completed"));
Transforming a stream
List<Integer> elements= new ArrayList<>();
Flux.just(1, 2, 3, 4)
.log()
.map(i -> i * 2)
.subscribe(elements::add);
FlatMap
Flux.just("Hello","world")
.flatMap(e->Flux.fromArray(e.split("")))
.distinct()
.sort()
.subscribe(System.out::println);
Subset of Flux
Flux.range(1,10)
.take(5)
// .takeLast(5)
// .takeUntil(e->e>2)
// .takeWhile(e->e<7)
.subscribe(System.out::println);
Collect
Flux.just(3,1,1,6,2,9,5,34,12)
.filter(e->e%2!=0)
.collectList() .subscribe(System.out::println);
Flux.just(3,1,1,6,2,9,5,34,12)
.filter(e->e%2!=0)
.distinct()
.collectSortedList()
.subscribe(System.out::println);
Flux.just(3,1,1,6,2,9,5,34,12)
.filter(e->e%2!=0)
.collectMap(e->e,e->e*2)
.subscribe(System.out::println);
Why do we need Mono ?
There will be a number of use cases where it will make sense to return only one element e.g count number of elements
Mono<Long> integerMono1 = Flux.just(1,2,3,4).count(); integerMono1.subscribe(System.out::print);
More examples of Mono
Flux.just(3,1,1,6,2,9,5,34,12)
.elementAt(0)
.subscribe(System.out::println);
Flux.just(3,1,1,6,2,9,5,34,12)
.hasElement(4)
.subscribe(System.out::println);
Flux.just(3,1,1,6,2,9,5,34,12)
.any(e->e<-100)
.subscribe(System.out::println);
Flux.just(3,1,1,6,2,9,5,34,12)
.hasElements()
.subscribe(System.out::println);
Flux.just(3,1,1,6,2,9,5,34,12)
.all(e->e<100)
.subscribe(System.out::println);
Subscribe with Subscriber Interface
List<Integer> integerList = new ArrayList<>(); Flux.just(1,2,3,4,5) .log() .subscribe(new Subscriber<Integer>() { @Override public void onSubscribe(Subscription s) { s.request(Long.MAX_VALUE); } @Override public void onNext(Integer integer) { integerList.add(integer); } @Override public void onError(Throwable t) { System.out.println(t); } @Override public void onComplete() { System.out.println("Subscription Completed..."); } });
Another way of Specifying Event Callbacks
Flux.range(1,10).subscribe(System.out::println,
System.out::println,
()-> System.out.println("All Data Pushed..."));
More declarative way of Specifying events
Flux.range(1,10).log() .doOnSubscribe(s->s.request(10)) .doOnNext(System.out::println) .doOnComplete(()-> System.out.println("All Data Pushed")) .doOnError(System.out::println) .subscribe();
Flux<Integer> integerFlux = Flux.just(1,2,3,4,5,6); integerFlux .map(e->e/0).log() .doOnError(error -> System.out.println(error)) .subscribe(System.out::println);
doOnError
Flux<Integer> integerFlux = Flux.just(1,2,3,4,5,6); integerFlux .map(e->2/0).log() .doOnError(error -> System.out.println(error)) .onErrorReturn(-1) .subscribe(System.out::println);
onErrorReturn
integerFlux .map(e->e/0).log() .doOnError(error -> System.out.println(error)) .onErrorResume(e-> {System.out.println(e); return Flux.just(11,22,33);}) .subscribe(System.out::println);
onErrorResume
Comparison to Java 8 Streams
- Stream once consumed cannot be reused again.
- Flux will give you a callback once all the data available has been pushed to the Subscriber whereas there is no such provision in Java 8 Stream.
- Error propagation handling mechanism in Reactor Flux is very declarative and provides you which a number of options.
Backpressure
Backpressure is when a downstream can tell an upstream to send it fewer data in order to prevent it from being overwhelmed.
List<Integer> elements= new ArrayList<>(); Flux.just(1, 2, 3, 4).log() .subscribe(new Subscriber<Integer>() { private Subscription s; int onNextAmount; @Override public void onSubscribe(Subscription s) { this.s = s; s.request(2); } @Override public void onNext(Integer integer) { elements.add(integer); onNextAmount++; if (onNextAmount % 2 == 0) { s.request(2); } } @Override public void onError(Throwable t) {} @Override public void onComplete() {} });
14:41:42.147 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
14:41:42.159 [main] INFO reactor.Flux.Array.1 - | onSubscribe([Synchronous Fuseable] FluxArray.ArraySubscription)
14:41:42.162 [main] INFO reactor.Flux.Array.1 - | request(2)
14:41:42.162 [main] INFO reactor.Flux.Array.1 - | onNext(1)
14:41:42.162 [main] INFO reactor.Flux.Array.1 - | onNext(2)
14:41:42.162 [main] INFO reactor.Flux.Array.1 - | request(2)
14:41:42.162 [main] INFO reactor.Flux.Array.1 - | onNext(3)
14:41:42.162 [main] INFO reactor.Flux.Array.1 - | onNext(4)
14:41:42.162 [main] INFO reactor.Flux.Array.1 - | request(2)
14:41:42.163 [main] INFO reactor.Flux.Array.1 - | onComplete()
Output
Combining 2 streams
We can combine 2 streams with the help of zip method in following ways: Flux.just(1,2,3,4) .zipWith(Flux.just(5,6,7,8),(a,b)-> a+b) .subscribe(System.out::println);
Flux.zip(Flux.just(1,2,3,4) ,Flux.just(5,6,7,8)) .map((element)-> element.getT1()+element.getT2()) .subscribe(System.out::println);
Creating an infinite Flux Stream
Flux.fromStream(Stream.generate(UUID::randomUUID)) .subscribe(System.out::println);
Throttling
Emit the item in particular time span
Flux.fromStream(Stream.generate(UUID::randomUUID))
.sample(Duration.ofSeconds(1))
.subscribe(System.out::println);
Exercise 1
- Create a flux and Subscribe to it.
- Run the Publisher and Subscriber in different threads.
- Transform the elements of a flux stream and then subscribe to it.
- Try following operatoions:
map, flatMap, sort, distinct, concatWith, takeLast, takeUntil, takeWhile, collectList, collectSortedList, collectMap
- Introduce the callback events in subscriber.
- Apply BackPressure on Flux stream.
- Create Infinite stream and apply throttling on it.
Webflux
WebFlux is a Spring reactive-stack web framework. It was added to Spring 5. It is fully non-blocking, supports reactive streams back pressure, and runs on such servers such as Netty, Undertow, and Servlet 3.1+ containers.
Spring WebFlux is an alternative to the traditional Spring MVC.
Required Dependencies
compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')
compile('org.springframework.boot:spring-boot-starter-webflux')
Creating a Reactive API
Customer
package com.reactive.webflux.webflux.dto;
public class Customer {
private int id;
private String name;
public Customer() {
}
public Customer(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
package com.reactive.webflux.webflux.dao;
import com.reactive.webflux.webflux.dto.Customer;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@Component
public class CustomerDao {
public List<Customer> getCustomers(){
return IntStream
.rangeClosed(1,10)
.mapToObj(e->{
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
return new Customer(e,"Customer"+e);
})
.collect(Collectors.toList());
}
public Flux<Customer> getCustomersStream(){
return Flux
.range(1,10)
.delayElements(Duration.ofSeconds(1))
.map(e->new Customer(e,"Customer"+e));
}
}
package com.reactive.webflux.webflux.controller;
import com.reactive.webflux.webflux.dao.CustomerDao;
import com.reactive.webflux.webflux.dto.Customer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.util.List;
import java.util.stream.Stream;
@RestController
@RequestMapping("/customers")
public class CustomerController {
@Autowired
CustomerDao customerDao;
@GetMapping("/")
public List<Customer> getCustomers() {
return customerDao.getCustomers();
}
@GetMapping(value = "/flux")
public Flux<Customer> getCustomersFlux() {
return customerDao.getCustomersStream();
}
@GetMapping(value = "/stream",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Customer> getCustomersStream() {
return customerDao.getCustomersStream();
}
@GetMapping(value = "/infinite",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<String> infinite(){
return Flux.fromStream(Stream.generate(()->"hello")).delayElements(Duration.ofSeconds(1)).log();
}
}
Reactive Mongo
package com.reactive.webflux.webflux;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@EnableReactiveMongoRepositories("com.reactive.webflux.webflux.repository")
public class WebfluxApplication {
public static void main(String[] args) {
SpringApplication.run(WebfluxApplication.class, args);
}
}
package com.reactive.webflux.webflux.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "products")
public class Product {
@Id
private String id;
private String name;
public Product() {
}
public Product( String name) {
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.reactive.webflux.webflux.repository;
import com.reactive.webflux.webflux.entity.Product;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
public interface ProductRepository extends ReactiveMongoRepository<Product, String> {
}
package com.reactive.webflux.webflux.controller;
import com.reactive.webflux.webflux.entity.Product;
import com.reactive.webflux.webflux.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.Duration;
@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired
ProductRepository productRepository;
@GetMapping(value = "/flux",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Product> getProducts(){
return productRepository.findAll().delayElements(Duration.ofSeconds(1)).log();
}
}
application.properties
server.port=9191
spring.data.mongodb.database=fluxDemoDB
Subscribing Reactive Endpoint with web client
package com.reactive.demo.reactivewebclient;
import com.reactive.demo.reactivewebclient.dto.Customer;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.util.List;
@SpringBootApplication
@RestController
@RequestMapping("/consume")
public class ReactiveWebClientApplication {
@GetMapping("/api")
String comingFlux(){
WebClient.create("http://localhost:9191/customers/")
.get()
.retrieve()
.bodyToFlux(Customer.class)
.log().subscribe(System.out::println);
return "Consuming Flux";
}
@GetMapping("/flux")
String consumingFlux(){
WebClient.create("http://localhost:9191/customers/flux")
.get()
.retrieve()
.bodyToFlux(Customer.class)
.log().subscribe(System.out::println);
return "Consuming Flux";
}
@GetMapping("/stream")
String consumingStream(){
WebClient.create("http://localhost:9191/customers/stream")
.get()
.retrieve()
.bodyToFlux(Customer.class)
.log().subscribe(System.out::println);
return "Consuming Stream";
}
public static void main(String[] args) {
SpringApplication.run(ReactiveWebClientApplication.class, args);
}
}
@GetMapping("/api")
String comingFlux(){
WebClient.create("http://localhost:9191/customers/")
.get()
.retrieve()
.bodyToFlux(Customer.class)
.log().subscribe(System.out::println);
return "Consuming Flux";
}
@GetMapping("/flux")
String consumingFlux(){
WebClient.create("http://localhost:9191/customers/flux")
.get()
.retrieve()
.bodyToFlux(Customer.class)
.log().subscribe(System.out::println);
return "Consuming Flux";
}
@GetMapping("/stream")
String consumingStream(){
WebClient.create("http://localhost:9191/customers/stream")
.get()
.retrieve()
.bodyToFlux(Customer.class)
.log().subscribe(System.out::println);
return "Consuming Stream";
}
@GetMapping(value = "/streamEvent",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<Customer> streamEvent(){
return WebClient.create("http://localhost:9191/customers/flux")
.get()
.retrieve()
.bodyToFlux(Customer.class)
.log();
}
@GetMapping("/throttle")
String throttle(){
WebClient.create("http://localhost:9191/customers/flux")
.get()
.retrieve()
.bodyToFlux(Customer.class)
.delayElements(Duration.ofSeconds(1))
.log().subscribe(System.out::println);
return "Consuming Stream";
}
@GetMapping(value = "/buffer",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<List<Customer>> buffer(){
return WebClient.create("http://localhost:9191/customers/flux")
.get()
.retrieve()
.bodyToFlux(Customer.class)
.buffer(2)
.log();
}
@GetMapping("/buffer2")
String buffer2(){
WebClient.create("http://localhost:9191/customers/stream")
.get()
.retrieve()
.bodyToFlux(Customer.class)
.buffer(2)
.subscribe(System.out::println);
return "Consuming Buffer";
}
@GetMapping(value = "/backPressure",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
String backPressure(){
WebClient.create("http://localhost:9191/customers/")
.get()
.retrieve()
.bodyToFlux(Customer.class)
.log()
.subscribe(new Subscriber<Customer>() {
Subscription subscription;
Integer itemCount=0;
@Override
public void onSubscribe(Subscription s) {
subscription=s;
s.request(3);
}
@Override
public void onNext(Customer customer) {
itemCount++;
if(itemCount%3==0) {
subscription.request(3);
}
}
@Override
public void onError(Throwable t) {
System.out.println(t);
}
@Override
public void onComplete() {
System.out.println("Data Pushed");
}
});
return "Consuming Buffer";
}
Exercise 2
- Create a reactive end point with TEXT_EVENT_STREAM_VALUE
- Creative a reactive endpoint to fetch multiple records from a mongo db collection
- Consume Flux using webClient in separate application
- Using webclient implement backpressure and buffer of flux.
Problem Statement
- We need to trade on the share of a company
- We will buy share of the company if the price gets below Rs 90
- We will sell the share of the company if the price goes above Rs 90
public class StockShare {
private String name;
private Integer price;
public StockShare(String name, Integer price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
@Override
public String toString() {
return "StockShare{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
package com.reactive.webflux.webflux.dao;
import com.reactive.webflux.webflux.dto.StockShare;
import reactor.core.publisher.Flux;
import java.util.Random;
import java.util.stream.Stream;
public class ShareDao {
public static Flux<StockShare> getStockShareFlux(){
return Flux.fromStream(Stream.generate(()-> new StockShare("Apple",generateRandomNumber()))) ;
}
public static int generateRandomNumber(){
Integer min=85;
Integer max=95;
Random random = new Random();
return min+ random.nextInt(max-min);
}
}
package com.reactive.webflux.webflux.dto;
public class SavedStockShare {
static private StockShare stockShare;
static public StockShare getStockShare() {
return stockShare;
}
static public void setStockShare(StockShare stockShare1) {
stockShare = stockShare1;
}
}
package com.reactive.webflux.webflux.controller;
import com.reactive.webflux.webflux.dao.ShareDao;
import com.reactive.webflux.webflux.dto.SavedStockShare;
import com.reactive.webflux.webflux.dto.StockShare;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.Duration;
@RestController
public class ShareController {
@GetMapping("/shares")
String getShares(){
Flux<StockShare> stockShareFlux = ShareDao.getStockShareFlux();
stockShareFlux
.delayElements(Duration.ofSeconds(1))
.subscribe(stockShare -> {
System.out.println(stockShare);
StockShare savedStockShare = SavedStockShare.getStockShare();
if(savedStockShare==null && stockShare.getPrice()<90){
SavedStockShare.setStockShare(stockShare);
System.out.println("Buying Share :"+ stockShare);
}
if(savedStockShare!=null && stockShare.getPrice()<savedStockShare.getPrice()){
SavedStockShare.setStockShare(stockShare);
System.out.println("Buying Share :"+ stockShare);
}
if(savedStockShare!=null && stockShare.getPrice()>90){
SavedStockShare.setStockShare(null);
System.out.println("Selling Share :"+ stockShare);
}
});
return "shares subscribed";
}
}
Reactive Programming with Spring 5
By Pulkit Pushkarna
Reactive Programming with Spring 5
- 1,112