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

Контроллеры

Контроллеры

Контроллер — это bean, помеченный @Controller или @RestController, методы которого обрабатывают HTTP-запросы. Он принимает HTTP-данные через параметры метода, вызывает сервисный слой, возвращает результат (view, DTO, ResponseEntity ...).

@Controller: - Помечает класс как Spring MVC controller. - Включает Component Scan. - По умолчанию возвращает имя view. - Для HTML-приложения - Использует ViewResolver - HttpMessageConverter используется для @ResponseBody методов

@Controller
@RequestMapping("/users")
public class UserController {
    @GetMapping("/{id}")
    public String getUser(@PathVariable Long id, Model model) {
        model.addAttribute("user", userService.findById(id));
        return "users/detail";
    }
}

@RestController: - @RestController = @Controller + @ResponseBody. - @ResponseBody на уровне класса означает: все методы возвращают тело ответа напрямую. - Не использует ViewResolver — использует HttpMessageConverter. - По умолчанию возвращает тело HTTP ответа - Для REST API - HttpMessageConverter используется для всех методов

@RestController
@RequestMapping("/api/users")
public class UserRestController {
    @GetMapping("/{id}")
    public UserDto getUser(@PathVariable Long id) {
        return userService.findById(id);  // сериализуется в JSON
    }
}

@ResponseBody - Отключает трактовку return value как имени view и передает значение в HttpMessageConverter.

@RequestMapping - Базовая аннотация маппинга. Может стоять на классе и/или методе. На классе — задает базовый url. На методе — уточняет маппинг.

@RequestMapping(
    value = "/users",               // URL pattern
    method = RequestMethod.GET,     // HTTP method
    params = "active=true",         // query param
    headers = "X-API=v2",           // header
    consumes = "application/json",  // Content-Type входящего запроса
    produces = "application/json"   // Accept клиента
)

Есть специализированные аннотации - Shortcutы над @RequestMapping. Все принимают те же параметры (params, headers, consumes, produces):

@GetMapping("/path")
@PostMapping("/path")
@PutMapping("/path")
@DeleteMapping("/path")
@PatchMapping("/path")

Mapping по URL Поддерживает паттерны:

@GetMapping("/users/{id}")          // path variable
@GetMapping("/files/**")            // wildcard (любой суффикс)
@GetMapping("/users/{id}/orders")   // вложенный ресурс
@GetMapping("/v{version}/users")    // переменная в сегменте

Паттерны используют PathPatternParser по умолчанию. Старый AntPathMatcher еще встречается в legacy-проектах, но новые приложения лучше проектировать под PathPatternParser.

// Mapping по HTTP method, Если метод не указан — маппинг работает для всех HTTP-методов.
@RequestMapping(value = "/users", method = RequestMethod.GET)
@GetMapping("/users")
----------------------------------------------------------------------------------------------------
// Mapping по params
@GetMapping(value = "/users", params = "active=true")
@GetMapping(value = "/users", params = "!active")       // параметра нет
@GetMapping(value = "/users", params = {"active", "role=admin"})
----------------------------------------------------------------------------------------------------
// Mapping по headers
@GetMapping(value = "/users", headers = "X-Version=2")
@GetMapping(value = "/users", headers = "!X-Version")
----------------------------------------------------------------------------------------------------
// consumes — Content-Type входящего запроса (что мы принимаем)
@PostMapping(value = "/users", consumes = MediaType.APPLICATION_JSON_VALUE)
// produces — Accept из запроса (что мы отдаем)
@GetMapping(value = "/users", produces = MediaType.APPLICATION_JSON_VALUE)
// Content negotiation — несколько вариантов
@GetMapping(value = "/users", produces = {
    MediaType.APPLICATION_JSON_VALUE,
    MediaType.APPLICATION_XML_VALUE
})
----------------------------------------------------------------------------------------------------
// API versioning в Spring Framework 7
// Spring Framework 7 добавил встроенную поддержку API versioning в Spring MVC.
// Сначала нужно настроить, откуда брать версию:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        configurer.useRequestHeader("API-Version");
    }
}

// После этого mapping может учитывать version:
@GetMapping(path = "/accounts/{id}", version = "1.0")
public AccountDto getV1(@PathVariable Long id)

@GetMapping(path = "/accounts/{id}", version = "2.0")
public AccountDto getV2(@PathVariable Long id)

