1. 概述

Vaadin Flow 是一个基于服务端的 Java Web 界面开发框架,所有 UI 逻辑运行在服务端,通过 WebSocket 或长轮询与浏览器通信。这意味着你完全可以用 Java 写前端,不用碰 HTML、JS —— 对 Java 后端开发者非常友好。

本文将带你用 Vaadin Flow 搭配 Spring Boot,实现一个完整的 CRUD 管理界面。后端用 Spring Data JPA 做持久化,前端用 Vaadin 构建响应式 UI。

如果你还不了解 Vaadin Flow,建议先看下 Baeldung 的 Vaadin 入门教程

2. 环境搭建

在标准的 Spring Boot 项目中添加 Vaadin 的 starter 依赖即可:

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

小贴士:Vaadin 已被 Spring Initializr 官方支持,创建项目时可以直接勾选 Vaadin 模块,省去手动加依赖的麻烦。

如果你不用 Initializr,也可以手动引入 Vaadin 的 BOM(Bill of Materials)来统一版本管理:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.vaadin</groupId>
            <artifactId>vaadin-bom</artifactId>
            <version>24.3.8</version> <!-- 建议查最新版本 -->
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

⚠️ 注意:Vaadin 版本更新较快,建议使用 LTS 或最新稳定版,避免踩坑。

3. 后端服务

我们以 Employee 实体为例,实现增删改查功能。字段包含 firstNamelastName,并加上基础校验:

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

    @Size(min = 2, message = "姓名至少 2 个字符")
    private String firstName;

    @Size(min = 2, message = "姓氏至少 2 个字符")
    private String lastName;

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

接着定义 Spring Data JPA 的 Repository 接口:

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

这里我们自定义了一个查询方法 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 前端界面

我们要构建一个带搜索框的员工列表,点击某行可编辑,下方弹出表单。整体结构如下:

A data grid displaying employees. The first employee is selected and displayed in a form below.

4.1. 自定义表单组件:EmployeeEditor

我们封装一个 EmployeeEditor 组件,用于编辑或新增员工。它由多个 Vaadin 内置组件组合而成。

继承 Composite<VerticalLayout> 而不是直接用 VerticalLayout,可以更好地封装内部实现,避免暴露不必要的 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
}

Binder 是 Vaadin 的数据绑定核心,结合 @Size 等注解,自动完成表单校验和数据绑定。

构造 UI 部分代码如下:

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));
}

✅ 注意点:

  • binder.bind() 自动映射字段,支持 JSR-303 校验
  • addClickShortcut(Key.ENTER) 实现回车保存,用户体验更佳
  • LUMO_PRIMARYLUMO_ERROR 是 Vaadin 内置主题,提升视觉一致性

4.2. 主视图:EmployeesView

EmployeesView 是应用的入口,通过 @Route("") 注解绑定到根路径 /

它是一个 Spring Bean,可以直接通过构造函数注入 EmployeeRepository

@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;

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

        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 {
        grid.asSingleSelect().setValue(null);
        editor.setVisible(false);
    }
}

接下来配置组件行为:

public EmployeesView(EmployeeRepository repo) {
    // ... 组件创建

    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()));

    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);
    });
}

⚠️ 踩坑提醒:

  • setValueChangeMode(EAGER) 表示输入时立即触发事件,若用 LAZY 则会在用户停止输入后延迟触发
  • grid.asSingleSelect() 将表格设为单选模式,监听选中变化
  • 编辑完成后调用 updateEmployees() 刷新列表,保证数据一致性

4.4. 运行应用

使用 Maven 启动:

mvn spring-boot:run

访问 http://localhost:8080 即可看到界面:

A data grid displaying employees. The first employee is selected and displayed in a form below.

功能验证:

  • ✅ 搜索框支持按姓氏模糊查询
  • ✅ 点击列表行,下方表单自动填充
  • ✅ 支持新增、保存、删除
  • ✅ 表单字段校验生效(少于 2 字符会提示)

5. 总结

本文通过一个简单但完整的示例,展示了如何用 Spring Boot + Vaadin Flow 快速构建企业级 CRUD 界面。

✅ 核心优势:

  • 全 Java 开发,适合后端主导的团队
  • 与 Spring 生态无缝集成
  • 组件化开发,UI 复用性强
  • 内置数据绑定与校验,开发效率高

❌ 注意事项:

  • Vaadin 服务端渲染会占用较多内存,高并发场景需评估
  • 定制 UI 主题需要学习 Lumo CSS 变量
  • 移动端适配需额外处理

代码已上传至 GitHub:https://github.com/baeldung/tutorials/tree/master/vaadin

适合想快速出 Demo 或内部管理系统的团队参考,简单粗暴,见效快。


原始标题:Sample Application with Spring Boot and Vaadin | Baeldung