Контроллеры¶
Контроллеры¶
Контроллер — это 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.
@MatrixVariable¶
Извлекает параметры внутри URI segment.
@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).
Запускает валидацию объекта:
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);
}