Spring Web MVC и статические страницы

В двадцать первом веке это сложно представить, но Web MVC создавался для генерации статических страниц и MVC в названии — акроним популярного шаблона Model-View-Controller. Spring Web MVC реализует поддержку двух частей этого шаблона — собственно контроллеры, которые реагируют на web запросы и связывают вид с моделью, а так же виды, которые в Spring Web MVC реализуются посредством того или иного механизма шаблонизации. Строго говоря, шаблонизаторы используются как раз для генерации статических страниц, а контроллеры могут генерировать и json, и xml, и что угодно.

Модель

Я напишу простое приложение, которое хранит в памяти список посылок, вес каждой и владельца. В качестве основы я взял пустое Spring boot приложение с поддержкой Spring Web MVC.  Модель представляет собой простой класс с данными и Spring Web MVC не делает никаких предположений о его внутреннем устройстве.  Поэтому класс будет как можно более простой (и в этом поможет lombok):

К модели сразу сделаю сервис, хранящий данные:

Так как я планирую реализовывать только web часть приложения, никакой базы данных не будет, а все данные будут храниться в статической Map. Из-за статической переменной пришлось добавить и блокировки в каждый метод. В настоящих приложениях так делать, конечно же, не стоит.

Контроллер

Контроллеры для статических страниц практически такие же, как и для REST Они точно так же назначаются на URL, точно так же могут обрабатывать данные запросов и вообще могут всё тоже самое. Отличий не так много — во-первых не надо добавлять аннотацию @RestController, во-вторых, и это самое главное отличие, методы контроллера возвращают не данные напрямую, а имя вида и данные модели:

Класс ModelAndView — вспомогательный, он помогает одним махом вернуть из метода контроллера и имя view и данные модели. Класс оборудован удобным конструктором — первый аргумент, это имя view, второй аргумент — имя модели, и третий аргумент — данные модели. Если необходимо передавать данные нескольких моделей, можно воспользоваться либо методом addObject(), добавляющим новую модель с новым именем, либо конструктором принимающим Map.

Когда Spring Web MVC видит метод возвращающий ModelAndView, он понимает, что необходимо найти view, указанные в ModelAndView, подложить в него данные модели и запустить обработку шаблона. Об этом я напишу в следующем разделе, а пока посмотрим, как контроллеры могут получать данные модели извне, например из формы:

Увидев параметр с аннотацией @ModelAttribute Spring Web MVC автоматически создаст объект требуемого типа и попытается его заполнить данными, переданными с application/x-www-form-urlencoded, используя имена параметров формы как имена полей класса. Пример выше интерестен тем, что возвращает строку, а не ModelAndView Возврат строки из контроллера интерпретируется как возврат имени view, который надо использовать. Данные модели в таком случае будут добавлены неявно — из аргументов метода. Однако в моём случае я возвращаю даже не имя вида, а указание вернуть страницу по адресу /packages, на что указывает префикс redirect.

Наконец, раз уж мы собираемся обрабатывать данные формы, давайте добавим метод, который создаст форму:

Он очень похож на то, что мы уже знаем, с одним изменением — я использую другой конструктор ModelAndView, который принимает имя view и http статус, что позволяет возвращать из метода подходящий код http.

Вид

Третья часть головоломки. Собственно то, из чего создаётся ответ сервера. В принципе, при должной доработке, Spring Web MVC может генерировать что угодно и создавать документы разных типов. Однако по умолчанию поддерживаются лишь несколько шаблонизаторов и XML/JSON/PDF. Все эти шаблонизаторы стоит, разумеется, рассмотреть подробно, но так как эта статья посвящена Spring, а не шаблонизаторам, а настраиваются они практически одинаковым образом, то я использую самый простой из них — JSP.

JSP, акроним от Java Server Pages, это весьма древняя технология, которой уже около двадцати лет. Представляет собой html файлы нашпигованные специальным вставками java кода (как в PHP, ага), которые компилируются в сервлеты, которые в свою очередь компилируются в исполняемые классы. В общем, держитесь от них подальше. Для нас же удобно то, что JSP поддерживаются везде и достаточно просты в написании.

В первую очередь, я напишу JSP для вида list:

Как я и обещал — это обычный html со вставками кода. Я не буду останавливаться на коде JSP, это сейчас не так важно. Главное, на что стоит обратить внимание, находится в строке 8: там я обращаюсь к модели (списку посылок), находящейся в переменной parcels, как и было сказано при создании ModelAndView  в контроллере. JSP надо сохранить в файл с именем /webapp/WEB-INF/jsp/list.jsp

Второй JSP файл, /webapp/WEB-INF/jsp/form.jsp, содержит форму редактирования данных модели:

Для создания формы используются Spring JSTL form тэги, которые умеют сами читать данные из полей модели и складывать их в форму.

Чтобы всё это заработало, требуется указать Spring Web MVC, как и где искать файлы, реализующие view. Для этого создадим дополнительный класс конфигурации:

Первая аннотация, @Configuration, говорит Spring boot, что этот класс — часть конфигурации контекста Spring. Вторая аннотация, @EnableWebMvc, включает в Spring Boot поддержку Spring Web MVC. Наконец, сам класс реализует интерфейс WebMvcConfigurer и переопределяет его метод configureViewResolvers который, как несложно догадаться из названия, настраивает поиск view. Там указывается префикс и суффикс, которые будут добавлены к имени view. Таким образом, если мы вернём имя view list, то Spring его преобразует в /WEB-INF/jsp/list.jsp и попытается открыть этот файл и исполнить его как JSP.

Последний шаг, который необходимо сделать, это добавить библиотеки поддержки JSP/JSTL в зависимости проекта:

Если теперь запустить готовое приложение и обратиться браузером по пути /packages, мы увидим следующую страницу:

По клику на любой строке откроется форма редактирования:

Код примера доступен на github.

PS Пожалуйста, не используйте JSP. Даже если вам кажется что это хорошая идея, это не так.