Reactive Programming with Reactor and Spring 5
- Pulkit Pushkarna
A bit about me
- Currently working for To The New as Senior Software Enigineer.
- Over 3 years of experience in Grails and Spring.
- Love to play with Java 8, Groovy and Spring.
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
Key Components of Reactive Programming
- Observable
- Observer
- Schedulers
Reactive Streams
- "Reactive Streams" defines an API specification that expose ways to define operations for asynchronous streams of data .
- With the introduction of backpressure, Reactive Streams allows the subscriber to control the data exchange rate from publishers.
Spring 5 and Reactor
- Reactive Streams is API specification for asynchronous streams of data .
-
Spring 5 Framework introduced Reactor as an implementation for the Reactive Streams specification.
-
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.
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)
- Mono : Can emit at most one element
Mono.just(1);
Flux and Mono are implementations of the Reactive Streams Pubisher interface
Subscribing to Stream
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()
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"));
//Observable
Flux.range(1,4)
//Schedulers
.subscribeOn(Schedulers.parallel()).
//Observer
subscribe(System.out::println);
Running Observable and Observer in Seperate Threads
Transforming a Stream
Flux.range(1,10).map( e->{ try { Thread.sleep(500); } catch (InterruptedException e1) { e1.printStackTrace(); } return e*2; }) .subscribe(System.out::println);
Flux.just("Hello","world")
.flatMap(e ->Flux.fromArray(e.split("")))
.distinct()
.sort()
.subscribe(System.out::println);
FlatMap
Mono.just("Hello")
.concatWith(Mono.just("World"))
.delayElements(Duration.ofMillis(500L))
.delaySubscription(Duration.ofMillis(500L))
.subscribe(System.out::println);
Combining Mono
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);
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);
More examples of Mono
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);
Getting more control on Events
Flux.range(1,10) .log() .subscribe(new Subscriber<Integer>() { @Override public void onSubscribe(Subscription s) { s.request(5); } @Override public void onNext(Integer integer) { System.out.println(integer); } @Override public void onError(Throwable t) { System.out.println(t); } @Override public void onComplete() { System.out.println("Subscription Completed..."); } });
BackPressure
Backpressure is when a downstream can tell an upstream to send it fewer data in order to prevent it from being overwhelmed.
BackPressure
Flux.range(1,10)
.log()
.subscribe(new Subscriber<Integer>() {
Subscription subscription;
Integer itemCount=0;
@Override public void onSubscribe(Subscription s) { subscription=s; s.request(2); } @Override public void onNext(Integer integer) { itemCount++; if(itemCount%2==0) { subscription.request(2); } } @Override public void onError(Throwable t) { System.out.println(t); } @Override public void onComplete() { System.out.println("Data Pushed"); } });
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();
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.
- Stream are based on pull model and Flux is based on push model
Generating and Consuming infinite Stream
Flux.fromStream( Stream.generate(UUID::randomUUID)) .subscribe(System.out::println);
Generate infinite Stream from Mono
Flux<Integer> flux = Mono.just(1).flatMapMany(e-> { Flux<Integer> integerFlux=Flux.fromStream(Stream.generate(()->e)); return integerFlux; });
Throttling
Flux.fromStream(Stream.generate(UUID::randomUUID))
.sample(Duration.ofSeconds(1))
.subscribe(System.out::println);
Emit the last item in particular time span
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, take, takeLast, takeUntil, takeWhile, collectList, collectSortedList, collectMap
- Perform following operations on Mono
count, elementAt, hasElement, any, all
- Introduce the callback events in subscriber.
- Apply BackPressure on Flux stream.
- Use declarative style for subscriber events.
- Create Infinite stream and apply throttling on it.
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 +
'}';
}
}
import reactor.core.publisher.Flux;
import java.util.Random;
import java.util.stream.Stream;
public class StockService {
static Flux<StockShare> getStockShareFlux(){
return Flux.fromStream(Stream.generate(()-> new StockShare("Apple",generateRandomNumber()))) ;
}
static int generateRandomNumber(){
Integer min=85;
Integer max=95;
Random random = new Random();
return min+ random.nextInt(max-min);
}
}
public class SavedStockShare {
static private StockShare stockShare;
static public StockShare getStockShare() {
return stockShare;
}
static public void setStockShare(StockShare stockShare1) {
stockShare = stockShare1;
}
}
Flux<StockShare> stockShareFlux = new StockShareService().getStockPriceFlux();
stockShareFlux
.sample(Duration.ofSeconds(1))
.subscribe(stockShare -> {
System.out.println(stockShare);
StockShare savedStockShare = SavedStockShare.getStockShare();
if(savedStockShare==null && stockShare.getStockPrice()<90){
SavedStockShare.setStockShare(stockShare);
System.out.println("Buying Share :"+ stockShare);
}
if(savedStockShare!=null && stockShare.getStockPrice()<savedStockShare.getStockPrice()){
SavedStockShare.setStockShare(stockShare);
System.out.println("Buying Share :"+ stockShare);
}
if(savedStockShare!=null && stockShare.getStockPrice()>90){
SavedStockShare.setStockShare(null);
System.out.println("Selling Share :"+ stockShare);
}
});
Broadcasting to Multiple Subscribers
ConnectableFlux<Integer> connectableFlux = Flux.range(1,1000000)
.sample(Duration.ofMillis(1))
.publish();
connectableFlux.subscribe(System.out::println);
connectableFlux.subscribe(System.out::println);
connectableFlux.connect();
ConnectableFlux<StockShare> connectableFlux = new StockShareService()
.getStockPriceFlux()
.publish();
connectableFlux
.sample(Duration.ofSeconds(1))
.filter(stockShare -> (stockShare.getStockPrice()<90 &&
SavedStockShare.getStockShare()==null)
||
(SavedStockShare.getStockShare()!=null &&
stockShare.getStockPrice() < SavedStockShare.getStockShare().getStockPrice()
))
.subscribe(stockShare -> {
SavedStockShare.setStockShare(stockShare);
System.out.println("Buying Shares : "+stockShare);
});
connectableFlux
.sample(Duration.ofSeconds(1))
.filter(stockShare ->
SavedStockShare.getStockShare()!=null
&&
stockShare.getStockPrice()>90).
subscribe(stockShare -> {
SavedStockShare.setStockShare(null);
System.out.println("Selling Shares :"+stockShare);
});
connectableFlux.connect();
Solving the stock problem through broadcasting
doOnError
Flux<Integer> flux = Flux.just(1,2,3,4)
.map(e->{
if(e==4){
throw new RuntimeException("Exception on 4");
}
return e;
});
flux
.doOnError(System.out::println)
.doOnNext(System.out::println)
.subscribe();
onErrorReturn
Flux<Integer> flux = Flux.just(1,2,3,4)
.map(e->{
if(e==4){
throw new RuntimeException("Exception on 4");
}
return e;
});
flux.onErrorReturn(0)
.doOnError(System.out::println)
.doOnNext(System.out::println)
.subscribe();
onErrorResume
Flux<Integer> flux = Flux.just(1,2,3,4,5,6,7);
flux.map(e->{
if(e==4){
throw new RuntimeException("Exception on 4");
}
return e;
}).onErrorResume(e->{
System.out.println(e+" element is causing error. Hence returning the entire flux");
return flux;
})
.doOnError(System.out::println)
.doOnNext(System.out::println)
.subscribe();
Checkpoints
Checkpoints can be used to get the details on the operators which are causing problem
Flux.range(1, 10)
.map(e->e/0)
.checkpoint("map",true)
.filter(e->(e/0)>1)
.checkpoint("filter",true)
.sort()
.subscribe(System.out::println);
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);
Buffer
Collect incoming values into multiple buffers that will be emitted by the returned each time the given max size is reached
Flux.range(1, 10).
buffer(2)
.subscribe(System.out::println);
Flux.range(1, 10)
.bufferUntil(e->e<5,false)
.subscribe(System.out::println);
Flux.range(1, 10)
.bufferWhile(e->e<5)
.subscribe(System.out::println);
Flux.range(1, 10)
.buffer(5,3)
.subscribe(System.out::println);
Exercise 2
- Write code for pollution alarming system with has following conditions
- If the pollution goes below 100 then mark Good
- Between 100 and 200 mark moderate
- Between 200 and 250 poor
- Between 250 and 300 very poor
- Broadcast the above functionality in 4 different subscribers
- Try out doOnError, onErrorReturn, onErrorResume
- Use checkpoints in you pipeline with errors
- Combine 2 Stream using zip and zipWith
- Try out different buffer options with Flux
Spring WebFlux provides a choice of two programming models.
Annotated controllers: These are the same as Spring MVC with some additional annotations provided by the Spring-Web module. Both Spring MVC and WebFlux controller support Reactive return types. In addition, WebFlux also supports Reactive @RequestBody arguments.
Functional Programming model: A lambda-based, lightweight, small library that exposes utilities to route and handles requests.
Required Dependencies
compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')
compile('org.springframework.boot:spring-boot-starter-webflux')
spring.data.mongodb.database=demoDB
Mention DB name in application.properties
Entity for MongoDB
package com.ttn.demo.document;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document
public class Employee {
@Id
private Integer id;
private String name;
private Integer age
//getter, setter and toString
}
Using Reactive Crud Repository
package com.springreactive.webflux.repositories;
import com.springreactive.webflux.Entity.Employee;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Mono;
public interface EmployeeRepository extends ReactiveCrudRepository<Employee, Integer> {
Mono<Employee> findById(Integer id);
}
package com.springreactive.webflux.events;
import com.springreactive.webflux.Entity.Employee;
import com.springreactive.webflux.repositories.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.stream.Stream;
@Component
public class Bootstrap {
@Autowired
EmployeeRepository employeeRepository;
@EventListener(ApplicationStartedEvent.class)
public void init(){
employeeRepository.deleteAll().subscribe(null,null,()->{
Stream.of(new Employee(1,"Emp1",21),
new Employee(2,"Emp2",22),
new Employee(3,"Emp3",23),
new Employee(4,"Emp3",24),
new Employee(5,"Emp3",25))
.forEach(employee -> employeeRepository.save(employee)
.subscribe(System.out::println));
});
}
}
Bootstrap Code
Reactive Action for Employee Controller
package com.springreactive.webflux.controller;
import com.springreactive.webflux.Entity.Employee;
import com.springreactive.webflux.repositories.EmployeeRepository;
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.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
@RestController
public class EmployeeController {
@Autowired
EmployeeRepository employeeRepository;
@GetMapping("/employees")
public Flux<Employee> getAllEmployee(){
return employeeRepository.findAll().log();
}
@GetMapping("/employees/{id}")
public Mono<Employee> getEmployee(@PathVariable Integer id){
return employeeRepository.findById(id).log();
}
@GetMapping(value = "/",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Employee> getEmployee() {
return employeeRepository
.findAll()
.delayElements(Duration.ofSeconds(1L))
.sample(Duration.ofSeconds(2L));
}
}
Applying Backpressure
@GetMapping(value="/webClient") String webClient(){ WebClient.create("http://localhost:8080/") .get() .uri("/employees") .accept(MediaType.APPLICATION_STREAM_JSON) .retrieve() .bodyToFlux(Employee.class) .log() .delayElements(Duration.ofSeconds(1L)) .subscribe(e-> System.out.println(">>>>>"+e)); return "Web Client Performed operations"; }
Getting Stream Data from WebClient
@GetMapping(value="/webClient")
String webClient(){
WebClient.create("http://localhost:8080/")
.get()
.uri("/")
.retrieve()
.bodyToFlux(Employee.class)
.log()
.subscribe(e-> System.out.println(">>>>>"+e));
return "Web Client Performed operations";
}
package com.springreactive.webflux.config;
import com.springreactive.webflux.Entity.Employee;
import com.springreactive.webflux.repositories.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
@Configuration
public class RouterConfig {
@Autowired
EmployeeRepository employeeRepository;
@Bean
RouterFunction<ServerResponse> getAllEmployeesRoute() {
return RouterFunctions.route(RequestPredicates.GET("/all/router"),
req -> ServerResponse.ok().body(
employeeRepository.findAll(), Employee.class));
}
@Bean
RouterFunction<ServerResponse> getEmployeeByIdRoute() {
return RouterFunctions.route(RequestPredicates.GET("/employees/{id}/router"),
req -> ServerResponse.ok().body(
employeeRepository.findById(Integer.parseInt(req.pathVariable("id"))), Employee.class));
}
}
Functional Programming model for Spring
Exercise 3
- Create following reactive endpoints:
- Fetch all Students
- Fetch Student by Id
- Stream Data for all the Students with a delay of 1 second
- Use WebClient to consume the endpoints you have created above.
- Use WebClient to perform BackPressure on the endpoint returning flux.
Reactive Programming
By Pulkit Pushkarna
Reactive Programming
- 1,131