Spring MVC

Model-View-Controller pattern for web applications

🎭 What is Spring MVC?

Spring MVC is a web framework that implements the Model-View-Controller design pattern. It provides a clean separation of concerns, flexible request handling, and powerful view resolution for building web applications.


// Simple MVC controller
@Controller
@RequestMapping("/home")
public class HomeController {
    
    @GetMapping
    public String home(Model model) {
        model.addAttribute("message", "Welcome to Spring MVC!");
        return "home"; // Returns home.html view
    }
}
                                    

MVC Architecture

🎯

Controller

Handles HTTP requests and user interactions

@Controller
@RequestMapping("/users")
📊

Model

Carries data between controller and view

model.addAttribute("user", user)
🖼️

View

Renders the user interface (HTML templates)

<h1 th:text="${message}"></h1>
🔄

DispatcherServlet

Front controller that routes requests

@EnableWebMvc
@Configuration

🔹 Basic MVC Controller

Create controllers to handle web requests:

@Controller
@RequestMapping("/products")
public class ProductController {
    
    @Autowired
    private ProductService productService;
    
    // Display all products
    @GetMapping
    public String listProducts(Model model) {
        List<Product> products = productService.findAll();
        model.addAttribute("products", products);
        return "products/list"; // Returns products/list.html
    }
    
    // Show product details
    @GetMapping("/{id}")
    public String showProduct(@PathVariable Long id, Model model) {
        Product product = productService.findById(id);
        if (product == null) {
            return "redirect:/products";
        }
        model.addAttribute("product", product);
        return "products/detail";
    }
    
    // Show create form
    @GetMapping("/new")
    public String newProductForm(Model model) {
        model.addAttribute("product", new Product());
        return "products/form";
    }
    
    // Handle form submission
    @PostMapping
    public String createProduct(@ModelAttribute Product product, 
                               RedirectAttributes redirectAttributes) {
        productService.save(product);
        redirectAttributes.addFlashAttribute("message", "Product created successfully!");
        return "redirect:/products";
    }
    
    // Show edit form
    @GetMapping("/{id}/edit")
    public String editProductForm(@PathVariable Long id, Model model) {
        Product product = productService.findById(id);
        model.addAttribute("product", product);
        return "products/form";
    }
    
    // Handle update
    @PostMapping("/{id}")
    public String updateProduct(@PathVariable Long id, 
                               @ModelAttribute Product product,
                               RedirectAttributes redirectAttributes) {
        product.setId(id);
        productService.save(product);
        redirectAttributes.addFlashAttribute("message", "Product updated successfully!");
        return "redirect:/products";
    }
    
    // Delete product
    @PostMapping("/{id}/delete")
    public String deleteProduct(@PathVariable Long id, 
                               RedirectAttributes redirectAttributes) {
        productService.deleteById(id);
        redirectAttributes.addFlashAttribute("message", "Product deleted successfully!");
        return "redirect:/products";
    }
}

URL Mappings:

GET /products → List all products

GET /products/new → Show create form

POST /products → Create product

GET /products/1/edit → Show edit form

🔹 Thymeleaf Templates

Create dynamic HTML views using Thymeleaf:

🔸 Product List Template (products/list.html)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Product List</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <h1>Products</h1>
        
        <!-- Flash message -->
        <div th:if="${message}" class="alert alert-success" th:text="${message}"></div>
        
        <a href="/products/new" class="btn btn-primary mb-3">Add New Product</a>
        
        <table class="table table-striped">
            <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>
                        <a th:href="@{/products/{id}(id=${product.id})}" 
                           th:text="${product.name}"></a>
                    </td>
                    <td th:text="${#numbers.formatCurrency(product.price)}"></td>
                    <td>
                        <a th:href="@{/products/{id}/edit(id=${product.id})}" 
                           class="btn btn-sm btn-warning">Edit</a>
                        <form th:action="@{/products/{id}/delete(id=${product.id})}" 
                              method="post" style="display: inline;">
                            <button type="submit" class="btn btn-sm btn-danger" 
                                    onclick="return confirm('Are you sure?')">Delete</button>
                        </form>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</body>
</html>

🔸 Product Form Template (products/form.html)

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Product Form</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-4">
        <h1 th:text="${product.id != null ? 'Edit Product' : 'New Product'}"></h1>
        
        <form th:action="${product.id != null ? '/products/' + product.id : '/products'}" 
              th:object="${product}" method="post">
            
            <div class="mb-3">
                <label for="name" class="form-label">Product Name</label>
                <input type="text" class="form-control" id="name" 
                       th:field="*{name}" required>
            </div>
            
            <div class="mb-3">
                <label for="description" class="form-label">Description</label>
                <textarea class="form-control" id="description" 
                          th:field="*{description}" rows="3"></textarea>
            </div>
            
            <div class="mb-3">
                <label for="price" class="form-label">Price</label>
                <input type="number" class="form-control" id="price" 
                       th:field="*{price}" step="0.01" required>
            </div>
            
            <div class="mb-3">
                <label for="category" class="form-label">Category</label>
                <select class="form-control" id="category" th:field="*{category}">
                    <option value="">Select Category</option>
                    <option value="ELECTRONICS">Electronics</option>
                    <option value="CLOTHING">Clothing</option>
                    <option value="BOOKS">Books</option>
                </select>
            </div>
            
            <button type="submit" class="btn btn-primary">Save</button>
            <a href="/products" class="btn btn-secondary">Cancel</a>
        </form>
    </div>
