Overview
Spring MVC is the request-handling layer core Spring provides for building server-rendered web applications, and it existed years before Spring Boot made most of its setup invisible. This project deliberately does it the pre-Boot way: no starter dependency silently registers a `DispatcherServlet` for you, no auto-configuration guesses which view technology you want. You will register the `DispatcherServlet` yourself, enable Spring MVC yourself with `@EnableWebMvc`, and configure a view resolver yourself — every decision Spring Boot's `spring-boot-starter-web` would otherwise make on your behalf happens here, in your own code, so you can see exactly what it was doing.
The application itself is a student registration form: a `GET /register` request shows an empty form, a `POST /register` submits it, and JSR-380 annotations (`@NotBlank`, `@Email`) on the form-backing object are enforced automatically the moment the handler method is annotated `@Valid`. Views are rendered as JSP — the classic Spring MVC pairing from before Thymeleaf became Spring Boot's default, chosen here because it needs no extra template-engine dependency: any Servlet container already knows how to compile and serve a `.jsp` file.
- A `WebApplicationInitializer` that registers `DispatcherServlet` in pure Java, with no `web.xml`.
- A `WebConfig` class enabling Spring MVC with `@EnableWebMvc` and configuring a JSP `InternalResourceViewResolver`.
- A `Student` form-backing object validated with JSR-380 annotations (`@NotBlank`, `@Email`, `@Size`).
- A `StudentController` with a `@GetMapping` handler that shows the form and a `@PostMapping` handler that validates and processes it.
- `@Valid` + `BindingResult` re-rendering the form with inline error messages when validation fails.
- Two JSP views — `register.jsp` and `success.jsp` — using Spring's `<form:>` tag library and JSTL.
Prerequisites
- Project 1 of this course (beans, `@Configuration`, `@Bean`, constructor injection) — this project builds directly on those ideas.
- Basic HTTP — GET vs. POST, request parameters, and how a browser submits an HTML form.
- Basic JSP/JSTL syntax — `<%@ taglib %>` directives and `${}` expression language, or willingness to follow along with heavy comments.
- Maven, and packaging a Java web application as a `.war` file.
- A local Servlet container to deploy to, such as Apache Tomcat 9 or 10.
Project Structure
This project is a Maven `war`-packaged web application, not a single runnable `.java` file — Spring MVC needs a Servlet container (Tomcat, Jetty) to actually receive HTTP requests and hand them to `DispatcherServlet`. Java classes live under `src/main/java/com/programinds/registration/`, and the two JSP views live under `src/main/webapp/WEB-INF/views/` — placing them inside `WEB-INF` is deliberate: files there can only be reached through a controller's view resolution, never by a visitor typing the `.jsp` URL directly into a browser.
`RegistrationWebAppInitializer` replaces `web.xml` and registers `DispatcherServlet`. `WebConfig` is the `@Configuration` class `DispatcherServlet` itself is built from, turning on Spring MVC's request-mapping and validation machinery with `@EnableWebMvc` and declaring the JSP view resolver. `Student` is the form-backing object, `StudentStore` is a minimal in-memory stand-in for a database, and `StudentController` is the one `@Controller` handling both the `GET` (show the form) and `POST` (process the submission) requests to `/register`.
<dependencies> <!-- spring-webmvc pulls in the DispatcherServlet, @Controller support, and everything @EnableWebMvc actually activates in Step 3. --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>6.1.13</version> </dependency>
<!-- Provided by the Servlet container at runtime, so scope="provided" keeps it off the deployed .war's own classpath to avoid a conflict. --> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.0.0</version> <scope>provided</scope> </dependency>
<!-- Hibernate Validator is a JSR-380 (Bean Validation) IMPLEMENTATION. spring-webmvc only defines the @Valid CONTRACT — without a provider like this on the classpath, @NotBlank/@Email annotations are silently ignored. --> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>8.0.1.Final</version> </dependency>
<!-- JSTL, for the <c:forEach> tag used in success.jsp (Step 7). --> <dependency> <groupId>jakarta.servlet.jsp.jstl</groupId> <artifactId>jakarta.servlet.jsp.jstl-api</artifactId> <version>3.0.0</version> </dependency></dependencies>
<packaging>war</packaging>Step 1: Add the Maven Dependencies
Four dependencies do all the work in this project: `spring-webmvc` for the framework itself, the Servlet API (marked `provided` since Tomcat supplies its own copy at deploy time), Hibernate Validator to actually enforce the JSR-380 annotations Step 4 will add, and JSTL for the view-side `<c:forEach>` loop in Step 7. The `pom.xml` snippet above already shows all four together — add them under your project's `<dependencies>` and set `<packaging>war</packaging>` so Maven produces a deployable `.war` instead of a plain `.jar`.
Step 2: Register the DispatcherServlet Without web.xml
Implementing `WebApplicationInitializer` replaces an old `web.xml` deployment descriptor entirely — Servlet 3.0+ containers automatically detect any class on the classpath that implements this interface and call `onStartup()` during container boot. Everything a `web.xml` used to declare in XML now happens here, in ordinary, refactorable Java code.
package com.programinds.registration;
import org.springframework.web.WebApplicationInitializer;import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;import org.springframework.web.servlet.DispatcherServlet;
import jakarta.servlet.ServletContext;import jakarta.servlet.ServletException;import jakarta.servlet.ServletRegistration;
public class RegistrationWebAppInitializer implements WebApplicationInitializer {
@Override public void onStartup(ServletContext servletContext) throws ServletException { // The Spring container this whole web app runs inside — built straight // from WebConfig, the @Configuration class from Step 3. AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.register(WebConfig.class);
// DispatcherServlet is Spring MVC's front controller: EVERY request // arrives here first, and it is DispatcherServlet that consults the // @Controller beans in the context to decide which method handles it. DispatcherServlet dispatcherServlet = new DispatcherServlet(context); ServletRegistration.Dynamic registration = servletContext.addServlet("dispatcher", dispatcherServlet); registration.setLoadOnStartup(1); // Start immediately at container boot, not lazily on the first request registration.addMapping("/"); // Route every request through DispatcherServlet }}Step 3: Configure Spring MVC With @EnableWebMvc and a View Resolver
`@EnableWebMvc` turns on Spring MVC's core machinery — request mapping, `@Valid`-triggered validation, request-parameter-to-object data binding — the same machinery Spring Boot's web starter enables silently through auto-configuration. Writing it out here is the entire difference between core Spring and Spring Boot on this point: this project makes every one of those decisions explicit instead of inheriting them invisibly from a starter.
package com.programinds.registration;
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.EnableWebMvc;import org.springframework.web.servlet.view.InternalResourceViewResolver;
@Configuration@EnableWebMvc // Activates Spring MVC's request-mapping and validation machinery@ComponentScan(basePackages = "com.programinds.registration") // Discovers @Controller and @Component classes belowpublic class WebConfig {
@Bean public InternalResourceViewResolver viewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); // A controller returning the view name "register" resolves to this // exact path: prefix + name + suffix = /WEB-INF/views/register.jsp resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".jsp"); return resolver; }}Step 4: Define the Student Form Object With JSR-380 Validation
A form-backing object's only job is to be filled in by Spring MVC's data binder from incoming request parameters, checked against the annotations below, and read back out by the JSP to redisplay whatever the visitor typed if validation fails. `Student` carries no Spring annotations of its own here — `@Valid` on the controller method in Step 6 is what actually tells Spring to enforce these rules; the annotations below are just JSR-380's standard vocabulary for describing them.
| Annotation | Rejects | Applied To |
|---|---|---|
| @NotBlank | null, empty string, and whitespace-only values | name, email, course |
| a non-blank value that is not a plausible email address | ||
| @Size(min, max) | a string shorter or longer than the given bounds | course |
package com.programinds.registration;
import jakarta.validation.constraints.Email;import jakarta.validation.constraints.NotBlank;import jakarta.validation.constraints.Size;
// A form-backing object: created empty by Spring's data binder, then// populated field by field from the request, then validated against the// annotations below — all before the controller method's own body ever runs.public class Student { @NotBlank(message = "Name is required.") private String name;
@NotBlank(message = "Email is required.") @Email(message = "Enter a valid email address.") // Only checked once the value is confirmed non-blank private String email;
@NotBlank(message = "Course is required.") @Size(min = 2, max = 50, message = "Course must be between 2 and 50 characters.") private String course;
// Required no-arg constructor: the data binder calls "new Student()" first, // then populates it via the setters below, one request parameter at a time. public Student() { }
public String getName() { return name; } public void setName(String name) { this.name = name; }
public String getEmail() { return email; } public void setEmail(String email) { this.email = email; }
public String getCourse() { return course; } public void setCourse(String course) { this.course = course; }}Step 5: Handle the Registration Form's GET Request
A `GET /register` request needs an empty `Student` in the model before the form can even render — the JSP's `<form:form modelAttribute="student">` tag in Step 7 requires a `student` model attribute to bind its `<form:input>` tags against, and on a first visit no data has been submitted yet, so the handler has to put one there itself.
package com.programinds.registration;
import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;
@Controller // @Controller, not @RestController — handler methods here return VIEW NAMES, not response bodiespublic class StudentController { private final StudentStore studentStore;
public StudentController(StudentStore studentStore) { this.studentStore = studentStore; // Constructor injection; Spring resolves the single StudentStore bean automatically }
@GetMapping("/register") public String showForm(Model model) { model.addAttribute("student", new Student()); // An empty Student for the form's fields to bind against return "register"; // Resolves to /WEB-INF/views/register.jsp via Step 3's view resolver }}package com.programinds.registration;
import org.springframework.stereotype.Component;
import java.util.ArrayList;import java.util.Collections;import java.util.List;
// A minimal singleton store standing in for a real database, so success.jsp// has something to list. @Component makes this a Spring-managed bean that// @ComponentScan in WebConfig discovers automatically, letting// StudentController receive it through constructor injection above.@Componentpublic class StudentStore { private final List<Student> registered = new ArrayList<>();
public void add(Student student) { registered.add(student); }
public List<Student> findAll() { return Collections.unmodifiableList(registered); // Callers can read, but never mutate, the real list }}Step 6: Handle the Registration Form's POST Request With @Valid and BindingResult
`@Valid` tells Spring to run the JSR-380 annotations from Step 4 against the populated `Student` BEFORE this method body executes. `BindingResult` must be declared IMMEDIATELY after the `@Valid`-annotated parameter — if any other parameter came between them, Spring would throw the validation errors as an exception instead of quietly capturing them into `bindingResult` for you to handle.
package com.programinds.registration;
import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.validation.BindingResult;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.PostMapping;
import jakarta.validation.Valid;
// (continuing the same @Controller class from Step 5)@PostMapping("/register")public String submitForm(@Valid @ModelAttribute("student") Student student, BindingResult bindingResult, // MUST directly follow the @Valid parameter Model model) { if (bindingResult.hasErrors()) { return "register"; // Re-render the SAME form; <form:errors> in the JSP reads its messages from bindingResult }
studentStore.add(student); model.addAttribute("registeredStudents", studentStore.findAll()); return "success"; // A different view, reached only once validation actually passed}Step 7: Build the JSP Views
Spring's `<form:>` tag library binds each `<form:input path="...">` directly to the matching `Student` property by name, and `<form:errors path="...">` renders whatever JSR-380 message is attached to that same property in `bindingResult` — no manual `if`/`else` error-checking is written in the JSP at all. `success.jsp` uses plain JSTL (`<c:forEach>`) and expression language (`${}`) instead, since it only needs to display data, not bind a form.
<%@ page contentType="text/html;charset=UTF-8" %><%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %><html><head><title>Student Registration</title></head><body> <h1>Register a Student</h1> <!-- modelAttribute="student" binds every <form:input path="..."> below to the matching Student property, and <form:errors> renders any validation message attached to that same property. --> <form:form modelAttribute="student" method="post" action="register"> <p> Name: <form:input path="name" /> <form:errors path="name" cssClass="error" /> </p> <p> Email: <form:input path="email" /> <form:errors path="email" cssClass="error" /> </p> <p> Course: <form:input path="course" /> <form:errors path="course" cssClass="error" /> </p> <button type="submit">Register</button> </form:form></body></html><%@ page contentType="text/html;charset=UTF-8" %><%@ taglib prefix="c" uri="jakarta.tags.core" %><html><head><title>Registration Successful</title></head><body> <h1>Registered Students</h1> <ul> <!-- c:forEach + EL (${}) read straight from the "registeredStudents" model attribute the controller added in Step 6 — no Java code lives in this view. --> <c:forEach var="s" items="${registeredStudents}"> <li>${s.name} \u2014 ${s.email} \u2014 ${s.course}</li> </c:forEach> </ul></body></html>Complete Code
Assemble the classes from Steps 2-6 under `src/main/java/com/programinds/registration/`, both JSPs from Step 7 under `src/main/webapp/WEB-INF/views/`, add the `pom.xml` dependencies from Step 1, then package and deploy with `mvn clean package` followed by copying the generated `.war` into Tomcat's `webapps/` directory (or run it locally without a manual Tomcat install using the Tomcat Maven Plugin's `mvn tomcat7:run`).
package com.programinds.registration;
import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.validation.BindingResult;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.PostMapping;
import jakarta.validation.Valid;
@Controllerpublic class StudentController { private final StudentStore studentStore;
public StudentController(StudentStore studentStore) { this.studentStore = studentStore; }
@GetMapping("/register") public String showForm(Model model) { model.addAttribute("student", new Student()); return "register"; }
@PostMapping("/register") public String submitForm(@Valid @ModelAttribute("student") Student student, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { return "register"; } studentStore.add(student); model.addAttribute("registeredStudents", studentStore.findAll()); return "success"; }}Sample Run
Click Run to see what this code prints.
Extend This Project
- Add a custom validator by implementing `ConstraintValidator` for a `@UniqueEmail` annotation that rejects an email already present in `StudentStore`.
- Add an `age` field to `Student` with `@Min(16)`/`@Max(100)`, and observe how a numeric binding failure (e.g. submitting "abc") is reported through `bindingResult` differently than a JSR-380 violation.
- Swap the `InternalResourceViewResolver` for a Thymeleaf `SpringTemplateEngine` view resolver, and compare its `${}`-free syntax against the JSP tags used here.
- Add a `RedirectAttributes` flash message and redirect (POST-redirect-GET) after a successful registration instead of returning "success" directly, to prevent a page refresh from re-submitting the form.
- Replace `StudentStore` with a `JdbcTemplate`-backed repository using the same pattern from the JDBC-Backed CRUD App project later in this course.
Summary
You built a Spring MVC application the pre-Boot way: a hand-registered `DispatcherServlet`, an explicit `@EnableWebMvc` configuration, and a JSP view resolver, with none of it inherited invisibly from a starter dependency. `@Valid` combined with `BindingResult` turned form validation into a declarative, annotation-driven concern instead of a pile of manual `if` checks in the controller, and the same `@GetMapping`/`@PostMapping`-to-JSP pattern you wrote here for student registration scales directly to any server-rendered form Spring MVC needs to handle.