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

Extend and reuse an existing AirByte destination connector

AirByte is an open-source ELT (Extract, Load, and Transformation) application. It heavily uses containerization for the deployment of its various components. On the local machine, we need docker to run it. AirByte has an impressive list of source and destination connectors available. One of my use case data destinations is the  ClickHouse data warehouse and its destination connector is not yet (2021-12-08) available. As per the documentation, It seems that creating a destination connector is a non-trivial job. It's a great idea to build an open-source ClickHouse destination connector. However, I tried avoiding the temptation to create one because of the required effort. AirByte has a  MySql destination connector available. ClickHouse provides a MySQL connector for access from any MySQL client. We need to configure Clickhouse to give support for the MySQL connector. Accessing ClickHouse from AirByte using its MySQL destination connector looks promising. However, when ...

Understanding Type Checking

A few examples of types in the context of programming language can be integer, float, character, string, array, etc.  When a program executes then data flow between instructions and values of specific types are assigned to a variable after some operation. It's important for the system to verify if the correct types are used as operands in operations. For e.g. In a sum operation, the expectation for operands to be of numeric type. The program's execution should fail in the case there is inconsistency. We can classify programming languages into two categories based as per their ability to cater to type safety: Dynamically Typed Language Statically Typed Language

Setting Clickhouse column data warehouse at Google Cloud Compute Engine VM

I didn't have a Google Cloud account associated with my email, so I signed up for one. It needs a valid Credit Card and mobile number to check if you are human. On successful sign up I get 300$ to spend within 3 months. Creating a free forever Google Cloud Compute Engine VM As per Google Cloud documentation you can have 1 non-preemptible e2-micro VM instance (1GB 2vCPU, 30GB Disk, etc.) per month free forever in some regions with some restrictions. I wanted the following stuff in my VM before I can install Clickhouse on to that: Ubuntu 20.x LTS SSH access from my machine Enabling SSH-based access to Google Compute Engine VM Step 1 Created an ssh private and public key on my mac using the following command ssh-keygen -t rsa -f ~/.ssh/gcloud-ssh-key -C mrityunjay -b 2048 Step 2 Copied the public key from the console using the following command: cat ~/.ssh/gcloud-ssh-key.pub output ssh-rsa <Gibrish :)> mrityunjay Step 3 I went to Google Cloud Console > Co...