Bộ sưu tập

Sử dụng Router Function trong Spring Webflux


https://codersontrang.com/2017/11/20/su-dung-router-function-trong-spring-webflux/
Trong bài viết trước mình đã giới thiệu đến các bạn về tạo một ứng dụng web sử dụng framework Spring Webflux. Spring Webflux là một framework mới ra mắt từ phiên bản Spring 5 và theo đuổi mô hình lập trình Reactive hỗ trợ dễ dàng để thực thi các xử lý Non-Blocking. Trong bài viết trước, ứng dụng của chúng ta khai báo các URL bằng việc sử dụng các annotation (@RequestMapping) trong các controller, cách này giống với cách phát triển ứng dụng trên framework Spring MVC. Ngoài cách này ra, Spring Webflux còn hỗ trợ để khai báo các URL bằng Router Function, ở đó các dòng code được viết theo phong cách lập trình hàm (functional programming).

Ví dụ của bài viết này sẽ là một sự sửa đổi lại ví dụ trong bài viết trước. Các bạn có thể đọc và so sánh sự khác biệt giữa hai cách lập trình được hỗ trợ trong Spring Webflux. Đầu tiên là về cấu trúc của project sẽ giống như hình dưới đây:

pom.xml


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.codersontrang</groupId>
    <artifactId>spring-webflux-router-function</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <repositories>
        <repository>
            <id>spring-milestone</id>
            <name>spring milestone</name>
            <url>http://repo.spring.io/milestone/</url>
        </repository>
        <repository>
            <id>maven-central</id>
            <name>maven central</name>
            <url>http://central.maven.org/maven2/</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
            <version>2.0.0.M6</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>2.0.0.M6</version>
        </dependency>

        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
            <version>3.0.9.RELEASE</version>
        </dependency>
    </dependencies>
</project>

Trong bài viết này ta vẫn sử dụng các dependencies để có thể sử dụng các thư viện của Spring Boot, Spring Webflux, và Thymeleaf, tuy nhiên các phiên bản cửa các thư viện được nâng cấp lên phiên bản mới nhất tính đến thời điểm viết bài.

WebConfig.java


package springwebfluxdemo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyExtractors;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import springwebfluxdemo.model.Student;
import springwebfluxdemo.service.DemoService;
import springwebfluxdemo.util.Utils;

import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;



@Configuration
public class WebConfig {

    @Bean
    RouterFunction<?> routes (DemoService demoService){
        return RouterFunctions
                .route(GET("/home"),
                        serverRequest -> ServerResponse.ok().contentType(MediaType.TEXT_HTML)
                        .render("index", demoService.home()))

                .andRoute(POST("/submitStudentInfo"),
                        serverRequest -> ServerResponse.ok().contentType(MediaType.TEXT_HTML)
                        .render("index", demoService.submitStudentInfo(
                                Utils.convertMapToObject(serverRequest.body(BodyExtractors.toFormData()).block().toSingleValueMap(), Student.class)
                        )));
    }
}

Đây là điểm khác biệt so với bài viết trước mà bài viết này muốn nói. bắt đầu bằng RouterFunctions, các bạn có thể thấy các khai báo URL sẽ được thực hiện qua việc gọi liên tiếp các hàm route()andRoute(). Các xử lý cho từng URL sẽ được chuyển đến lớp code logic ở phía dưới mà ở đây là đối tượng của lớp DemoService được inject qua hàm routes(). Các Thymeleaf view dùng để hiển thị giao diện ra cho người dùng được khai báo trong hàm render() với tham số đầu tiên là tên view, tham số thứ hai là model object có chứa dữ liệu cho hiển thị.

Ở đây nếu chú ý các bạn có thể thấy một lời gọi Utils.convertMapToObject(). Không giống như bài viết trước ta có thể sử dụng annotation @ModelAttribute để lấy thông tin từ dưới form chuyển về một object trong tham số của controller. Nhưng hiện tại không có một thứ tương đương như vậy khi sử dụng Router Function, vì thế ta tự viết một hàm trong lớp Utils.java để chuyển đối dữ liệu từ form submit lên ở dạng Map sang đối tượng của lớp Student, rồi sau đó tất cả sẽ được chuyển xuống lớp xử lý logic để tiếp tục quá trình xử lý.

DemoService.java


package springwebfluxdemo.service;

import org.springframework.stereotype.Service;
import springwebfluxdemo.model.Student;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Service
public class DemoService {
    public Map<String, Object> home(){

        Student student = new Student();
        student.setName("Coder Tien Sinh");

        List<String> books = new ArrayList<String>();
        books.add("book1");
        books.add("book2");
        student.setBooks(books);

        return Map.of("model", student);
    }

    public Map<String, Object> submitStudentInfo(Student student){
        List<String> listBooks = student.getBooks();
        boolean bookSameName = checkSameBookName(listBooks);

        String message = "Update success !!!";
        if(bookSameName){
           message = "Books must have different name !!!";
        }

        return Map.of("model", student, "message", message);
    }

    private boolean checkSameBookName(List<String> listBooks) {
        for(int i =0; i<listBooks.size(); i++){
            String firstBookName = listBooks.get(i);
            for(int j = i+1; j<listBooks.size(); j++){
                String secondBookName = listBooks.get(j);
                if(firstBookName.equalsIgnoreCase(secondBookName)){
                    return true;
                }
            }
        }
        return false;
    }
}

DemoService là lớp xử lý logic, trong bài viết trước thì đoạn code logic này được viết chung vào cùng với nội dung của controller và điều đó là không tốt về mặt thiết kế. Trong bài viết này ta tách ra một lớp xử lý riêng thế này cũng là để cho đoạn code khai báo các URL bằng Router Function được ngắn gọn sáng sủa hơn.

Để ý bạn cũng sẽ thấy ở đoạn code trên có các để tạo ra đối tượng thuộc kiểu Map bằng việc gọi hàm Map.of(). Đây là một trong các hàm tiện ích được thêm vào các lớp thuộc họ Collection từ Java 9 để giúp code ngắn gọn sáng sủa hơn.

Utils.java


package springwebfluxdemo.util;

import java.lang.reflect.Field;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

public class Utils {

    private Utils (){}

    public static <T> T convertMapToObject(Map<String, String> map, Class<T> clazz) {
        try {
            T obj = clazz.getDeclaredConstructor(null).newInstance();
            for(Map.Entry<String, String> entry : map.entrySet()){
                String key = entry.getKey();
                if(key.indexOf("[") != -1){
                    String fieldName = key.substring(0, key.indexOf("["));
                    Field collections = clazz.getDeclaredField(fieldName);
                    collections.setAccessible(true);
                    Object fieldValue = collections.get(obj);
                    if(fieldValue == null){
                        fieldValue = new LinkedList<>();
                    }

                    if(fieldValue instanceof List){
                        ((List) fieldValue).add(entry.getValue());
                    }
                    collections.set(obj, fieldValue);
                }else{
                    Field field = clazz.getDeclaredField(key);
                    field.setAccessible(true);
                    field.set(obj, entry.getValue());
                }
            }

            return obj;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

Như đã nói ở trên lớp Utils sẽ nhận vào tham số thứ nhất là một Map của các thuộc tính và các giá trị tương ứng. Tham số thứ hai là kiểu đối tượng muốn chuyển thành mà ở đây là kiểu Student. Đầu ra sẽ là một đối tượng của lớp Student với các thuộc tính mang giá trị được khai báo ở trong Map.

Về nội dung của các file mã nguồn khác như là Student.java, Spring5WebfluxDemoApp.java, index.html thì không có gì thay đổi và các bạn có thể tìm thấy nội dung của chúng trong bài viết trước.

Bây giờ chạy chương trình từ phương thức main() trong lớp Spring5WebfluxDemoApp.java sau đó truy cập vào địa http://localhost:8080/home ta vẫn có được những hình ảnh như ở bài viết trước.

Nhập các giá trị khác nhau rồi bấm Submit ta cũng vẫn sẽ thấy các message được hiển thị lên như sau

Good luck!

Advertisements

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Google photo

Bạn đang bình luận bằng tài khoản Google Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s