编程

使用 Spring Boot 和 Jmix 快速开发 web 应用

11 2024-10-16 05:25:00

1. 介绍

本文中,我们将学习如何使用 Jmix Studio 和 IntelliJ IDEA 的 Jmix 框架。我们将构建一个全栈 MVP 的 Spring Boot 应用,用以跟踪员工费用。从快速设置我们的项目环境到生成响应式 UI 和实现基于角色的访问,我们将看到这个框架如何在保持灵活性的同时加速开发。

2. 项目概述及设置

这个所谓的 MVP 是一个 web 应用,员工可以在其中等级他们的费用。管理员定义了一些基本类型的费用,如午餐、出租车和机票,而员工则从中进行选择,并包括支出金额、日期和任何其他必要的详细信息。我们还将引入授权角色,以区分管理员和员工,这样员工只能为自己登记费用,但管理员可以完全访问任何用户的费用。

在 Jmix Studio 中点击几下,我们就能导入丰富且响应迅速的 UI,用于管理用户和费用。包括高级搜索过滤器和开箱即用的基本表单验证:

 

2.1. 环境设置

Jmix 要求有账号。因此,免费开通账户后,我们将安装以下这些工具来设置环境:

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 已经添加了 idversion 属性,我们现在只添加 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 实体,并将其与 ExpenseUser 和关联属性的引用相关联:

NAMEATTRIBUTE TYPETYPECARDINALITYMANDATORY
userASSOCIATIONUserMany to Onetrue
expenseASSOCIATIONExpenseMany to Onetrue
amountDATATYPEDoubletrue
dateDATATYPELocalDatetrue
detailsDATATYPEStringfalse

新实体也使用 “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 对象中选择 namecategory 属性:

除了接收新列元素的数据网格外,获取计划(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 进行读取,以便我们在等级自己的费用时可以选择一种可用的费用类型:

ENTITYCREATEREADUPDATE
Expense X 
User XX
UserExpenseXXX

然后,在 “User Interface” 选项卡中,我们将选择此角色有权访问的视图。由于视图可以相互连接,因此此页面允许在视图访问(通过页面上的组件)或菜单访问之间进行选择。我们的 UserExpense 视图连接到 User 视图(用于显示其详细信息)和 Expense 视图(用于列出可用的费用类型),因此我们将检查 User.listExpense.listUserExpense.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