Добрый день. В этой статье я хотел бы поделиться своим первым знакомством с такими вещами как Maven, Spring, Hibernate, MySQL и Tomcat в процессе создания простого CRUD приложения. Это вторая часть из 4. Статья рассчитана в первую очередь на тех, кто уже прошел здесь 30-40 уровней, но за пределы чистой джавы пока не выбирался и только начинает (или собирается начинать) выходить в открытый мир со всеми этими технологиями, фреймворками и прочими незнакомыми словами. Это вторая часть статьи "Знакомство с Maven, Spring, MySQL, Hibernate и первое CRUD приложение". Первую часть можно увидеть перейдя по этой ссылке: Знакомство с Maven, Spring, MySQL, Hibernate и первое CRUD приложение (часть 1)

Содержание:

Ну что ж, будем двигаться дальше, попробуем теперь наколдовать целое хранилище фильмов. В нашем маленьком и простеньком приложении конечно можно просто тупо запилить всю логику прямо в контроллере, но, как уже отмечалось, лучше сразу учиться делать все правильно. Поэтому сообразим несколько слоев. У нас будет DAO, отвечающий за работу с данными, Service, где будет всякая разная прочая логика, ну и Controller будет только обрабатывать запросы и вызывать нужные методы сервиса.

Data Access Object

Data Access Object (DAO) — это такой паттерн проектирования. Смысл в том, чтобы создать специальную прослойку, которая будет отвечать исключительно за доступ к данным (работа с базой данных или другим механизмом хранения). В пакете dao создадим интерфейс FilmDAO в котором будут такие методы как добавить, удалить и т.д. Я их назвал несколько иначе, но они соответствуют основным CRUD операциям (Create, Read, Update, Delete).

Тут стоит отметить, что помимо DAO существует еще и такой подход как Repository, они, вроде как, очень похожи, оба используются для работы с данными. Я пока не разобрался, какие у этих подходов особенности и какая между ними разница. Поэтому я возможно здесь ошибаюсь и это следует называть именно репозиторием, а не дао, а может это вообще что-то среднее. Но в большинстве примеров, которые я видел и изучал, это называют именно DAO, так что и я, пожалуй, назову так же. При этом, возможно, где-то далее по тексту употреблю слово репозиторий. В любом случае, если я с этим где-то не прав, прошу меня простить.


package testgroup.filmography.dao;

import testgroup.filmography.model.Film;

import java.util.List;

public interface FilmDAO {
    List&ltFilm> allFilms();
    void add(Film film);
    void delete(Film film);
    void edit(Film film);
    Film getById(int id);
}
Теперь нам нужна его реализация. Подключать базу данных пока не будем, все еще страшновато. Чтобы потренироваться и попривыкнуть для начала имитируем хранилище в памяти, создадим список с несколькими фильмами. Для хранения списка будем использовать не List, а Map, чтобы было удобно получать конкретный фильм по его id, не перебирая для этого весь список. Для генерации id используем AtomicInteger. Создадим класс FilmDAOImpl, реализуем все методы и заполним мапу. Что-то вроде этого.

package testgroup.filmography.dao;

import testgroup.filmography.model.Film;

import java.util.*;

public class FilmDAOImpl implements FilmDAO {
    private static final AtomicInteger AUTO_ID = new AtomicInteger(0);
    private static Map<Integer, Film> films = new HashMap<>();

    static {
        Film film1 = new Film();
        film1.setId(AUTO_ID.getAndIncrement());
        film1.setTitle("Inception");
        film1.setYear(2010);
        film1.setGenre("sci-fi");
        film1.setWatched(true);
        films.put(film1.getId(), film1);

        // + film2, film3, film4, ...
    }
    @Override
    public List<Film> allFilms() {
        return new ArrayList<>(films.values());
    }

    @Override
    public void add(Film film) {
        film.setId(AUTO_ID.getAndIncrement());
        films.put(film.getId(), film);
    }

    @Override
    public void delete(Film film) {
        films.remove(film.getId());
    }

    @Override
    public void edit(Film film) {
        films.put(film.getId(), film);
    }

    @Override
    public Film getById(int id) {
        return films.get(id);
    }
}

Service

Теперь добавим сервисный слой. В принципе в данном примере вполне можно обойтись и без него, ограничившись DAO, приложение будет очень простое и какой-то сложной логики в сервисе не планируется. Но вдруг потом в будущем захочется добавить в проект всяких сложностей и интересностей, поэтому для полноты картины все-таки пусть будет. Пока же в нем просто будут вызываться методы из DAO. В пакете service создадим интерфейс FilmService.

package testgroup.filmography.service;

import testgroup.filmography.model.Film;

import java.util.List;

public interface FilmService {
    List<Film> allFilms();
    void add(Film film);
    void delete(Film film);
    void edit(Film film);
    Film getById(int id);
}
И его реализация:

package testgroup.filmography.service;

import testgroup.filmography.dao.FilmDAO;
import testgroup.filmography.dao.FilmDAOImpl;
import testgroup.filmography.model.Film;

import java.util.List;

public class FilmServiceImpl implements FilmService {
    private FilmDAO filmDAO = new FilmDAOImpl();

    @Override
    public List<Film> allFilms() {
        return filmDAO.allFilms();
    }

    @Override
    public void add(Film film) {
        filmDAO.add(film);
    }

    @Override
    public void delete(Film film) {
        filmDAO.delete(film);
    }

    @Override
    public void edit(Film film) {
        filmDAO.edit(film);
    }

    @Override
    public Film getById(int id) {
        return filmDAO.getById(id);
    }
}
Структура проекта теперь выглядит следующим образом:
Знакомство с Maven, Spring, MySQL, Hibernate и первое CRUD приложение (часть 2) - 1

Контроллер и представления

Поработаем теперь над методами контроллера и наполнением страниц. При наполнении страниц нам понадобятся некоторые приемы. Например чтобы вывести список фильмов нужен цикл, если, допустим, хотим менять какую-то надпись, в зависимости от параметров, нужны условия и т.д. Формат JSP (JavaServer Pages) позволяет использовать вставки java-кода, с которыми это все можно реализовать. Но использовать на странице java-код вперемешку с HTML-кодом не хочется. Это было бы, как минимум, очень некрасиво. К счастью, для решения этой проблемы существует такая замечательная штука как JSTL (JavaServer Pages Standard Tag Library) или Стандартная Библиотека Тегов JSP. Она позволяет использовать в наших jsp-страницах целую кучу дополнительных тегов для самых разных нужд. Подключим ее в pom.xml:

<dependency>
      <groupId>jstl</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
</dependency>
Теперь займемся контроллером. Первым делом уберем оттуда создание объекта Film, это делалось для пробы и больше нам не нужно. Добавим туда сервис и будем вызывать его методы.

public class FilmController {
    private FilmService filmService = new FilmServiceImpl();
Ну и соответственно сделаем методы для каждого случая, добавить, удалить и т.д. Сначала метод для отображения главной страницы со списком фильмов:

@RequestMapping(method = RequestMethod.GET)
    public ModelAndView allFilms() {
        List<Film> films = filmService.allFilms();
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("films");
        modelAndView.addObject("filmsList", films);
        return modelAndView;
    }
Тут ничего нового. Получаем список фильмов из сервиса и добавляем его в модель. Теперь сделаем главную страницу, films.jsp, которую возвращает этот метод:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>FILMS</title>
</head>
<body>

<h2>Films</h2>
<table>
    <tr>
        <th>id</th>
        <th>title</th>
        <th>year</th>
        <th>genre</th>
        <th>watched</th>
        <th>action</th>
    </tr>
    <c:forEach var="film" items="${filmsList}">
        <tr>
            <td>${film.id}</td>
            <td>${film.title}</td>
            <td>${film.year}</td>
            <td>${film.genre}</td>
            <td>${film.watched}</td>
            <td>
                <a href="/edit/${film.id}">edit</a>
                <a href="/delete/${film.id}">delete</a>
            </td>
        </tr>
    </c:forEach>
</table>

<h2>Add</h2>
<c:url value="/add" var="add"/>
<a href="${add}">Add new film</a>
</body>
</html>
Рассмотрим эту страницу подробнее, что тут вообще к чему. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> — тут подключается JSTL core, которая включает основные теги создания циклов, условий и т.д.
  • <table> — тег для создания таблицы.
  • <tr> — строка таблицы
  • <th> — заголовок столбца
  • <td> — ячейка таблицы
Сначала делаем строку-шапку таблицы, с названиями столбцов. Затем <c:forEach var="film" items="${filmsList}"> — в цикле (который мы взяли из JSTL core) пробегаемся по всем элементам переданного списка (filmsList), для каждого элемента (film) создаем новую строку и в каждую ячейку записываем соответствующее значение. Тут есть один момент, запись вроде film.id нужно понимать как film.getId(), т.е. не напрямую к полю обращение, а именно геттер вызывается. В последнем столбце (action) делаем ссылки для удаления и редактирования (соответствующие методы сейчас сделаем). Ну и внизу ссылка на метод добавления нового фильма. Вот как это выглядит: Далее займемся методом, который будет возвращать страницу редактирования конкретного фильма:

@RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
    public ModelAndView editPage(@PathVariable("id") int id) {
        Film film = filmService.getById(id);
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("editPage");
        modelAndView.addObject("film", film);
        return modelAndView;
    }
Здесь появилось кое-что новенькое — это аннотация @PathVariable. Она указывает на то, что данный параметр (int id) получается из адресной строки. Чтобы указать место этого параметра в адресной строке используется конструкция {id} (кстати, если имя переменной совпадает, как в данном случае, то в скобках это можно не указывать, а написать просто @PathVariable int id). Итак, на главной странице мы сделали ссылки для каждого фильма с указанием id:

<a href="/edit/${film.id}">edit</a>
Затем это значение присваивается параметру метода и далее по нему мы через сервис из репозитория получаем конкретный фильм и добавляем его в модель. Это был метод для получения страницы редактирования, теперь нужен метод для самого редактирования:

@RequestMapping(value = "/edit", method = RequestMethod.POST)
    public ModelAndView editFilm(@ModelAttribute("film") Film film) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("redirect:/");
        filmService.edit(film);
        return modelAndView;
    }