@GetMapping(path = "/accounts/{id}", version = "2.0+")
public AccountDto getV2AndNewer(@PathVariable Long id)

// Версию можно резолвить из header, query parameter, path segment или media type parameter.
// Если запрошенная версия не поддерживается, Spring выбрасывает `InvalidApiVersionException`,
// что обычно приводит к `400 Bad Request`.

Параметры методов контроллера

@PathVariable

Извлекает значение из path template.

@GetMapping("/users/{id}")
public UserDto getUser(@PathVariable Long id) { ... }

@GetMapping("/users/{userId}/orders/{orderId}")
public OrderDto getOrder(@PathVariable Long userId, @PathVariable Long orderId) { ... }

// Если имя переменной отличается от имени параметра:
@GetMapping("/users/{user-id}")
public UserDto get(@PathVariable("user-id") Long userId) { ... }

// Условно-опциональный вариант:
@GetMapping({"/users/{id}", "/users"})
public UserDto get(@PathVariable(required = false) Long id) { ... }

@RequestParam

Берет query parameter или form parameter.

// GET /users?role=admin&page=2
@GetMapping("/users")
public List<UserDto> getUsers(
    @RequestParam String role,
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(required = false) String name
) { ... }

// Несколько значений одного параметра:
// GET /filter?tag=java&tag=spring
@GetMapping("/filter")
public List<Item> filter(@RequestParam List<String> tag) { ... }

@RequestBody

Читает body HTTP-запроса и десериализует его через HttpMessageConverter.

@PostMapping("/users")
public UserDto create(@RequestBody CreateUserRequest request) { ... }

@PostMapping("/users")
public UserDto create(@RequestBody @Valid CreateUserRequest request) { ... }

@ModelAttribute

Используется для binding form/query parameters в Java-объект.

// GET /search?name=Alice&age=25
@GetMapping("/search")
public String search(@ModelAttribute SearchForm form, Model model) { ... }

// POST /register с form data
@PostMapping("/register")
public String register(@ModelAttribute @Valid RegisterForm form, BindingResult result) { ... }

@ModelAttribute на методе может заранее положить данные в model:

@ModelAttribute("categories")
public List<Category> categories() {
    return categoryService.findAll();
}

@RequestHeader

Извлекает значение HTTP header.

@GetMapping("/profile")
public UserDto getProfile(
    @RequestHeader("Authorization") String authHeader,
    @RequestHeader(value = "X-Request-Id", required = false) String requestId
) { ... }

@CookieValue

Извлекает значение cookie.

@GetMapping("/dashboard")
public String dashboard(@CookieValue("sessionId") String sessionId) { ... }

@RequestPart

Используется в multipart/form-data, когда одна часть запроса - файл, а другая - JSON или другой объект.

@PostMapping("/upload")
public void upload(
    @RequestPart("file") MultipartFile file,
    @RequestPart("meta") @Valid FileMeta meta
) { ... }

HttpServletRequest / HttpServletResponse

Низкоуровневый доступ к servlet API.

@GetMapping("/info")
public String info(HttpServletRequest request, HttpServletResponse response) {
    String ip = request.getRemoteAddr();
    response.setHeader("X-Custom", "value");
    return "info";
}

HttpSession

Прямой доступ к server-side session.

@GetMapping("/cart")
public CartDto getCart(HttpSession session) {
    return (CartDto) session.getAttribute("cart");
}

Principal / Authentication

Текущий аутентифицированный пользователь. Чаще используется вместе со Spring Security.

@GetMapping("/me")
public UserDto getCurrentUser(Principal principal) {
    return userService.findByUsername(principal.getName());
}

Locale

Текущая локаль запроса.

@GetMapping("/greeting")
public String greeting(Locale locale, Model model) {
    model.addAttribute("msg", messageSource.getMessage("hello", null, locale));
    return "greeting";
}

Model / ModelMap / Map

Используются для передачи данных в HTML view.

@GetMapping("/users/{id}")
public String profile(@PathVariable Long id, Model model) {
    model.addAttribute("user", userService.findById(id));
    model.addAttribute("roles", roleService.findAll());
    return "users/profile";
}

@SessionAttribute

Читает уже существующий session attribute. Это не то же самое, что @SessionAttributes, который сохраняет model attributes в session для controller workflow.

