Перейти к содержанию

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:

<span th:text="${user.name}"></span>
<span th:text="${stats.total}"></span>

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

  1. Контроллер возвращает view name.
  2. ViewResolver находит шаблон.
  3. View.render() берет model attributes и рендерит шаблон в HTML.
  4. 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 устанавливается автоматически.

@GetMapping
public List<OrderDto> getAll() {
    return orderService.findAll(); // -> JSON array
}

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 -> JSON
  • Accept: 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

Используется для передачи формы в контроллер:

@PostMapping("/register")
public String register(@ModelAttribute RegisterForm form)

Также используется для подготовки пустого объекта формы для 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>