Spring MVC Architecture

Building web applications with Model-View-Controller

← Back to Index

What is Spring MVC?

Think of Spring MVC like a restaurant:

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
                    └────────────────────┘
  1. Request arrives at DispatcherServlet (Front Controller)
  2. HandlerMapping finds the right Controller method
  3. Controller processes request, returns Model and View name
  4. ViewResolver finds the actual View template
  5. 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