В методе editPage мы добавили в модель атрибут:

modelAndView.addObject("film", filmService.getById(id));
И теперь с помощью аннотации @ModelAttribute мы получаем этот атрибут и можем его изменить. Метод запроса POST потому что здесь мы будем передавать данные. "redirect:/" означает, что после выполнения данного метода мы будем перенаправлены на адрес "/", т.е. запустится метод allFilms и мы вернемся на главную страницу. Теперь сделаем саму страницу editPage.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>Edit</title>
</head>
<body>
<c:url value="/edit" var="var"/>
<form action="${var}" method="POST">
    <input type="hidden" name="id" value="${film.id}">
    <label for="title">Title</label>
    <input type="text" name="title" id="title">
    <label for="year">Year</label>
    <input type="text" name="year" id="year">
    <label for="genre">Genre</label>
    <input type="text" name="genre" id="genre">
    <label for="watched">Watched</label>
    <input type="text" name="watched" id="watched">
    <input type="submit" value="Edit film">
</form>
</body>
</html>
  • <form> — форма для сбора и отправки данных, с указанием кто их будет обрабатывать (/edit)
  • <input> — элементы интерфейса для взаимодействия с пользователем (кнопки, поля ввода и т.д.)
  • <label> — текстовая метка
Итак, при нажатии кнопки <input type="submit" value="Edit film"> данные из формы будут отправлены на сервер (специально добавлено невидимое поле со значением id, чтобы сервер знал какую именно запись в БД нужно обновить). В методе editFilm они будут присвоены соответствующим полям атрибута film. Затем мы вернемся на главную страницу с обновленным списком. Выглядит страница редактирования так: Теперь займемся добавлением новых фильмов в список. Для этого также понадобится форма для ввода и отправки данных. Можно сделать форму на главной странице или можно сделать отдельную страницу, наподобие editPage.jsp. Но, с другой стороны, форма для добавления ведь будет точно такая же, как и для редактирования, т.е. 4 поля для ввода и кнопка отправки. Так зачем тогда создавать новую страницу, снова используем editPage.jsp. Метод для получения страницы:

@RequestMapping(value = "/add", method = RequestMethod.GET)
    public ModelAndView addPage() {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("editPage");
        return modelAndView;
    }
В методе editPage мы дополнительно передавали атрибут, чтобы потом его изменить, а тут мы просто получаем страницу. И метод для добавления:

@RequestMapping(value = "/add", method = RequestMethod.POST)
    public ModelAndView addFilm(@ModelAttribute("film") Film film) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("redirect:/");
        filmService.add(film);
        return modelAndView;
    }
Поскольку атрибут мы сюда не передавали, здесь будет создан новый объект Film. Ну а так здесь в принципе ничего нового. Стоит также обратить внимание, что у нас оба метода доступны по адресу "/add". Это возможно благодаря тому, что они реагируют на разные типы запроса. Переходя по ссылке на главной странице мы делаем GET-запрос, что приводит нас в метод addPage. А когда на странице добавления мы жмем кнопку отправки данных, делается POST-запрос, за это уже отвечает метод addFilm. Для добавления нового фильма мы решили использовать ту же страницу, что и для редактирования. Но там ведь данные отправляются на адрес "/edit":

<c:url value="/edit" var="var"/>
<form action="${var}" method="POST">
    <input type="submit" value="Edit film">
