Understand how Spring form tags bind data to Java objects, what @Valid and BindingResult actually do, how to create custom validators, and how @ExceptionHandler and @ControllerAdvice handle errors globally — all in plain English.
Spring Form Taglib@ModelAttribute Binding@Valid & BindingResultCustom Validators@ExceptionHandler@ControllerAdviceInterview Q&A
Spring MVC Form Taglib — Why Not Plain HTML Forms?
“Spring's form tags do magic that plain HTML cannot”
🧩 Smart Form Analogy
Regular HTML forms are “blind” — they just send raw text. Spring MVC form tags are “intelligent” — they know about your Java objects. They automatically populate form fields with existing Java object values (great for edit forms), and they automatically bind submitted values back to the Java object. They also know how to show validation error messages next to the right fields.
Plain HTML form (basic)
<formaction="/save"method="post"><inputtype="text"name="employeeName"/><!-- No auto-population, no binding, no error display --></form>
🔑 The key attribute is modelAttribute. This tells the form which Java object in the Model to bind to. Every form field uses path to specify which field of that object to bind to. The attribute name must match what the controller puts in the Model.
“Spring fills your Java object from the form automatically”
🧲 Stamp Machine Analogy
Imagine a form with 10 fields. Without @ModelAttribute, you'd write request.getParameter("name") for each field, then manually set each value on the object. @ModelAttribute is like a stamp machine — it reads all form fields, matches them to object fields by name, and fills the entire object automatically in one step.
@ModelAttribute on method parameter
When used on a parameter in a controller method, Spring automatically creates the object, reads all HTTP request parameters, and sets matching fields (by name).
// Form field name="employeeName" → emp.setEmployeeName()@PostMapping("/save")
publicStringsave(@ModelAttributeEmployee emp) {
// emp is already populated from form fields!
service.save(emp);
return"success";
}
@ModelAttribute on a method (pre-populating model)
When used on a method in the controller class, that method runs BEFORE every handler method in the class. Used to pre-load common data like dropdown options.
@ModelAttribute("countries")
publicList<String> loadCountries() {
returnArrays.asList("India", "USA", "UK");
// model.get("countries") is available in every view
}
🚨 The modelAttribute and @ModelAttribute Connection
The modelAttribute="employee" on <form:form> must match the attribute name put in the Model by the controller for the GET request. If the controller does model.addAttribute("employee", new Employee()), then modelAttribute must be "employee". Case-sensitive!
03 — Static Form Tags
Static UI Components — Text, Password, Textarea, Checkbox, Radio
“Form fields where the options don't change”
Tag
HTML Equivalent
Use Case
<form:input path="name"/>
<input type="text">
Text input fields
<form:password path="pwd"/>
<input type="password">
Password field (never pre-filled)
<form:textarea path="address"/>
<textarea>
Multi-line text
<form:checkbox path="agree"/>
<input type="checkbox">
Single boolean checkbox
<form:radiobutton path="gender" value="M"/>
<input type="radio">
Single radio button option
<form:errors path="name"/>
No HTML equivalent
Displays validation error for the field
🔑 path attribute = Java object field name. Spring reads the field value to pre-fill the control (GET request), and writes the submitted value back to the field (POST request). This is two-way binding.
“Form fields where the options come from a Java Collection”
📷 Dynamic vs Static Analogy
Static: You hardcode the options in JSP. Dynamic: The options come from a Java List, Map, or Array loaded by the controller (e.g., list of countries from a database). Spring's dynamic form tags automatically render all items in the collection as HTML options.
<!-- Controller: puts a list in model -->
model.addAttribute("countries", Arrays.asList("India", "USA", "UK"));
<!-- JSP: renders a dropdown from that list --><form:selectpath="country"items="${countries}"/><!-- Renders multiple checkboxes from a list --><form:checkboxespath="hobbies"items="${hobbyList}"/><!-- Renders multiple radio buttons from a list --><form:radiobuttonspath="gender"items="${genderOptions}"/>
Items attribute with a Map
When items is a Map, the Map KEY becomes the option VALUE sent to server, and the Map VALUE becomes the visible text shown to user. Perfect for coded dropdowns (e.g., "IN" → "India").
<form:checkbox> = single checkbox bound to a boolean field. <form:checkboxes> = multiple checkboxes generated from a collection, bound to a List field. Submitted values are collected into a List automatically.
05 — Why Validation?
Why Do We Need Server-Side Validation?
“Client-side validation is not enough”
👥 Border Security Analogy
Client-side validation (JavaScript in browser) is like airport security at departure. Important, but a determined person can bypass it (just disable JavaScript). Server-side validation is like immigration at the destination airport — it's the real checkpoint. Even if someone bypasses the browser check and sends a raw POST request with garbage data, server-side validation catches it.
Client-side only (Bad)
Can be bypassed by disabling JavaScript
Can be bypassed by sending raw HTTP requests (Postman, curl)
Cannot access database to check business rules
Server-side validation (Essential)
Cannot be bypassed — always runs on server
Can access database, session, application context
Can enforce complex business rules (e.g., username must be unique)
06 — Bean Validation
Bean Validation Annotations on Your Entity/DTO
“Declare validation rules directly on your Java fields”
@NotNull(message = "Name cannot be null")
@NotEmpty(message = "Name cannot be empty")
@Size(min = 2, max = 50, message = "Name must be 2-50 characters")
privateString employeeName;
@Min(value = 18, message = "Age must be at least 18")
@Max(value = 65, message = "Age must not exceed 65")
private int age;
@Email(message = "Must be a valid email")
privateString email;
@Pattern(regexp = "[A-Z]{3}[0-9]{3}", message = "Must be like ABC123")
privateString employeeCode;
@Past(message = "Joining date must be in the past")
privateDate joiningDate;
Annotation
What it checks
Applies to
@NotNull
Value is not null
Any type
@NotEmpty
Not null AND not empty string/collection
String, Collection
@NotBlank
Not null AND not whitespace-only
String
@Size(min,max)
Length within range
String, Collection
@Min(value)
Number ≥ value
int, long, etc.
@Max(value)
Number ≤ value
int, long, etc.
@Email
Valid email format
String
@Pattern(regexp)
Matches regex pattern
String
@Past
Date is in the past
Date, LocalDate
@Future
Date is in the future
Date, LocalDate
🚨 @NotEmpty vs @NotBlank vs @NotNull
@NotNull: Passes for "" (empty string). @NotEmpty: Fails for null AND "". @NotBlank: Fails for null, "", AND " " (spaces only). For form fields, @NotBlank is usually what you want for String fields.
07 — Triggering Validation
@Valid and BindingResult — How Validation Is Triggered
“The two annotations that make validation actually work”
✅ Customs & Report Analogy
@Valid is like telling customs: “Please inspect this luggage.” The inspection (validation) runs. BindingResult is like the customs report — it collects all violations found. You then check the report: if there are violations, send the passenger back. If clean, let them through.
@PostMapping("/saveEmployee")
publicStringsaveEmployee(
@Valid@ModelAttributeEmployee employee, // @Valid triggers validationBindingResult result, // collects errors (must be immediately after!)Model model) {
if (result.hasErrors()) {
// Validation failed! Redisplay form with error messagesreturn"employeeForm"; // <form:errors> in JSP will show errors
}
// Validation passed! Proceed with business logic
service.saveEmployee(employee);
return"success";
}
🚨 Critical Rule: BindingResult must be IMMEDIATELY after the @Valid object
If any other parameter appears between the @Valid @ModelAttribute parameter and BindingResult, Spring throws a BindException and the method never executes. Always keep them adjacent: @Valid Employee emp, BindingResult result.
What BindingResult provides
result.hasErrors() — true if any validation failed
result.getFieldErrors() — List of all field-level errors
result.getFieldError("fieldName") — Error for a specific field
result.getErrorCount() — Total number of errors
Showing errors in JSP
<form:inputpath="employeeName"/><!-- Shows validation error for employeeName field --><form:errorspath="employeeName"cssClass="error-msg"/><!-- Show all errors at once --><form:errorspath="*"/>
08 — Custom Validators
Custom Validation — Two Approaches
“When built-in annotations aren't enough”
Approach 1: Spring Validator Interface
Implement org.springframework.validation.Validator. Write validation logic in Java code. Register it in the controller and call it manually (or with @InitBinder).
@Componentpublic classEmployeeValidatorimplementsValidator {
public booleansupports(Class<?> cls) {
returnEmployee.class.equals(cls);
}
public voidvalidate(Object target, Errors errors) {
Employee emp = (Employee) target;
if (emp.getSalary() < 10000) {
errors.rejectValue("salary", "min.salary",
"Salary must be at least 10000");
}
}
}
Approach 2: Custom Constraint Annotation
Create your own annotation (like @NotBlank or @Email) backed by a ConstraintValidator class. More reusable — you can apply it to any field in any class.
// 1. Create the annotation@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SalaryValidator.class)
public @interfaceValidSalary {
Stringmessage() default"Invalid salary";
Class[] groups() default {};
Class[] payload() default {};
}
// 2. Create the validator classpublic classSalaryValidatorimplementsConstraintValidator<ValidSalary, Double> {
public booleanisValid(Double salary, ConstraintValidatorContext ctx) {
return salary != null && salary >= 10000;
}
}
// 3. Use it just like built-in annotations:@ValidSalaryprivateDouble salary;
09 — Exception Handling
@ExceptionHandler — Handling Errors in the Controller
“Catch exceptions and show friendly error pages instead of stack traces”
🥞 Safety Net Analogy
Without exception handling, when an error occurs the user sees an ugly Java stack trace. @ExceptionHandler is like a safety net below your controller. When any exception is thrown and not caught, it falls through to the @ExceptionHandler method, which gracefully shows a user-friendly error page.
@Controllerpublic classEmployeeController {
@GetMapping("/employee/{id}")
publicStringgetEmployee(@PathVariableint id, Model model) {
Employee emp = service.findById(id); // may throw EmployeeNotFoundException
model.addAttribute("employee", emp);
return"employeeDetail";
}
// This method catches EmployeeNotFoundException from ANY method in THIS controller@ExceptionHandler(EmployeeNotFoundException.class)
publicStringhandleNotFound(EmployeeNotFoundException ex, Model model) {
model.addAttribute("message", ex.getMessage());
return"error404"; // show a friendly error page
}
}
🚨 Limitation of @ExceptionHandler inside a Controller
@ExceptionHandler inside a controller class only handles exceptions thrown by that controller. If the same exception occurs in a different controller, it won't be caught. That's why @ControllerAdvice was created — for global exception handling.
10 — Global Exception Handling
@ControllerAdvice — Global Exception Handling for All Controllers
“One class to handle exceptions from ALL controllers”
🏢 Company HR Analogy
@ExceptionHandler inside a controller = each department handles its own problems. @ControllerAdvice = the HR department that handles problems from ALL departments. Any unhandled exception from any controller flows to @ControllerAdvice methods. One place to define all error handling logic.
@ControllerAdvice// Applies to ALL controllers in the applicationpublic classGlobalExceptionHandler {
// Handles EmployeeNotFoundException from any controller@ExceptionHandler(EmployeeNotFoundException.class)
publicModelAndViewhandleNotFound(EmployeeNotFoundException ex) {
ModelAndView mav = newModelAndView("error/notFound");
mav.addObject("message", ex.getMessage());
return mav;
}
// Generic fallback handler for any other exception@ExceptionHandler(Exception.class)
publicModelAndViewhandleGeneral(Exception ex) {
ModelAndView mav = newModelAndView("error/general");
mav.addObject("error", "An unexpected error occurred");
return mav;
}
}
What @ControllerAdvice can contain
@ExceptionHandler methods — Global exception handlers
@InitBinder methods — Custom data binding for all controllers
@ModelAttribute methods — Data added to every model in every controller
Scoping @ControllerAdvice
By default, @ControllerAdvice applies to ALL controllers. You can restrict it:
// Only for controllers in this package@ControllerAdvice("com.acc.controller")
// Only for specific controllers@ControllerAdvice(assignableTypes = {EmpController.class})
🔑 Best practice: Always have one @ControllerAdvice with a generic @ExceptionHandler(Exception.class) as a last resort. This prevents users from ever seeing raw stack traces. More specific exception handlers are checked first, then the generic one is the fallback.
11 — Interview Prep
Top Interview Questions with Answers
“Questions about forms, validation, and exception handling”
Why use Spring MVC form tags instead of plain HTML form tags?
Spring form tags (form:form, form:input, etc.) support two-way data binding. They automatically populate form fields from a bound Java object (great for edit forms) and automatically bind submitted values back to the object. They also display field-specific validation error messages via form:errors. Plain HTML forms cannot do any of this automatically.
What is the modelAttribute attribute on form:form?
It specifies the name of the Model attribute (Java object) that the form is bound to. The path attributes on each form field refer to fields of this bound object. If you set modelAttribute="employee", Spring will pre-populate fields from model.getAttribute("employee") and bind submitted values back to it.
How does @Valid and BindingResult work?
@Valid on the @ModelAttribute parameter triggers Bean Validation (runs all @NotNull, @Size, etc. constraints on the object). BindingResult, placed immediately after the @Valid parameter, collects all validation errors. You check result.hasErrors() — if true, return the form view (errors are shown via form:errors). If false, proceed with business logic.
What happens if BindingResult is not placed immediately after the @Valid parameter?
Spring throws a BindException and the controller method never executes. BindingResult MUST be the very next parameter after the @Valid @ModelAttribute parameter. No other parameter can be between them.
What is the difference between @ExceptionHandler and @ControllerAdvice?
@ExceptionHandler in a controller handles exceptions only from THAT controller. @ControllerAdvice is a class-level annotation that applies @ExceptionHandler methods globally to ALL controllers. Use @ControllerAdvice for application-wide error handling to avoid repeating exception handling in every controller.
How do you create a custom validation annotation?
1) Create an annotation with @Constraint(validatedBy = YourValidator.class). 2) Create YourValidator implementing ConstraintValidator<YourAnnotation, FieldType> with an isValid() method. 3) Apply your annotation to any field. Spring auto-discovers and runs it when @Valid is triggered.
What is the difference between @NotNull, @NotEmpty, and @NotBlank?
@NotNull: fails only if value is null. @NotEmpty: fails if null OR empty string/collection. @NotBlank: fails if null, empty string, or whitespace-only string. For form text fields, @NotBlank is usually the right choice since users can submit spaces.
What does form:errors do?
form:errors displays validation error messages from BindingResult for a specific field (path="fieldName") or all fields (path="*"). When @Valid fails and you return to the form view, Spring automatically transfers the errors from BindingResult to the model, and form:errors reads them and renders the error messages next to the appropriate form fields.