Spring framework version 5 với support từ Java 8 trở lên giúp chúng ta có thể sử dụng Functional Programming trong Java code. Và Spring WebFlux cũng không ngoại lệ, chúng ta cũng có thể xây dựng các ứng dụng Reactive web sử dụng Lambda Expression. Trong bài viết này, mình sẽ hướng dẫn các bạn làm thế nào để sử dụng Functional Programming với Spring WebFlux các bạn nhé!
Đầu tiên, mình cũng sẽ tạo mới Spring Boot project với Reactive Web support sử dụng Spring Initializr Web để làm ví dụ:
Nếu bạn nào chưa biết cách tạo Spring Boot project với Spring Initializr Web thì có thể tham khảo bài viết này nhé.
Lưu ý là sau khi tạo project xong, các bạn hãy mở tập tin Maven pom.xml và remove dependency spring-boot-starter-web đi:
1 2 3 4 |
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> |
Nếu không thì chúng ta sẽ không chạy ví dụ được!
Tương tự như trong bài viết về Spring WebFlux sử dụng annotation, trong bài viết này, mình cũng sẽ tạo một ứng dụng cung cấp danh sách sinh viên với dữ liệu được thêm vào sau mỗi một giây. Khi người dùng request tới ứng dụng của chúng ta thì mỗi khi một sinh viên mới được thêm vào, thông tin của sinh viên này sẽ được publish tới cho người dùng.
Thông tin sinh viên sẽ chứa trong đối tượng Student như sau:
1 2 3 4 5 6 7 8 9 10 11 |
package com.huongdanjava.springwebfluxfunctional; import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor public class Student { private String name; } |
Ở đây, mình cũng đã sử dụng Project Lombok.
1 2 3 4 5 6 |
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.18</version> <scope>provided</scope> </dependency> |
Với Functional Programming trong Spring WebFlux, thay vì chúng ta sử dụng các annotation trong Spring MVC, bây giờ chúng ta sẽ sử dụng đối tượng HandlerFunction với Lambda Expression để xử lý request và map request URL tới đối tượng HandlerFunction sử dụng đối tượng RouterFunction.
Ở đây, chúng ta có thể xem RouterFunction giống như annotation @RequestMapping dùng để định nghĩa request URL, HTTP method, … và HandlerFunction là phương thức được khai báo với annotation @RequestMapping trong Spring MVC.
- HandlerFunction là một Functional Interface sẽ process request từ người dùng từ đối tượng ServerRequest và trả về client đối tượng ServerResponse.
Nội dung của interface này như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.web.reactive.function.server; import reactor.core.publisher.Mono; /** * Represents a function that handles a {@linkplain ServerRequest request}. * * @author Arjen Poutsma * @since 5.0 * @param <T> the type of the response of the function * @see RouterFunction */ @FunctionalInterface public interface HandlerFunction<T extends ServerResponse> { /** * Handle the given request. * @param request the request to handle * @return the response */ Mono<T> handle(ServerRequest request); } |
Ở đây, các đối tượng ServerRequest và ServerResponse là những đối tượng mới được giới thiệu cùng với Spring WebFlux để nắm giữ các thông tin về request và response của một request URL trong các ứng dụng Reactive web. Chúng ta có thể lấy được các đối tượng Mono hay Flux trong Project Reactor từ các đối tượng này.
Trong ví dụ của bài viết này, mình sẽ tạo một đối tượng HandlerFunction với nội dung như sau:
1 |
HandlerFunction<ServerResponse> handlerFunction = (request) -> ServerResponse.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(all(), Student.class); |
Phương thức ServerResponse.ok() nghĩa là chúng ta sẽ return lại kết quả cho client với HTTP status code là 200. Ở đây, mình cũng sử dụng Content-Type là “text/event-stream” để mỗi khi có dữ liệu mới thì server sẽ cập nhập dữ liệu đó cho client.
Phương thức body() định nghĩa data sẽ trả về cho client. Trong phương thức này, mình cũng định nghĩa phương thức all() để tạo mới thông tin sinh viên sau thời gian một giây như sau:
1 2 3 4 5 6 7 8 |
private Flux<Student> all() { RandomStringGenerator rsg = new RandomStringGenerator.Builder() .withinRange('a', 'z') .build(); return Flux.generate((SynchronousSink<Student> sink) -> sink.next(new Student(rsg.generate(10)))) .delayElements(Duration.ofSeconds(1L)); } |
Với đối tượng RandomStringGenerator được sử dụng từ thư viện Apache Commons Text:
1 2 3 4 5 |
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.1</version> </dependency> |
- RouterFunction cũng là một Functional Interface và nó sẽ map request URL tới một HandlerFunction sử dụng đối tượng RequestPredicate để xử lý request từ người dùng.
Nội dung của interface RouterFunction như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
/* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.web.reactive.function.server; import reactor.core.publisher.Mono; /** * Represents a function that routes to a {@linkplain HandlerFunction handler function}. * * @author Arjen Poutsma * @since 5.0 * @param <T> the type of the {@linkplain HandlerFunction handler function} to route to * @see RouterFunctions */ @FunctionalInterface public interface RouterFunction<T extends ServerResponse> { /** * Return the {@linkplain HandlerFunction handler function} that matches the given request. * @param request the request to route * @return an {@code Mono} describing the {@code HandlerFunction} that matches this request, * or an empty {@code Mono} if there is no match */ Mono<HandlerFunction<T>> route(ServerRequest request); /** * Return a composed routing function that first invokes this function, * and then invokes the {@code other} function (of the same response type {@code T}) * if this route had {@linkplain Mono#empty() no result}. * @param other the function of type {@code T} to apply when this function has no result * @return a composed function that first routes with this function and then the * {@code other} function if this function has no result * @see #andOther(RouterFunction) */ default RouterFunction<T> and(RouterFunction<T> other) { return new RouterFunctions.SameComposedRouterFunction<>(this, other); } /** * Return a composed routing function that first invokes this function, * and then invokes the {@code other} function (of a different response type) if this route had * {@linkplain Mono#empty() no result}. * @param other the function to apply when this function has no result * @return a composed function that first routes with this function and then the * {@code other} function if this function has no result * @see #and(RouterFunction) */ default RouterFunction<?> andOther(RouterFunction<?> other) { return new RouterFunctions.DifferentComposedRouterFunction(this, other); } /** * Return a composed routing function that routes to the given handler function if this * route does not match and the given request predicate applies. This method is a convenient * combination of {@link #and(RouterFunction)} and * {@link RouterFunctions#route(RequestPredicate, HandlerFunction)}. * @param predicate the predicate to test if this route does not match * @param handlerFunction the handler function to route to if this route does not match and * the predicate applies * @return a composed function that route to {@code handlerFunction} if this route does not * match and if {@code predicate} applies */ default RouterFunction<T> andRoute(RequestPredicate predicate, HandlerFunction<T> handlerFunction) { return and(RouterFunctions.route(predicate, handlerFunction)); } /** * Return a composed routing function that routes to the given router function if this * route does not match and the given request predicate applies. This method is a convenient * combination of {@link #and(RouterFunction)} and * {@link RouterFunctions#nest(RequestPredicate, RouterFunction)}. * @param predicate the predicate to test if this route does not match * @param routerFunction the router function to route to if this route does not match and * the predicate applies * @return a composed function that route to {@code routerFunction} if this route does not * match and if {@code predicate} applies */ default RouterFunction<T> andNest(RequestPredicate predicate, RouterFunction<T> routerFunction) { return and(RouterFunctions.nest(predicate, routerFunction)); } /** * Filter all {@linkplain HandlerFunction handler functions} routed by this function with the given * {@linkplain HandlerFilterFunction filter function}. * @param <S> the filter return type * @param filterFunction the filter to apply * @return the filtered routing function */ default <S extends ServerResponse> RouterFunction<S> filter(HandlerFilterFunction<T, S> filterFunction) { return new RouterFunctions.FilteredRouterFunction<>(this, filterFunction); } /** * Accept the given visitor. Default implementation calls * {@link RouterFunctions.Visitor#unknown(RouterFunction)}; composed {@code RouterFunction} * implementations are expected to call {@code accept} for all components that make up this * router function * @param visitor the visitor to accept */ default void accept(RouterFunctions.Visitor visitor) { visitor.unknown(this); } } |
Chúng ta có thể sử dụng các phương thức static của đối tượng RequestPredicates để tạo đối tượng RequestPredicate:
1 |
RequestPredicate predicate = RequestPredicates.GET("/students"); |
và các phương thức static của đối tượng RouterFunctions để tạo đối tượng RouterFunction từ đối tượng RequestPredicate và HandlerFunction ở trên:
1 |
RouterFunction<ServerResponse> routerFunction = RouterFunctions.route(predicate, handlerFunction); |
Toàn bộ code để tạo request URL như sau:
1 2 3 |
HandlerFunction<ServerResponse> handlerFunction = (request) -> ServerResponse.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(all(), Student.class); RequestPredicate predicate = RequestPredicates.GET("/students"); RouterFunction<ServerResponse> routerFunction = RouterFunctions.route(predicate, handlerFunction); |
Nếu sử dụng Lambda Expression, code chúng ta có thể viết gọn lại như sau:
1 |
RouterFunctions.route(RequestPredicates.GET("/students"), (request) -> ServerResponse.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(all(), Student.class)); |
- Để đăng ký request URL này với Spring WebFux, chúng ta cần khai báo RouterFunction trong Spring container.
Để đơn giản, mình sẽ khai báo bean cho đối tượng RouterFunction ngay trong tập tin SpringWebfluxFunctionalApplication luôn. Nội dung của tập tin này lúc này sẽ như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package com.huongdanjava.springwebfluxfunctional; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; import static org.springframework.web.reactive.function.server.RouterFunctions.route; import java.time.Duration; import org.apache.commons.text.RandomStringGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.server.*; import reactor.core.publisher.Flux; import reactor.core.publisher.SynchronousSink; @SpringBootApplication public class SpringWebfluxFunctionalApplication { public static void main(String[] args) { SpringApplication.run(SpringWebfluxFunctionalApplication.class, args); } @Bean public RouterFunction<ServerResponse> routerFunction() { return route(GET("/students"), (request) -> ServerResponse.ok().contentType(MediaType.TEXT_EVENT_STREAM).body(all(), Student.class)); } private Flux<Student> all() { RandomStringGenerator rsg = new RandomStringGenerator.Builder() .withinRange('a', 'z') .build(); return Flux.generate((SynchronousSink<Student> sink) -> sink.next(new Student(rsg.generate(10)))) .delayElements(Duration.ofSeconds(1L)); } } |
Vậy là xong rồi đó các bạn, thử chạy xem nào!
Các bạn cũng sẽ thấy cứ sau một giây, một sinh viên mới được thêm vào và trả về cho người dùng như sau: