Контроллеры и HTTP
В Spring контроллер - это класс, обрабатывающий веб-запросы по протоколу HTTP. Для начала, краткий экскурс в то, из чего собственно состоит запрос и ответ в HTTP
Структура HTTP запроса и ответа
С точки зрения протокола HTTP работает довольно просто. Ваш сервер слушает определенный порт (по умолчанию 80). Браузер отправляет в него текстовый запрос, содержащий в себе:
Метод запроса: наиболее распространенные GET, POST, всего их довольно много разных.
Путь запроса - URI запрашиваемого ресурса на сервере, например /files/somefile.txt
Заголовки запроса: пары ключ-значение, определяющие дополнительные параметры запроса. Тут может быть и авторизация различных видов, и поддерживаемые клиентом типы данных, кодировки, и куча всего другого.
Тело запроса - какие-то данные, которые клиент отправляет на сервер.
Все это передается просто в виде текста. Получив такой текст, HTTP-сервер парсит его и пытается понять, что ему надо сделать. Как правило, он ищет какой-то код, который знает что нужно сделать с запрошенным URI и методом. Затем сервер возвращает клиенту ответ, содержащий:
Код ответа, 200 если все хорошо, один из кодов ошибок если все плохо
Заголовки ответа: такие же пары ключ-значение, могут содержать дополнительную информацию о содержимом ответа: его тип, кодировку и что-нибудь еще.
Тело ответа - основной массив данных в каком-то формате.
Подробнее можно прочитать, например, в https://habr.com/ru/post/215117/
По сути, типичный HTTP-сервер это простой парсер HTTP протокола + обработчики для разных URI. Собственно эти обработчики в терминологии Спринга и называются Контроллерами.
Контроллеры
В Spring контроллер - это класс, который умеет обрабатывать запросы, пришедшие на определенный URI. С точки зрения кода это класс, помеченный аннотацией @Controller или @RestController, на методы которого навешены специальные аннотации, показывающие спрингу, для каких URI нужно этот метод вызывать.
Например, хотим мы сделать контроллер, возвращающий текущее серверное время. Выглядеть оно будет так:
@RestController
public class TimeController {
@GetMapping("/getTime")
public Long getTime() {
return System.currentTimeMillis();
}
}
Классы, помеченные как @Controller/@RestController Spring найдет автоматически, просканирует их методы и найдет нужные. В данном примере мы говорим спрингу, что наш метод будет обрабатывать GET запросы по URL /getTime
Мы можем теперь выполнить запрос в браузере и посмотреть, что получится. Запрос наш в текстовом виде будет выглядеть как:
GET /getTime HTTP/1.1
Host: localhost:8080
Встроенный в Spring веб-сервер распарсит его, найдет метод (GET), URI запроса (/getTime), сверится со своей внутренней таблицей контроллеров и найдет, что за такое сочетание метода и адреса отвечает метод TimeController.getTime().
Вызовет его, после чего возьмет возвращенное им значение, преобразует его в корректный HTTP ответ и отправит клиенту:
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 12 Mar 2022 17:03:57 GMT
Keep-Alive: timeout=60
Connection: keep-alive
1647104637808
Тут в первой строке идет код состояния (200 - все прошло нормально), затем идут различные заголовки с мета-информацией об ответе (их состав может зависеть от версии и настроек веб-сервера), и затем, после пустой строки, собственно тело ответа - то, что вернул наш метод контроллера.
А что будет, если мы попробуем вместо GET выполнить POST, или укажем неправильный URI? Веб-сервер попробует найти соответствующий контроллер, не найдет его, и вернет клиенту ошибку (в данном случае 404 - запрашиваемый объект не найден):
HTTP/1.1 404
Content-Language: ru-RU
Content-Length: 275
Date: Sat, 12 Mar 2022 17:15:20 GMT
Keep-Alive: timeout=60
Connection: keep-alive
Итак, контроллер - это специальным образом аннотированный класс, которому Spring передает управление при получении определенных HTTP запросов. Который их обрабатывает, передает ответ Spring, после чего тот возвращает его клиенту.
Так чем отличается @Controller и @RestController?
Отличия минимальны. Если класс помечен @Controller, то возможно два варианта поведения его методов:
Если метод помечен аннотацией @ResponseBody, то ответ этого метода выдается пользователю в виде строки. Если метод вернул строку или число - они так и возвращаются. Если вернул объект - объект сперва сериализуется в строку (по умолчанию в JSON формате) и возвращается клиенту.
Если метод не помечен этой аннотацией, он должен возвращать строку. В таком случае эта строка трактуется как идентификатор шаблона, который Spring должен найти, подставить в него нужные значения и отправить клиенту в виде HTML страницы. Работа с шаблонами, генерация веб-страниц на стороне сервера - это отдельная объемная тема, почитать можно, например, тут https://habr.com/ru/post/435062/ Стоит отметить, что сейчас в современнных веб-приложениях этот подход уже мало востребован. Веб-фронтенд обычно пишется на одном из фронтенд-фреймворков а-ля Angular, React и подобных, и раздается статическим веб-сервером. А бэкенд на Spring выставляет наружу только API для него, сам при этом не работая с HTML и визуальным представлением.
Так как часто приходится разрабатывать веб-сервисы, не работающие напрямую с HTML и человекочитаемым представлением, а реализующие только какое-то API, второй вариант часто просто не нужен. Чтобы не заставлять пользователя ставить на каждый метод аннотацию @ResponseBody, Spring имеет дополнительную вспомогательную аннотацию @RestController. По сути она просто равна @Controler + @ResponseBody по умолчанию на все методы.
Параметры запроса и структура ответа
В HTTP есть несколько способов передать параметры в наш метод контроллера. Например, у нас есть метод, возвращающий объект по его ID, и нам надо этот ID передать. В URI можно такие параметры передать после вопросительного знака в виде пар ключ-значение: GET /getItem?itemId=5.
Если мы определим наш метод контроллера вот так, то Spring автоматически сматчит itemId в запросе и нашу переменную параметр метода.
@RestController
public class ItemController {
@GetMapping("/getItem")
public Item getTime(@RequestParam("itemId") Long itemId) {
Item item = // где-то как-то получаем наш item по его id;
return item;
}
}
Тут сразу два интересных момента. Во-первых, мы добавили параметр запроса. Спринг сам будет отвечать за то, чтобы преобразовывать параметры из HTTP формата в соответствующие Java-объекты.
Во-вторых, мы определили, что наш метод возвращает Java-объект типа Item (не важно сейчас, что именно это за класс). В этом случае Spring сам сконвертирует наш объект в строковое представление, по умолчанию - с помощью JSON-библиотеки Jackson.
Таким образом мы можем реализовывать достаточно сложные API, умеющие выполнять запросы с различными параметрами и возвращать сложные структуры данных.
Если вам понадобится более полный контроль над возвращаемым значением (например, захочется сформировать ответ вручную, не отдавая на откуп Spring), в ваш метод контроллера можно добавить дополнительный параметр HttpServletResponse. Увидев этот параметр в методе контроллера, Spring заполнит его объектом, через который вы сможете управлять вручную отсылкой ответа клиенту. Подробнее можно прочитать в https://www.baeldung.com/spring-response-header
Почему не стоит писать логику прямо в контроллерах
Казалось бы, прямо в методе контроллера можно было бы и написать всю необходимую бизнес-логику обработки запроса. Однако, в реальных проектах делать так не стоит. Как было сказано в прошлой главе, принято разделять бизнес-логику (модель) и интерфейсы взаимодействия (контроллеры).
Чтобы проще было переиспользовать и тестировать бизнес-логику, ее стоит вынести в отдельные классы, которые уже не будут ничего знать про сложности работы с HTTP. Да, контроллеры тоже можно тестировать с помощью MockMVC, но это намного сложнее и медленнее.
Поэтому типичное организацией является разбиение логики на два слоя:
Классы-контроллеры отвечают лишь за работу с HTTP: конвертацию и валидацию параметров запросов, подготовку ответов. Они содержат аннотации Spring связанные с HTTP
Классы-сервисы реализуют бизнес-логику, и это обычные Java-классы, принимающие обычные параметры. Такие классы можно переиспользовать из любой точки кода, а также из тестов
Контроллеры связываются с сервисами через механизм Dependency Injection и вызывают их методы в нужном порядке.
Last updated