Обработка HTTP-запросов в Spring Boot упрощается аннотациями вроде @RequestMapping. Хотя отображения лаконичнее определяются при помощи @GetMapping и @PostMapping, в основу этих аннотаций положена @RequestMapping. Расскажем, как в Spring Boot обрабатываются аннотации, включая сопоставление путей, разрешение HTTP-методов и привязку параметров. Сфокусируемся на их внутренних механизмах.

Основы @RequestMapping и специализированных аннотаций

В Spring Boot @RequestMapping  —  основополагающая аннотация, ею HTTP-запросы отображаются на методы обработчика в классах контроллеров. При этом указываются путь URL-адреса, HTTP-метод, заголовки и другие детали запросов, которыми активируются конкретные методы контроллера.

Пример синтаксиса:

@RestController
public class ExampleController {

@RequestMapping(value = "/example", method = RequestMethod.GET)
public String handleGetRequest() {
return "Handled GET request";
}
}

Хотя для определения отображений запросов в @RequestMapping имеется гибкая структура, для типичных HTTP-методов в Spring представили специализированные аннотации, которыми сокращается шаблонный код:

  • @GetMapping для RequestMethod.GET;
  • @PostMapping для RequestMethod.POST;
  • @PutMapping для RequestMethod.PUT;
  • @DeleteMapping для RequestMethod.DELETE;
  • @PatchMapping для RequestMethod.PATCH.

Эти аннотации  —  синтаксические конструкции, которые предопределенными HTTP-методами, по сути, делегируются в @RequestMapping.

Пример сравнения:

// Эквивалент «@RequestMapping(method = RequestMethod.GET)»
@GetMapping("/example")
public String handleGetRequest() {
return "Handled GET request";
}

Несмотря на удобство этих специализированных аннотаций, базовый процесс обработки запросов по-прежнему управляется теми же основными компонентами, которыми контролируется @RequestMapping.

Как внутри Spring Boot выполняется отображение запросов

Когда запускается приложение Spring Boot, в нем инициируются процессы для обнаружения, регистрации и управления отображениями HTTP-запросов. Это осуществляется основными компонентами фреймворка Spring Web MVC, в частности классами RequestMappingHandlerMapping и RequestMappingHandlerAdapter. Чтобы понять, как входящие запросы соотносятся в Spring Boot с конкретными методами обработчика в классах контроллеров, разберем функционирование этих компонентов.

Обнаружение отображения запросов при запуске

Процесс отображения запросов начинается на этапе инициализации контекста приложения. Когда запускается Spring Boot, в компонентах определенных базовых пакетов разыскиваются классы, аннотированные как @Controller или @RestController. Эти классы считаются кандидатами для отображения запросов.

Обнаруженные классы проверяются в Spring на наличие методов, аннотированных как @RequestMapping, или ее специализированных вариантов: @GetMapping, @PostMapping и т. д. Обнаружение этих методов контролируется в RequestMappingHandlerMapping  —  центральном компоненте, который занимается отображением HTTP-запросов на методы обработчика.

RequestMappingHandlerMapping  —  регистрация отображений

Классом RequestMappingHandlerMapping расширяется AbstractHandlerMethodMapping  —  обобщенный класс для отображения HTTP-запросов на методы. На этапе запуска им выполняются такие задачи:

  1. Сканирование обработчиков: в контексте приложения ищутся компоненты, аннотированные как @Controller или @RestController.
  2. Нахождение методов обработчика: для каждого обнаруженного контроллера выявляются методы, аннотированные как @RequestMapping или ее синтаксическими конструкциями.
  3. Сбор информации об отображении: создаются объекты RequestMappingInfo, в которые инкапсулируются сведения: путь URL-адреса, HTTP-метод, заголовки, типы носителей.
  4. Регистрация отображений: эти объекты RequestMappingInfo затем привязываются к соответствующим методам обработчика и, чтобы ускорить поиск при обработке запроса, сохраняются в реестре отображений.

RequestMappingInfo  —  это контейнер метаданных со всей необходимой информацией о направлении конкретного HTTP-запроса в метод контроллера, которая хранится в структуре данных  —  обычно в Map, где ключом является RequestMappingInfo, а значением  —  HandlerMethod.

