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