知識庫 / Spring / Spring Boot RSS 訂閱

Spring Boot 和 Vaadin 示例應用

Spring Boot
HongKong
4
01:43 PM · Dec 06 ,2025

1. 概述

Vaadin Flow 是一款用於創建 Web 用户界面的 基於 Java 的服務器端框架

在本教程中,我們將探索如何為基於 Spring Boot 的後端構建一個 基於 Vaadin Flow 的 CRUD UI。有關 Vaadin Flow 的介紹,請參考本教程。

2. 部署準備

讓我們從在標準 Spring Boot 應用程序中添加 Maven 依賴項開始:

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

Vaadin 也是 Spring Initializr 認可的依賴項。

如果未使用 Spring Initializr,我們也可以手動將 Vaadin 的 Bill of Materials 添加到項目中。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-bom</artifactId>
            <version>24.3.8</version> <!-- check latest version -->
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

3. 後端服務

我們將使用一個包含 firstNamelastName 屬性的 Employee 實體,對其執行 CRUD 操作。我們還定義了這些屬性的驗證規則,以便在構建的 UI 中強制執行它們:

@Entity
public class Employee {
    @Id
    @GeneratedValue
    private Long id;

    @Size(min = 2, message = "Must have at least 2 characters")
    private String firstName;

    @Size(min = 2, message = "Must have at least 2 characters")
    private String lastName;

    public Employee() {
    }
    
    // Getters and setters
}

以下是用於管理 CRUD 操作的簡單 Spring Data 存儲庫:

public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    List<Employee> findByLastNameStartsWithIgnoreCase(String lastName);
}

我們聲明在 EmployeeRepository 接口上定義一個查詢方法 findByLastNameStartsWithIgnoreCase。它將返回給定姓氏的員工列表。

我們還應該使用幾個樣本員工預填充數據庫:

@Bean
public CommandLineRunner loadData(EmployeeRepository repository) {
    return (args) -> {
        repository.save(new Employee("Bill", "Gates"));
        repository.save(new Employee("Mark", "Zuckerberg"));
        repository.save(new Employee("Sundar", "Pichai"));
        repository.save(new Employee("Jeff", "Bezos"));
    };
}

4. Vaadin Flow UI

該應用程序將包含一個可過濾的數據網格,用於顯示員工信息,以及一個用於編輯和創建員工的表單。我們首先將創建表單作為自定義組件,然後使用標準 Vaadin 組件和我們的自定義表單組件創建主佈局:

4.1. EmployeeEditor 表單組件

EmployeeEditor 是一個自定義組件,通過組合現有的 Vaadin 組件並定義自定義 API 創建的。

EmployeeForm 使用 VerticalLayout 作為其基礎。 VerticalLayout 是一個 Layout,它以添加的順序顯示子組件 (垂直)。 通過擴展 Composite<VerticalLayout> 而不是 VerticalLayout,我們不暴露所有 VerticalLayout 的方法,從而完全控制我們組件的 API。

讓我們首先定義我們組件的 API,以便用户可以設置要編輯的員工,並訂閲保存、取消和刪除事件:

public class EmployeeEditor extends Composite<VerticalLayout> {
    public interface SaveListener {
        void onSave(Employee employee);
    }

    public interface DeleteListener {
        void onDelete(Employee employee);
    }

    public interface CancelListener {
        void onCancel();
    }

    private Employee employee;

    private SaveListener saveListener;
    private DeleteListener deleteListener;
    private CancelListener cancelListener;

    private final Binder<Employee> binder = new BeanValidationBinder<>(Employee.class);

    public void setEmployee(Employee employee) {
        this.employee = employee;
        binder.readBean(employee);
    }

    // Getters and setters
}

setEmployee 方法保存當前員工的引用,並將 Bean 讀取到 Vaadin 的 BeanValidationBinder 中,以便將其綁定到我們表單的輸入字段並進行驗證。

接下來,我們使用 TextField 和 Button 組件構建組件的 UI。我們使用 binder 將輸入字段映射到模型中的字段。將 Bean 讀取到 binder 中意味着,當我們調用 setEmployee 時,輸入字段的值會得到更新。

我們將所有組件添加到 VerticalLayout 中,它是我們的 Composition 的根佈局:

public EmployeeEditor() {
    var firstName = new TextField("First name");
    var lastName = new TextField("Last name");

    var save = new Button("Save", VaadinIcon.CHECK.create());
    var cancel = new Button("Cancel");
    var delete = new Button("Delete", VaadinIcon.TRASH.create());

    binder.forField(firstName).bind("firstName");
    binder.forField(lastName).bind("lastName");

    save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
    save.addClickListener(e -> save());
    save.addClickShortcut(Key.ENTER);

    delete.addThemeVariants(ButtonVariant.LUMO_ERROR);
    delete.addClickListener(e -> deleteListener.onDelete(employee));

    cancel.addClickListener(e -> cancelListener.onCancel());

    getContent().add(firstName, lastName, new HorizontalLayout(save, cancel, delete));
}

最後,我們通過讀取輸入字段的值,創建一個新的 Employee 對象,前提是字段驗證通過。我們避免將這些值保存到原始對象中,因為該對象與 UI 的其他部分綁定,並且我們希望在不產生副作用的情況下使用我們的組件。一旦我們獲得了更新後的員工對象,我們就會調用 saveListener 來通知父組件。

4.2. 主視圖

<em>EmployeesView</em> 類是我們的應用程序的入口點。<strong>@Route(“”)` 註解告訴 Vaadin Flow 組件在應用程序啓動時將其映射到根路徑。

我們以 <em>VerticalLayout</em> 為基礎構建我們的視圖,然後使用標準 Vaadin 組件和我們創建的自定義 <em>EmployeeEditor</em> 構建 UI。

Vaadin Flow 視圖是 Spring 豆,這意味着我們可以通過構造函數將 <em>EmployeeRepository</em> 自動注入到視圖中。

@Route("")
public class EmployeesView extends VerticalLayout {
    private final EmployeeRepository employeeRepository;

    private final TextField filter;
    private final Grid<Employee> grid;
    private final EmployeeEditor editor;

    public EmployeesView(EmployeeRepository repo) {
        employeeRepository = repo;

        // Create components
        var addButton = new Button("New employee", VaadinIcon.PLUS.create());
        filter = new TextField();
        grid = new Grid<>(Employee.class);
        editor = new EmployeeEditor();

        // Compose layout
        var actionsLayout = new HorizontalLayout(filter, addButton);
        add(actionsLayout, grid, editor);
    }
}

4.3. 配置組件和數據

接下來,我們創建兩個輔助方法:一個用於根據搜索字符串更新網格,另一個用於處理員工選擇:

private void updateEmployees(String filterText) {
    if (filterText.isEmpty()) {
        grid.setItems(employeeRepository.findAll());
    } else {
        grid.setItems(employeeRepository.findByLastNameStartsWithIgnoreCase(filterText));
    }
}

private void editEmployee(Employee employee) {
    editor.setEmployee(employee);

    if (employee != null) {
        editor.setVisible(true);
    } else {
        // Deselect grid
        grid.asSingleSelect().setValue(null);
        editor.setVisible(false);
    }
}

最後,我們配置組件:

  • 我們配置 EmployeeEditor 組件使其初始時隱藏,並定義處理保存、刪除和取消事件的監聽器。
  • 我們將 ValueChangeMode 設置為 LAZY,應用於過濾器 TextField,以便在用户停止輸入時調用 updateEmployees 方法。
  • 我們為網格設置固定 200px 高度,並在選定行發生變化時調用 editEmployee 方法。
public EmployeesView(EmployeeRepository repo) {
    // Component creation code from above

    // Configure components
    configureEditor();

    addButton.addClickListener(e -> editEmployee(new Employee()));

    filter.setPlaceholder("Filter by last name");
    filter.setValueChangeMode(ValueChangeMode.EAGER);
    filter.addValueChangeListener(e -> updateEmployees(e.getValue()));

    grid.setHeight("200px");
    grid.asSingleSelect().addValueChangeListener(e -> editEmployee(e.getValue()));

    // List customers
    updateEmployees("");
}

private void configureEditor() {
    editor.setVisible(false);

    editor.setSaveListener(employee -> {
        var saved = employeeRepository.save(employee);
        updateEmployees(filter.getValue());
        editor.setEmployee(null);
        grid.asSingleSelect().setValue(saved);
    });

    editor.setDeleteListener(employee -> {
        employeeRepository.delete(employee);
        updateEmployees(filter.getValue());
        editEmployee(null);
    });

    editor.setCancelListener(() -> {
        editEmployee(null);
    });
}

4.4. 運行應用程序

我們可以使用 Maven 啓動應用程序:

mvn spring-boot:run

應用程序現在正在 localhost:8080 上運行:

5. 結論

在本文中,我們使用 Spring Boot 和 Spring Data JPA 構建了一個功能齊全的 CRUD UI 應用程序,用於持久化。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.