</form>
Нам нужно немного подправить страницу, чтобы она вела себя по-разному для добавления и редактирования. Для решения этого вопроса воспользуемся условиями из все той же библиотеки JSTL core:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <c:if test="${empty film.title}">
        <title>Add</title>
    </c:if>
    <c:if test="${!empty film.title}">
        <title>Edit</title>
    </c:if>
</head>
<body>
<c:if test="${empty film.title}">
    <c:url value="/add" var="var"/>
</c:if>
<c:if test="${!empty film.title}">
    <c:url value="/edit" var="var"/>
</c:if>
<form action="${var}" method="POST">
    <c:if test="${!empty film.title}">
        <input type="hidden" name="id" value="${film.id}">
    </c:if>
    <label for="title">Title</label>
    <input type="text" name="title" id="title">
    <label for="year">Year</label>
    <input type="text" name="year" id="year">
    <label for="genre">Genre</label>
    <input type="text" name="genre" id="genre">
    <label for="watched">Watched</label>
    <input type="text" name="watched" id="watched">
    <c:if test="${empty film.title}">
        <input type="submit" value="Add new film">
    </c:if>
    <c:if test="${!empty film.title}">
        <input type="submit" value="Edit film">
    </c:if>
</form>
</body>
</html>
Т.е. мы просто проверяем поле film.title. Если оно пустое, значит это новый фильм, мы должны заполнить для него все данные и добавить в список. Если это поле не пустое, значит это фильм из списка и его нужно просто изменить. Т.о. получаем два варианта нашей странички: Ну и последний метод контроллера для удаления фильма из списка:

@RequestMapping(value="/delete/{id}", method = RequestMethod.GET)
    public ModelAndView deleteFilm(@PathVariable("id") int id) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("redirect:/");
        Film film = filmService.getById(id);
        filmService.delete(film);
        return modelAndView;
    }
Думаю, нет нужды тут что-то комментировать, все это уже рассмотрели. Ссылки на этот адрес на главной странице мы уже сделали. Ну что ж, тут вроде как все готово, можно еще раз запустить и посмотреть, как все работает.

Repository и Service как компоненты Spring

Сделаем еще одну небольшую поправку. Дело в том, что сейчас наши хранилище и сервис это просто классы, и чтобы их использовать приходится самим создавать объект класса (new FilmServiceImpl()). Но у нас ведь не просто так подключен Spring, так пусть он сам это дело и контролирует. Чтобы отдать наши классы под управление Spring'а, нужно обозначить, что они являются компонентами. Для этого отметим их специальными аннотациями:

@Repository
public class FilmDAOImpl implements FilmDAO {

@Service
public class FilmServiceImpl implements FilmService {
Аннотации @Repository и @Service, так же как и @Controller являются производными от @Component. В чем конкретные особенности и различия этих трех аннотаций и чем они отличаются от простого компонента стоит почитать отдельно в документации или гайдах. Пока же достаточно знать, что эти аннотации сообщают Spring о том, что данные классы являются репозиторием и сервисом соответственно. И теперь нам больше не нужно самим создавать конкретные объекты этих классов:

private FilmService filmService = new FilmServiceImpl(); 
Вместо этого можно пометить поле специальной аннотацией и Spring сам подберет подходящую реализацию:

@Autowired
private FilmService filmService;
Аннотация @Autowired (автосвязывание) сообщает Spring о том, что он должен покопаться у себя в контексте и подставить сюда подходящий бин. Очень удобно. Если до этого мы использовали интерфейсы, чтобы не беспокоиться насчет конкретной реализации методов, то теперь нам не нужно беспокоиться даже насчет реализации самого интерфейса и даже знать ее название. Идея подсказывает, что использовать автосвязывание на поле не рекомендуется, лучше использовать конструктор или сеттер. Подробнее об этом почитаем в документации. Для нас в принципе это не важно, можно смело оставлять так. Но, раз уж идея просит, то уважим, чтоб все было красиво и без всяких желтых предупреждений. В классе контроллера создадим сеттер и пометим аннотацией его:

@Controller
public class FilmController {
    
    private FilmService filmService;

    @Autowired
    public void setFilmService(FilmService filmService) {
        this.filmService = filmService;
    }
И аналогично делаем сеттер для FilmDAO в классе FilmServiceImpl. Продолжение следует... Знакомство с Maven, Spring, MySQL, Hibernate и первое CRUD приложение (часть 1) Знакомство с Maven, Spring, MySQL, Hibernate и первое CRUD приложение (часть 2) Знакомство с Maven, Spring, MySQL, Hibernate и первое CRUD приложение (часть 3) Знакомство с Maven, Spring, MySQL, Hibernate и первое CRUD приложение (часть 4)