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/");
}
}