В двадцать первом веке это сложно представить, но Spring Web MVC создавался для генерации статических страниц и MVC в названии — акроним популярного шаблона Model-View-Controller. Spring Web MVC реализует поддержку двух частей этого шаблона — собственно контроллеры, которые реагируют на web запросы и связывают вид с моделью, а так же виды, которые в Spring Web MVC реализуются посредством того или иного механизма шаблонизации. Строго говоря, шаблонизаторы используются как раз для генерации статических страниц, а контроллеры могут генерировать и json, и xml, и что угодно.
Модель
Я напишу простое приложение, которое хранит в памяти список посылок, вес каждой и владельца. В качестве основы я взял пустое Spring boot приложение с поддержкой Spring Web MVC. Модель представляет собой простой класс с данными и Spring Web MVC не делает никаких предположений о его внутреннем устройстве. Поэтому класс будет как можно более простой (и в этом поможет lombok):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /** * Package data entity. */ @AllArgsConstructor @Data public class Package { /** * Id of a package. */ private Integer id; /** * Name of parcel's owner. */ private String owner; /** * Weight of parcel in kilograms. */ private Integer weight; } |
К модели сразу сделаю сервис, хранящий данные:
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 | @Service public class PackageService { static Map<Integer, Package> warehouse = new LinkedHashMap<>(); static { warehouse.put(1, new Package(1, "John Dow", 3)); warehouse.put(2, new Package(2, "Jane Dow", 7)); warehouse.put(3, new Package(3, "Douglas Adams", 42)); } public Collection<Package> list() { synchronized (this) { return warehouse.values(); } } public Package get(Integer id) { synchronized (this) { return warehouse.get(id); } } public void update(Package p) { synchronized (this) { warehouse.put(p.getId(), p); } } } |
Так как я планирую реализовывать только web часть приложения, никакой базы данных не будет, а все данные будут храниться в статической Map. Из-за статической переменной пришлось добавить и блокировки в каждый метод. В настоящих приложениях так делать, конечно же, не стоит.
Контроллер
Контроллеры для статических страниц практически такие же, как и для REST Они точно так же назначаются на URL, точно так же могут обрабатывать данные запросов и вообще могут всё тоже самое. Отличий не так много — во-первых не надо добавлять аннотацию @RestController, во-вторых, и это самое главное отличие, методы контроллера возвращают не данные напрямую, а имя вида и данные модели:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Controller @RequestMapping("/packages") public class PackageController { @Autowired private PackageService packageService; @GetMapping public ModelAndView list() { return new ModelAndView("list", "parcels", packageService.list()); } } |
Класс ModelAndView — вспомогательный, он помогает одним махом вернуть из метода контроллера и имя view и данные модели. Класс оборудован удобным конструктором — первый аргумент, это имя view, второй аргумент — имя модели, и третий аргумент — данные модели. Если необходимо передавать данные нескольких моделей, можно воспользоваться либо методом addObject(), добавляющим новую модель с новым именем, либо конструктором принимающим Map.
Когда Spring Web MVC видит метод возвращающий ModelAndView, он понимает, что необходимо найти view, указанные в ModelAndView, подложить в него данные модели и запустить обработку шаблона. Об этом я напишу в следующем разделе, а пока посмотрим, как контроллеры могут получать данные модели извне, например из формы:
1 2 3 4 5 | @PostMapping("/{id}") public String edit(@ModelAttribute Package p) { packageService.update(p); return "redirect:/packages"; } |
Увидев параметр с аннотацией @ModelAttribute Spring Web MVC автоматически создаст объект требуемого типа и попытается его заполнить данными, переданными с application/x-www-form-urlencoded, используя имена параметров формы как имена полей класса. Пример выше интерестен тем, что возвращает строку, а не ModelAndView Возврат строки из контроллера интерпретируется как возврат имени view, который надо использовать. Данные модели в таком случае будут добавлены неявно — из аргументов метода. Однако в моём случае я возвращаю даже не имя вида, а указание вернуть страницу по адресу /packages, на что указывает префикс redirect.
Наконец, раз уж мы собираемся обрабатывать данные формы, давайте добавим метод, который создаст форму:
1 2 3 4 5 6 7 8 | @GetMapping("/{id}") public ModelAndView view(@PathVariable Integer id) { Package p = packageService.get(id); if (p == null) { return new ModelAndView("list", HttpStatus.NOT_FOUND); } return new ModelAndView("form", "parcel", packageService.get(id)); } |
Он очень похож на то, что мы уже знаем, с одним изменением — я использую другой конструктор ModelAndView, который принимает имя view и http статус, что позволяет возвращать из метода подходящий код http.
Вид
Третья часть головоломки. Собственно то, из чего создаётся ответ сервера. В принципе, при должной доработке, Spring Web MVC может генерировать что угодно и создавать документы разных типов. Однако по умолчанию поддерживаются лишь несколько шаблонизаторов и XML/JSON/PDF. Все эти шаблонизаторы стоит, разумеется, рассмотреть подробно, но так как эта статья посвящена Spring, а не шаблонизаторам, а настраиваются они практически одинаковым образом, то я использую самый простой из них — JSP.
JSP, акроним от Java Server Pages, это весьма древняя технология, которой уже около двадцати лет. Представляет собой html файлы нашпигованные специальным вставками java кода (как в PHP, ага), которые компилируются в сервлеты, которые в свою очередь компилируются в исполняемые классы. В общем, держитесь от них подальше. Для нас же удобно то, что JSP поддерживаются везде и достаточно просты в написании.
В первую очередь, я напишу JSP для вида list:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head><title>List of parcels</title></head> <body> <h1>Parcels list</h1> <ul> <c:forEach items="${parcels}" var="parcel"> <li><a href="/packages/${parcel.id}">Owner: <b>${parcel.owner}</b>, weight: <b>${parcel.weight}</b> kg</a> </c:forEach> </ul> </body> </html> |
Как я и обещал — это обычный html со вставками кода. Я не буду останавливаться на коде JSP, это сейчас не так важно. Главное, на что стоит обратить внимание, находится в строке 8: там я обращаюсь к модели (списку посылок), находящейся в переменной parcels, как и было сказано при создании ModelAndView в контроллере. JSP надо сохранить в файл с именем /webapp/WEB-INF/jsp/list.jsp
Второй JSP файл, /webapp/WEB-INF/jsp/form.jsp, содержит форму редактирования данных модели:
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 | <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head><title>Parcel for ${parcel.owner}</title></head> <body> <h1>Parcel for ${parcel.owner}</h1> <form:form modelAttribute="parcel"> <form:hidden path="id"/> <table> <tr> <td>Owner:</td> <td><form:input path="owner"/></td> </tr> <tr> <td>Weight:</td> <td><form:input path="weight"/></td> </tr> <tr> <td colspan="2"><input type="submit" value="Save Changes"/></td> </tr> </table> </form:form> </body> </html> |
Для создания формы используются Spring JSTL form тэги, которые умеют сами читать данные из полей модели и складывать их в форму.
Чтобы всё это заработало, требуется указать Spring Web MVC, как и где искать файлы, реализующие view. Для этого создадим дополнительный класс конфигурации:
1 2 3 4 5 6 7 8 | @Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public void configureViewResolvers(ViewResolverRegistry registry) { registry.jsp("/WEB-INF/jsp/", ".jsp"); } } |
Первая аннотация, @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 в зависимости проекта:
1 2 3 4 5 6 7 8 9 | <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> |
Если теперь запустить готовое приложение и обратиться браузером по пути /packages, мы увидим следующую страницу:
По клику на любой строке откроется форма редактирования:
Код примера доступен на github.
PS Пожалуйста, не используйте JSP. Даже если вам кажется что это хорошая идея, это не так.