Контроллер в Spring является компонентом, который обрабатывает входящие HTTP-запросы от клиентов и возвращает соответствующие HTTP-ответы. В Spring-проектах контроллер представляет собой обычный Java-класс, который помечен специальными аннотациями.
Вперёд к созданию контроллеров!
@Controller и @RestController
Всё-таки, контроллеры бывают разными. Давайте рассмотрим две аннотации и выберем, какую из них использовать в этом конкретном случае.
@Controller используется для создания контроллера, который возвращает представления или шаблоны HTML. Он обычно используется в приложениях, где требуется генерация HTML-страниц на основе данных. То есть возвращаем название страницы, которая будет отрисовываться у клиента в браузере:
1 2 3 4 5 6 7 8 |
@Controller public class MvcExampleController { @GetMapping("/login") public String getLoginPage(Model model) { return "login_page"; } } |
Здесь login_page – это название страницы, название шаблона, который нужно вернуть пользователю.
@RestController, с другой стороны, используется для создания RESTful веб-служб, которые возвращают данные в формате JSON или XML. Он объединяет аннотации @Controller и @ResponseBody. Благодаря тому контроллер возвращает данные напрямую без необходимости использовать представления. То есть возвращаем чисто объекты, без каких-либо страниц. Эти объекты потом сможет красиво разложить в HTML-странице или ещё где захочет приложение, запросившее данные:
1 2 3 4 5 6 7 8 9 |
@RestController public class RestExampleController { @GetMapping("/book") public Book getBook() { Book book = new Book(); return book; } } |
Так как мы пишем back-end, то нам подходит @RestController. Мы будем возвращать чистые данные. Что с ними делать дальше и как отображать – будет решать тот, кто за этими данными к нам обратится.
Создание класса контроллера
Создадим контроллеры для обработки HTTP-запросов из нашего примера с ранее созданными сущностями User и Post. Что нам для этого понадобится?
Аннотацию контроллера мы уже выбрали. Это @RestController, сырые данные. Дальше нужно привязать сервисы в контроллеры. Почему? Потому что контроллеры эксплуатируют сервисы так же, как сервисы эксплуатируют репозитории:
Итак, благодаря аннотации @RequiredArgsConstructor из утилиты Lombok я могу просто указать сервис как final-поле контроллера. Тогда Spring автоматически подставит туда объект сервиса.
1 2 3 4 5 6 |
@RestController @RequiredArgsConstructor public class UserController { private final UserService userService; } |
Также можно использовать аннотацию @Autowired, к примеру, чтобы Spring ровно так же понял, что туда класть:
1 2 3 4 5 6 |
@RestController public class UserController { @Autowired private UserService userService; } |
Ровно такие же манипуляции нужно проделать в классе Controller для сущности Post:
1 2 3 4 5 6 |
@RestController @RequiredArgsConstructor public class PostController { private final PostService postService; } |
Отлично. Контроллер по умолчанию обрабатывает все запросы (после самого адреса ресурса, на котором висит ваше веб-приложение). Если вы хотите, чтобы он отвечал только на запросы с некоторым префиксом, то есть начинающиеся на определённые символы, тогда следует указать над классом аннотацию @RequestMapping со значением префикса:
1 2 3 4 5 |
@RestController @RequestMapping("/books") public class BookController { ... } |
Теперь пора приступать к написанию обработчиков HTTP-запросов.
Запросы get, post, put, delete
У нас есть 2 сущности: User и Post. Для них мы создали таблицы users и posts в БД. Также мы создавали для них репозитории (UserRepository, PostRepository) и сервисы (UserService, PostService) в предыдущих статьях. Теперь время написать ответы на запросы в двух классах контроллеров – UserController и PostController.
Есть такая аннотация @RequestMapping. Мы её уже прописывали над классом контроллера. Но теперь мы будем её использовать для методов. @RequestMapping в сущности задаёт путь запросов. И для каждого метода класса контроллера у нас будут разные пути.
@RequestMapping принимает любой тип запроса (например, get, post и другие). Однако есть аннотации-уточнения аннотации @RequestMapping. Они обрабатывают уже только указанный тип запроса. Вот 4 основных, которые используются в подавляющем большинстве случаев:
- @GetMapping – обработка get-запросов, которые обычно используются для запроса данных с сервера. Он не должен изменять состояние сервера и не должен иметь побочных эффектов.
- @PostMapping – обработка post-запросов, которые применяются для отправки данных на сервер для обработки. В связи с тем они часто используются при создании новых ресурсов или изменении состояния на сервере.
- @PutMapping – обработка put-запросов, которые вносят изменения и позволяют обновить данные на сервере.
- @DeleteMapping – обработка delete-запросов, которые (неожиданно) удаляют ресурсы на сервере.
Пример метода с @GetMapping:
1 2 3 4 |
@GetMapping("/books") public List<Book> getAllBooks() { return bookService.getAllBooks(); } |
Методы именно с такими 4-мя видами аннотаций мы сейчас и напишем в два контроллера. При этом есть определённые соглашения о путях запросов. Кроме того, есть ещё 2 важные аннотации.
Первая из них – @PathVariable – позволяет обрабатывать переменную из пути запроса прямо в методе, даже указывая её тип данных (в данном случае Long):
1 2 3 4 |
@GetMapping("/books/{id}") public Book getBookById(@PathVariable Long id) { return bookService.getBookById(id); } |
Вторая аннотация, @RequestBody, сообщает о теле запроса, которое также можно извлечь из запроса и обработать в методе контроллера:
1 2 3 4 |
@PostMapping("/books") public Book createBook(@RequestBody Book book) { return bookService.saveBook(book); } |
Итак, вот, какой получился UserController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
@RestController @RequiredArgsConstructor @RequestMapping("/users") public class UserController { private final UserService userService; @GetMapping public List<User> getAllUsers() { return userService.getAllUsers(); } @GetMapping("/{id}") public User getUserById(@PathVariable Long id) { return userService.getUserById(id); } @PostMapping public User createUser(@RequestBody User user) { return userService.saveUser(user); } @PutMapping("/{id}") public User updateUser(@PathVariable Long id, @RequestBody User user) { user.setId(id); return userService.saveUser(user); } @DeleteMapping("/{id}") public void deleteUser(@PathVariable Long id) { userService.deleteUserById(id); } } |
Все пути запросов для обращения к пользователям будут начинаться с “/users“. Что ж, здесь есть запросы чтения, создания, обновления и удаления. Всё, как надо! Теперь взглянем на PostController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
@RestController @RequiredArgsConstructor @RequestMapping("/posts") public class PostController { private final PostService postService; @GetMapping public List<Post> getAllPosts() { return postService.getAllPosts(); } @GetMapping("/{id}") public Post getPostById(@PathVariable Long id) { return postService.getPostById(id); } @PostMapping public Post createPost(@RequestBody Post post) { return postService.savePost(post); } @PutMapping("/{id}") public Post updatePost(@PathVariable Long id, @RequestBody Post post) { post.setId(id); return postService.savePost(post); } @DeleteMapping("/{id}") public void deletePost(@PathVariable Long id) { postService.deletePostById(id); } @GetMapping("/user/{user_id}") public List<Post> getUserPosts(@PathVariable("user_id") Long userId) { return postService.getPostsByUserId(userId); } } |
Для обращения к записям, в свою очередь, потребуется писать префикс “/posts” в запросах.
Контроллеры готовы. Но для чего весь сыр-бор? Давайте пробовать, как это работает!
Как обратиться к контроллеру из браузера
В случае, если сайт не размещён в интернетах, а просто покоится на самом компьютере, хост имеет название localhost, или же 127.0.0.1 – ip-адрес самого себя. То есть любой компьютер, обращающийся к 127.0.0.1 или localhost, обратится сам к себе.
После названия хоста идёт порт – несколько цифр. Если в настройках вашего Spring-проекта не указано что-то другое, вы будете подключаться по порту 8080. Итого, адрес обращения к контроллеру будет “localhost:8080“. Адрес обращения к UserController составит “localhost:8080/users“, когда адрес для PostController будет “localhost:8080/posts“. При этом некоторые методы имеют дальше приписки, которые также надо будет вводить, если мы захотим их вызвать.
Итак, пора запускать! Нажмём на зелёный треугольник в среде разработки, выбрав к запуску наш Spring-проект. У вас могут быть там записаны и другие проекты, так что убедитесь, что вы выбрали именно то, что нужно. Проект Spring можно отличить по его особому символу – зелёному весеннему листу.
Ура, запустилось! Последняя строка в консоли говорит о том, что проект запустился за 5 секунд. Причём у меня указано, что Tomcat слушает именно на 8090 порту, так как порт 8080 у меня занят другим приложением, и я изменила порт в файле application.properties:
1 |
server.port=8090 |
Что ж, попробуем написать запрос к контроллеру UserController в адресной строке браузера. Получим всех пользователей. Которых на данный момент 0. Как и записей.
Пусто. Чего, в общем-то, следовало ожидать. Ведь такая же ситуация сейчас в базе данных – таблица users не имеет ни одной строки.
Как, впрочем, и таблица posts. В этом можно убедиться, обратившись к PostController запросом localhost:8090/posts. Отчего встаёт вопрос: что будем делать?
Давайте добавим новую строку в таблицу users:
Да, id = 3, так как я уже создавала двух пользователей до этого и потом удаляла. Но это неважно. Важно – что этот единственный пользователь появился в табличке. Обновим страницу с запросом localhost:8090/users:
Попался! Здесь мы получили JSON-объект, который на самом деле по запросу, принятом контроллером, сформировал сервис, обратившись к репозиторию, который вытащил информацию из базы данных. Так это и работает.
Добавим ещё одного пользователя.
Обновляем страницу с запросом. Что на выходе? Обе строки из базы данных подъехали. Да, именно в таком виде потом front-end может оперировать этими данными, запрашивая их у back-end. Взяв эти JSON-объекты, front-end может красиво расположить список пользователей на странице, добавить возможность написать им сообщение, например, или осуществить поиск и фильтрацию. И это классно, когда ответственности разделяются. В back-end мы оперируем данными. Их представление – уже другая ответственность.
Как видно, массив posts у обоих пользователей пока пуст. Это потому, что в таблице posts по-прежнему 0 строк, так что ни одна запись не привязана к какому-либо из пользователей. Но если попытаться внести данные в таблицу posts, случится что-то страшное…
Дело в том, что оба класса User и Post имеют ссылки друг на друга. И если запросить объект типа User или объект типа Post, тогда начнётся рекурсия, которая закончится Exception-ом. А нам это не надо.
Как такую проблему решать? Для этого создают DTO и mapper-ы к ним. DTO – data transfer object – убирает связь на одной из сторон (со стороны User либо Post) при транспортировке данных.
Но это уже совсем другая история! Про контроллеры мы, безусловно, уже закончили.