Быстрая разработка 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

Не используйте ddl-auto со значением update на живой базе или при разработке проекта, так как он автоматически обновляет схему базы данных.
Существующие параметры для ddl-auto:
create — создаст таблицу в базе данных, предварительно удалив старую версию таблицы (потеря данных в случае изменения схемы)
validate — проверяет таблицу в базе данных, если она не соответствует сущности то hibernate выбросит исключение
update — проверяет таблицу, и автоматически ее обновляет без удаления несуществующих полей из сущности
create-drop — проверяет таблицу, создает или обновляет ее, а потом удаляет, предназначен для модульного тестирования


С установленным значением ddl-auto:update — hibernate автоматически создает таблицу в базе данных на основе нашей сущности, т.к. мы делаем простую адресную книгу то создадим класс контакта.


Contact.class
@Entity(name = "Contacts")
@Getter
@Setter
@EqualsAndHashCode
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.

ContactRepository.class
public interface ContactRepository extends CrudRepository<Contact, Integer> {
    List<Contact> 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<Integer> {}

Создание списка контактов


В конструкторе объекта ContactList, указываем используемые компоненты предварительно сделав их полями объекта. Так как данные будут браться из базы данных, то необходимо подключить репозиторий в поле объекта.


@Route("contacts")
public class ContactList extends AppLayout {
    VerticalLayout layout;
    Grid<Contact> 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);
    }
}

Об Autowired (обновлено)

Не пытайтесь получить доступ к contactRepository из конструктора объекта это непременно вызовет NullPointerException, получайте доступ из методов с аннотацией PostConstruct, или методов уже созданного объекта.
По наводке zesetup: contactRepository можно инъецировать через конструктор:


