Skip to main content

Spring WebFlux Functional HTML Form Handling

1. Intro

In this guide, you will learn to handle the HTML Form using the Spring WebFlux Functional approach in Java. You will also learn a few important concepts related to HTML Form processing in Spring Webflux Functional.

2. Dependencies

The following tools & libraries are used and tested for the given source code:

  1. Spring Boot v 2.1.9.RELEASE
  2. Spring Framework v 5.1.10RELEASE (Comes with mentioned Spring Boot version)
  3. Netty non-blocking server v 4.1.39FINAL (embedded)
  4. Thymleaf v 3.0.11.RELEASE template engine

Source code is available on GitHub for clone and download.

3. Sample Requirement - Employee Data

3.1 Create an HTML form to capture employee Details

Employee Data Capture Form (GET http://localhost:8080/form )

3.2 Display captured employee data.

 HTML Page to display captured Employee Data (POST http://localhost:8080/form)

4. HTML Form concepts for Spring WebFlux Functional

In Spring WebFlux, FormHttpMessageReader & FormHttpMessageWriter decodes and encodes "application/x-www-form-urlencoded" request & response.

ServerRequest.formData() parse the Form data from the body using FormHttpMessageReader and caches the result for repeated use. After the first invocation of formData() the body, the original raw content is no more available in the request body. In subsequent call ServerRequest.formData() provide form data from cache.

While using ServerRequest.body(BodyExtractors.toFormData()) to get the form data we can get empty Mono<MultiValueMap<String, String>> if form data is already parsed from the body, resulting in the unavailability of raw content. Hence use ServerRequest.formData(), which has access to cached form data.

Remember, In the case of JSON content type when we use BodyExtractors the method like toMono() or toFlux(), we always get the desired object. We can swap them with ServerRequest methods bodyToMono() and bodyToFlux() without any problem.

FormHttpMessageReader comes with a maximum number of bytes to buffer in memory, You can configure maxInMemorySize at ServerCodecConfigurer. ServerCodecConfigurer is responsible for the configuration of all codecs.

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        // ...configurations
    }
}

In general, avoid using Spring WebFlux for application which has several HTML Forms because of the lack of automatic mapping of form data to the model. You have to write a utility method to convert Mono<MultiValueMap<String, String>> it into a model.

These findings are based on Spring Boot v 2.1.9 and Spring v 5.1.10.

5. Implementation Approach

  1. Define Employee model.
  2. Create AppRoute Route class and define two routes:
    1. GET /form - Display employee data input form
    2. POST /form - Display employee data
  3. Create a FormHandler handler class to handle the above-defined routes.
  4. Create two ThymLeaf based HTML files:
    1. input-employee-data-form.html
    2. display-employee-data.html
  5. Use ServerRequest.formData() method to get Form data in Mono<MultiValueMap<String, String>>.
  6. Convert Mono<MultiValueMap<String, String>> form data to Employee objects using a utility method.

6. Model - Employee.java

package org.geekmj.springwebfluxform.model;

import java.util.List;

import lombok.Data;

@Data
public class Employee {

	private String name;
	private String dateOfBirth;
	private String gender;
	private String addressLine1;
	private String addressLine2;
	private String country;
	private String state;
	private String city;
	private String zipCode;
	private String mobile;
	private String email;
	private List<String> skills;
	private String biography;
	private String website;
}

7. Route - AppRoute.java

package org.geekmj.springwebfluxform.route;

import static org.springframework.web.reactive.function.server.RequestPredicates.accept;

import org.geekmj.springwebfluxform.route.handler.FormHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

@Configuration
public class AppRoute {

	@Bean
	public RouterFunction<ServerResponse> route(FormHandler formHandler) {

		return RouterFunctions.route()
				.GET("/form", formHandler::sampleForm)
				.POST("/form", accept(MediaType.APPLICATION_FORM_URLENCODED), formHandler::displayFormData)
				.build();
	}
}

You define two routes as mentioned earlier. It is self-explanatory.

8. Handler - FormHandler.java

package org.geekmj.springwebfluxform.route.handler;

import static org.geekmj.springwebfluxform.constant.AppConstant.*;

import org.geekmj.springwebfluxform.model.Employee;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;

import reactor.core.publisher.Mono;

@Component
public class FormHandler {

	public Mono<ServerResponse> sampleForm(ServerRequest request) {

		return ServerResponse.ok().render(FORM);
	}

	public Mono<ServerResponse> displayFormData(ServerRequest request) {
		
		Mono<MultiValueMap<String, String>> formData = request.formData();
		
		// BodyExtractor based. It didn't result any value for our program
		// It looks any earlier piece of code (Filter ?) already accessed the body
		// making it empty.
		
		// Mono<MultiValueMap<String, String>> formData = request.body(BodyExtractors.toFormData());		

		return ServerResponse.ok().render(DISPLAY_FORM_DATA, formDataToEmployee(formData));
	}

