Search code examples
javaspring-bootthymeleafspring-webfluxproject-reactor

How to get model attributes when using Spring WebFlux router functions?


Spring WebFlux provides a choice of two programming options: Annotated Controllers and Functional Endpoints. With the first one, we can use @ModelAttribute annotation to transfer model attributes from controller to the view (such as Thymeleaf html-template) and vise versa. However, when it comes to router functions, so far I have just figured out how to attach model attributes to ServerResponse, but can't find a way to get them back. Consider the following code snippet:

@Configuration
public class StudentsRouterFunctions {

    // inject repository
    private final StudentsCrudRepository repo;

    public StudentsRouterFunctions(StudentsCrudRepository repo) {
        this.repo = repo;
    }

    @Bean
    RouterFunction<?> routs() {
        return RouterFunctions.route()
                .GET("/students", this::showStudents)
                .POST("/students", this::saveStudent)
                .build();
    }

    // #1: GET-handler
    private Mono<ServerResponse> showStudents(ServerRequest request) {

        // set model attributes
        Map<String, Object> model = new HashMap<>();
        Mono<Student> studentsList = repo.findAll().collectList();
        Mono<Student> newStudent = Mono.just(new Student());
        model.put("students", studentsList);
        model.put("studentForm", newStudent);

        // render the view
        return ServerResponse.ok()
                .contentType(MediaType.TEXT_HTML)
                .render("students-template", model);
    }

    // #2: POST-handler
    private Mono<ServerResponse> saveStudent(ServerRequest request) {

        // and here I somehow need to get my new student object 
        // back from the view via "studentForm" model attribute

        // Student newStudent = request.getModel().get("studentForm"); 
        // !!! however, ServerRequest.getModel() method doesn't exist

        return repo.save(newStudent)
                .then(ServerResponse.status(HttpStatus.PERMANENT_REDIRECT)
                        .render("redirect:/students", new Object()));
    }

Solution

  • There is no ServerRequest::getModel method but ServerRequest::bodyToMono(Class) that extracts the body where is your model to Mono<T>.

    Then use the advantage of the return type of the reactive repository's method ReactiveCrudRepository::save returning Mono<T> using Mono::flatMap.

    I have not tested it out, but it should work.

    private Mono<ServerResponse> saveStudent(ServerRequest request) {
        return request.bodyToMono(Student.class)               // Mono<Student> (a new one)
                      .flatMap(repo::save)                     // Mono<Student> (a saved one)
                      .then(ServerResponse                     // redirect sequence
                          .status(HttpStatus.PERMANENT_REDIRECT)
                          .render("redirect:/students", new Object()));
    }
    
    // .flatMap(repo::save) is the same as .flatMap(newStudent -> repo.save(newStudent))
    

    Note the method Mono::then discards the element from the source so the redirected object remains as a new Object(), so you want to use Mono::map.

    private Mono<ServerResponse> saveStudent(ServerRequest request) {
        return request.bodyToMono(Student.class)               // Mono<Student> (a new one)
                      .flatMap(repo::save)                     // Mono<Student> (a saved one)
                      .map(savedStudent -> ServerResponse      // redirect sequence
                          .status(HttpStatus.PERMANENT_REDIRECT)
                          .render("redirect:/students", savedStudent));
    }