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

  1. Stream once consumed cannot be reused again.
  2. 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.
  3. 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