Контроллеры¶
Контроллер - бин с @Controller или @RestController, методы которого обрабатывают HTTP-запросы. Принимает
данные запроса через параметры метода, вызывает сервисный слой и возвращает результат: имя view, DTO,
ResponseEntity. См. Путь запроса.
@Controller и @RestController¶
@Controller - стереотип MVC-контроллера для HTML-приложений.
- класс находится component scan'ом, как любой
@Component - по умолчанию результат метода - имя view, его ищет
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 - @Controller + @ResponseBody на уровне класса, для REST API.
- все методы возвращают тело ответа напрямую
ViewResolverне используется, результат сериализуетHttpMessageConverter
@RestController
@RequestMapping("/api/users")
public class UserRestController {
@GetMapping("/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.findById(id); // сериализуется в JSON
}
}
@ResponseBody - отключает трактовку результата как имени view и передаёт значение в HttpMessageConverter.
Частая ошибка - перепутать эти два вида контроллеров (см. Частые ошибки).
Маппинг запросов¶
@RequestMapping - базовая аннотация маппинга. На классе задаёт базовый URL, на методе уточняет маппинг. Без
method маппинг работает для всех HTTP-методов.
@RequestMapping(
value = "/users", // шаблон URL
method = RequestMethod.GET, // HTTP-метод
params = "active=true", // query-параметр
headers = "X-API=v2", // заголовок
consumes = "application/json", // Content-Type входящего запроса
produces = "application/json" // что отдаём, сверяется с Accept клиента
)
Сокращения - @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping. Принимают те же
атрибуты: params, headers, consumes, produces.
Шаблоны URL:
@GetMapping("/users/{id}") // path variable
@GetMapping("/files/**") // любой суффикс
@GetMapping("/users/{id}/orders") // вложенный ресурс
@GetMapping("/v{version}/users") // переменная внутри сегмента
Шаблоны по умолчанию разбирает PathPatternParser. Старый AntPathMatcher встречается в legacy-проектах, новые
приложения проектируют под PathPatternParser.
Маппинг по params, headers, consumes, produces:
// по params
@GetMapping(value = "/users", params = "active=true")
@GetMapping(value = "/users", params = "!active") // параметра нет
@GetMapping(value = "/users", params = {"active", "role=admin"})
// по 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), несколько вариантов - content negotiation
@GetMapping(value = "/users", produces = {
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_XML_VALUE
})
Версионирование API (Spring Framework 7)¶
В Spring Framework 7 в MVC появилась встроенная поддержка версий API. Сначала указывают, откуда брать версию:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer.useRequestHeader("API-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+") // 2.0 и новее
public AccountDto getV2AndNewer(@PathVariable Long id)
- версию можно брать из заголовка, query-параметра, сегмента пути или параметра media type
- неподдерживаемая версия -
InvalidApiVersionException, обычно это400 Bad Request
Параметры методов¶
Шпаргалка - откуда что берётся:
| Аннотация или тип | Откуда |
|---|---|
@RequestParam |
Query-параметры, form-data, x-www-form-urlencoded |
@PathVariable |
Переменные в URL |
@ModelAttribute |
Данные формы или query-параметры в POJO |
@RequestBody |
JSON или XML тело запроса |
@RequestHeader |
HTTP-заголовки |
@CookieValue |
Cookie |
@SessionAttribute |
Атрибут из HTTP-сессии |
@RequestAttribute |
Атрибут запроса, который положил фильтр или interceptor |
@RequestPart |
Часть multipart/form-data: файл или JSON |
@MatrixVariable |
Параметры внутри сегмента URI |
HttpServletRequest, HttpServletResponse |
Сырой servlet API |
HttpSession |
Сессия целиком |
Principal, Authentication |
Текущий пользователь |
Locale |
Локаль запроса |
Model, ModelMap, Map |
Данные для HTML view |
Аргументы собирает HandlerMethodArgumentResolver (см. HandlerAdapter).
@PathVariable - значение из шаблона пути.
@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-параметр или поле формы. Для простых типов: String, int, boolean и т.п.
// 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 - тело запроса, десериализованное через
HttpMessageConverter.
@ModelAttribute - связывает поля формы или query-параметры с объектом (POJO).
// 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 заранее кладёт данные в модель для всех методов контроллера, а в @ControllerAdvice -
для всех контроллеров:
@ModelAttribute("categories")
public List<Category> categories() {
return categoryService.findAll();
}
См. HTML-формы.
@RequestHeader - значение HTTP-заголовка.
@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(name = "sessionId", defaultValue = "unknown") 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 - прямой доступ к серверной сессии.
@GetMapping("/cart")
public CartDto getCart(HttpSession session) {
return (CartDto) session.getAttribute("cart");
}
@SessionAttribute - читает уже существующий атрибут сессии. Не путать с @SessionAttributes, который сохраняет
атрибуты модели в сессию между запросами (см. Сессии).
@GetMapping("/checkout")
public String checkout(@SessionAttribute("cart") Cart cart, Model model) {
model.addAttribute("cart", cart);
return "checkout";
}
@RequestAttribute - атрибут, который раньше положил фильтр, interceptor или servlet-контейнер.
Principal, Authentication - текущий пользователь, обычно вместе со Spring Security (см. 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 (см. Model).
@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";
}
@MatrixVariable - параметры внутри сегмента URI. Встречается редко. Сегмент с параметрами должен совпадать с URI-переменной в шаблоне.
@GetMapping("/cars/{model}")
public List<CarDto> cars(@PathVariable String model,
@MatrixVariable String color,
@MatrixVariable int year) { ... }
С PathPatternParser матричные переменные работают сразу. Со старым UrlPathHelper нужно было отключить удаление
содержимого после ;.
Data Binding¶
Data Binding - автоматическое превращение данных из URL, query и полей формы в Java-объекты. Выполняет
WebDataBinder. JSON и XML тело для @RequestBody читает не он, а HttpMessageConverter. После binding'а или
десериализации может запуститься валидация.
// 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) // query-параметры лягут в поля
WebDataBinder - объект, который делает binding: конвертация типов (String -> int, String -> дата),
заполнение полей, валидация.
@InitBinder - метод, который настраивает WebDataBinder перед binding'ом аргументов. Ставится в контроллере или в
@ControllerAdvice. Частые задачи:
- запретить опасные поля (
id,role,createdAt) - защита от mass assignment - зарегистрировать legacy
PropertyEditor - подключить свой validator для конкретной формы
@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)); // legacy java.util.Date
}
Для java.time вместо PropertyEditor используют @DateTimeFormat на поле или параметре.
Встроенные преобразования:
String->int,long,double,booleanString->LocalDate,LocalDateTime,OffsetDateTimeв ISO-формате или через@DateTimeFormatString->enumString->UUID
Converter - одностороннее преобразование одного типа в другой.
@Component
public class StringToStatusConverter implements Converter<String, Status> {
@Override
public Status convert(String source) {
return Status.valueOf(source.toUpperCase());
}
}
Formatter - преобразование String <-> объект с учётом 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());
}
}
| Аннотация или API | Где | Что делает |
|---|---|---|
@InitBinder |
Метод контроллера или advice | Настраивает WebDataBinder |
@DateTimeFormat |
Поле или параметр | Формат дат и времени |
@NumberFormat |
Поле или параметр | Формат чисел |
Converter<S, T> |
Бин или конфиг | Одностороннее преобразование типа |
Formatter<T> |
Бин или конфиг | String <-> объект с учётом Locale |
WebMvcConfigurer#addFormatters |
Конфиг | Глобальная регистрация converters и formatters |
Ошибки binding'а. Если binding не удался, например буква вместо числа для int:
- создаётся
BindingResultсFieldError BindingResultобъявлен сразу после bindable-объекта - контроллер сам решает, что делатьBindingResultнет - Spring бросает исключение, тип зависит от аргумента:MethodArgumentNotValidException,BindException,MethodArgumentTypeMismatchException,HandlerMethodValidationException
Валидация¶
@Valid (jakarta.validation.Valid) - запускает Bean Validation объекта.
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) |
Регулярное выражение |
@Positive, @PositiveOrZero |
> 0 и >= 0 |
@Past, @Future |
Дата в прошлом или будущем |
@Valid |
Вложенная валидация |
Ошибки валидации без BindingResult:
@RequestBody @Valid-MethodArgumentNotValidException- ограничения прямо на параметрах (
@PathVariable @Min(1),@RequestParam @NotBlank) - в Spring 6.1+HandlerMethodValidationException - обрабатываются через
@ExceptionHandlerв@RestControllerAdvice(см. Обработку исключений)
@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()));
}
}
Итого: валидируй на границе приложения - DTO в контроллере, а не сущности в репозитории (см. Лучшие практики).
Возвращаемые значения¶
String - имя view:
@GetMapping("/home")
public String home() {
return "home"; // ViewResolver ищет templates/home.html
}
Model + имя view:
@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 - ответ пишется вручную через HttpServletResponse:
@GetMapping("/download")
public void download(HttpServletResponse response) throws IOException {
response.setContentType("application/pdf");
response.getOutputStream().write(fileBytes);
}
DTO или любой объект - только при @ResponseBody или @RestController, сериализуется в JSON:
@GetMapping("/users/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.findById(id);
}
@ResponseBody на отдельном методе обычного контроллера:
@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(ResponseEntity::ok)
.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); // 201 + Location
}
// свои заголовки
return ResponseEntity.ok()
.header("X-Custom-Header", "value")
.body(dto);
HttpEntityResponseEntity, но без статуса: только заголовки и тело.
@GetMapping("/info")
public HttpEntity<InfoDto> info() {
HttpHeaders headers = new HttpHeaders();
headers.add("X-Info", "value");
return new HttpEntity<>(new InfoDto(), headers);
}
Redirect:
@PostMapping("/login")
public String login() {
return "redirect:/dashboard";
}
@PostMapping("/submit")
public View submit() {
return new RedirectView("/success");
}
@GetMapping("/old-path")
public ResponseEntity<Void> redirect() {
return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY)
.location(URI.create("/new-path"))
.build();
}
Forward - внутренний переход, URL в браузере не меняется:
См. Redirect и Forward.
Файл через 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);
}
См. Файлы.