Spring MVC handles an HTTP request through a front controller. The DispatcherServlet finds a matching controller method, converts request values into Java arguments, invokes application code, and then either renders a view or writes a response body. This lesson is for Java developers who already understand controllers and need to see where routing, binding, validation, and view rendering belong.
After working through the examples, you should be able to trace a request from URL to response, choose the correct mapping annotation, preserve validation errors on a form, and distinguish a view controller from a REST endpoint.
Spring MVC is a web framework built on the Servlet API. It follows the Model-View-Controller pattern. The central component is the DispatcherServlet, which acts as a front controller - it receives all requests and delegates them to the appropriate handler (controller).
Request flow: Browser -> DispatcherServlet -> HandlerMapping -> Controller -> Service -> Model -> ViewResolver -> View (JSP/Thymeleaf) -> Response
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import jakarta.validation.Valid;
@Controller
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
// GET /products - List all products
@GetMapping
public String listProducts(Model model) {
model.addAttribute("products", productService.findAll());
model.addAttribute("title", "Product List");
return "products/list"; // View name: templates/products/list.html
}
// GET /products/{id} - Show product detail
@GetMapping("/{id}")
public String showProduct(@PathVariable Long id, Model model) {
Product product = productService.findById(id);
model.addAttribute("product", product);
return "products/detail";
}
// GET /products/new - Show create form
@GetMapping("/new")
public String showCreateForm(Model model) {
model.addAttribute("product", new Product());
return "products/form";
}
// POST /products - Create product
@PostMapping
public String createProduct(@Valid @ModelAttribute Product product,
BindingResult result) {
if (result.hasErrors()) {
return "products/form"; // Return to form with errors
}
productService.save(product);
return "redirect:/products"; // Post-Redirect-Get pattern
}
// GET /products/{id}/edit - Show edit form
@GetMapping("/{id}/edit")
public String showEditForm(@PathVariable Long id, Model model) {
model.addAttribute("product", productService.findById(id));
return "products/form";
}
// POST /products/{id} - Update product
@PostMapping("/{id}")
public String updateProduct(@PathVariable Long id,
@Valid @ModelAttribute Product product,
BindingResult result) {
if (result.hasErrors()) return "products/form";
product.setId(id);
productService.save(product);
return "redirect:/products/" + id;
}
// POST /products/{id}/delete - Delete product
@PostMapping("/{id}/delete")
public String deleteProduct(@PathVariable Long id) {
productService.deleteById(id);
return "redirect:/products";
}
}
| Annotation | HTTP Method | Equivalent |
|---|---|---|
| @RequestMapping | Any | Base mapping |
| @GetMapping | GET | @RequestMapping(method=GET) |
| @PostMapping | POST | @RequestMapping(method=POST) |
| @PutMapping | PUT | @RequestMapping(method=PUT) |
| @DeleteMapping | DELETE | @RequestMapping(method=DELETE) |
| @PatchMapping | PATCH | @RequestMapping(method=PATCH) |
<!-- templates/products/list.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="${title}">Products</title>
</head>
<body>
<h1 th:text="${title}">Product List</h1>
<a th:href="@{/products/new}">Add New Product</a>
<table>
<thead>
<tr><th>ID</th><th>Name</th><th>Price</th><th>Actions</th></tr>
</thead>
<tbody>
<tr th:each="product : ${products}">
<td th:text="${product.id}"></td>
<td th:text="${product.name}"></td>
<td th:text="${#numbers.formatCurrency(product.price)}"></td>
<td>
<a th:href="@{/products/{id}(id=${product.id})}">View</a>
<a th:href="@{/products/{id}/edit(id=${product.id})}">Edit</a>
<form th:action="@{/products/{id}/delete(id=${product.id})}" method="post">
<button type="submit">Delete</button>
</form>
</td>
</tr>
</tbody>
</table>
</body>
</html>
@Controller
public class BindingExamples {
// @PathVariable: /users/42
@GetMapping("/users/{id}")
public String getUser(@PathVariable Long id, Model model) {
model.addAttribute("user", userService.findById(id));
return "user/detail";
}
// Multiple path variables: /orders/5/items/3
@GetMapping("/orders/{orderId}/items/{itemId}")
public String getOrderItem(@PathVariable Long orderId,
@PathVariable Long itemId, Model model) {
model.addAttribute("item", orderService.getItem(orderId, itemId));
return "order/item";
}
// @RequestParam: /search?q=java&page=1&size=10
@GetMapping("/search")
public String search(@RequestParam String q,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String category,
Model model) {
model.addAttribute("results", searchService.search(q, page, size, category));
return "search/results";
}
// @ModelAttribute: binds form fields to object
@PostMapping("/register")
public String register(@ModelAttribute @Valid UserForm form,
BindingResult errors, Model model) {
if (errors.hasErrors()) {
return "auth/register";
}
userService.register(form);
return "redirect:/login?registered=true";
}
}
DispatcherServlet is the web entry point, but it does not contain application rules. Handler mappings locate the controller method, handler adapters invoke it, argument resolvers supply values such as path variables and model attributes, and return-value handlers interpret the result. A String returned by a view controller is usually a logical view name; an object returned from a @RestController is normally serialized into the response body.
Keep controllers narrow. They should translate HTTP input into a use-case call, reject invalid input, and select an HTTP or view response. Database work and business decisions belong in services so they can be tested without constructing a servlet request.
Use @PathVariable for identity embedded in the route, @RequestParam for optional filtering or paging values, and @ModelAttribute for HTML form fields. Put BindingResult immediately after the validated model attribute. When errors exist, return the same form view so Spring can expose field errors and the values the learner already entered.
For successful form submissions, redirect instead of rendering the success page directly. The Post-Redirect-Get pattern prevents a browser refresh from repeating the write. For APIs, validate request DTOs rather than persistence entities and translate failures into stable status codes and error bodies.
A 404 often means no handler pattern matched; inspect the class-level and method-level mappings together. A 400 commonly points to conversion or validation failure before the controller body ran. A template-not-found error occurs later, after the controller returned a view name. Knowing the stage narrows the search much faster than adding logs everywhere.
Test the HTTP contract with MockMvc: assert the route, status, selected view, model attributes, and validation behavior. Test business rules separately in the service. This division makes a failing test say whether the problem is web translation or application logic.
Try this next
0 of 2 completed
Explore 500+ free tutorials across 20+ languages and frameworks.