Цель данного практического руководства - добавить форму редактирования в Spring MVC приложение из первой части {Spring MVC шаг за шагом 

В результате прохождения по шагам, должен получиться справочник пользователей: отображение списка, добавление и редактирование пользователя. Пока без привязки к базе данных - будем хранить состояние в памяти сервера.

Что нужно для начала:

  1. выполнить первую часть Spring MVC шаг за шагом, либо взять готовый проект
  2. Среда разработки и остальное окружение из первой части.

 

 
Шаг 1: Создание модели данных

Перед началом разработки непосредственной логики приложения, нужно определить модель данных, с которой мы будем работать. Создадим класс User в новом пакете ru.mai.dep806.mvcapp.model. Конечно, классы можно создавать где угодно, но если мешать все в кучу, то проект очень быстро примет форму и содержание этой самой кучи.

package ru.mai.dep806.mvcapp.model;

import java.util.Date;

/**
 * Экземпляр класса модели представляет собой одного пользователя.
 */
public class User {
 private Long id;
 private String login;
 private String name;
 private String email;
 private Date birthDate;
 private Boolean active;

}

Классы модели представляют собой POJO (plain old java objects) с конструктором без параметров и с методами get/set представляющими свойства - атрибуты нашего пользователя. методы get/set никто руками не пишет - их генерирует среда разработки, поэтому поставьте курсор в конец класса перед }, нажмите Alt-Insert и выберите Getter and Setter, затем во всплывающем окне выберите все поля и Ok. После этого в класс User добавиться 12 методов - 6 get*() и 6 set*(). Методы скрывают данные модели, и для JSP страниц и MVC-библиотеки, обращение к свойствам модели будет на самом деле вызывать методы get/set.

Тем же способом сгенерируйте 2 конструктора - контруктор со всеми полями, кроме id и конструктор без параметров (так как у нас появился явный конструктор, то конструктора по-умолчанию без параметров уже нет).

 

Шаг 2: Создание Mock-репозитория пользователей

Теперь данные нужно где-то хранить. Работа с базой данных - тема отдельного руководства, поэтому пока сохраним данные в памяти сервера. Классы, отвечающие за хранение и извлечение данных часто называют Repository или Data Access Object. В нашем случае он не настоящий, так как данные после перезагрузки теряются, поэтому назовем его MockUserDao и положим в отдельный пакет ru.mai.dep806.mvcapp.dao

package ru.mai.dep806.mvcapp.dao;

import ru.mai.dep806.mvcapp.model.User;

import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Объект доступа к данным о пользователях. Реализация в памяти.
 */
public class MockUserDao {
 private Map<Long, User> users = new HashMap<Long, User>();
 private AtomicLong sequence = new AtomicLong(0);

 public MockUserDao() {
    saveUser(new User("john", "John Smith", "jsmith@mail.com", new Date(), true));
    saveUser(new User("steve", "Steve Brown", "sbrown@mail.com", new Date(), true));
 }

 public List<User> getAllUsers() {
    return new ArrayList<User>(users.values());
 }

 public User findUserById(Long id) {
    return users.get(id);
 }

 public User saveUser(User user) {
    if (user.getId() == null) {
       user.setId(sequence.getAndIncrement());
    }
    users.put(user.getId(), user);
    return user;
 }
}

При создании объекта MockUserDao у нас уже будет 2 пользователя.

Метод saveUser() предусматривает что к нему придет вновь созданный пользователь без id и умеет генерировать id из последовательности. Но для чего используется AtomicLong? Почему нельзя обойтись обычным long?

 

Шаг 3: Контроллер для отображения списка пользователей

Теперь создадим класс-контроллер, в котором добавим метод, который достанет и вернет список пользователей.

package ru.mai.dep806.mvcapp.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import ru.mai.dep806.mvcapp.dao.MockUserDao;
/**
 * Контроллер для работы с пользователями.
 */
@Controller
public class UserController {
 private MockUserDao userDao = new MockUserDao();
 
 @RequestMapping("/users.html")
 public ModelAndView listUsers() {
    return new ModelAndView("WEB-INF/jsp/users.jsp", "users", userDao.getAllUsers());
 }
}

В данном случае - "users" - это имя атрибута модели (и request-а) под которым мы получим список пользователей на JSP-странице.

Шаг 4: Новые зависимости

Прежде чем добавлять JSP, надо включить несколько зависимостей в наш pom.xml. Это будет servlet-api и JSTL теги, который мы будем использовать.

<dependencies>
...
 <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
    <scope>provided</scope>
 </dependency>
 <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
 </dependency>
</dependencies>

Обратите внимание на scope provided - он означает, что библиотека требуется для компиляции проекта, но не будет включена в пакет дистрибутива (war-файл), так как предполагается что она уже есть в сервере приложений. Если все же случайно servlet-api (или что-то еще что есть в сервере приложений) попадет в WEB-INF/lib, вы рано или поздно получите ClassCastException. Почему? - этот вопрос часто задают на собеседованиях.

Шаг 5: JSP-страница для отображения списка

В контроллере мы сослались на страницу /WEB-INF/jsp/users.jsp, которую сейчас попытаемся реализовать:

<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt_rt" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
   <title>Пользователи</title>
