В двадцать первом веке это сложно представить, но 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. Даже если вам кажется что это хорошая идея, это не так.