Spring MVC Form Validator Guide

When we accept user inputs in any web application, it becomes necessary to validate them. We can validate the user input at the client-side using JavaScript but it’s also necessary to validate them at server side to make sure we are processing valid data in case the user has javascript disabled.

Validation with Spring MVC Form Validator

Spring MVC Framework supports JSR-303 specs by default and all we need is to add JSR-303 and it’s implementation dependencies in Spring MVC application. Spring also provides @Validator annotation and BindingResult class through which we can get the errors raised by Validator implementation in the controller request handler method. We can create our custom validator implementations in two ways – the first one is to create an annotation that confirms to the JSR-303 specs and implement its Validator class. Second approach is to implement the org.springframework.validation.Validator interface and add set it as validator in the Controller class using @InitBinder annotation. Let’s create a simple Spring MVC project in Spring Tool Suite where we will use JSR-303 specs with it’s implementation artifact hibernate-validator. We will use annotation based form validation and create our own custom validator based on JSR-303 specs standards. We will also create our own custom validator class by implementing Validator interface and use it in one of the controller handler methods.

Spring MVC Form Validator

Our final pom.xml file looks like below. Apart from standard Spring MVC artifacts, we have validation-api and hibernate-validator dependencies in the project.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.journaldev</groupId>
	<artifactId>spring</artifactId>
	<name>SpringFormValidation</name>
	<packaging>war</packaging>
	<version>1.0.0-BUILD-SNAPSHOT</version>
	<properties>
		<java-version>1.7</java-version>
		<org.springframework-version>4.0.2.RELEASE</org.springframework-version>
		<org.aspectj-version>1.7.4</org.aspectj-version>
		<org.slf4j-version>1.7.5</org.slf4j-version>
	</properties>
	<dependencies>
	<!-- Form Validation using Annotations -->  
		<dependency>
			<groupId>javax.validation</groupId>
			<artifactId>validation-api</artifactId>
			<version>1.1.0.Final</version>
		</dependency>
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-validator</artifactId>
			<version>4.1.0.Final</version>
		</dependency>
		
		<!-- Spring -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${org.springframework-version}</version>
			<exclusions>
				<!-- Exclude Commons Logging in favor of SLF4j -->
				<exclusion>
					<groupId>commons-logging</groupId>
					<artifactId>commons-logging</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>

		<!-- AspectJ -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjrt</artifactId>
			<version>${org.aspectj-version}</version>
		</dependency>

		<!-- Logging -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>${org.slf4j-version}</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>jcl-over-slf4j</artifactId>
			<version>${org.slf4j-version}</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>${org.slf4j-version}</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>log4j</groupId>
			<artifactId>log4j</artifactId>
			<version>1.2.15</version>
			<exclusions>
				<exclusion>
					<groupId>javax.mail</groupId>
					<artifactId>mail</artifactId>
				</exclusion>
				<exclusion>
					<groupId>javax.jms</groupId>
					<artifactId>jms</artifactId>
				</exclusion>
				<exclusion>
					<groupId>com.sun.jdmk</groupId>
					<artifactId>jmxtools</artifactId>
				</exclusion>
				<exclusion>
					<groupId>com.sun.jmx</groupId>
					<artifactId>jmxri</artifactId>
				</exclusion>
			</exclusions>
			<scope>runtime</scope>
		</dependency>

		<!-- @Inject -->
		<dependency>
			<groupId>javax.inject</groupId>
			<artifactId>javax.inject</artifactId>
			<version>1</version>
		</dependency>

		<!-- Servlet -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
			<version>2.5</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>jsp-api</artifactId>
			<version>2.1</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
			<version>1.2</version>
		</dependency>

		<!-- Test -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.7</version>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<artifactId>maven-eclipse-plugin</artifactId>
				<version>2.9</version>
				<configuration>
					<additionalProjectnatures>
						<projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
					</additionalProjectnatures>
					<additionalBuildcommands>
						<buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
					</additionalBuildcommands>
					<downloadSources>true</downloadSources>
					<downloadJavadocs>true</downloadJavadocs>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>2.5.1</version>
				<configuration>
					<source>1.6</source>
					<target>1.6</target>
					<compilerArgument>-Xlint:all</compilerArgument>
					<showWarnings>true</showWarnings>
					<showDeprecation>true</showDeprecation>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>exec-maven-plugin</artifactId>
				<version>1.2.1</version>
				<configuration>
					<mainClass>org.test.int1.Main</mainClass>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

</pre>

Deployment Descriptor

