What is Spring MVC?
Think of Spring MVC like a restaurant:
- DispatcherServlet: The host who directs customers (requests)
- Controller: The waiter who takes orders
- Service: The kitchen that prepares food
- Model: The food being served (data)
- View: The plate presentation (HTML/JSON)
Spring MVC is a web framework built on the Servlet API that follows the Model-View-Controller design pattern. It provides:
- Clean separation of concerns
- Flexible request mapping
- Support for multiple view technologies
- Data binding and validation
Request Flow
┌──────────────┐ ┌────────────────────┐ ┌────────────────┐
│ Browser │───▶│ DispatcherServlet │───▶│ HandlerMapping│
└──────────────┘ └────────────────────┘ └────────────────┘
│ │
│ ▼
│ ┌────────────────┐
│ │ Controller │
│ └────────────────┘
│ │
▼ ▼
┌────────────────────┐ ┌────────────────┐
│ ViewResolver │◀───│ Model │
└────────────────────┘ └────────────────┘
│
▼
┌────────────────────┐
│ View │───▶ Response
└────────────────────┘
- Request arrives at DispatcherServlet (Front Controller)
- HandlerMapping finds the right Controller method
- Controller processes request, returns Model and View name
- ViewResolver finds the actual View template
- View renders the Model into HTML/JSON response
Controllers
Basic Controller
@Controller // Returns views (HTML pages)
public class HomeController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("message", "Welcome!");
return "home"; // Returns view name "home.html"
}
}
@RestController // Returns data (JSON/XML)
public class ApiController {
@GetMapping("/api/data")
public DataResponse getData() {
return new DataResponse("Hello"); // Converted to JSON
}
}
// @RestController = @Controller + @ResponseBody
Request Mapping
@Controller
@RequestMapping("/products") // Base path for all methods
public class ProductController {
// GET /products
@GetMapping
public String list(Model model) {
return "products/list";
}
// GET /products/123
@GetMapping("/{id}")
public String show(@PathVariable Long id, Model model) {
return "products/show";
}
// GET /products/new
@GetMapping("/new")
public String newForm(Model model) {
model.addAttribute("product", new Product());
return "products/form";
}
// POST /products
@PostMapping
public String create(@ModelAttribute Product product) {
productService.save(product);
return "redirect:/products"; // Redirect after POST
}
// PUT /products/123
@PutMapping("/{id}")
public String update(@PathVariable Long id,
@ModelAttribute Product product) {
productService.update(id, product);
return "redirect:/products/" + id;
}
// DELETE /products/123
@DeleteMapping("/{id}")
public String delete(@PathVariable Long id) {
productService.delete(id);
return "redirect:/products";
}
}
Request Parameters
@Controller
public class SearchController {
// Path variable: /users/123
@GetMapping("/users/{id}")
public String getUser(@PathVariable Long id) {
// id = 123
}
// Query parameter: /search?q=java&page=1
@GetMapping("/search")
public String search(
@RequestParam String q,
@RequestParam(defaultValue = "1") int page,
@RequestParam(required = false) String sort) {
// q = "java", page = 1, sort = null
}
// Request header
@GetMapping("/api/data")
public String getData(@RequestHeader("Authorization") String authHeader) {
// Access Authorization header
}
// Cookie value
@GetMapping("/prefs")
public String getPrefs(@CookieValue("theme") String theme) {
// Access cookie value
}
// Request body (for POST/PUT)
@PostMapping("/api/users")
public User createUser(@RequestBody User user) {
// JSON body converted to User object
}
// Form data
@PostMapping("/register")
public String register(@ModelAttribute RegistrationForm form) {
// Form fields bound to object
}
}
Model and View
Adding Data to Model
@Controller
public class DashboardController {
// Method 1: Model parameter
@GetMapping("/dashboard")
public String dashboard(Model model) {
model.addAttribute("user", getCurrentUser());
model.addAttribute("stats", getStats());
return "dashboard";
}
// Method 2: ModelAndView
@GetMapping("/profile")
public ModelAndView profile() {
ModelAndView mav = new ModelAndView("profile");
mav.addObject("user", getCurrentUser());
return mav;
}
// Method 3: Return object directly with @ModelAttribute
@ModelAttribute("categories")
public List<Category> categories() {
// Available to ALL handler methods in this controller
return categoryService.findAll();
}
}
View Resolution with Thymeleaf
<!-- src/main/resources/templates/dashboard.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Dashboard</title>
</head>
<body>
<h1 th:text="${user.name}">Username</h1>
<ul>
<li th:each="stat : ${stats}">
<span th:text="${stat.name}">Stat</span>:
<span th:text="${stat.value}">0</span>
</li>
</ul>
<!-- Categories from @ModelAttribute -->
<select>
<option th:each="cat : ${categories}"
th:value="${cat.id}"
th:text="${cat.name}">Category</option>
</select>
</body>
</html>
Form Handling
// Form backing object
public class ProductForm {
@NotBlank(message = "Name is required")
private String name;
@Min(value = 0, message = "Price must be positive")
private BigDecimal price;
@Size(max = 1000)
private String description;
// getters/setters
}
@Controller
@RequestMapping("/products")
public class ProductController {
@GetMapping("/new")
public String newForm(Model model) {
model.addAttribute("product", new ProductForm());
return "products/form";
}
@PostMapping
public String create(
@Valid @ModelAttribute("product") ProductForm form,
BindingResult result,
RedirectAttributes redirectAttrs) {
// Check for validation errors
if (result.hasErrors()) {
return "products/form"; // Show form with errors
}
Product product = productService.create(form);
// Flash attribute - available after redirect
redirectAttrs.addFlashAttribute("message", "Product created!");
redirectAttrs.addAttribute("id", product.getId());
return "redirect:/products/{id}";
}
}
Thymeleaf Form
<!-- products/form.html -->
<form th:action="@{/products}" th:object="${product}" method="post">
<div th:if="${#fields.hasErrors('*')}" class="errors">
Please correct the errors below.
</div>
<div>
<label>Name:</label>
<input type="text" th:field="*{name}" />
<span th:if="${#fields.hasErrors('name')}"
th:errors="*{name}"
class="error">Name error</span>
</div>
<div>
<label>Price:</label>
<input type="number" th:field="*{price}" step="0.01" />
<span th:if="${#fields.hasErrors('price')}"
th:errors="*{price}"
class="error">Price error</span>
</div>
<div>
<label>Description:</label>
<textarea th:field="*{description}"></textarea>
</div>
<button type="submit">Save</button>
</form>
Exception Handling
// Controller-level exception handling
@Controller
public class ProductController {
@GetMapping("/products/{id}")
public String show(@PathVariable Long id, Model model) {
Product product = productService.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
model.addAttribute("product", product);
return "products/show";
}
// Handle exception in this controller only
@ExceptionHandler(ProductNotFoundException.class)
public String handleNotFound(ProductNotFoundException ex, Model model) {
model.addAttribute("message", ex.getMessage());
return "error/404";
}
}
// Global exception handling
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handleNotFound(ResourceNotFoundException ex, Model model) {
model.addAttribute("error", ex.getMessage());
return "error/404";
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleGeneral(Exception ex, Model model) {
model.addAttribute("error", "An error occurred");
return "error/500";
}
}
// REST API exception handling
@RestControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
return new ErrorResponse("NOT_FOUND", ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.toList();
return new ErrorResponse("VALIDATION_ERROR", errors);
}
}
Interceptors
// Custom interceptor
@Component
public class LoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// Before controller method
System.out.println("Request: " + request.getRequestURI());
request.setAttribute("startTime", System.currentTimeMillis());
return true; // Continue to controller
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) {
// After controller, before view rendering
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
// After view rendering, cleanup
long startTime = (Long) request.getAttribute("startTime");
System.out.println("Duration: " + (System.currentTimeMillis() - startTime) + "ms");
}
}
// Register interceptor
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoggingInterceptor loggingInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingInterceptor)
.addPathPatterns("/**") // Apply to all paths
.excludePathPatterns("/static/**"); // Except static resources
}
}
Static Resources
# Default static resource locations (in order):
# 1. /META-INF/resources/
# 2. /resources/
# 3. /static/
# 4. /public/
# Customize in application.properties:
spring.web.resources.static-locations=classpath:/static/,classpath:/custom/
# Cache control for static resources
spring.web.resources.cache.period=31536000
spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**
// Custom static resource configuration
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/files/**")
.addResourceLocations("file:/var/uploads/")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS));
}
}
Summary
- Spring MVC: Model-View-Controller web framework
- DispatcherServlet: Front controller that routes requests
- @Controller: Returns views (HTML pages)
- @RestController: Returns data (JSON/XML)
- @RequestMapping: Maps URLs to handler methods
- @PathVariable: Extracts URL path segments
- @RequestParam: Extracts query parameters
- @RequestBody: Binds JSON/XML to objects
- @ModelAttribute: Binds form data to objects
- @Valid: Triggers bean validation
- @ExceptionHandler: Handles exceptions
- @ControllerAdvice: Global exception handling