- Pulkit Pushkarna
A bit about me
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
Spring 5 and Reactor
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.just(1,2,3,4)
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
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
map, flatMap, sort, distinct, concatWith, take, takeLast, takeUntil, takeWhile, collectList, collectSortedList, collectMap
count, elementAt, hasElement, any, all
Problem Statement
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
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