	private Employee formDataToEmployee(Mono<MultiValueMap<String, String>> formData) {

		Employee employee = new Employee();

		formData.subscribe(formDatamap -> {
			employee.setName(formDatamap.get(NAME).get(0));
			employee.setDateOfBirth(formDatamap.getFirst(DATE_OF_BIRTH));
			employee.setGender(formDatamap.getFirst(GENDER));
			employee.setAddressLine1(formDatamap.getFirst(ADDRESS_LINE_1));
			employee.setAddressLine2(formDatamap.getFirst(ADDRESS_LINE_2));
			employee.setCountry(formDatamap.getFirst(COUNTRY));
			employee.setState(formDatamap.getFirst(STATE));
			employee.setCity(formDatamap.getFirst(CITY));
			employee.setZipCode(formDatamap.getFirst(ZIP_CODE));
			employee.setMobile(formDatamap.getFirst(MOBILE));
			employee.setEmail(formDatamap.getFirst(EMAIL));
			employee.setSkills(formDatamap.get(SKILLS));
			employee.setWebsite(formDatamap.getFirst(WEBSITE));
			employee.setBiography(formDatamap.getFirst(BIOGRAPHY));
		});

		return employee;
	}
}

You define two methods in the handler for two routes.

In displayFormData() method, you used request.formData() for extracting form data into MultiValueMap. You create formDataToEmployee(..) method for converting MultiValueMap into Employee Data Model. Further ThymLeaf HTML template uses the Employee model to display the data entered by you using the Employee form.

9. Thymleaf - Templates

Please checkout the source code for two templates on GitHub. We are leaving them for brevity.

  1. input-employee-data-form.html
  2. display-employee-data.html

10. Summary

In this guide, we learned about the handling of content type ("application/x-www-form-urlencoded") by Spring WebFlux Functional. How the encoding and decoding of body contents are carried out? What are a few caveats?

The source code for this guide is available on GitHub.

11. References

  1. Official Spring WebFlux Functional documentation
  2. GitHub Source Code For This Guide
  3. BodyExtractors.toFormData() API documentation
  4. ServerRequest.formData() API documentation
  5. API Documentation For FormHttpMessageReader
  6. API Documentation For FormHttpMessageWriter
  7. ServerRequest.bodyToMono API documentation
  8. GitHub Source Code For This Guide

Comments

Popular posts from this blog

Working with request header in Jersey (JAX-RS) guide

In the  previous post , we talked about, how to get parameters and their values from the request query string. In this guide learn how to get request header values in Jersey (JAX-RS) based application. We had tested or used the following tools and technologies in this project: Jersey (v 2.21) Gradle Build System (v 2.9) Spring Boot (v 1.3) Java (v 1.8) Eclipse IDE This is a part of  Jersey (JAX-RS) Restful Web Services Development Guides series. Please read Jersey + Spring Boot getting started guide . Gradle Build File We are using Gradle for our build and dependency management (Using Maven rather than Gradle is a very trivial task). File: build.gradle buildscript { ext { springBootVersion = '1.3.0.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'java' apply plugin: 'eclipse' a

Ajax Cross Domain Resource Access Using jQuery

Some time back in our project we faced a problem while making an Ajax call using jQuery. Chrome Browser console had given some weird error message like below when we try to access one of our web pages: When we try to access the same web page in the Firefox browser, it doesn't give any error in the console but some parsing error occurred. In our case, we were accessing XML as an Ajax request resource. I was curious to check if the non-XML cross-domain resource was successfully loading or not. But finally, I realized that it is not going through. jersey-spring-boot-quick-starter-guide In our Ajax call, requesting domain was not the same as the requested URL domain. $.ajax({ url: "https://10.11.2.171:81/xxxxxx/xxxxxxx.xml" , type : "get" , success: function (response) { alert( "Load was performed." ); }, error : function (xhr, status) {

FastAPI first shot

Setup on my Mac (Macbook Pro 15 inch Retina, Mid 2014) Prerequisite Python 3.6+ (I used 3.7.x. I recently reinstalled OS after cleaning up disk, where stock Python 2.7 was available. I installed Pyenv and then used it to install 3.7.x). I already had a git repo initialized at Github for this project. I checked that out. I use this approach to keep all the source code safe or at a specific place 😀. I set the Python version in .python-version file. I also initialize the virtual environment using pyenv in venv folder. I started the virtual environment. FastAPI specific dependencies setup Now I started with basic pip commands to install dependency for the project. I saved dependencies in requirements.txt  the file. Minimal viable code to spin an API Server FastAPI is as cool as NodeJS or Go Lang (?) to demonstrate the ability to spin an API endpoint up and running in no time. I had the same feeling for the Flask too, which was also super cool. app/main.py: from typing i