Spring Boot 与 Docker
观察 GraphQL 的实际运行

本指南将引导您完成构建一个应用程序的过程,该应用程序在前端使用基于 Vaadin 的 UI,后端则基于 Spring Data JPA。

你将构建什么

您将为简单的 JPA 仓库构建一个 Vaadin 用户界面。您将获得一个具有完整 CRUD(创建、读取、更新和删除)功能的应用程序,以及一个使用自定义仓库方法的过滤示例。

您可以按照以下两种不同的路径之一进行操作:

  • 从项目中已有的 initial 项目开始。

  • 重新开始。

差异将在本文档后面讨论。

您需要什么

如何完成本指南

与大多数 Spring 入门指南 一样,您可以从头开始并完成每个步骤,或者跳过您已经熟悉的基本设置步骤。无论哪种方式,最终您都会得到可运行的代码。

从头开始,请继续阅读 使用 Spring Initializr 开始

跳过基础部分,请执行以下操作:

完成后,您可以对照 gs-crud-with-vaadin/complete 中的代码检查您的结果。

从 Spring Initializr 开始

您可以使用这个预初始化项目,点击生成以下载一个 ZIP 文件。该项目已配置为适合本教程中的示例。

您也可以从 Github 上 fork 该项目,并在您的 IDE 或其他编辑器中打开它。

手动初始化(可选)

如果您想要手动初始化项目,而不是使用前面显示的链接,请按照以下步骤操作:

  1. 访问 https://start.spring.io。该服务会为您拉取应用程序所需的所有依赖项,并为您完成大部分设置。

  2. 选择 Gradle 或 Maven 以及您要使用的语言。本指南假设您选择了 Java。

  3. 点击 Dependencies,然后选择 VaadinSpring Data JPAH2 Database

  4. 点击 Generate

  5. 下载生成的 ZIP 文件,这是一个根据您的选择配置的 Web 应用程序的存档。

如果您的 IDE 集成了 Spring Initializr,您可以直接在 IDE 中完成这一过程。

创建后端服务

本指南是使用 JPA 访问数据的延续。唯一的区别在于实体类中包含了 getter 和 setter 方法,并且仓库中的自定义搜索方法对最终用户来说更为优雅。您无需阅读那篇指南即可理解本篇内容,但如果您愿意,也可以阅读。

如果您是从一个全新的项目开始的,您需要添加实体和仓库对象。如果您是从 initial 项目开始的,这些对象已经存在。

以下代码清单(来自 src/main/java/com/example/crudwithvaadin/Customer.java)定义了客户实体:

package com.example.crudwithvaadin;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class Customer {

    @Id
    @GeneratedValue
    private Long id;

    private String firstName;

    private String lastName;

    protected Customer() {
    }

    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public Long getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id,
                firstName, lastName);
    }

}

以下代码清单(来自 src/main/java/com/example/crudwithvaadin/CustomerRepository.java)定义了客户仓库:

package com.example.crudwithvaadin;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface CustomerRepository extends JpaRepository<Customer, Long> {

    List<Customer> findByLastNameStartsWithIgnoreCase(String lastName);
}

以下代码清单(来自 src/main/java/com/example/crudwithvaadin/CrudWithVaadinApplication.java)展示了应用程序类,它为您创建了一些数据:

package com.example.crudwithvaadin;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class CrudWithVaadinApplication {

    private static final Logger log = LoggerFactory.getLogger(CrudWithVaadinApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(CrudWithVaadinApplication.class);
    }

    @Bean
    public CommandLineRunner loadData(CustomerRepository repository) {
        return (args) -> {
            // save a couple of customers
            repository.save(new Customer("Jack", "Bauer"));
            repository.save(new Customer("Chloe", "O'Brian"));
            repository.save(new Customer("Kim", "Bauer"));
            repository.save(new Customer("David", "Palmer"));
            repository.save(new Customer("Michelle", "Dessler"));

            // fetch all customers
            log.info("Customers found with findAll():");
            log.info("-------------------------------");
            for (Customer customer : repository.findAll()) {
                log.info(customer.toString());
            }
            log.info("");

            // fetch an individual customer by ID
            Customer customer = repository.findById(1L).get();
            log.info("Customer found with findOne(1L):");
            log.info("--------------------------------");
            log.info(customer.toString());
            log.info("");

            // fetch customers by last name
            log.info("Customer found with findByLastNameStartsWithIgnoreCase('Bauer'):");
            log.info("--------------------------------------------");
            for (Customer bauer : repository
                    .findByLastNameStartsWithIgnoreCase("Bauer")) {
                log.info(bauer.toString());
            }
            log.info("");
        };
    }

}

