Добрый день.
В этой статье я хотел бы поделиться своим первым знакомством с такими вещами как 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<Film> 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);
}
}
Структура проекта теперь выглядит следующим образом:
Контроллер и представления
Поработаем теперь над методами контроллера и наполнением страниц. При наполнении страниц нам понадобятся некоторые приемы. Например чтобы вывести список фильмов нужен цикл, если, допустим, хотим менять какую-то надпись, в зависимости от параметров, нужны условия и т.д. Формат 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)
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