When createing a Spring MVC project from STS, it creates two context configuration files. We have cleaned it up a bit and have only one spring bean configuration file. Our final web.xml file looks like below:

 

        <?xml version="1.0" encoding="UTF-8"?>
        <web-app version="2.5" xmlns="https://java.sun.com/xml/ns/javaee"
        xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
        
        <!-- Processes application requests -->
        <servlet>
            <servlet-name>appServlet</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>/WEB-INF/spring/spring.xml</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
        </servlet>
            
        <servlet-mapping>
            <servlet-name>appServlet</servlet-name>
            <url-pattern>/</url-pattern>
        </servlet-mapping>

        </web-app>

Spring Bean Configuration File

Usually we look into spring wirings at the last, but this time we don’t have much configurations in the spring bean configuration file. Our final spring.xml file looks like below.

        <?xml version="1.0" encoding="UTF-8"?>
        <beans:beans xmlns="https://www.springframework.org/schema/mvc"
        xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
        xmlns:beans="https://www.springframework.org/schema/beans"
        xmlns:context="https://www.springframework.org/schema/context"
        xsi:schemaLocation="https://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
            https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
            https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

        <!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
        
        <!-- Enables the Spring MVC @Controller programming model -->
        <annotation-driven />

        <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
        <resources mapping="/resources/**" location="/resources/" />

        <!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
        <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <beans:property name="prefix" value="/WEB-INF/views/" />
            <beans:property name="suffix" value=".jsp" />
        </beans:bean>
        
        <beans:bean id="employeeValidator" class="com.journaldev.spring.form.validator.EmployeeFormValidator" />
        
        <beans:bean id="messageSource"
            class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
            <beans:property name="basename" value="classpath:message" />
            <beans:property name="defaultEncoding" value="UTF-8" />
        </beans:bean>
        
        <context:component-scan base-package="com.journaldev.spring" />
        
        </beans:beans>


The only important point to note are employeeValidator bean that we will inject into one of the controller and messageSource bean to read the localized data from resource bundles. Rest of the part is to support annotations, view resolvers and providing package to scan for Controller classes and other components.

Model Classes

Customer Model Class

        package com.journaldev.spring.form.model;

        import java.util.Date;
        import javax.validation.constraints.Max;
        import javax.validation.constraints.Min;
        import javax.validation.constraints.NotNull;
        import javax.validation.constraints.Past;
        import javax.validation.constraints.Size;
        import org.hibernate.validator.constraints.Email;
        import org.hibernate.validator.constraints.NotEmpty;
        import org.springframework.format.annotation.DateTimeFormat;
        import com.journaldev.spring.form.validator.Phone;

        public class Customer {
            @Size(min=2, max=30) 
            private String name;
             
            @NotEmpty @Email
            private String email;
             
            @NotNull @Min(18) @Max(100)
            private Integer age;
             
            @NotNull
            private Gender gender;
             
            @DateTimeFormat(pattern="MM/dd/yyyy")
            @NotNull @Past
            private Date birthday;
            
            @Phone
            private String phone;
            
            public enum Gender {
                MALE, FEMALE
            }

            // Getters and Setters
        }

Employee Model Class

        package com.journaldev.spring.form.model;

        public class Employee {
            private int id;
            private String name;
            private String role;

            // Getters and Setters
        }

Custom Validator Implementations

