使用 Spring Boot 和 Jmix 快速开发 web 应用
1. 介绍
本文中,我们将学习如何使用 Jmix Studio 和 IntelliJ IDEA 的 Jmix 框架。我们将构建一个全栈 MVP 的 Spring Boot 应用,用以跟踪员工费用。从快速设置我们的项目环境到生成响应式 UI 和实现基于角色的访问,我们将看到这个框架如何在保持灵活性的同时加速开发。
2. 项目概述及设置
这个所谓的 MVP 是一个 web 应用,员工可以在其中等级他们的费用。管理员定义了一些基本类型的费用,如午餐、出租车和机票,而员工则从中进行选择,并包括支出金额、日期和任何其他必要的详细信息。我们还将引入授权角色,以区分管理员和员工,这样员工只能为自己登记费用,但管理员可以完全访问任何用户的费用。
在 Jmix Studio 中点击几下,我们就能导入丰富且响应迅速的 UI,用于管理用户和费用。包括高级搜索过滤器和开箱即用的基本表单验证:
2.1. 环境设置
Jmix 要求有账号。因此,免费开通账户后,我们将安装以下这些工具来设置环境:
- IntelliJ IDEA
- Jmix 插件 用于 IntelliJ
- Java 17 或更新版本
2.2. 在 IntelliJ 中创建 Jmix 项目
这些工具完成后,打开 IntelliJ 就能看到 “Jmix Project” 选项现在可用了:
选择 “Full-Stack Application (Java)” 并点击 next。然后,选择项目名称和基础包。接下来,等待所有依赖下载完毕以及项目索引完成。
最重要的是,第一次创建 Jmix 项目时,我们会看到一个登录对话框。登录后,让我们点击 IntelliJ 侧边栏中的 Jmix 按钮,看看我们的项目结构是什么样子的:
该结构包括基本功能,如 User
实体(将用于表示员工)、一些 UI 视图和基本的身份验证/授权功能。它还包括一个国际化的消息包和一个基本的 CSS 主题。通过 Jmix 插件访问项目结构还允许各种操作的快捷方式,如新视图或实体。
同时,它也是一个常规的 Spring Boot 应用,因此很容易熟悉且易于修改。最重要的是,对于那些刚接触 Spring Boot 的人来说,很容易浏览项目并了解事情是如何运作的。
3. 创建新实体
创建新实体很简单,只需聚焦于插件的面板,单击加号,然后选择 “new JPA entity…”。我们需要选择类名,并确保实体类型字段选择了 “Entity”。该插件还允许我们选择 ID 类型,其默认为 UUID
。Jmix 还建议检查 “versioned” 选项,因为它会激活实体的乐观锁定。
同时,在创建实体后,我们也可以通过单击属性部分中的加号来添加新属性。让我们从一个 Expense
实体开始,以表示常见的费用类型:
由于 Jmix 已经添加了 id
和 version
属性,我们现在只添加 String 类型的 name
属性。最重要的是,通过 Studio 创建实体也会生成 Liquibase 更新日志,用以创建或修改数据库对象。
在添加新属性时,大多数选项都是不言自明的,但也有一些值得注意的地方:
- Read-only:不会为该属性创建 setter,并且它在 UI 中不可编辑
- Mandatory:在数据库中创建非 null 约束,并在 UI 中检查
- Transient:不会在数据库中创建列,也不会显示在 UI 中
在我们的 Expense
实体仍然打开的情况下,我们将切换到 “Text” 选项卡来查找生成的代码,该代码将 JPA 注释与 Jmix 特定的注释混合在一起:
@JmixEntity
@Table(name = "EXPENSE")
@Entity
public class Expense {
@JmixGeneratedValue
@Column(name = "ID", nullable = false)
@Id
private UUID id;
@Column(name = "VERSION", nullable = false)
@Version
private Integer version;
@InstanceName
@Column(name = "NAME", nullable = false)
@NotNull
private String name;
// standard getters and setters
}
代码的更改反射在 “设计器(Designer)” 选项卡中,反之亦然。此外,我们可以通过代码编辑器的内联操作使用 JPA 开发工具。让我们看看当我们在任何一行上 alt+enter
时出现的选项:
这些选项也出现在顶部面板中。
3.1. 为实体添加 CRUD UI
在 Designer 中关注实体时,让我们单击 “Views” 、“Create view”,然后单击 “Entity list and detail views”,这将创建一个用于罗列和过滤现有项目的页面以及一个用于创建/查看/编辑项目的页面。我们可以为具有完整 CRUD 功能的实体开发 UI,而无需接触任何代码:
向导会建议足够好的默认值,并浏览几个关键选项:
- 实体(Entity):任何
@JmixEntity
注释的类。默认为当前选中的那个 - 包名:默认为项目创建时选择的基础包 + 视图 + 实体名
- 父级菜单项:默认为与项目一起创建的那个
- Table 操作:默认为所有 CRUD 操作
- 生成通用过滤器:运行我们在列表视图中实现基础的搜索过滤
- 获取计划(Fetch Plan):默认获取该行的所有字段(排除外键)
- 可本地化消息:允许我们更改正在创建的两个页面的标题
3.2. 创建枚举
我们将把费用限制在几个类别。为此,我们将创建一个名为 ExpenseCategory
的枚举,其中有几个选项:
- Education
- Food
- Health
- Housing
- Transportation
我们来使用 “New Enumeration” 选项创建这个枚举:
然后,我们可以将 ExpenseCategory
作为新的强制属性添加到我们的 Expense
实体中,选择 “ENUM” 作为属性类型:
既然我们在创建视图后创建了此属性,那么就可以使用 “Add to Views” 按钮添加该属性,并选择要在哪些视图中查看此属性。这将生成一个select 标签,并将其添加到详情视图:
<formLayout id="form" dataContainer="expenseDc">
<select id="categoryField" property="category"/>
<textField id="nameField" property="name"/>
</formLayout>
同时,也将一个 column 添加到列表视图的 columns 标签:
<columns resizable="true">
<column property="category"/>
<column property="name"/>
</columns>
完成向导后,让我们启动应用。Jmix Studio 会根据数据 schema 自动分析数据模型,并生成带有更新的 Liquibase 脚本,因此我们将看到一个对话框,显示要应用的脚本,我们只需要执行它们即可继续:
在那之后,我们在 “Application” 菜单中查看我们的 “Expenses” UI:
请注意,我们已经可以管理 Expense
项目,并且 UI 是响应式的。此外,我们在登录时会收到填写的凭据。此行为有助于测试,同时也被添加到 application.properties
中:
ui.login.defaultUsername = admin
ui.login.defaultPassword = admin
3.3. 创建 Unique 约束
为费用名称创建 unique 约束,可用避免重复。当通过插件添加时,它与 @UniqueConstraint
一起被添加到 Java 类中,同时也在 UI 中引入了验证:
再次,生成的 Liquibase 更新日志负责将其添加到数据库中:
<databaseChangeLog>
<changeSet id="1" author="expense-tracker">
<addUniqueConstraint columnNames="NAME" constraintName="IDX_EXPENSE_UNQ" tableName="EXPENSE"/>
</changeSet>
</databaseChangeLog>
我们可以通过切换到 Jmix 面板,导航到项目结构中的 “User Interface” ,然后打开 “Message Bundle” 来个性化定制违反约束时的错误消息。来自应用的所有消息都在那里。让我们添加一个新的:
databaseUniqueConstraintViolation.IDX_EXPENSE_UNQ=An expense with the same name already exists
Jmix 可识别本地化消息的一些特定前缀。对于唯一约束违规,我们必须使用以 “databaseUniqueConstraintViolation.” 为开头的消息键,后跟数据库中的约束名称 IDX_EXPENSE_UNQ
。
4. 添加具有引用属性的实体
我们需要一个新的实体来表示员工的 Expense
。该插件支持添加引用属性,因此让我们创建一个 UserExpense
实体,并将其与 Expense
、User
和关联属性的引用相关联:
NAME | ATTRIBUTE TYPE | TYPE | CARDINALITY | MANDATORY |
---|---|---|---|---|
user | ASSOCIATION | User | Many to One | true |
expense | ASSOCIATION | Expense | Many to One | true |
amount | DATATYPE | Double | – | true |
date | DATATYPE | LocalDate | – | true |
details | DATATYPE | String | – | false |
新实体也使用 “versioned” 选项:
打开我们新创建的实体,我们可以看到 Jmix 如何使用熟悉的 JPA 注释并索引我们的外键:
@JmixEntity
@Table(name = "USER_EXPENSE", indexes = {
@Index(name = "IDX_USER_EXPENSE_USER", columnList = "USER_ID"),
@Index(name = "IDX_USER_EXPENSE_EXPENSE", columnList = "EXPENSE_ID")
})
@Entity
public class UserExpense {
// standard ID and version fields...
}
同时,它使用了 FetchType.LAZY
而非默认的 EAGER
获取关联,这样我们不会 不会意外地影响性能。让我们检查生成的 User
关联:
@JoinColumn(name = "USER_ID", nullable = false)
@NotNull
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private User user;
该关联使用 JPA 注释,因此如果此后需要容易对其进行修改。
4.1. 添加日期验证
当我们转到设计器中的 “Attributes” 区域并选择日期属性时,我们会注意到出现了一些验证选项。让我们点击 “not set” 将其设置为 PastOrPresent
。我们可以通过单击地球图标而不是使用文字消息来包含本地化消息:
该验证阻止用户创建具有未来日期的费用。包含此注释还会关闭 Jmix 在我们的视图中创建的日期选择器组件的未来日期选择。
4.2. 创建 Master-Detail 视图
我们为 Expense
实体创建了一个列表视图和一个详情视图。现在,对于 UserExpense
实体,让我们尝试 Master-Detail 信息视图,它将两者组合在一个 UI 中。这一次,在实体列表/详情获取计划(fetch plan)中,我们将添加 expense
属性,将其包含在列出所有用户费用的初始查询中。而且,仅对于获取详细信息的计划,我们将添加 user
属性,以便查询打开费用详细信息所需的所有信息:
如前,我们将看到 ”User expenses" 下面的应用菜单中找到新的视图,用来包含一些项目:
检查 user-expense-list-view.xml
的生成代码,我们会注意到该插件已经为我们包含了复杂的组件,比如日期选择器,所以我们不必费心处理前端的东西:
<datePicker id="dateField" property="date"/>
但是,此页面允许应用中的任何用户完全访问。稍后我们将看到如何控制访问。
4.3. 添加组件属性
现在,让我们回到 User
实体,并在我们的详情信息视图中包含一个 expenses
属性来列出用户费用,以便快速引用。我们将通过添加一个具有 “composition” 类型的新属性并选择具有一对多基数的 UserExpense
来实现这一点:
要将其添加到用户详情视图,请在设计器中选择 expenses
属性,然后单击 “Add to Views” 按钮,在 User.detail
布局视图中选中其框:
如果检查 user-detail-view.xml
,我们将看到 Jmix 为 expenses
属性添加了一个 fetchPlan
:
<fetchPlan extends="_base">
<property name="expenses" fetchPlan="_base"/>
</fetchPlan>
获取计划(Fetch plan)通过在初始查询中包含任何必要的连接而不是进行额外的查询,以避免 N+1 问题s。我们还将找到一个用于绑定到视图对象的数据容器:
<collection id="expensesDc" property="expenses"/>
同时添加一个允许在 expenses 上进行 CRUD 操作的数据网格(data grid)元素:
<dataGrid id="expensesDataGrid" dataContainer="expensesDc">
<actions>
<action id="create" type="list_create"/>
<action id="edit" type="list_edit"/>
<action id="remove" type="list_remove"/>
</actions>
<columns>
<column property="version"/>
<column property="amount"/>
...
</columns>
</dataGrid>
这些都是用 Jmix 的声明性布局标签和组件定义的。
4.4. Adding Fetched Columns to an Existing View
让我们通过将 Expense
类中的属性添加到我们创建的 expensesDataGrid
组件来探索此声明性布局定义的工作原理。首先,我们将打开 user-detail-view.xml
并选择数据网格。然后,在 Jmix UI 面板中,我们单击列并选择 “Add” 和 “Column”。最后,让我们从 expense
对象中选择 name
和 category
属性:
除了接收新列元素的数据网格外,获取计划(fetch Plan)现在还包括 expense
对象:
<fetchPlan extends="_base">
<property name="expenses" fetchPlan="_base">
<property name="expense" fetchPlan="_base"/>
</property>
</fetchPlan>
因此,在访问 User
详情视图时,我们现在将直接看到用户费用的数据网格。
4.5. 热部署
使用 Jmix Studio,大多数简单的更改(如我们添加的新列)都是热部署的,因此我们不需要重新启动应用来查看更改;刷新页面就够了:
在本地使用 Jmix 进行开发时,应用会自动更新。
5. 设置用户权限
访问应用程序时,我们会在 “Security” 下的 “Resource roles” 菜单中看到与项目一起创建的初始安全角色。这些角色包括完全访问角色和仅接受登录应用的最小化角色。
我们将创建一个角色,以便员工可以访问 UserExpense
菜单并管理他们的费用。然后,我们将创建一个行级访问角色来限制用户,以便他们只能检索与其 ID 匹配的项目。此限制保证用户只能看到自己的费用。
5.1. 创建新的资源角色(Resource Role)
资源角色(Resource roles)控制对特定对象(如实体、实体属性和 UI 视图)的访问,以及通过 UI 或 API 请求的项目中的操作。由于我们只使用视图,让我们通过右键单击 Jmix 中的 “Security” 节点,选择 “New”,并确保将安全范围(security scope)标记为 “UI”,来创建一个名为 EmployeeRole
的视图:
该角色开始为空,仅包含也将出现在 UI 中的代码:
@ResourceRole(name = "Employee Role", code = EmployeeRole.CODE, scope = "UI")
public interface EmployeeRole {
String CODE = "employee-role";
}
因此,在 EmployeeRole
页面上,让我们单击 Entities
来定义此角色将允许对实体进行哪些操作。我们需要对 UserExpense
进行所有操作,除了对 User
进行删除、读取和更新之外,还有对 Expense
进行读取,以便我们在等级自己的费用时可以选择一种可用的费用类型:
ENTITY | CREATE | READ | UPDATE |
---|---|---|---|
Expense | X | ||
User | X | X | |
UserExpense | X | X | X |
然后,在 “User Interface” 选项卡中,我们将选择此角色有权访问的视图。由于视图可以相互连接,因此此页面允许在视图访问(通过页面上的组件)或菜单访问之间进行选择。我们的 UserExpense
视图连接到 User
视图(用于显示其详细信息)和 Expense
视图(用于列出可用的费用类型),因此我们将检查 User.list
、Expense.list
和 UserExpense.list
的“视图(Views)”。最后,对于 UserExpense.list
,我们还将选中 “Menu” ,使其可以通过 UI 访问:
5.2. 使用 JPQL 策略创建新行级角色
为了确保员工在访问菜单时只能看到自己的f费用,我们将创建一个行级策略,该策略将限制每个查询只能获取与登录用户 ID 匹配的行。
为此,我们还需要添加一个 JPQL 策略。因此,让我们通过右键单击 Jmix 中的 “Security” 节点并选择 “New” 来创建一个名为 EmployeeRowLevelAccessRole
的行级角色。然后,在我们新创建的角色中,我们将单击 “Add Policy”,然后单击 “Add JPQL Policy” 为引用 User
的实体添加策略:
- 实体:
UserExpense
;where 子句:{E}.user.id = :current_user_id
- 实体:
User
;where 子句:{E}.id = :current_user_id
添加 JPQL 策略时,{E}
表示当前实体。我们还获得了一些唯一的变量,如 current_user_id
,它解析为当前登录的变量。插件中提供了自动补全功能,因此我们可以很容易地找到方法。
最后,我们可以通过访问 “Security” 菜单中的 “Resource roles” 和 “Row-level roles”,通过 UI 将这些角色分配给任何用户。或者,更方便的是,通过使用列表视图中的 “Role assignments” 按钮,我们可以将这两个角色添加给列表中的任何人。
具有这些角色的用户现在可以访问费用列表视图并等他们的费用。最重要的是,他们将无法获取他人的费用。
6. 小结
本文中,我们演示了如何轻松创建一个功能强大、安全的 web 应用,着重于 Jmix Studio 在保持高功能和用户体验标准的同时,显著加快我们的开发周期的潜力。对 CRUD 操作、基于角色的访问控制和验证的全面支持确保了安全性和可用性。
Github 代码: https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/jmix