@GetMapping("/checkout")
public String checkout(@SessionAttribute("cart") Cart cart, Model model) {
    model.addAttribute("cart", cart);
    return "checkout";
}

@RequestAttribute

Читает attribute, который ранее положил filter, interceptor или servlet container.

@GetMapping("/trace")
public String trace(@RequestAttribute("traceId") String traceId) { ... }

@MatrixVariable

Извлекает параметры внутри URI segment.

GET /cars;color=red;year=2024
@GetMapping("/cars")
public List<CarDto> cars(@MatrixVariable String color,
                         @MatrixVariable int year) { ... }

На практике @MatrixVariable встречается редко и может требовать явной настройки path matching / URL handling.


Data Binding

Data Binding — автоматическое преобразование HTTP-данных из URL/query/form fields в Java-объекты. Spring MVC выполняет binding через WebDataBinder. JSON/XML body для @RequestBody читается прежде всего через HttpMessageConverter. После десериализации может запускаться validation.

Привязка request-параметров к Java-объекту

// GET /search?name=Alice&minAge=18&maxAge=30
public class SearchCriteria {
    private String name;
    private int minAge;
    private int maxAge;
    // getters/setters
}

@GetMapping("/search")
public List<UserDto> search(@ModelAttribute SearchCriteria criteria)
// Spring сам свяжет query params с полями SearchCriteria

WebDataBinder

WebDataBinder выполняет: - type conversion (String -> int, String -> Date, ...), - binding полей, - валидацию.

Можно кастомизировать через @InitBinder в контроллере или @ControllerAdvice:

@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.setDisallowedFields("id", "createdAt"); // запрещаем биндить эти поля
    SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy");
    binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}

Аннотация @InitBinder помечает метод, который настраивает WebDataBinder перед binding'ом аргументов контроллера. Частые задачи: - запретить опасные поля (id, role, createdAt) для защиты от mass assignment, - зарегистрировать legacy PropertyEditor, - подключить кастомный validator для конкретной формы.

Типовые преобразования данных

Встроенные конверторы и formatter'ы: - String -> int, long, double, boolean - String -> LocalDate, LocalDateTime, OffsetDateTime при стандартных ISO-форматах или через @DateTimeFormat - String -> enum - String -> UUID

Custom converters и formatters

Converter — конвертирует один тип в другой:

@Component
public class StringToStatusConverter implements Converter<String, Status> {
    @Override
    public Status convert(String source) {
        return Status.valueOf(source.toUpperCase());
    }
}

Аннотации и API раздела:

Аннотация / API Где Что делает
@InitBinder метод controller/advice Настраивает WebDataBinder для binding'а request data.
@DateTimeFormat поле/параметр Задает формат parsing/printing для date/time типов в MVC binding.
@NumberFormat поле/параметр Задает формат чисел в MVC binding.
Converter<S,T> bean/config Одностороннее преобразование типа.
Formatter<T> bean/config String ↔ object с учетом Locale.
WebMvcConfigurer#addFormatters config Регистрирует converters/formatters глобально для MVC.

Formatter — конвертирует String ↔ Object с учетом Locale:

@Component
public class MoneyFormatter implements Formatter<Money> {
    @Override
    public Money parse(String text, Locale locale)
    @Override
    public String print(Money money, Locale locale)
}

Регистрация:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToStatusConverter());
    }
}

Ошибки binding

Если binding не удался (например, буква вместо числа для int-поля): - создается BindingResult с ошибками типа FieldError, - если BindingResult объявлен сразу после bindable-объекта — контроллер сам решает, что делать с ошибками, - если подходящего BindingResult нет — Spring выбрасывает exception; конкретный тип зависит от аргумента: MethodArgumentNotValidException, BindException, MethodArgumentTypeMismatchException, HandlerMethodValidationException и др.


Валидация данных

@Valid - Стандартная аннотация из Jakarta Bean Validation (jakarta.validation.Valid). Запускает валидацию объекта:

@PostMapping("/users")
public UserDto create(@RequestBody @Valid CreateUserRequest request)

BindingResult - Содержит ошибки валидации и binding. Должен идти сразу после валидируемого объекта:

@PostMapping("/register")
public String register(@Valid @ModelAttribute RegisterForm form, BindingResult result) {
    if (result.hasErrors()) {
        return "register"; // вернуть форму с ошибками
    }
    userService.register(form);
    return "redirect:/login";
}

