Model, View и REST¶
Model в Spring MVC¶
Model — контейнер атрибутов, которые контроллер передает во view.
Это Map String -> Object: ключ — имя атрибута в шаблоне, значение — Java-объект.
Передача данных из controller во view:
@GetMapping("/dashboard")
public String dashboard(Model model) {
model.addAttribute("user", currentUser());
model.addAttribute("stats", statsService.getStats());
return "dashboard";
}
В Thymeleaf:
Model, Map, ModelMap
Три взаимозаменяемых варианта. Все три работают одинаково — Spring их поддерживает:
// 1. Model (интерфейс Spring)
public String page(Model model) {
model.addAttribute("key", value);
model.addAllAttributes(...);
return "view";
}
// 2. Map<String, Object>
public String page(Map<String, Object> model) {
model.put("key", value);
return "view";
}
// 3. ModelMap (extends LinkedHashMap)
public String page(ModelMap modelMap) {
modelMap.addAttribute("key", value);
return "view";
}
View и ViewResolver¶
View — интерфейс Spring MVC, представляющий объект, который рендерит response.
В HTML-сценариях это обычно шаблон, но View может быть и redirect/internal resource/custom renderer.
View получает атрибуты модели и рендерит HTML (или другой формат) в response.
ViewResolver — интерфейс, преобразующий строковое имя view в объект View.
Spring перебирает зарегистрированные ViewResolver по приоритету до первого успешного.
Разрешение имени представления
return "users/profile";
// ViewResolver: "users/profile" -> templates/users/profile.html
// Thymeleaf: prefix = "classpath:/templates/", suffix = ".html"
Для redirect/forward:
return "redirect:/login"; // специальный redirect-префикс, обычно превращается в RedirectView
return "forward:/internal"; // внутренний forward
Рендеринг HTML
- Контроллер возвращает view name.
ViewResolverнаходит шаблон.View.render()берет model attributes и рендерит шаблон в HTML.- HTML записывается в
HttpServletResponse.
REST в Spring MVC¶
Spring MVC — это не только HTML. Он является полноценным REST-фреймворком. Исторически создавался для HTML, но REST-поддержка добавлялась постепенно и сейчас это основной сценарий.
@RestController
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
// все методы возвращают тело ответа напрямую
}
JSON-ответы
По умолчанию Jackson сериализует Java-объекты в JSON.
Content-Type: application/json устанавливается автоматически.
DTO как формат обмена
DTO (Data Transfer Object) — отдельные классы для входа и выхода API:
// Request DTO (входящие данные)
public record CreateOrderRequest(
@NotNull Long userId,
@NotEmpty List<@Valid OrderItemRequest> items
) {}
// Response DTO (исходящие данные)
public record OrderDto(
Long id,
String status,
BigDecimal total,
List<OrderItemDto> items
) {}
Никогда не возвращайте Entity напрямую из контроллера.
ResponseEntity
@GetMapping("/{id}")
public ResponseEntity<OrderDto> getById(@PathVariable Long id) {
return orderService.findById(id)
.map(ResponseEntity::ok)
.orElseThrow(() -> new OrderNotFoundException(id));
}
Status codes
| Ситуация | Статус |
|---|---|
| Успешный GET | 200 OK |
| Успешный POST (создание) | 201 Created |
| Успешный DELETE / PUT без тела | 204 No Content |
| Ресурс не найден | 404 Not Found |
| Ошибка валидации | 400 Bad Request |
| Ошибка авторизации | 401 Unauthorized |
| Нет прав | 403 Forbidden |
| Конфликт | 409 Conflict |
| Внутренняя ошибка | 500 Internal Server Error |
@ResponseStatus(HttpStatus.CREATED)
@PostMapping
public OrderDto create(@RequestBody @Valid CreateOrderRequest req)
Headers
@GetMapping("/{id}")
public ResponseEntity<OrderDto> get(@PathVariable Long id) {
return ResponseEntity.ok()
.header("X-Request-Id", UUID.randomUUID().toString())
.body(orderService.findById(id));
}
Content negotiation
Spring выбирает формат ответа на основе заголовка Accept:
Accept: application/json-> JSONAccept: application/xml-> XML (если добавленjackson-dataformat-xml)Accept: */*-> первый доступный converter
@GetMapping(value = "/users", produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
})
public List<UserDto> getUsers()
Аннотации REST-раздела
| Аннотация / тип | Где | Что делает |
|---|---|---|
@RestController |
класс | Делает все методы body-oriented: return value пишется через HttpMessageConverter. |
@RequestBody |
параметр | Десериализует body запроса в DTO. |
@ResponseBody |
метод/класс | Сериализует return value в body ответа. |
@ResponseStatus |
метод/exception | Задает HTTP status, когда он статичен. |
ResponseEntity<T> |
return value | Status + headers + body, когда ответ зависит от результата. |
ProblemDetail |
return value | Стандартный формат REST-ошибки. |
@JsonProperty, @JsonIgnore, @JsonInclude, @JsonFormat |
DTO fields/accessors | Jackson-аннотации для управления JSON-контрактом. |
15. HttpMessageConverter¶
15.1. Что это такое¶
HttpMessageConverter<T> — интерфейс для преобразования HTTP message body ↔ Java object.
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, MediaType mediaType);
boolean canWrite(Class<?> clazz, MediaType mediaType);
T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException;
void write(T t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException;
}
15.2. Преобразование JSON ↔ Java object¶
MappingJackson2HttpMessageConverter:
- @RequestBody CreateUserRequest request -> Jackson читает JSON в Java object
- Java object из @ResponseBody / @RestController -> Jackson пишет JSON в response body
Условие: Content-Type: application/json для запроса, Accept: application/json для ответа.
Если параметр метода имеет тип String, чаще используется StringHttpMessageConverter, а не Jackson.
15.3. @RequestBody и converters¶
@PostMapping("/users")
public UserDto create(@RequestBody CreateUserRequest request) {
// Spring:
// 1. Смотрит Content-Type: application/json
// 2. Ищет converter, который canRead(CreateUserRequest.class, application/json)
// 3. MappingJackson2HttpMessageConverter.read() -> объект
}
15.4. @ResponseBody и converters¶
@GetMapping("/users/{id}")
@ResponseBody
public UserDto getUser(@PathVariable Long id) {
// Spring:
// 1. Смотрит Accept: application/json
// 2. Ищет converter, который canWrite(UserDto.class, application/json)
// 3. MappingJackson2HttpMessageConverter.write() -> JSON в body
}
15.5. Jackson и JSON serialization¶
Jackson — дефолтная библиотека в Spring Boot.
Кастомизация через ObjectMapper:
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> builder
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.featuresToEnable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
}
Аннотации на DTO:
@JsonProperty("user_name") // имя поля в JSON
@JsonIgnore // исключить поле
@JsonInclude(NON_NULL) // не включать null-поля
@JsonFormat(pattern = "dd.MM.yyyy") // формат даты
15.6. XML / text / binary converters¶
Встроенные конверторы:
| Converter | MediaType |
|---|---|
StringHttpMessageConverter |
text/plain, text/* |
MappingJackson2HttpMessageConverter |
application/json |
MappingJackson2XmlHttpMessageConverter |
application/xml (если jackson-dataformat-xml) |
ByteArrayHttpMessageConverter |
application/octet-stream |
ResourceHttpMessageConverter |
*/* (для Resource) |
FormHttpMessageConverter |
application/x-www-form-urlencoded |
AllEncompassingFormHttpMessageConverter |
form data + multipart parts |
16. Работа с HTML-формами¶
16.1. GET- и POST-формы¶
<!-- GET-форма: данные в URL query string -->
<form th:action="@{/search}" method="get">
<input type="text" name="query"/>
<button type="submit">Search</button>
</form>
<!-- POST-форма: данные в body как application/x-www-form-urlencoded -->
<form th:action="@{/register}" th:object="${form}" method="post">
<input type="text" th:field="*{name}"/>
<input type="email" th:field="*{email}"/>
<button type="submit">Register</button>
</form>
16.2. @ModelAttribute¶
Используется для передачи формы в контроллер:
Также используется для подготовки пустого объекта формы для GET-запроса:
@GetMapping("/register")
public String showForm(Model model) {
model.addAttribute("form", new RegisterForm()); // передаем пустую форму
return "register";
}
16.3. Binding полей формы¶
Thymeleaf th:field автоматически:
- генерирует id, name атрибуты,
- вставляет текущее значение из объекта модели,
- работает с th:object для привязки к форме.
<form th:object="${form}">
<input th:field="*{email}"/> <!-- name="email", id="email", value="${form.email}" -->
</form>
16.4. Validation формы¶
@PostMapping("/register")
public String register(@Valid @ModelAttribute("form") RegisterForm form,
BindingResult result) {
if (result.hasErrors()) {
return "register"; // вернуть форму с ошибками
}
userService.register(form);
return "redirect:/login";
}
Важно: BindingResult должен идти сразу после валидируемого объекта!
16.5. Повторный показ формы с ошибками¶
При result.hasErrors() -> return "register":
- Spring оставляет заполненный form-объект в модели,
- шаблон отображает ранее введенные данные,
- Thymeleaf показывает ошибки через th:errors.
<div th:if="${#fields.hasErrors('name')}" class="error">
<span th:errors="*{name}">Name error</span>
</div>
16.6. Связка Controller + Model + Thymeleaf form¶
// Контроллер
@Controller
@RequestMapping("/register")
public class RegisterController {
@GetMapping
public String showForm(Model model) {
model.addAttribute("form", new RegisterForm());
return "register";
}
@PostMapping
public String submit(@Valid @ModelAttribute("form") RegisterForm form,
BindingResult result) {
if (result.hasErrors()) return "register";
userService.register(form);
return "redirect:/login?registered";
}
}
<!-- register.html -->
<form th:action="@{/register}" th:object="${form}" method="post">
<input type="text" th:field="*{name}" placeholder="Name"/>
<span th:errors="*{name}" class="error"></span>
<input type="email" th:field="*{email}" placeholder="Email"/>
<span th:errors="*{email}" class="error"></span>
<button type="submit">Register</button>
</form>