使用 Vaadin 创建 CRUD UI

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

您将构建什么

您将为简单的 JPA 存储库构建一个 Vaadin UI。您将获得一个具有完整 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. 点击依赖项并选择VaadinSpring Data JPAH2 Database

  4. 单击生成

  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 启动器依赖项集合,因此您只需要添加以下 Maven 代码段(或相应的 Gradle 配置)

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

该示例使用的 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 不支持 BOM,但有一个方便的 插件。查看 build.gradle 构建文件以获取如何完成相同任务的示例

定义主视图类

主视图类(在本指南中称为 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 组件,并将其值插入到该过滤器方法中。当用户键入时,ValueChangeListener 会自动调用,因为您在过滤器文本字段上定义了 ValueChangeMode.LAZY。以下示例显示了如何设置这样的侦听器

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 文件

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

总结

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

另请参阅

以下指南也可能有所帮助

想写新指南或为现有指南做贡献吗?请查看我们的贡献指南

所有指南的代码均采用 ASLv2 许可,文字内容采用署名-禁止演绎知识共享许可

获取代码