Bean Validation - Аннотации из jakarta.validation:

Аннотация Назначение
@NotNull не null
@NotBlank не null, не пустая строка (включает trim)
@NotEmpty не null, не пустая коллекция/строка
@Size(min, max) длина строки или размер коллекции
@Min(value) минимальное числовое значение
@Max(value) максимальное числовое значение
@Email формат email
@Pattern(regexp) regex
@Positive > 0
@PositiveOrZero >= 0
@Past / @Future дата в прошлом/будущем
@Valid вложенная валидация

Обработка ошибок валидации

При использовании @RequestBody @Valid без BindingResult: - выбрасывается MethodArgumentNotValidException, - обрабатывается через @ExceptionHandler или @ControllerAdvice.

При validation constraints прямо на параметрах метода (@PathVariable @Min(1), @RequestParam @NotBlank) в Spring 6.1+ обычно выбрасывается HandlerMethodValidationException.

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult().getFieldErrors().stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .toList();
        return new ErrorResponse("Validation failed", errors);
    }

    @ExceptionHandler(HandlerMethodValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleMethodValidation(HandlerMethodValidationException ex) {
        return new ErrorResponse("Validation failed", List.of(ex.getMessage()));
    }
}

Возвращаемые значения контроллеров

String как имя view

@GetMapping("/home")
public String home() {
    return "home"; // ViewResolver ищет templates/home.html
}

Model + view name

@GetMapping("/users/{id}")
public String user(@PathVariable Long id, Model model) {
    model.addAttribute("user", userService.findById(id));
    return "users/detail";
}

ModelAndView

@GetMapping("/users/{id}")
public ModelAndView user(@PathVariable Long id) {
    ModelAndView mav = new ModelAndView("users/detail");
    mav.addObject("user", userService.findById(id));
    return mav;
}

void

Если response пишется вручную через HttpServletResponse:

@GetMapping("/download")
public void download(HttpServletResponse response) throws IOException {
    response.setContentType("application/pdf");
    response.getOutputStream().write(fileBytes);
}

DTO / object

Только при @ResponseBody или @RestController:

@GetMapping("/users/{id}")
public UserDto getUser(@PathVariable Long id) {
    return userService.findById(id); // сериализуется в JSON
}

@ResponseBody

Указывает, что return value должен писаться прямо в тело ответа:

@Controller
public class UserController {
    @GetMapping("/api/users/{id}")
    @ResponseBody
    public UserDto getUser(@PathVariable Long id)
}

ResponseEntity

Полный контроль над ответом: статус, заголовки, тело:

@GetMapping("/users/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
    return userService.findById(id)
        .map(user -> ResponseEntity.ok(user))
        .orElse(ResponseEntity.notFound().build());
}

@PostMapping("/users")
public ResponseEntity<UserDto> create(@RequestBody @Valid CreateUserRequest req) {
    UserDto created = userService.create(req);
    URI location = URI.create("/api/users/" + created.getId());
    return ResponseEntity.created(location).body(created);
}

// Кастомные заголовки:
return ResponseEntity.ok()
    .header("X-Custom-Header", "value")
    .body(dto);

HttpEntity

Как ResponseEntity, но без статус-кода (только заголовки + тело):

@GetMapping("/info")
public HttpEntity<InfoDto> info() {
    HttpHeaders headers = new HttpHeaders();
    headers.add("X-Info", "value");
    return new HttpEntity<>(new InfoDto(), headers);
}

redirect

// Простой redirect:
@PostMapping("/login")
public String login() {
    return "redirect:/dashboard";
}

// Через RedirectView:
@PostMapping("/submit")
public View submit() {
    return new RedirectView("/success");
}

// Через ResponseEntity:
@GetMapping("/old-path")
public ResponseEntity<Void> redirect() {
    return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY)
        .location(URI.create("/new-path"))
        .build();
}

forward

@GetMapping("/old")
public String forward() {
    return "forward:/new";  // внутренний forward, URL не меняется
}

Возврат файлов и ресурсов

// через Resource:
@GetMapping("/files/{filename}")
public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
    Resource resource = new FileSystemResource(Paths.get(uploadDir, filename));
    return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
        .contentType(MediaType.APPLICATION_OCTET_STREAM)
        .body(resource);
}