Phone Validator


        package com.journaldev.spring.form.validator;

        import java.lang.annotation.Documented;
        import java.lang.annotation.Retention;
        import java.lang.annotation.Target;
        import java.lang.annotation.ElementType;
        import java.lang.annotation.RetentionPolicy;
        import javax.validation.Constraint;
        import javax.validation.Payload;

        @Documented
        @Constraint(validatedBy = PhoneValidator.class)
        @Target({ ElementType.METHOD, ElementType.FIELD })
        @Retention(RetentionPolicy.RUNTIME)
        public @interface Phone {
            String message() default "{Phone}";
            Class<?>[] groups() default {};
            Class<? extends Payload>[] payload() default {};
        }


        package com.journaldev.spring.form.validator;

        import javax.validation.ConstraintValidator;
        import javax.validation.ConstraintValidatorContext;

        public class PhoneValidator implements ConstraintValidator<Phone, String> {
            @Override
            public void initialize(Phone paramA) {
            }

            @Override
            public boolean isValid(String phoneNo, ConstraintValidatorContext ctx) {
                if (phoneNo == null) {
                    return false;
                }
                // validate phone numbers of format "1234567890"
                if (phoneNo.matches("\\d{10}")) return true;
                // validating phone number with -, . or spaces
                else if (phoneNo.matches("\\d{3}[-\\.\\s]\\d{3}[-\\.\\s]\\d{4}")) return true;
                // validating phone number with extension length from 3 to 5
                else if (phoneNo.matches("\\d{3}-\\d{3}-\\d{4}\\s(x|(ext))\\d{3,5}")) return true;
                // validating phone number where area code is in braces ()
                else if (phoneNo.matches("\\(\\d{3}\\)-\\d{3}-\\d{4}")) return true;
                // return false if nothing matches the input
                else return false;
            }
        }


        package com.journaldev.spring.form.validator;

        import org.springframework.validation.Errors;
        import org.springframework.validation.ValidationUtils;
        import org.springframework.validation.Validator;
        import com.journaldev.spring.form.model.Employee;

        public class EmployeeFormValidator implements Validator {
            @Override
            public boolean supports(Class<?> paramClass) {
                return Employee.class.equals(paramClass);
            }

            @Override
            public void validate(Object obj, Errors errors) {
                ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id", "id.required");
                Employee emp = (Employee) obj;
                if (emp.getId() <= 0) {
                    errors.rejectValue("id", "negativeValue", new Object[] { "'id'" }, "id can't be negative");
                }
                ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "name.required");
                ValidationUtils.rejectIfEmptyOrWhitespace(errors, "role", "role.required");
            }
        }

Controller Classes

CustomerController Class

        package com.journaldev.spring.form.controllers;

        import java.util.HashMap;
        import java.util.Map;
        import javax.validation.Valid;
        import org.slf4j.Logger;
        import org.slf4j.LoggerFactory;
        import org.springframework.stereotype.Controller;
        import org.springframework.ui.Model;
        import org.springframework.validation.BindingResult;
        import org.springframework.web.bind.annotation.RequestMapping;
        import org.springframework.web.bind.annotation.RequestMethod;
        import com.journaldev.spring.form.model.Customer;

        @Controller
        public class CustomerController {
            private static final Logger logger = LoggerFactory.getLogger(CustomerController.class);
            private Map<String, Customer> customers = null;

            public CustomerController() {
                customers = new HashMap<String, Customer>();
            }

            @RequestMapping(value = "/cust/save", method = RequestMethod.GET)
            public String saveCustomerPage(Model model) {
                logger.info("Returning custSave.jsp page");
                model.addAttribute("customer", new Customer());
                return "custSave";
            }

            @RequestMapping(value = "/cust/save.do", method = RequestMethod.POST)
            public String saveCustomerAction(@Valid Customer customer, BindingResult bindingResult, Model model) {
                if (bindingResult.hasErrors()) {
                    logger.info("Returning custSave.jsp page");
                    return "custSave";
                }
                logger.info("Returning custSaveSuccess.jsp page");
                model.addAttribute("customer", customer);
                customers.put(customer.getEmail(), customer);
                return "custSaveSuccess";
            }
        }

EmployeeController Class


        package com.journaldev.spring.form.controllers;

        import java.util.HashMap;
        import java.util.Map;
        import org.slf4j.Logger;
        import org.slf4j.LoggerFactory;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.beans.factory.annotation.Qualifier;
        import org.springframework.stereotype.Controller;
        import org.springframework.ui.Model;
        import org.springframework.validation.BindingResult;
        import org.springframework.validation.Validator;
        import org.springframework.validation.annotation.Validated;
        import org.springframework.web.bind.WebDataBinder;
        import org.springframework.web.bind.annotation.InitBinder;
        import org.springframework.web.bind.annotation.ModelAttribute;
        import org.springframework.web.bind.annotation.RequestMapping;
        import org.springframework.web.bind.annotation.RequestMethod;
        import com.journaldev.spring.form.model.Employee;

        @Controller
        public class EmployeeController {
            private static final Logger logger = LoggerFactory.getLogger(EmployeeController.class);
            private Map<Integer, Employee> emps = null;

            @Autowired
            @Qualifier("employeeValidator")
            private Validator validator;

            @InitBinder
            private void initBinder(WebDataBinder binder) {
                binder.setValidator(validator);
            }

            public EmployeeController() {
                emps = new HashMap<Integer, Employee>();
            }

            @ModelAttribute("employee")
            public Employee createEmployeeModel() {
                return new Employee();
            }

            @RequestMapping(value = "/emp/save", method = RequestMethod.GET)
            public String saveEmployeePage(Model model) {
                logger.info("Returning empSave.jsp page");
                return "empSave";
            }

            @RequestMapping(value = "/emp/save.do", method = RequestMethod.POST)
            public String saveEmployeeAction(@ModelAttribute("employee") @Validated Employee employee, BindingResult bindingResult, Model model) {
                if (bindingResult.hasErrors()) {
                    logger.info("Returning empSave.jsp page");
                    return "empSave";
                }
                logger.info("Returning empSaveSuccess.jsp page");
                model.addAttribute("emp", employee);
                emps.put(employee.getId(), employee);
                return "empSaveSuccess";
            }
        }