Vaadin 依赖项

如果您检出了 initial 项目,或者您使用 initializr 创建了您的项目,那么所有必要的依赖项已经设置好了。然而,本节的其余部分将介绍如何在一个全新的 Spring 项目中添加 Vaadin 支持。Spring 的 Vaadin 集成包含了一个 Spring Boot starter 依赖集合,因此您只需要添加以下 Maven 片段(或相应的 Gradle 配置):

<dependency>
    <groupId>com.vaadin</groupId>
    <artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>

该示例使用了比 starter 模块默认引入的版本更新的 Vaadin 版本。要使用更新的版本,请按照如下方式定义 Vaadin 材料清单 (BOM):

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-bom</artifactId>
            <version>${vaadin.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

在开发者模式下,依赖项已经足够,但在为生产环境构建时,您需要为您的应用程序启用生产构建

默认情况下,Gradle 不支持 BOMs,但有一个方便的插件可以解决这个问题。查看build.gradle 构建文件中的示例,了解如何实现相同的功能

定义 Main View 类

主视图类(在本指南中称为 MainView)是 Vaadin UI 逻辑的入口点。在 Spring Boot 应用程序中,如果您用 @Route 注解它,它会被自动识别并显示在 Web 应用程序的根路径下。您可以通过为 @Route 注解提供参数来自定义视图显示的 URL。以下代码清单(来自 initial 项目中的 src/main/java/com/example/crudwithvaadin/MainView.java)展示了一个简单的“Hello, World”视图:

package com.example.crudwithvaadin;

import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;

@Route
public class MainView extends VerticalLayout {

    public MainView() {
        add(new Button("Click me", e -> Notification.show("Hello, Spring+Vaadin user!")));
    }
}

在数据网格中列出实体

为了获得良好的布局,您可以使用 Grid 组件。您可以通过 setItems 方法将构造器注入的 CustomerRepository 中的实体列表传递给 Grid。然后,您的 MainView 的主体将如下所示:

@Route
public class MainView extends VerticalLayout {

    private final CustomerRepository repo;
    final Grid<Customer> grid;

    public MainView(CustomerRepository repo) {
        this.repo = repo;
        this.grid = new Grid<>(Customer.class);
        add(grid);
        listCustomers();
    }

    private void listCustomers() {
        grid.setItems(repo.findAll());
    }

}

如果您有大型表格或大量并发用户,您很可能不希望将整个数据集绑定到您的UI组件上。\ 尽管Vaadin Grid会从服务器懒加载数据到浏览器,但上述方法会将整个数据列表保留在服务器内存中。为了节省内存,您可以通过分页或使用懒加载来仅显示最顶部的结果,例如使用grid.setItems(VaadinSpringDataHelpers.fromPagingRepository(repo))方法。

数据过滤

在大数据集对您的服务器造成问题之前,它很可能会给您的用户带来困扰,因为他们试图找到要编辑的相关行。您可以使用 TextField 组件来创建一个过滤器输入框。为此,首先需要修改 listCustomer() 方法以支持过滤功能。以下示例(来自 complete 项目中的 src/main/java/com/example/crudwithvaadin/MainView.java 文件)展示了如何实现这一点:

void listCustomers(String filterText) {
    if (StringUtils.hasText(filterText)) {
        grid.setItems(repo.findByLastNameStartsWithIgnoreCase(filterText));
    } else {
        grid.setItems(repo.findAll());
    }
}

这正是 Spring Data 的声明式查询派上用场的地方。在 CustomerRepository 接口中,编写 findByLastNameStartsWithIgnoringCase 只需一行定义。

您可以将监听器挂接到 TextField 组件,并将其值插入到该过滤方法中。由于您在过滤文本字段上定义了 ValueChangeMode.LAZY,因此当用户输入时,ValueChangeListener 会自动调用。以下示例展示了如何设置此类监听器:

TextField filter = new TextField();
filter.setPlaceholder("Filter by last name");
filter.setValueChangeMode(ValueChangeMode.LAZY);
filter.addValueChangeListener(e -> listCustomers(e.getValue()));
add(filter, grid);

定义编辑器组件

由于 Vaadin UI 是纯 Java 代码,您从一开始就可以编写可重用的代码。为此,为您的 Customer 实体定义一个编辑器组件。您可以将其设置为由 Spring 管理的 bean,以便直接将 CustomerRepository 注入到编辑器中,并处理创建、更新和删除部分或您的 CRUD 功能。以下示例(来自 src/main/java/com/example/crudwithvaadin/CustomerEditor.java)展示了如何做到这一点:

package com.example.crudwithvaadin;

import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.KeyNotifier;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.spring.annotation.SpringComponent;
import com.vaadin.flow.spring.annotation.UIScope;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * A simple example to introduce building forms. As your real application is probably much
 * more complicated than this example, you could re-use this form in multiple places. This
 * example component is only used in MainView.
 * <p>
 * In a real world application you'll most likely using a common super class for all your
 * forms - less code, better UX.
 */
@SpringComponent
@UIScope
public class CustomerEditor extends VerticalLayout implements KeyNotifier {

    private final CustomerRepository repository;

    /**
     * The currently edited customer
     */
    private Customer customer;

    /* Fields to edit properties in Customer entity */
    TextField firstName = new TextField("First name");
    TextField lastName = new TextField("Last name");

    /* Action buttons */
    Button save = new Button("Save", VaadinIcon.CHECK.create());
    Button cancel = new Button("Cancel");
    Button delete = new Button("Delete", VaadinIcon.TRASH.create());
    HorizontalLayout actions = new HorizontalLayout(save, cancel, delete);

    Binder<Customer> binder = new Binder<>(Customer.class);
    private ChangeHandler changeHandler;

    @Autowired
    public CustomerEditor(CustomerRepository repository) {
        this.repository = repository;

        add(firstName, lastName, actions);

        // bind using naming convention
        binder.bindInstanceFields(this);

        // Configure and style components
        setSpacing(true);

        save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        delete.addThemeVariants(ButtonVariant.LUMO_ERROR);

        addKeyPressListener(Key.ENTER, e -> save());

        // wire action buttons to save, delete and reset
        save.addClickListener(e -> save());
        delete.addClickListener(e -> delete());
        cancel.addClickListener(e -> editCustomer(customer));
        setVisible(false);
    }

    void delete() {
        repository.delete(customer);
        changeHandler.onChange();
    }

    void save() {
        repository.save(customer);
        changeHandler.onChange();
    }

    public interface ChangeHandler {
        void onChange();
    }

    public final void editCustomer(Customer c) {
        if (c == null) {
            setVisible(false);
            return;
        }
        final boolean persisted = c.getId() != null;
        if (persisted) {
            // Find fresh entity for editing
            // In a more complex app, you might want to load
            // the entity/DTO with lazy loaded relations for editing
            customer = repository.findById(c.getId()).get();
        }
        else {
            customer = c;
        }
        cancel.setVisible(persisted);

        // Bind customer properties to similarly named fields
        // Could also use annotation or "manual binding" or programmatically
        // moving values from fields to entities before saving
        binder.setBean(customer);

        setVisible(true);

        // Focus first name initially
        firstName.focus();
    }

    public void setChangeHandler(ChangeHandler h) {
        // ChangeHandler is notified when either save or delete
        // is clicked
        changeHandler = h;
    }

}

在大型应用程序中,您可以在多个地方使用这个编辑器组件。同时请注意,在大型应用程序中,您可能需要应用一些常见的模式(如 MVP)来组织您的 UI 代码。

配置编辑器

在前面的步骤中,您已经了解了基于组件编程的一些基础知识。通过使用 Button 并向 Grid 添加选择监听器,您可以将编辑器完全集成到主视图中。以下代码清单(来自 src/main/java/com/example/crudwithvaadin/MainView.java)展示了 MainView 类的最终版本:

package com.example.crudwithvaadin;

import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.Route;
import org.springframework.util.StringUtils;

@Route
public class MainView extends VerticalLayout {

    private final CustomerRepository repo;

    private final CustomerEditor editor;

    final Grid<Customer> grid;

    final TextField filter;

    private final Button addNewBtn;

    public MainView(CustomerRepository repo, CustomerEditor editor) {
        this.repo = repo;
        this.editor = editor;
        this.grid = new Grid<>(Customer.class);
        this.filter = new TextField();
        this.addNewBtn = new Button("New customer", VaadinIcon.PLUS.create());

        // build layout
        HorizontalLayout actions = new HorizontalLayout(filter, addNewBtn);
        add(actions, grid, editor);

        grid.setHeight("300px");
        grid.setColumns("id", "firstName", "lastName");
        grid.getColumnByKey("id").setWidth("50px").setFlexGrow(0);

        filter.setPlaceholder("Filter by last name");

        // Hook logic to components

        // Replace listing with filtered content when user changes filter
        filter.setValueChangeMode(ValueChangeMode.LAZY);
        filter.addValueChangeListener(e -> listCustomers(e.getValue()));

        // Connect selected Customer to editor or hide if none is selected
        grid.asSingleSelect().addValueChangeListener(e -> {
            editor.editCustomer(e.getValue());
        });

        // Instantiate and edit new Customer the new button is clicked
        addNewBtn.addClickListener(e -> editor.editCustomer(new Customer("", "")));

        // Listen changes made by the editor, refresh data from backend
        editor.setChangeHandler(() -> {
            editor.setVisible(false);
            listCustomers(filter.getValue());
        });

        // Initialize listing
        listCustomers(null);
    }

    // tag::listCustomers[]
    void listCustomers(String filterText) {
        if (StringUtils.hasText(filterText)) {
            grid.setItems(repo.findByLastNameStartsWithIgnoreCase(filterText));
        } else {
            grid.setItems(repo.findAll());
        }
    }
    // end::listCustomers[]

}

构建可执行的 JAR 文件

您可以使用 Gradle 或 Maven 从命令行运行应用程序。您还可以构建一个包含所有必要依赖项、类和资源的可执行 JAR 文件并运行它。构建可执行 JAR 文件使得在开发生命周期中跨不同环境轻松地发布、版本管理和部署服务成为可能。

如果您使用 Gradle,可以通过 ./gradlew bootRun 运行应用程序。或者,您可以通过 ./gradlew build 构建 JAR 文件,然后运行该 JAR 文件,如下所示:

java -jar build/libs/gs-crud-with-vaadin-0.1.0.jar

如果您使用的是 Maven,可以通过 ./mvnw spring-boot:run 来运行应用程序。或者,您也可以使用 ./mvnw clean package 构建 JAR 文件,然后按如下方式运行该 JAR 文件:

java -jar target/gs-crud-with-vaadin-0.1.0.jar

这里描述的步骤创建了一个可运行的 JAR 文件。您也可以构建一个传统的 WAR 文件

您可以在 http://localhost:8080 看到您的 Vaadin 应用程序正在运行。

总结

恭喜!您已经使用 Spring Data JPA 作为持久化层编写了一个功能齐全的 CRUD UI 应用程序。而且,您在没有暴露任何 REST 服务或编写任何一行 JavaScript 或 HTML 代码的情况下完成了这一任务。

另请参阅

以下指南可能也会有所帮助:

本页目录