Spring Boot
Model
Model is the Spring MVC interface for passing data from a controller to a view template. When a @Controller method renders a Thymeleaf or JSP template, it populates a Model with named attributes — objects, lists, strings, flags — that the template can access by name. Model, ModelMap, and ModelAndView are the three ways to work with model data in Spring MVC.
Model, ModelMap, and @ModelAttribute
Spring MVC provides three ways to add attributes to the model. Model is the recommended interface — it is injected by Spring and provides a clean API for adding named attributes. ModelMap is the concrete implementation of Model with additional Map operations. @ModelAttribute on a method parameter binds a form object from the request.
Java
@Controller
@RequestMapping("/users")
public class UserPageController {
private final UserService userService;
// ── Model — injected directly as a method parameter: ──────────────
@GetMapping
public String listUsers(Model model) {
model.addAttribute("users", userService.findAll());
model.addAttribute("pageTitle", "All Users");
model.addAttribute("totalCount", userService.count());
return "users/list"; // view name → templates/users/list.html
}
// ── ModelMap — concrete implementation, same result: ──────────────
@GetMapping("/{id}")
public String userDetail(@PathVariable Long id, ModelMap modelMap) {
modelMap.addAttribute("user", userService.findById(id));
modelMap.addAttribute("orders", orderService.findByUser(id));
return "users/detail";
}
// ── Map<String, Object> — simplest form, works identically: ───────
@GetMapping("/dashboard")
public String dashboard(Map<String, Object> model) {
model.put("stats", userService.getStats());
model.put("recentUsers", userService.findRecent(10));
return "users/dashboard";
}
// ── @ModelAttribute on a method — adds attribute to ALL handlers: ─
// Runs before every handler method in this controller:
@ModelAttribute("appName")
public String appName() {
return "MyApplication"; // available as ${appName} in every template
}
@ModelAttribute("currentUser")
public UserResponse currentUser(Principal principal) {
return userService.findByUsername(principal.getName());
}
}Passing Data to Templates
Model attributes become variables in the template context. Thymeleaf accesses them with ${attributeName} expressions. The controller is responsible for naming attributes clearly and consistently.
Java
// ── Controller: ───────────────────────────────────────────────────────
@GetMapping
public String listUsers(
Model model,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "") String search) {
Page<UserResponse> users = userService.search(search, PageRequest.of(page, 20));
model.addAttribute("users", users.getContent());
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", users.getTotalPages());
model.addAttribute("totalElements", users.getTotalElements());
model.addAttribute("search", search);
model.addAttribute("hasNext", users.hasNext());
model.addAttribute("hasPrevious", users.hasPrevious());
return "users/list";
}
<!-- Template — accessing model attributes: -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<input type="text" th:value="${search}" name="search" />
<p th:text="|Showing ${users.size()} of ${totalElements} users|"></p>
<tr th:each="user : ${users}">
<td th:text="${user.name}"></td>
<td th:text="${user.email}"></td>
</tr>
<a th:if="${hasPrevious}"
th:href="@{/users(page=${currentPage - 1}, search=${search})}">Previous</a>
<a th:if="${hasNext}"
th:href="@{/users(page=${currentPage + 1}, search=${search})}">Next</a>
</body>
</html>@ModelAttribute on Method Parameters
@ModelAttribute on a method parameter binds the request (form fields, query parameters) to a Java object. This is the primary mechanism for receiving HTML form submissions in Spring MVC.
Java
// ── Form command object — fields match form input names: ─────────────
public class CreateUserForm {
@NotBlank private String name;
@NotBlank @Email private String email;
private User.Role role = User.Role.USER;
// getters + setters (required for binding)
}
@Controller
@RequestMapping("/users")
public class UserFormController {
// ── GET — display the empty form: ─────────────────────────────────
@GetMapping("/new")
public String newUserForm(Model model) {
model.addAttribute("userForm", new CreateUserForm()); // empty form object
model.addAttribute("roles", User.Role.values());
return "users/form";
}
// ── POST — receive the submitted form: ────────────────────────────
@PostMapping
public String createUser(
@ModelAttribute("userForm") @Valid CreateUserForm form,
BindingResult bindingResult,
Model model) {
if (bindingResult.hasErrors()) {
// Re-render the form — form object retains the invalid values:
model.addAttribute("roles", User.Role.values());
return "users/form"; // back to the form template
}
userService.create(form);
return "redirect:/users"; // PRG pattern — redirect after success
}
}
// ── @ModelAttribute binding sources (in order): ───────────────────────
// 1. @ModelAttribute method in the same controller (pre-populated object)
// 2. HTTP session (@SessionAttributes)
// 3. Request parameters and path variables (form fields, query params)
// If no source found: a new instance is created via the default constructor.RedirectAttributes — Passing Data Across Redirects
A redirect (return "redirect:/...") causes the browser to make a new GET request, which clears the model. RedirectAttributes passes data across the redirect boundary — flash attributes survive exactly one redirect and are removed automatically.
Java
@Controller
@RequestMapping("/users")
public class UserFormController {
// ── POST → redirect → GET (Post/Redirect/Get pattern): ────────────
@PostMapping
public String create(
@ModelAttribute @Valid CreateUserForm form,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
return "users/form";
}
UserResponse created = userService.create(form);
// Flash attribute — survives one redirect, then removed:
redirectAttributes.addFlashAttribute("successMessage",
"User '" + created.name() + "' created successfully");
// Regular redirect attribute — appended to the URL as query param:
redirectAttributes.addAttribute("highlight", created.id());
return "redirect:/users";
// Redirects to: /users?highlight=43
// Flash: successMessage available in /users model, then gone
}
}
// ── Receiving in the redirect target: ─────────────────────────────────
@GetMapping
public String listUsers(Model model,
@RequestParam(required = false) Long highlight) {
model.addAttribute("users", userService.findAll());
model.addAttribute("highlight", highlight);
// successMessage is already in the model automatically from flash storage
return "users/list";
}
<!-- Template — display the flash message: -->
<div th:if="${successMessage}" class="alert alert-success">
<p th:text="${successMessage}"></p>
</div>
<tr th:each="user : ${users}"
th:classappend="${user.id == highlight} ? 'highlighted' : ''">
...
</tr>