[Из песочницы] Быстрая разработка Web приложения на Vaadin и Spring Boot
Целью данной статьи является систематизация процесса разработки веб приложения на Vaadin 14 с использованием Spring Boot.
Перед прочтением данной статьи, рекомендую прочитать следующий материал:
Впечатления
Vaadin 14 — довольно удобное средство для проектирования веб приложений, до знакомства с ним разрабатывал графические интерфейсы только на JavaFX, Android, и даже J2ME, и избегал при этом frontend разработки (базовые знания HTML, CSS, JS имеются) потому что считал что это не мое.
Disclaimer
Те кто не работал еще с Spring Boot рекомендую пропустить быстрый старт с помощью Spring Initializr, вернуться к рекомендуемому материалу, и попробовать настроить все самостоятельно, наткнувшись на множество различных проблем, иначе в дальнейшем возникнут пробелы в понимании различных вещей.
Быстрый старт
Создадим проект для нашего web-приложения с помощью Spring Initializr, необходимые зависимости для нашего маленького web-приложения:
- Spring Data JPA (для работы с базой данных)
- Vaadin (для разработки веб-приложения)
- Lombok (для уменьшения boiler-plate кода)
- MySQL Driver (я использую mariadb, в spring initializr’e его нет)
Настройка application.properties и базы данных
Проект созданный на Spring Initializr практически готов к запуску, нам остается только настроить application.properties указав путь к базе данных, логин и пароль
spring.datasource.url = jdbc:mariadb://127.0.0.1:3306/test
spring.datasource.username=user
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
Не используйте ddl-auto со значением update на живой базе или при разработке проекта, так как он автоматически обновляет схему базы данных.
Существующие параметры для ddl-auto:
create — создаст таблицу в базе данных, предварительно удалив старую версию таблицы (потеря данных в случае изменения схемы)
validate — проверяет таблицу в базе данных, если она не соответствует сущности то hibernate выбросит исключение
update — проверяет таблицу, и автоматически ее обновляет без удаления несуществующих полей из сущности
create-drop — проверяет таблицу, создает или обновляет ее, а потом удаляет, предназначен для модульного тестирования
С установленным значением ddl-auto: update — hibernate автоматически создает таблицу в базе данных на основе нашей сущности, т.к. мы делаем простую адресную книгу то создадим класс контакта.
@Entity(name = "Contacts")
@Getter
@Setter
public class Contact {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String firstName;
private String secondName;
private String fatherName;
private String numberPhone;
private String email;
}
Создадим интерфейс для работы с базой данных, и добавим метод возвращающий List т.к. Spring Data JPA по умолчанию возвращает Iterable вместо List.
public interface ContactRepository extends CrudRepository {
List findAll();
}
Разработка интерфейса на Vaadin включает в себя добавление компонентов форм ввода, визуализации и взаимодействия в объекты макетов для необходимого позиционирования элементов. Список всех компонентов можно посмотреть на официальном сайте framework’а
Основной страницей нашего приложения будет ContactList. Все объекты созданных страниц будем наследовать от AppLayout — это типовой макет веб приложения состоящий из:
- Navbar (шапка)
- Drawer (боковая панель)
- Content (содержимое)
При этом в Navbar и Drawer добавляются компоненты, а в Content устанавливается компонент в качестве содержимого, к примеру VerticalLayout в котором будут размещаться пользовательские элементы в вертикальном расположении.
Страницей редактирования и создания контактов будет ManageContact, и реализуем в нем интерфейс HasUrlParameter для передачи id контакта, при включении данного интерфейса обязательно наличие переданного параметра странице.
Для того чтобы страницу привязать к определенному URL используется аннотация Route:
@Route("contacts")
public class ContactList extends AppLayout {}
@Route("manageContact")
public class ManageContact extends AppLayout implements HasUrlParameter {}
Создание списка контактов
В конструкторе объекта ContactList, указываем используемые компоненты предварительно сделав их полями объекта. Так как данные будут браться из базы данных, то необходимо подключить репозиторий в поле объекта.
@Route("contacts")
public class ContactList extends AppLayout {
VerticalLayout layout;
Grid grid;
RouterLink linkCreate;
@Autowired
ContactRepository contactRepository;
public ContactList(){
layout = new VerticalLayout();
grid = new Grid<>();
linkCreate = new RouterLink("Создать контакт",ManageContact.class,0);
layout.add(linkCreate);
layout.add(grid);
addToNavbar(new H3("Список контактов"));
setContent(layout);
}
}
Не пытайтесь получить доступ к contactRepository из конструктора объекта это непременно вызовет NullPointerException, получайте доступ из методов с аннотацией PostConstruct, или методов уже созданного объекта.
В класс ContactList добавлен компонент VerticalLayout для вертикального расположения элементов в содержимом, в него добавим RouterLink (для перехода на страницу создания контакта) и Grid для отображения таблицы. Grid типизирован объектом Contact для того чтобы мы могли загрузить из списка данные и они автоматом подтянулись при вызове метода setItems ();
Grid grid;
public ContactList(){
grid = new Grid<>(); // Создаст пустую таблицу без колонок
grid = new Grid<>(Contact.class); // Автоматически заполнит таблицу полями из объекта, дав им названия соответствующие наименованию поля
}
Поведение автоматического создания колонок нам не интересно, т.к. в рамках статьи стоит показать добавление колонок, и отображение кнопок для удаления или редактирования контактов.
Для заполнения таблицы получим данные из contactRepository, для этого создадим метод с аннотацией PostConstruct
@PostConstruct
public void fillGrid(){
List contacts = contactRepository.findAll();
if (!contacts.isEmpty()){
//Выведем столбцы в нужном порядке
grid.addColumn(Contact::getFirstName).setHeader("Имя");
grid.addColumn(Contact::getSecondName).setHeader("Фамилия");
grid.addColumn(Contact::getFatherName).setHeader("Отчество");
grid.addColumn(Contact::getNumberPhone).setHeader("Номер");
grid.addColumn(Contact::getEmail).setHeader("E-mail");
//Добавим кнопку удаления и редактирования
grid.addColumn(new NativeButtonRenderer("Редактировать", contact -> {
UI.getCurrent().navigate(ManageContact.class,contact.getId());
}));
grid.addColumn(new NativeButtonRenderer("Удалить", contact -> {
Dialog dialog = new Dialog();
Button confirm = new Button("Удалить");
Button cancel = new Button("Отмена");
dialog.add("Вы уверены что хотите удалить контакт?");
dialog.add(confirm);
dialog.add(cancel);
confirm.addClickListener(clickEvent -> {
contactRepository.delete(contact);
dialog.close();
Notification notification = new Notification("Контакт удален",1000);
notification.setPosition(Notification.Position.MIDDLE);
notification.open();
grid.setItems(contactRepository.findAll());
});
cancel.addClickListener(clickEvent -> {
dialog.close();
});
dialog.open();
}));
grid.setItems(contacts);
}
}
Для добавления колонок с кнопками редактирования и удаления используется NativeButtonRenderer, в аргументы конструктора передаем название кнопки, и обработчик нажатия на кнопку.
grid.addColumn(new NativeButtonRenderer("Редактировать", contact -> {
//DO SOMETHING
}));
grid.addColumn(new NativeButtonRenderer("Редактирование", new ClickableRenderer.ItemClickListener() {
@Override
public void onItemClicked(Contact contact) {
//DO SOMETHING
}}));
Создание страницы редактирования контактов
Страница редактирования контактов принимает параметр в виде id контакта, поэтому нам необходимо имплементировать метод setParameter ():
@Override
public void setParameter(BeforeEvent beforeEvent, Integer contactId) {
id = contactId;
if (!id.equals(0)){
addToNavbar(new H3("Редактирование контакта"));
}
else {
addToNavbar(new H3("Создание контакта"));
}
fillForm(); //Заполнение формы
}
Добавление компонентов аналогично ContactList, только в данном случае мы не используем VerticalLayout, а используем FormLayout специальную разметку для отображения подобных форм. Заполняем форму данными уже не с помощью метода с аннотацией PostConstruct, а после получения номера контакта из URL, потому что цепочка: Конструктор объекта → @PostConstruct → Override
@Route("manageContact")
public class ManageContact extends AppLayout implements HasUrlParameter {
Integer id;
FormLayout contactForm;
TextField firstName;
TextField secondName;
TextField fatherName;
TextField numberPhone;
TextField email;
Button saveContact;
@Autowired
ContactRepository contactRepository;
public ManageContact(){
//Создаем объекты для формы
contactForm = new FormLayout();
firstName = new TextField("Имя");
secondName = new TextField("Фамилия");
fatherName = new TextField("Отчество");
numberPhone = new TextField("Номер телефона");
email = new TextField("Электронная почта");
saveContact = new Button("Сохранить");
//Добавим все элементы на форму
contactForm.add(firstName, secondName,fatherName,numberPhone,email,saveContact);
setContent(contactForm);
}
@Override
public void setParameter(BeforeEvent beforeEvent, Integer contactId) {
id = contactId;
if (!id.equals(0)){
addToNavbar(new H3("Редактирование контакта"));
}
else {
addToNavbar(new H3("Создание контакта"));
}
fillForm();
}
public void fillForm(){
if (!id.equals(0)){
Optional contact = contactRepository.findById(id);
contact.ifPresent(x -> {
firstName.setValue(x.getFirstName());
secondName.setValue(x.getSecondName());
fatherName.setValue(x.getFatherName());
numberPhone.setValue(x.getNumberPhone());
email.setValue(x.getEmail());
});
}
saveContact.addClickListener(clickEvent->{
//Создадим объект контакта получив значения с формы
Contact contact = new Contact();
if (!id.equals(0)){
contact.setId(id);
}
contact.setFirstName(firstName.getValue());
contact.setSecondName(secondName.getValue());
contact.setFatherName(fatherName.getValue());
contact.setEmail(email.getValue());
contact.setNumberPhone(numberPhone.getValue());
contactRepository.save(contact);
Notification notification = new Notification(id.equals(0)? "Контакт успешно создан":"Контакт был изменен",1000);
notification.setPosition(Notification.Position.MIDDLE);
notification.addDetachListener(detachEvent -> {
UI.getCurrent().navigate(ContactList.class);
});
contactForm.setEnabled(false);
notification.open();
});
}
}
Итоги: Vaadin 14 довольно удобный framework для создания простых web-приложений, с помощью него можно быстро сделать приложение имея в багаже знаний только Java, и оно будет работать. Но к большому сожалению весь интерфейс создается на стороне сервера и ресурсов необходимо гораздо больше нежели если использовать HTML5+JS. Данный framework больше подходит для небольших проектов которые нужно быстро сделать не изучая front-end технологии.
В данной статье было показано как можно быстро и легко создать web-приложение не проектируя предварительно базу данных, избежать длинных xml конфигураций, и то как быстро можно разработать web-интерфейс. По большей части Spring Boot и Spring Data JPA облегчает жизнь разработчика и упрощает разработку. Статья не откроет ничего нового уже состоявшимся разработчикам, но поможет новичку начать осваивать Spring framework.
Репозиторий с проектом
В статье возможны грамматические и пунктационные ошибки, при обнаружении прошу присылать в личку