Пример внутреннего отображения:

  • RequestMappingInfo для GET /example.
  • Путь: /example.
  • Метод: GET.
  • HandlerMethod: ExampleController.handleGetRequest().

Когда получается запрос, этим отображением в Spring быстро отыскивается корректный метод обработчика.

Стратегия сопоставления путей

Когда отображения зарегистрированы, стратегией сопоставления путей в Spring Boot определяется, каким обработчиком обрабатывать входящий запрос. По умолчанию классом AntPathMatcher здесь поддерживается простое сопоставление с образцом на основе синтаксиса в стиле Ant:

  • *  —  сопоставляется ноль или более символов в одном сегменте пути;
  • **  —  сопоставляется ноль или более каталогов в пути;
  • {variable}  —  обозначаются переменные пути, динамически извлекаемые из URL-адреса.

Сопоставление путей в действии
Например, в этих отображениях:

  • /files/* сопоставляется с /files/image.jpg;
  • /files/** сопоставляется с /files/images/2023/photo.png;
  • /files/{filename} сопоставляется с /files/report.pdf, при этом filename привязано к report.pdf.

Входящие запросы обрабатываются в AntPathMatcher сравнением унифицированного идентификатора ресурса запроса с зарегистрированными шаблонами, при необходимости используются совпадение с трафаретным символом и извлечение переменных пути.

Парсинг шаблонов путей в Spring 5.3+

В Spring 5.3 появился PathPatternParser  —  альтернатива AntPathMatcher. В этом парсере выше производительность, поддерживается дополнительный функционал вроде чувствительного к регистру сопоставления и более эффективного парсинга сложных шаблонов путей.

Разработчики переключаются на PathPatternParser, настроив свойства приложения:

spring.mvc.pathmatch.matching-strategy=path_pattern_parser

Это изменение сказывается на том, как в Spring обрабатываются и сопоставляются пути запросов, контроль над поведением при сопоставлении путей становится более детальным.

Разрешение HTTP-метода

Когда получается HTTP-запрос, в Spring Boot определяется соответствующий метод обработчика: указанный в заголовке запроса HTTP-метод проверяется и сопоставляется с зарегистрированными отображениями. В каждом зарегистрированном через RequestMappingHandlerMapping отображении содержатся сведения о разрешенных HTTP-методах, запросы направляются в корректный метод.

Если HTTP-метод в запросе совпадает с одним из методов, зарегистрированных для запрашиваемого пути, вызывается соответствующий обработчик. Если совпадения нет, в Spring возвращается ответ 405 Method Not Allowed.

Пример разрешения HTTP-метода
Рассмотрим такой контроллер:

@RestController
public class ExampleController {

@RequestMapping(value = "/example", method = RequestMethod.GET)
public String handleGetRequest() {
return "Handled GET request";
}

@RequestMapping(value = "/example", method = RequestMethod.POST)
public String handlePostRequest() {
return "Handled POST request";
}
}

Здесь для пути /example регистрируется два метода обработчика:

  1. handleGetRequest() для GET /example.
  2. handlePostRequest() для POST /example.

Теперь посмотрим, как в Spring Boot разрешаются входящие запросы:

  1. В /example получен GET-запрос.
    В Spring проверяется реестр отображений и обнаруживается, что /example для GET-метода отображается на handleGetRequest(). Поскольку имеется совпадение, в Spring запрос перенаправляется в handleGetRequest() и возвращается ответ "Handled GET request".
  2. В /example получен POST-запрос.
    В Spring проверяются отображения и в качестве обработчика для POST-метода в /example обнаруживается handlePostRequest(), куда направляется запрос, а обратно возвращается "Handled POST request".
  3. В /example получен PUT-запрос.
    В Spring разыскивается обработчик, которым путь /example сопоставляется с методом PUT. Поскольку такого отображения нет, в Spring возвращается 405 Method Not Allowed, то есть запрошенный HTTP-метод для этого пути не поддерживается.

Что происходит внутри
Во время запуска в Spring сканируется контекст приложения и для пути /example регистрируется два отображения: одним handleGetRequest() связывается с RequestMethod.GET, другим handlePostRequest()  —  с RequestMethod.POST. Когда поступает запрос, в Spring сначала проверяется соответствие пути запроса какому-либо из зарегистрированных путей. Если найдено совпадение, проверяется соответствие указанного в запросе HTTP-метода разрешенным для этого пути методам. Если и путь, и метод совпадают с зарегистрированным отображением, вызывается соответствующий метод обработчика. Если путь совпадает, а HTTP-метод  —  нет, в Spring возвращается ошибка 405 Method Not Allowed. Этим процессом обеспечивается обработка каждого запроса исключительно методами обработчика, явно сконфигурированными для поддержки заданных HTTP-методов, чем поддерживается точный контроль над обработкой запросов.

Комбинирование сопоставления путей и методов

На практике, чтобы для каждого запроса выявить корректный метод обработки, сопоставление путей и разрешение HTTP-методов в Spring Boot комбинируются. Процесс этот состоит из таких этапов:

  1. Сопоставление путей: унифицированный идентификатор ресурса запроса сопоставляется с зарегистрированными шаблонами путей.
  2. Сопоставление методов: для каждого совпадающего пути в Spring проверяется, соответствует ли HTTP-метод в запросе разрешенным методам для этого пути.
  3. Выбор оптимального соответствия: если запросу соответствует несколько обработчиков, на основе шаблона путей и HTTP-метода в Spring выбирается наиболее точное соответствие.

Рассмотрим такие отображения:

@GetMapping("/items")
public String getAllItems() { return "All items"; }

@GetMapping("/items/{id}")
public String getItem(@PathVariable String id) { return "Item: " + id; }
  • Первому методу соответствует запрос в GET /items.
  • Второму методу соответствует запрос в GET /items/123 с id = 123.

В Spring выбирается обработчик, который наиболее соответствует и пути, и HTTP-методу, так что запрос направляется в корректный метод.

RequestMappingHandlerAdapter  —  вызов метода

Когда корректный метод обработки выявлен, он вызывается в Spring Boot при помощи RequestMappingHandlerAdapter. Этот адаптер занимается подготовкой аргументов метода, вызовом метода и обработкой возвращаемого значения.

Если в RequestMappingHandlerMapping на обработчики отображаются запросы, то в RequestMappingHandlerAdapter выполняются методы обработчика и, чтобы управлять полным жизненным циклом запроса и ответа, осуществляется взаимодействие с компонентами вроде ArgumentResolvers, ReturnValueHandlers и HttpMessageConverters.

Адаптером из реестра отображений извлекается метод обработчика, разрешаются любые необходимые аргументы, через рефлексию вызывается метод. После того как метод выполнен, адаптером обрабатывается возвращаемое значение и преобразуется в соответствующий формат ответа: JSON, XML или обычный текст.

Обработка исключений и генерирование ответов

Если при отображении запросов появляются ошибки, например не найден обработчик или не поддерживается HTTP-метод, соответствующие ответы генерируются в Spring Boot механизмами обработки исключений:

  • 404 Not Found: возвращается, когда для запрошенного пути не найден соответствующий обработчик;
  • 405 Method Not Allowed: возвращается, когда путь совпадает, а HTTP-метод  —  нет;
  • 400 Bad Request: возвращается при сбое привязки параметров или сбое преобразования типов.

Чтобы управлять этими исключениями и настраивать ответы на ошибки, в Spring Boot применяются методы @ExceptionHandler или глобальные классы @ControllerAdvice. Так обеспечивается согласованная обработка ошибок в приложении.

Привязка параметров и вызов метода

После того как в Spring Boot выявляется соответствующий метод обработчика для входящего запроса, следующий этап  —  подготовка метода к выполнению. Это привязка данных запроса к параметрам метода и управление процессом вызова. Процесс привязки очень гибкий: данные из разных частей HTTP-запроса, таких как параметры запросов, переменные путей, заголовки и текст запроса, добавляются непосредственно в аргументы метода. Рассмотрим, как в Spring Boot выполняются привязка параметров и вызов метода.

Разрешение аргументов метода

Аргументы методов в Spring Boot разрешаются интерфейсом HandlerMethodArgumentResolver, которым определяется контракт для преобразования частей HTTP-запроса в объекты Java, передаваемые в методы контроллера.

При запуске приложения в Spring Boot регистрируется набор резолверов аргументов, каждый из которых предназначен для обработки конкретных типов аннотаций и типов-параметров. Вот самые распространенные резолверы:

  • RequestParamMethodArgumentResolver для обработки @RequestParam
  • PathVariableMethodArgumentResolver для обработки @PathVariable
  • RequestHeaderMethodArgumentResolver для обработки @RequestHeader
  • RequestBodyMethodArgumentResolver для обработки @RequestBody

Когда HTTP-запрос направляется в метод обработчика, в RequestMappingHandlerAdapter перебираются резолверы зарегистрированных аргументов и определяется, как заполнять каждый параметр метода.

Пример разрешения аргументов

@GetMapping("/user/{id}")
public String getUser(@PathVariable int id, @RequestParam String name) {
return "User ID: " + id + ", Name: " + name;
}

В Spring данные запроса привязываются к параметрам метода различными резолверами. Резолвером PathVariableMethodArgumentResolver из пути URL-адреса извлекается значение {id} и преобразуется в int, а в RequestParamMethodArgumentResolver из URL-адреса запроса получается параметр запроса name. Извлеченные данные перед передачей в метод преобразуются обоими резолверами с помощью ConversionService в корректные типы Java.

Это поэтапный процесс:

  1. Проверка параметров метода
    Для выявления аннотаций вроде @PathVariable и @RequestParam в Spring проверяется сигнатура метода.
  2. Выбор резолвера
    На основе аннотаций параметров и типов-параметров фреймворком выбираются соответствующие резолверы аргументов.
  3. Извлечение и преобразование данных
    Релевантные данные извлекаются резолвером из запроса и преобразуются в соответствующий тип Java благодаря ConversionService.
  4. Добавление параметров
    Преобразованные значения добавляются в аргументы метода, после чего метод готов к вызову.

Привязка тела запроса с @RequestBody

Для методов, которыми обрабатывается тело запроса, в Spring Boot используется RequestBodyMethodArgumentResolver. Этим резолвером считывается полезная нагрузка входящего запроса и с помощью HttpMessageConverter преобразуется в объект Java.

Пример:

@PostMapping("/submit")
public String submitData(@RequestBody Data data) {
return "Received data: " + data.toString();
}

В RequestBodyMethodArgumentResolver обрабатывается привязка тела запроса к объекту Data. Сначала проверкой заголовка Content-Type входящего запроса определяется, как обрабатывать текст запроса, например, интерпретировать его как JSON или XML. Исходя из этого, в Spring выбирается соответствующий HttpMessageConverter. Для полезной нагрузки JSON обычно применяется MappingJackson2HttpMessageConverter с библиотекой Jackson для десериализации данных JSON в объект Data. При проблемах с десериализацией  —  некорректно сформированый JSON или несоответствие типов данных  —  в Spring выбрасывается исключение HttpMessageNotReadableException, которое чревато ответом «400 Bad Request», если не реализована пользовательская обработка исключений.

Преобразование данных и обработка типов

Преобразования типов во время привязки параметров осуществляются в Spring Boot при помощи ConversionService. При этом данные преобразуются из типичных для HTTP-запросов строковых представлений в сложные типы Java, требуемые методу обработчика.

В Spring Boot реализацией ConversionService по умолчанию является DefaultFormattingConversionService, которой поддерживаются разнообразные преобразования стандартных типов:

  • Примитивные типы: строки в int, double, boolean и т. д.
  • Дата и время: строки в LocalDate, LocalDateTime и другие классы Java Time API.
  • Коллекции: строки в массивы или коллекции, например преобразование разделенной запятыми строки в список List.

Кроме того, в Spring регистрируются пользовательские конвертеры  —  для преобразования сложных или специфичных для предметной области типов данных.

Пример преобразования типов:

@GetMapping("/convert")
public String convertDate(@RequestParam LocalDate date) {
return "Converted date: " + date.toString();
}

В этом примере параметр запроса date=2023-10-09 автоматически преобразуется из строки в объект LocalDate благодаря зарегистрированным конвертерам ConversionService.

Проверка параметров

Чтобы проверять параметры во время привязки, Spring Boot интегрируется с Bean Validation API  —  JSR 380. Аннотациями @Valid или @Validated запускается автоматическая проверка параметров метода или текстов запросов на основе ограничений, определенных в классах Java.

Пример проверки параметров:

@PostMapping("/validate")
public String validateData(@RequestBody @Valid Data data) {
return "Valid data received";
}

Здесь аннотацией @Valid процесс проверки запускается сразу после привязывания тела запроса к объекту Data, но до вызова метода. С Bean Validation API, обычно поддерживаемым Hibernate Validator, в Spring проверяются любые нарушения определенных в классе Data ограничений: @NotNull, @Size или @Pattern. Если ограничение нарушено, в Spring выбрасывается исключение MethodArgumentNotValidException, что чревато ответом «400 Bad Request». Разработчики обрабатывают эти ошибки валидации пользовательским @ExceptionHandler, если требуется более конкретная обратная связь или форматирование ошибок.

Вызов метода

После привязки параметров в Spring Boot вызывается метод обработчика при помощи рефлексии, управляемой в RequestMappingHandlerAdapter.

Сначала из реестра извлекается соответствующий объект HandlerMethod, выявленный при отображении запросов. Затем, чтобы передавать методу корректные типы данных, с помощью различных резолверов и ConversionService в Spring разрешаются и преобразуются все аргументы метода.

Когда аргументы подготовлены, метод вызывается через API-интерфейсы Java Reflection. Любые исключения во время этого процесса, такие как недопустимые аргументы или ошибки времени выполнения, перехватываются и обрабатываются механизмами исключений Spring. Как только метод выполняется, возвращаемое значение обрабатывается в HandlerMethodReturnValueHandler и преобразуется в HTTP-ответ. Для сложных объектов данные в Spring при помощи HttpMessageConverters сериализуются в JSON или XML  —  в зависимости от запроса клиента.

Пример вызова метода и обработки ответа

@GetMapping("/status")
public ResponseEntity<String> getStatus() {
return new ResponseEntity<>("Service is running", HttpStatus.OK);
}

Когда в /status, отправляется запрос, в Spring отыскивается и вызывается метод getStatus(). Возвращаемым типом ResponseEntity указываются тело ответа и код состояния HTTP. Перед возвращением клиенту ответ в Spring обрабатывается и сериализуется.

Обработка исключений во время вызова

Если во время вызова метода возникают ошибки вроде несоответствий типов, сбоев проверки или исключений времени выполнения внутри метода обработчика, ответ в Spring Boot управляется встроенной системой обработки исключений.

  • Несоответствие типов
    Если параметр нельзя преобразовать в ожидаемый тип, в Spring выбрасывается исключение MethodArgumentTypeMismatchException, что чревато ответом «400 Bad Request».
  • Ошибки валидации
    Сбои валидации чреваты появлением MethodArgumentNotValidException, которое тоже обрабатывается с ответом «400», если не определен пользовательский обработчик.
  • Необработанные исключения
    Исключения времени выполнения, выбрасываемые из метода обработчика, управляются методами @ExceptionHandler или глобальными классами @ControllerAdvice, благодаря чему разработчики настраивают ответы на ошибки для различных сценариев.

Пример обработки исключений:

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<String> handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
return new ResponseEntity<>("Invalid input type", HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGenericException(Exception ex) {
return new ResponseEntity<>("An error occurred", HttpStatus.INTERNAL_SERVER_ERROR);
}
}

Здесь несоответствия типов и другие исключения эффективно отлавливаются и обрабатываются, а клиентом получается информативная обратная связь.

Заключение

Система отображения запросов Spring Boot построена на хорошо структурированном процессе, при котором входящие HTTP-запросы соотносятся с корректными методами обработчика. Работа по эффективному обнаружению, регистрации и обработке запросов за аннотации вроде @RequestMapping и @GetMapping выполняется такими компонентами, как RequestMappingHandlerMapping и RequestMappingHandlerAdapter. От сопоставления путей и разрешения HTTP-методов до привязки параметров и вызова метода  —  каждый этап выполняется комбинированием рефлексии, преобразования данных и обработки сообщений. Благодаря этим механизмам осуществляется точное управление сложными веб-запросами, поэтому Spring Boot  —  надежный фреймворк для создания веб-приложений.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Alexander Obregon: The Mechanics of Request Mapping in Spring Boot

Предыдущая статьяuseEffectEvent: почему так строго?