ContactRepository contactRepository;
@Autowired
public ContactList(ContactRepository contactRepository){
this.contactRepository = contactRepository;

По наводке markellg: Стоить отметить также то, что при использовании Spring версии 4.3 и выше, аннотация «Autowired» на конструктор необязательна если класс содержит единственный конструктор.


В класс ContactList добавлен компонент VerticalLayout для вертикального расположения элементов в содержимом, в него добавим RouterLink (для перехода на страницу создания контакта) и Grid для отображения таблицы. Grid типизирован объектом Contact для того чтобы мы могли загрузить из списка данные и они автоматом подтянулись при вызове метода setItems();


Grid<Contact> grid;
public ContactList(){
        grid = new Grid<>(); // Создаст пустую таблицу без колонок
        grid = new Grid<>(Contact.class); // Автоматически заполнит таблицу полями из объекта, дав им названия соответствующие наименованию поля
    }

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


Для заполнения таблицы получим данные из contactRepository, для этого создадим метод с аннотацией PostConstruct


Заполнение таблицы с помощью fillGrid()
    @PostConstruct
    public void fillGrid(){
        List<Contact> 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>("Редактировать", contact -> {
                UI.getCurrent().navigate(ManageContact.class,contact.getId());
            }));
            grid.addColumn(new NativeButtonRenderer<Contact>("Удалить", 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>("Редактировать", contact -> {
       //DO SOMETHING
}));
grid.addColumn(new NativeButtonRenderer<Contact>("Редактирование", new ClickableRenderer.ItemClickListener<Contact>() {
          @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


ManageContact.class
@Route("manageContact")
public class ManageContact extends AppLayout implements HasUrlParameter<Integer> {

    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> 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.


Репозиторий с проектом


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

Подробнее
Реклама

Комментарии 12

    0
    Полезно, как раз нужно было сделать несколько страниц с вводом данных, но вникать во фронт ни малейшего желание нет, да и возможности.
      0
      ИМХО для красоты можно использовать конструкторы с параметрами. :-)
      А так, для прикладушки на коленках, даже jsp+Spring MVC можно использовать.
      Для jsf — есть проект JoinFaces
        0
        Безусловно Vaadin ускоряет разработку, но есть проблемы.

        Я работал также с Vaadin 8, он еще основан на GWT. И я очень быстро собрал сложный интерфейс с кучей форм и гридов без написания CSS и JavaScript. Также много готовых компонентов написало сообщество разработчиков. В ситуации с Vaadin 14 все происходит иначе. Разработчик данного фреймворка начиная с версии 10 решил отказаться от использования GWT, поскольку проект старый и не развивается.

        Разработчики Vaadin написали свой Vaadin Flow с использованием Polymer WebComponents. Фреймворк стал гибче.

        Но при написании своего Polymer компонента и внедрения в него других JS библиотек возникают проблемы с конфликтом версий в npm.

        Babel для сборки Vaadin компонентов имеет старую версию, и если ваша библиотека не может быть собрана с этой версией, а ей нужна новее, то возникает проблема. Меняя на версию новее перестают собираться компоненты от Vaadin Flow.

        Поэтому мне пришлось отказаться от использования данного фреймворка в своем проекте.
          0
          И на чем остановились в итоге?
            0
            Решил делать всё на Spring Rest, а для UI взять готовые сверстанные шаблоны. Плюс есть свой небольшой фреймворк.
            0

            Начиная с версии 10 разработчиками было внедрено спорное решение: интегрироваться с экосистемой JS и использоваь ее средства сборки. Если раньше все было достаточно просто и быстро, то сейчас оно тащит node с двухсотмегабайтным бонусом пакетов, и собирается минут 5. Пробовал Vaadin 14, у которого даже официальные сэмплы не собирались. В офисе сборка внезапно подвисает минут на 10, но в лог ничего не пишется — подозреваю, что что-то пытается пробиться сквозь корпоративные прокси. Кроме того, для Vaadin Flow на порядок меньше стало плагинов. Вобщем, все стало как-то тяжко и хрупко. Поэтому продолжаю пользовать Vaadin 8.


            P.S. Хоть сам Vaadin 8 и написан на GWT, но практически все аддоны написаны на JS или предоставляют враппер над существующей JS-библиотекой, а посему не требуют никакой дополнительной компиляции при сборке.

              0
              GWT, поскольку проект старый и не развивается

              Хмм… А последний коммит сутки назад был (https://github.com/gwtproject/gwt). А в качестве альтернативы Vaadin, можно Domino UI использовать
              0
              Не пытайтесь получить доступ к contactRepository из конструктора объекта это непременно вызовет NullPointerException, получайте доступ из методов с аннотацией PostConstruct, или методов уже созданного объекта.

              Можно инъецировать через конструктор:


                  ContactRepository contactRepository;
              
                   @Autowired
                   public ContactList(ContactRepository contactRepository){
                   this.contactRepository = contactRepository;
                   ...
                0
                Стоить отметить также то, что при использовании Spring версии 4.3 и выше, аннотация «Autowired» на конструктор необязательна если класс содержит единственный конструктор.

                P.S. Не очевидным моментом в статье является то, что при использовании Spring классы аннотированные как «Route» являются аналогами классов, помеченных как «Component» в обычном Spring приложении. Например, инспектор IDEA выводит предупреждение на этот счет.
                  0
                  Почему не очевидным? Route все-же не является полным аналогом, он несет в себе дополнительный функционал для нормальной работы Vaadin.
                  Так используя Component вместо Route — не возможно будет использовать полностью Vaadin Flow, назначение у них все же разное.
                  Используя Component в ContactList сразу получаешь:
                  Could not navigate to 'contacts'
                  Reason: Couldn't find route for 'contacts'

                  Можете привести рабочий пример с использованием Component?
                    0
                    Извиняюсь, я неверно выразился. Не очевидно то, что для классов аннотированных как Route создаются бины в контексте Spring. Этот момент описан в документации к Vaadin, возможно стоит его добавить:
                    The only difference between using the router in a standard application and a Spring application, is that in Spring you can use dependency injection in components annotated with Route. These components are instantiated by Spring and become Spring-initialized beans. In particular, this means you can autowire other Spring-managed beans.
                  0
                  Спасибо за наводку, обновил статью

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое