Контроллеры и HTTP

В Spring контроллер - это класс, обрабатывающий веб-запросы по протоколу HTTP. Для начала, краткий экскурс в то, из чего собственно состоит запрос и ответ в HTTP

Структура HTTP запроса и ответа

С точки зрения протокола HTTP работает довольно просто. Ваш сервер слушает определенный порт (по умолчанию 80). Браузер отправляет в него текстовый запрос, содержащий в себе:

  1. Метод запроса: наиболее распространенные GET, POST, всего их довольно много разных.

  2. Путь запроса - URI запрашиваемого ресурса на сервере, например /files/somefile.txt

  3. Заголовки запроса: пары ключ-значение, определяющие дополнительные параметры запроса. Тут может быть и авторизация различных видов, и поддерживаемые клиентом типы данных, кодировки, и куча всего другого.

  4. Тело запроса - какие-то данные, которые клиент отправляет на сервер.

Все это передается просто в виде текста. Получив такой текст, HTTP-сервер парсит его и пытается понять, что ему надо сделать. Как правило, он ищет какой-то код, который знает что нужно сделать с запрошенным URI и методом. Затем сервер возвращает клиенту ответ, содержащий:

  1. Код ответа, 200 если все хорошо, один из кодов ошибок если все плохо

  2. Заголовки ответа: такие же пары ключ-значение, могут содержать дополнительную информацию о содержимом ответа: его тип, кодировку и что-нибудь еще.

  3. Тело ответа - основной массив данных в каком-то формате.

Подробнее можно прочитать, например, в 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, но это намного сложнее и медленнее.

Поэтому типичное организацией является разбиение логики на два слоя:

  1. Классы-контроллеры отвечают лишь за работу с HTTP: конвертацию и валидацию параметров запросов, подготовку ответов. Они содержат аннотации Spring связанные с HTTP

  2. Классы-сервисы реализуют бизнес-логику, и это обычные Java-классы, принимающие обычные параметры. Такие классы можно переиспользовать из любой точки кода, а также из тестов

  3. Контроллеры связываются с сервисами через механизм Dependency Injection и вызывают их методы в нужном порядке.

Last updated