Form Validation Error Messages Resource Bundle

It’s time to look at our resource bundle where we have different types of messages to be used for validation errors. message_en.properties file:

        #application defined error messsages
        id.required=Employee ID is required
        name.required=Employee Name is required
        role.required=Employee Role is required
        negativeValue={0} can't be negative or zero

        #Spring framework error messages to be used when conversion from form data to bean fails
        typeMismatch.int={0} Value must be an integer
        typeMismatch.java.lang.Integer={0} must be an integer
        typeMismatch={0} is of invalid format

        #application messages for annotations, {ValidationClass}.{modelObjectName}.{field}
        #the {0} is field name, other fields are in alphabatical order, max and then min  
        Size.customer.name=Customer {0} should be between {2} and {1} characters long
        NotEmpty.customer.email=Email is a required field
        NotNull.customer.age=Customer {0} should be in years

        #Generic annotation class messages
        Email=Email address is not valid
        NotNull=This is a required field
        NotEmpty=This is a required field
        Past=Date should be Past

        #Custom validation annotation
        Phone=Invalid format, valid formats are 1234567890, 123-456-7890 x1234

View Pages with Form and Errors

Customer Save Page (custSave.jsp)

        <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
        <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://www.w3.org/TR/html4/loose.dtd">
        <%@ taglib uri="https://www.springframework.org/tags/form" prefix="springForm"%>
        <html>
        <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Customer Save Page</title>
        <style>
        .error {
            color: #ff0000;
            font-style: italic;
            font-weight: bold;
        }
        </style>
        </head>
        <body>

        <springForm:form method="POST" commandName="customer" action="save.do">
            <table>
                <!-- Form Fields -->
            </table>
        </springForm:form>

        </body>
        </html>

Customer Save Success Page (custSaveSuccess.jsp)

        <%@ page session="false" %>
        <html>
        <head>
            <title>Customer Saved Successfully</title>
        </head>
        <body>
        <h3>
            Customer Saved Successfully.
        </h3>

        <!-- Display Customer Details -->

        </body>
        </html>

Employee Save Page (empSave.jsp)

        <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
        <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "https://www.w3.org/TR/html4/loose.dtd">
        <%@ taglib uri="https://www.springframework.org/tags/form" prefix="springForm"%>
        <html>
        <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Employee Save Page</title>
        <style>
        .error {
            color: #ff0000;
            font-style: italic;
            font-weight: bold;
        }
        </style>
        </head>
        <body>

        <springForm:form method="POST" commandName="employee" action="save.do">
            <table>
                <!-- Form Fields -->
            </table>
        </springForm:form>

        </body>
        </html>

Employee Save Success Page (empSaveSuccess.jsp)


        <%@ page session="false" %>
        <html>
        <head>
            <title>Employee Saved Successfully</title>
        </head>
        <body>
        <h3>
            Employee Saved Successfully.
        </h3>

        <!-- Display Employee Details -->

        </body>
        </html>


Test the Spring MVC Form Validation Application

Our application is ready to deploy and run some tests, deploy it in your favorite servlet container. We are using Apache Tomcat 7.

Conclusion

That’s all for Spring MVC Form validation with different ways and using resource bundles for localized error messages – Validator Guide

Create a Free Account

Register now and get access to our Cloud Services.

Posts you might be interested in:

centron Managed Cloud Hosting in Deutschland

JSP Exception Handling – Tutorial

Apache
JSP Exception Handling – Tutorial To handle exceptions thrown by the JSP page, all we need is an error page and define the error page in JSP using jsp page…
centron Managed Cloud Hosting in Deutschland

JUnit Setup Maven – Tutorial

Apache
JUnit 4 and JUnit 5 in Maven Projects JUnit 4 and JUnit 5 are completely different frameworks. They both serve the same purpose, but the JUnit 5 is a completely…
centron Managed Cloud Hosting in Deutschland

JUnit HTML Report – Tutorial

Apache
JUnit HTML Report – Tutorial When we configure maven-surefire-plugin to run our JUnit tests, it generates surefire-reports directory. This directory contains a txt file and an XML file for every…