</body>
</html>

🔹 Form Handling and Validation

Handle form submissions with validation:

// Product entity with validation
@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "Product name is required")
    @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
    private String name;
    
    @Size(max = 500, message = "Description cannot exceed 500 characters")
    private String description;
    
    @NotNull(message = "Price is required")
    @DecimalMin(value = "0.0", inclusive = false, message = "Price must be greater than 0")
    private BigDecimal price;
    
    @NotNull(message = "Category is required")
    @Enumerated(EnumType.STRING)
    private Category category;
    
    // Constructors, getters, setters
}

// Controller with validation
@Controller
@RequestMapping("/products")
public class ProductController {
    
    @PostMapping
    public String createProduct(@Valid @ModelAttribute Product product, 
                               BindingResult result, 
                               Model model,
                               RedirectAttributes redirectAttributes) {
        
        if (result.hasErrors()) {
            // Return to form with validation errors
            return "products/form";
        }
        
        productService.save(product);
        redirectAttributes.addFlashAttribute("message", "Product created successfully!");
        return "redirect:/products";
    }
    
    @PostMapping("/{id}")
    public String updateProduct(@PathVariable Long id,
                               @Valid @ModelAttribute Product product, 
                               BindingResult result, 
                               Model model,
                               RedirectAttributes redirectAttributes) {
        
        if (result.hasErrors()) {
            product.setId(id); // Preserve ID for edit form
            return "products/form";
        }
        
        product.setId(id);
        productService.save(product);
        redirectAttributes.addFlashAttribute("message", "Product updated successfully!");
        return "redirect:/products";
    }
}

🔹 File Upload Handling

Handle file uploads in Spring MVC:

@Controller
@RequestMapping("/products")
public class ProductController {
    
    @PostMapping("/upload")
    public String uploadProductImage(@RequestParam("file") MultipartFile file,
                                   @RequestParam("productId") Long productId,
                                   RedirectAttributes redirectAttributes) {
        
        if (file.isEmpty()) {
            redirectAttributes.addFlashAttribute("error", "Please select a file to upload");
            return "redirect:/products/" + productId;
        }
        
        try {
            // Validate file type
            String contentType = file.getContentType();
            if (!contentType.startsWith("image/")) {
                redirectAttributes.addFlashAttribute("error", "Only image files are allowed");
                return "redirect:/products/" + productId;
            }
            
            // Save file
            String filename = fileStorageService.storeFile(file);
            
            // Update product with image filename
            Product product = productService.findById(productId);
            product.setImageFilename(filename);
            productService.save(product);
            
            redirectAttributes.addFlashAttribute("message", "Image uploaded successfully!");
            
        } catch (IOException e) {
            redirectAttributes.addFlashAttribute("error", "Failed to upload image");
        }
        
        return "redirect:/products/" + productId;
    }
    
    // Serve uploaded files
    @GetMapping("/images/{filename:.+}")
    @ResponseBody
    public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
        try {
            Resource file = fileStorageService.loadFileAsResource(filename);
            return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, 
                       "attachment; filename=\"" + file.getFilename() + "\"")
                .body(file);
        } catch (Exception e) {
            return ResponseEntity.notFound().build();
        }
    }
}

🔸 File Upload Form

<form th:action="@{/products/upload}" method="post" enctype="multipart/form-data">
    <input type="hidden" name="productId" th:value="${product.id}">
    
    <div class="mb-3">
        <label for="file" class="form-label">Product Image</label>
        <input type="file" class="form-control" id="file" name="file" 
               accept="image/*" required>
    </div>
    
    <button type="submit" class="btn btn-primary">Upload Image</button>
</form>

🔹 Interceptors and Filters

Add cross-cutting concerns with interceptors:

// Custom interceptor
@Component
public class LoggingInterceptor implements HandlerInterceptor {
    
    private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class);
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
        logger.info("Request URL: {} {}", request.getMethod(), request.getRequestURL());
        request.setAttribute("startTime", System.currentTimeMillis());
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, 
                          HttpServletResponse response, 
                          Object handler, 
                          ModelAndView modelAndView) throws Exception {
        long startTime = (Long) request.getAttribute("startTime");
        long endTime = System.currentTimeMillis();
        logger.info("Request processed in {} ms", (endTime - startTime));
    }
}

// Register interceptor
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    
    @Autowired
    private LoggingInterceptor loggingInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loggingInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/js/**", "/images/**");
    }
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/");
    }
}

🧠 Test Your Knowledge

What does the @ModelAttribute annotation do in Spring MVC?