本指南将引导您完成构建一个应用程序的过程,该应用程序在前端使用基于 Vaadin 的 UI,后端则基于 Spring Data JPA。
你将构建什么
您将为简单的 JPA 仓库构建一个 Vaadin 用户界面。您将获得一个具有完整 CRUD(创建、读取、更新和删除)功能的应用程序,以及一个使用自定义仓库方法的过滤示例。
您可以按照以下两种不同的路径之一进行操作:
-
从项目中已有的
initial
项目开始。 -
重新开始。
差异将在本文档后面讨论。
您需要什么
-
大约 15 分钟
-
喜欢的文本编辑器或集成开发环境(IDE)
-
Java 17 或更高版本
-
您也可以直接将代码导入到您的 IDE 中:
如何完成本指南
与大多数 Spring 入门指南 一样,您可以从头开始并完成每个步骤,或者跳过您已经熟悉的基本设置步骤。无论哪种方式,最终您都会得到可运行的代码。
要从头开始,请继续阅读 使用 Spring Initializr 开始。
要跳过基础部分,请执行以下操作:
-
下载并解压本指南的源码仓库,或使用 Git 克隆它:
git clone https://github.com/spring-guides/gs-crud-with-vaadin.git
-
进入
gs-crud-with-vaadin/initial
目录 -
直接跳转到 创建后端服务。
完成后,您可以对照 gs-crud-with-vaadin/complete
中的代码检查您的结果。
从 Spring Initializr 开始
您可以使用这个预初始化项目,点击生成以下载一个 ZIP 文件。该项目已配置为适合本教程中的示例。
您也可以从 Github 上 fork 该项目,并在您的 IDE 或其他编辑器中打开它。
手动初始化(可选)
如果您想要手动初始化项目,而不是使用前面显示的链接,请按照以下步骤操作:
-
访问 https://start.spring.io。该服务会为您拉取应用程序所需的所有依赖项,并为您完成大部分设置。
-
选择 Gradle 或 Maven 以及您要使用的语言。本指南假设您选择了 Java。
-
点击 Dependencies,然后选择 Vaadin、Spring Data JPA 和 H2 Database。
-
点击 Generate。
-
下载生成的 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 代码的情况下完成了这一任务。
另请参阅
以下指南可能也会有所帮助: