WHAT’S CONTAINED, AND NOT. ➤ This talk contains ➤ Writing Reactive Programming code with Reactor and WebFlux ➤ Operations like Stream API’s stream(), map(), collect() methods ➤ This talk does NOT contain ➤ Reactive Systems ➤ R2DBC, RSocket, Reactive Streams
GETTING STARTED WITH REACTOR - REACTOR IS ➤ Reactor is … ➤ like Stream API monster. ➤ https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html
GETTING STARTED WITH REACTOR - MONO AND FLUX ➤ Mono and Flux ➤ Mono ➤ handles single elements ➤ like java.util.Optional ➤ Flux ➤ handles multiple elements ➤ like java.util.stream.Stream
GETTING STARTED WITH REACTOR - FIRST EXAMPLE Mono mono = Mono.just("Hello, world!"); mono.subscribe(s -> System.out.println(s)); Flux flux = Flux.just("H", "e", "l", "l", "o"); flux.subscribe(s -> System.out.println(s)); Hello, world! H e l l o
GETTING STARTED WITH REACTOR - FIRST EXAMPLE Mono.just("Hello, world!") .subscribe(System.out::println); Flux.just("H", "e", "l", "l", "o") .subscribe(System.out::println);
GETTING STARTED WITH REACTOR - FIRST EXAMPLE Mono.just("Hello, world!") .subscribe(System.out::println); Flux.just("H", "e", "l", "l", "o") .subscribe(System.out::println); Hello, world! H e l l o
GETTING STARTED WITH REACTOR - FIRST THINGS TO LEARN ➤ Create Mono, Flux ➤ Mono.just(T), Flux.just(T...) ➤ Creates a Mono or Flux from concrete instance(s) ➤ Flux.interval(Duration) ➤ Create a Flux that emits numbers starting with 0 ➤ Use Mono, Flux ➤ subscribe(Consumer) ➤ Subscribes the Mono or Flux and executes the Consumer method ➤ map(Function) ➤ Transforms the item by applying the function to each item
GETTING STARTED WITH REACTOR - MAP AND SUBSCRIBE Flux.interval(Duration.ofMillis(300)) .map(i -> i + " " + LocalDateTime.now()) .subscribe(System.out::println);
GETTING STARTED WITH REACTOR - MAP AND SUBSCRIBE Flux.interval(Duration.ofMillis(300)) .map(i -> i + " " + LocalDateTime.now()) .subscribe(System.out::println); (Output nothing)
GETTING STARTED WITH REACTOR - SECOND THINGS TO LEARN ➤ Use Flux ➤ Flux#take(long) ➤ takes only the first given N value from Flux, and then Flux completes ➤ Flux#doOnComplete(Runnable) ➤ executes the given function when Flux completes
GETTING STARTED WITH REACTOR - SECOND THINGS TO LEARN ➤ java.util.concurrent.CountDownLatch ➤ new CountDownLatch(int) - sets the count to given number ➤ countDown() - decrements the count ➤ await() - waits until the latch has counted down to zero
GETTING STARTED WITH REACTOR - AWAIT UNTIL COMPLETE var latch = new CountDownLatch(1); Flux.interval(Duration.ofMillis(300)) .map(i -> i + " " + LocalDateTime.now()) .take(10) .doOnComplete(latch::countDown) .subscribe(System.out::println); latch.await();
GETTING STARTED WITH REACTOR - NEXT THINGS TO LEARN ➤ From Optional to the value ➤ Optional#get() ➤ From Stream to Collection such as List, Set, Map or other values ➤ Stream#collect(Collector) ➤ From Mono to the value or Optional of the value ➤ Mono#block(), blockOptional() ➤ From Flux to List ➤ Flux#collectList().block() ➤ Flux#collectList() converts Flux to Mono> Not recommended to use
GETTING STARTED WITH REACTOR - BLOCK() BREAKS NON-BLOCKING List list = Flux.interval(Duration.ofMillis(300)) .map(i -> i + " " + LocalDateTime.now()) .take(10) .collectList() .block(); list.forEach(System.out::println);
GETTING STARTED WITH REACTOR - BLOCK() BREAKS NON-BLOCKING ➤ Blocking breaks non-blocking! ➤ Mono#block(), blockOptional() ➤ Flux#blockFirst(), blockLast() ➤ blocks until the next/first/last signal using the thread (blocking operation) ➤ blocks breaks non-bloking!
GETTING STARTED WITH WEBFLUX - WEBFLUX IS ➤ WebFlux is ➤ Non-blocking web framework ➤ Spring MVC + Reactor ➤ Runs on Netty, Undertow, Servlet Container like Tomcat ➤ Reactive WebClient is available instead of RestTemplate
GETTING STARTED WITH WEBFLUX - FIRST EXAMPLE @RestController public class WebFluxDemoController { @GetMapping public Flux demo() { return Flux.interval(Duration.ofMillis(300)) .map(i -> i + " " + LocalDateTime.now() + "\n") .take(10); } }
GETTING STARTED WITH WEBFLUX - FIRST EXAMPLE @RestController public class WebFluxDemoController { @GetMapping public Flux demo() { return Flux.interval(Duration.ofMillis(300)) .map(i -> i + " " + LocalDateTime.now() + "\n") .take(10); } } Annotations are same as Spring MVC Annotations are same as Spring MVC Arguments and return value can be a Mono and Flux
GETTING STARTED WITH WEBFLUX - WEBFLUX IS ➤ To make ➤ Add Request-Header "Accept: text/event-stream” ➤ curl -H "Accept: text/event-stream" localhost:8080 ➤ Force Response-Header "Content-Type: text/event-stream" ➤ @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
GETTING STARTED WITH WEBFLUX - FIRST EXAMPLE @RestController public class WebFluxDemoController { @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux demo() { return Flux.interval(Duration.ofMillis(300)) .map(i -> i + " " + LocalDateTime.now()) .take(10); } }
GETTING STARTED WITH WEBFLUX - FIRST EXAMPLE @RestController public class WebFluxDemoController { public List demo() { return Flux.interval(Duration.ofMillis(300)) .map(i -> i + " " + LocalDateTime.now()) .take(10) .collectList() .block() } }
GETTING STARTED WITH WEBFLUX - FIRST EXAMPLE @RestController public class WebFluxDemoController { public List demo() { return Flux.interval(Duration.ofMillis(300)) .map(i -> i + " " + LocalDateTime.now()) .take(10) .collectList() .block() } } { "timestamp": "2019-09-18T14:01:39.094+0000", "path": "/", "status": 500, "error": "Internal Server Error", "message": "block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-1" }
GETTING STARTED WITH WEBFLUX - WEBCLIENT ➤ Classic Http client ➤ Hard or impossible to handle stream-event ➤ New WebClient has come ➤ Can handle stream-event ➤ Modern style APIs
GETTING STARTED WITH WEBFLUX - CLASSIC RESTTEMPLATE // RestTemplate was like … RestTemplate restTemplate = new RestTemplate(); ParameterizedTypeReference> type = new ParameterizedTypeReference<>() {}; List list = restTemplate .exchange("http://localhost:8081/list", HttpMethod.GET, null, type) .getBody();
GETTING STARTED WITH WEBFLUX - FIRST EXAMPLE var webClient = WebClient.builder().build(); Flux flux = webClient.get() // HTTP GET method .uri("localhost:8081/students/flux") // to this URL .retrieve() // accesses to the server and retrieve .bodyToFlux(Student.class); // get response body as Flux
TRYING N+1 PROBLEM - PATTERN #1 ➤ Overview ➤ First, get N data from one microservice (1 access) ➤ Second, get detail data from another microservice for each N data (N access) ➤ Finally, merge them ➤ More concrete ➤ First, get a list of 33 students (List) ➤ Second, get scores for each student (List) ➤ Finally merge them (List)
TRYING N+1 PROBLEM - PATTERN #1 ➤ (Arbitrary) performance limitation ➤ Student Resource Service ➤ 33 students ➤ 100ms for each student ➤ Flux - returns one by one every 100ms (total duration: 33 * 100ms = 3.3 secs) ➤ List - returns all after 3.3 secs ➤ Score Resource Service ➤ Only 100ms overhead regardless of the number of data
TRYING N+1 PROBLEM - SCORE RESOURCE SERVICE static Map> scoreStore = new HashMap<>(); // Initialization operation is omitted. @GetMapping("/{ids}") public List getAsList(@PathVariable List ids) throws InterruptedException { Thread.sleep(100); return ids.stream() .flatMap(id -> scoreStore.get(id).stream()) .collect(Collectors.toList()); }
TRYING N+1 PROBLEM - SCORE RESOURCE SERVICE static Map> scoreStore = new HashMap<>(); // Initialization operation is omitted. @GetMapping("/{ids}") public List getAsList(@PathVariable List ids) throws InterruptedException { Thread.sleep(100); return ids.stream() .flatMap(id -> scoreStore.get(id).stream()) .collect(Collectors.toList()); } SELECT * FROM score WHERE ids in (1, 2, 3)
TRYING N+1 PROBLEM - BFF SERVICE (RESTTEMPLATE) ParameterizedTypeReference> studentType = new ParameterizedTypeReference<>() {}; ParameterizedTypeReference> scoreType = new ParameterizedTypeReference<>() {}; List students = restTemplate .exchange("http://localhost:8081/students/list", HttpMethod.GET, null, studentType) .getBody(); List studentScores = students.stream() .map(student -> { String url = "http://localhost:8081/scores/" + student.id; List scores = restTemplate .exchange(url, HttpMethod.GET, null, scoreType) .getBody(); return new StudentScore(student, scores); }) .collect(Collectors.toList()); Get student list from Student Resource Service
TRYING N+1 PROBLEM - BFF SERVICE (RESTTEMPLATE) ParameterizedTypeReference> studentType = new ParameterizedTypeReference<>() {}; ParameterizedTypeReference> scoreType = new ParameterizedTypeReference<>() {}; List students = restTemplate .exchange("http://localhost:8081/students/list", HttpMethod.GET, null, studentType) .getBody(); List studentScores = students.stream() .map(student -> { String url = "http://localhost:8081/scores/" + student.id; List scores = restTemplate .exchange(url, HttpMethod.GET, null, scoreType) .getBody(); return new StudentScore(student, scores); }) .collect(Collectors.toList()); Get score list from Score Resource Service for each student
TRYING N+1 PROBLEM - BFF SERVICE (RESTTEMPLATE) ParameterizedTypeReference> studentType = new ParameterizedTypeReference<>() {}; ParameterizedTypeReference> scoreType = new ParameterizedTypeReference<>() {}; List students = restTemplate .exchange("http://localhost:8081/students/list", HttpMethod.GET, null, studentType) .getBody(); List studentScores = students.stream() .map(student -> { String url = "http://localhost:8081/scores/" + student.id; List scores = restTemplate .exchange(url, HttpMethod.GET, null, scoreType) .getBody(); return new StudentScore(student, scores); }) .collect(Collectors.toList()); Merge student and scores into StudentScore. And returns it as a List
TRYING N+1 PROBLEM - BFF SERVICE (WEBCLIENT) Flux students = webClient.get() .uri("localhost:8081/students/flux") .retrieve() .bodyToFlux(Student.class); Flux studentScore = students.flatMap(student -> webClient.get() .uri("localhost:8081/scores/" + student.id) .retrieve() .bodyToFlux(Score.class) .collectList() .map(scores -> new StudentScore(student, scores))); Get student list from Student Resource Service
TRYING N+1 PROBLEM - BFF SERVICE (WEBCLIENT) Flux students = webClient.get() .uri("localhost:8081/students/flux") .retrieve() .bodyToFlux(Student.class); Flux studentScore = students.flatMap(student -> webClient.get() .uri("localhost:8081/scores/" + student.id) .retrieve() .bodyToFlux(Score.class) .collectList() .map(scores -> new StudentScore(student, scores))); Get score list from Score Resource Service for each student Merge student and scores into StudentScore.
TRYING N+1 PROBLEM - BFF SERVICE (WEBCLIENT) Flux studentScore = students.flatMap(student -> { Flux flux = webClient.get() .uri("localhost:8081/scores/" + student.id) .retrieve() .bodyToFlux(Score.class); Mono> listMono = flux.collectList(); Mono mono = listMono.map(scores -> new StudentScore(student, scores)); return mono; }); Get scores and keep them as Flux. But I want List for StudentScore
TRYING N+1 PROBLEM - PERFORMANCE DIFFERENCE ➤ RestTemplate version ➤ Get all students: 3.3 secs (100ms * 33 data) ➤ Get scores: 3.3 secs (another 100ms * 33 accesses) ➤ More than 6.6 sec ➤ WebClient version ➤ Get each student: 3.3 secs (100ms * 33 data) ➤ Get scores while getting student : 3.3 secs (another 100ms * 33 accesses) ➤ More than 3.3 sec
TRYING N+1 PROBLEM - BE BETTER ➤ To be better ➤ When getting score, arguments should be Flux (by HTTP request body), ➤ Not String (by HTTP query string) ➤ webClient.post() .uri("localhost:8081/scores/flux") .body(fluxStudents) ➤ Only access once, not 33 times.