</head>
<body>
<h3>Пользователи</h3>
<a href="<c:url value="/addUser.html"/>">Добавить пользователя</a>

 <table>
 <thead>
 <tr>
     <td>Логин</td>
     <td>Имя</td>
     <td>E-mail</td>
     <td>Дата рождения</td>
     <td>Статус</td>
     <td>Действия</td>
   </tr>
 </thead>
   <c:forEach items="${users}" var="user">
   <tr>
     <td>${user.login}</td>
     <td><c:out value="${user.name}" escapeXml="true"/></td>
     <td><a href="mailto:${user.email}">${user.email}</a></td>
     <td><fmt:formatDate value="${user.birthDate}" pattern="dd-MM-yyyy"/></td>
     <td>${user.active ? "Активен" : "Деактивирован"}</td>
     <td><a href="<c:url value="/editUser.html?id=${user.id}"/>">Редактировать</a></td>
   </tr>
   </c:forEach>
 </table>
</body>
</html>

Тут требуется несколько пояснений:

  1. Тег <c:url value=""/> превращается в правильно сформированную гиперссылку. Т.е. к ней добавляется префикс контекста приложения (/webapp в данном случае, но может поменяться на любой другой) и JSESSIONID если отлючены cookie для поддержки сессии.
  2. Выражения ${user.login} и <c:out value=${user.login}"/> равнозначны, но второй позволяет использовать дополнительные параметры, типа escapeXml="true|false". По-умолчанию теги в полях экранируются, но если у вас в поле должен быть html, то экранирование этим способом можно отключить.
  3. <c:forEach - это цикл по элементам коллекции users (это наша модель - список пользователей), переменная цикла объявлена под именем user.
  4. <fmt: - это набор тегов для форматирования дат, чисел и вывода локализованных сообщений (на языке пользователя)

После запуска (mvn clean package tomcat6:run) должно получиться что-то вроде этого

Шаг 6: Контроллер для отображения страниц добавления и редактирования пользователя

Теперь приступим к реализации логики добавления и редактирования пользователей. Тут стоит отметить, что формы добавления и редактирования часто визуально одинаковы, с точностью до названия диалога, но логика работы немного разная: в случае добавления нужно подготовить чистый объект User, установив в него значения полей по-умолчанию, при редактировании - прочитать пользователя по id из Dao. Таким образом, оптимально когда методы контроллера будут разные, а форма представления (jsp) - одна и та же. Реализуем методы контроллера для отображения форм:

class UserController {
 // ... 
 @RequestMapping(value = "/addUser.html", method = RequestMethod.GET)
 public String showCreateUser(Model model) {
    User user = new User();
    user.setActive(true);
    model.addAttribute("user", user);
    return "WEB-INF/jsp/addEditUser.jsp";
 }

 @RequestMapping(value = "/editUser.html", method = RequestMethod.GET)
 public String showEditUser(@RequestParam("id") Long id, Model model) {
    model.addAttribute("user", userDao.findUserById(id));
    return "WEB-INF/jsp/addEditUser.jsp";
 }
 // ...
}

Оба метода будут срабатывать только на GET запросы, т.е. на клик по ссылке с другой страницы или прямой ввод URL пользователем. Первый метод готовит новый объект и добавляет его в модель. На вход второму методу приходит параметр запроса id, по которому он достает пользователя с помощью userDao и, аналогично первому, кладет в модель. Таким образом, jsp-страница ожидает получить в моделе объект User под именем "user" в обоих случаях, чем достигается повторное использование jsp-view.

 

Шаг 7: JSP-страница для создания/редактирования пользователя

Добавим требуемую jsp страницу в WEB-INF/jsp

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
 <title>
 <c:if test="${empty user.id}"> Добавление пользователя</c:if>
 <c:if test="${not empty user.id}"> Редактирование пользователя </c:if>
 </title>
</head>
<body>
<form:form method="POST" commandName="user">
 <form:hidden path="id"/>
 <table>
   <tr>
     <td>Логин</td>
     <td><form:input path="login"/></td>
   </tr>
   <tr>
     <td>Имя пользователя</td>
     <td><form:input path="name"/></td>
   </tr>
   <tr>
     <td>E-mail</td>
     <td><form:input path="email"/></td>
   </tr>
   <tr>
     <td>Дата рождения</td>
     <td><form:input path="birthDate"/></td>
   </tr>
   <tr>
     <td>Активен</td>
     <td><form:checkbox path="active"/></td>
   </tr>
   <tr>
     <td colspan="2"><input type="submit" value="Сохранить"/></td>
   </tr>
 </table>
</form:form>
</body>
</html>

В jsp странице мы активно использовали теги Spring Form, но это не обязательно. Можно обойтись и обычным html c EL выражениями в ${}. Теги form немного укорачивают html-код, используя контекст текущей формы. Ключевой атрибут всех тегов - path - это путь до свойства начиная от объекта-команды user. В нашем случае это свойства, объявленные в классе User, но может быть и более вложенная структура, например "address.country.name". Так как мы не указали action для формы, то при нажатии Сохранить (при отправке формы на сервер), браузер пошлет серверу запрос на тот же URL но методом POST.

Шаг 8: Контроллер для кнопки Submit
Шаг 9: Добавление правил валидации