1. 引言

本文将介绍如何使用Jmix Studio和Jmix框架在IntelliJ IDEA中快速构建全栈应用。我们将开发一个员工费用跟踪系统的最小可行产品(MVP),从项目环境搭建到生成响应式UI,再到实现基于角色的权限控制,展示该框架如何在保持灵活性的同时加速开发。

2. 项目概览与搭建

我们的MVP是一个员工费用登记系统:管理员定义基本费用类型(如午餐、出租车、机票),员工选择类型并填写金额、日期等详细信息。系统包含管理员和员工两种角色,员工只能查看自己的费用记录,而管理员可访问所有用户的费用数据。

通过Jmix Studio的简单操作,我们将获得包含高级搜索过滤和基本表单验证的响应式UI:

MVP

2.1 环境准备

使用Jmix需要注册账户,完成免费注册后,安装以下工具完成环境搭建

2.2 在IntelliJ中创建Jmix项目

工具准备就绪后,打开IntelliJ会发现新增了"Jmix Project"选项:

New Jmix Project

选择"Full-Stack Application (Java)",点击下一步。设置项目名称和基础包名后等待依赖下载和项目索引完成。

首次创建项目时会出现登录对话框,登录后点击IntelliJ侧边栏的Jmix按钮查看项目结构:

Initial setup

项目结构包含基础功能:代表员工的User实体、几个UI视图、基本认证/授权功能、国际化消息包和CSS主题。 通过Jmix插件访问项目结构时,还提供了创建新视图或实体等操作的快捷入口。

本质上这是个标准的Spring Boot应用,熟悉且易于修改。对Spring Boot新手来说也很友好,可以轻松理解项目结构和工作原理。

3. 创建新实体

创建新实体非常简单:在插件面板点击加号,选择"New JPA Entity..."。输入类名,确保实体类型选择"Entity"。插件默认使用UUID作为ID类型,建议勾选"versioned"选项以启用乐观锁。

创建实体后,可在属性区域点击加号添加新属性。首先创建Expense实体表示费用类型:

New Expense Entity

由于Jmix已自动添加id和version属性,我们只需添加String类型的name属性。重要提示:通过Studio创建实体会自动生成Liquibase变更日志来管理数据库对象。

添加属性时,大部分选项不言自明,但有几个值得注意:

  • Read-only:不生成setter方法,UI中不可编辑
  • Mandatory:创建数据库非空约束,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;

    // 标准getter和setter方法
}

代码修改会实时反映到"Designer"标签页,反之亦然。 还可通过代码编辑器的内联操作使用JPA开发工具。在任意行按alt+enter查看可用选项:

JPA dev tools

这些选项同样出现在顶部面板中。

3.1 为实体添加CRUD UI

在实体设计器中,点击"Views" → "Create view" → "Entity list and detail views",这将创建列表/过滤页面和详情/编辑页面。无需编写任何代码即可获得完整的CRUD功能:

Expense CRUD view

向导提供了合理的默认选项,包含几个关键设置:

  • Entity:所有@JmixEntity注解的类,默认为当前选中实体
  • Package name:默认为项目创建时选择的基础包 + view + 实体名
  • Parent menu item:默认为项目创建时生成的菜单项
  • Table actions:默认包含所有CRUD操作
  • Create a generic filter:在列表视图中启用基本搜索过滤
  • Fetch plan:默认获取所有列(外键除外)
  • Localizable messages:可自定义两个页面的标题

3.2 创建枚举

我们将费用限制为几个类别。创建名为ExpenseCategory的枚举,包含以下选项:

  • Education(教育)
  • Food(餐饮)
  • Health(健康)
  • Housing(住房)
  • Transportation(交通)

通过"New Enumeration"选项创建:

Enum Expense Category

然后将ExpenseCategory作为必需属性添加到Expense实体,属性类型选择"ENUM":

Add category to Expense

由于在创建视图后添加了该属性,可通过"Add to Views"按钮选择要在哪些视图中显示。这会在详情视图中生成select标签:

<formLayout id="form" dataContainer="expenseDc">
    <select id="categoryField" property="category"/>
    <textField id="nameField" property="name"/>
</formLayout>

同时列表视图的columns标签中会添加新列:

<columns resizable="true">
    <column property="category"/>
    <column property="name"/>
</columns>

完成向导后启动应用。Jmix Studio会自动分析数据模型与数据库架构差异并生成Liquibase脚本,出现执行对话框时只需确认即可:

Liquibase changelog

之后在"Application"菜单中查看"Expenses" UI:

Expenses CRUD

注意现在可以管理Expense数据,且UI具有响应式。登录时自动填充凭据,这对测试很有帮助,配置在application.properties中:

ui.login.defaultUsername = admin
ui.login.defaultPassword = admin

3.3 创建唯一约束

为费用名称添加唯一约束是个好主意,避免重复。通过插件添加约束时,会使用*@UniqueConstraint*注解标记Java类,并在UI中包含验证:

Unique constraint

同样,生成的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=已存在同名费用类型

Jmix识别特定的本地化消息前缀。对于唯一约束违反,消息键必须以"databaseUniqueConstraintViolation."开头,后跟数据库中的约束名IDX_EXPENSE_UNQ

4. 添加带引用属性的实体

需要创建新实体表示员工的Expense。插件支持添加引用属性,因此创建UserExpense实体,关联到ExpenseUser和相关属性:

名称 属性类型 类型 基数 必需
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"选项:

New entity UserExpense

打开新创建的实体,可以看到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 {
    // 标准ID和version字段...
}

此外,它使用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 可点击地球图标添加本地化消息而非硬编码:

Date Validation

此验证阻止用户创建未来日期的费用。添加该注解还会禁用Jmix在视图中创建的日期选择组件的未来日期选择功能。

4.2 创建主从视图

我们为Expense实体创建了列表和详情视图。现在为UserExpense实体尝试Master-detail view,它将两者合并在单个UI中。这次在实体列表/详情的获取计划中,添加expense属性以包含在初始查询中。 仅在详情获取计划中添加user属性,以便打开费用详情时获取所有必要信息:

Master Detail View

与之前一样,在"Application"菜单的"User expenses"下找到新视图,准备添加数据:

Master view

检查生成的user-expense-list-view.xml代码,会发现插件已为我们包含复杂组件如日期选择器,无需操心前端:

<datePicker id="dateField" property="date"/>

但此页面目前授予所有用户完全访问权限,稍后我们将介绍如何控制访问。

4.3 添加组合属性

回到User实体,添加expenses属性以在用户详情视图中快速列出费用。通过添加"composition"类型的新属性,选择UserExpense并设置一对多基数:

Composition Attribute

要将其添加到用户详情视图,在设计器中选择expenses属性,点击"Add to Views"按钮,在User.detail布局视图中勾选:

Add expenses to view

检查user-detail-view.xml,会发现Jmix为expenses属性添加了fetchPlan

<fetchPlan extends="_base">
    <property name="expenses" fetchPlan="_base"/>
</fetchPlan>

获取计划通过在初始查询中包含必要连接来避免N+1问题 还会找到绑定到视图对象的数据容器:

<collection id="expensesDc" property="expenses"/>

以及允许CRUD操作的数据网格:

<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 向现有视图添加获取列

通过向创建的expensesDataGrid组件添加Expense类的属性来探索声明式布局的工作原理。首先打开user-detail-view.xml,选择数据网格。然后在Jmix UI面板中点击columns → "Add" → "Column"。最后从expense对象中选择namecategory属性:

Add fetch columns

除了数据网格接收新列元素外,获取计划现在包含expense对象:

<fetchPlan extends="_base">
    <property name="expenses" fetchPlan="_base">
        <property name="expense" fetchPlan="_base"/>
    </property>
</fetchPlan>

结果现在访问User详情视图时会直接显示用户费用数据网格。

4.5 热部署

使用Jmix Studio时,大多数简单更改(如添加的新列)会热部署,无需重启应用,刷新页面即可看到效果:

Completed User Expenses

使用Jmix本地开发时,更新会自动应用。

5. 设置用户权限

访问应用时,在"Security"下的"Resource roles"菜单中会看到项目初始创建的安全角色。包括完全访问角色和仅允许登录的最小角色。

我们将创建一个角色,允许员工访问UserExpense菜单并管理自己的费用。 然后创建行级访问角色,限制用户只能检索匹配自己ID的记录。这确保用户只能看到自己的费用。

5.1 创建新资源角色

资源角色控制对项目中特定对象(如实体、实体属性、UI视图)和通过UI或API请求的操作的访问。由于我们只使用视图,创建名为EmployeeRole的角色:右键点击Jmix中的"Security"节点,选择"New",确保安全范围标记为"UI":

New Employee Role

角色初始为空,仅包含将出现在UI中的代码:

@ResourceRole(name = "Employee Role", code = EmployeeRole.CODE, scope = "UI")
public interface EmployeeRole {
    String CODE = "employee-role";
}

EmployeeRole页面点击Entities定义该角色允许的实体操作。我们需要对UserExpense的所有操作(删除除外)、对User的读取和更新、对Expense的读取(以便登记费用时选择可用类型):

实体 创建 读取 更新
Expense X
User X X
UserExpense X X X

然后在User Interface标签页选择该角色可访问的视图。由于视图可能相互连接,此页面允许选择视图访问(通过页面组件)或菜单访问。我们的UserExpense视图连接到User视图(显示详情)和Expense视图(列出可用类型),因此勾选User.listExpense.listUserExpense.list的"View"。最后为UserExpense.list勾选"Menu",使其可通过UI访问:

UI role permissions

5.2 使用JPQL策略创建行级角色

为确保员工访问菜单时只能看到自己的费用,创建行级策略,将每个查询限制为仅获取匹配登录User ID的行。

需要添加JPQL策略。创建名为EmployeeRowLevelAccessRole的行级角色:右键点击Jmix中的"Security"节点,选择"New"。在新创建的角色中点击"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*,解析为当前登录用户ID。 插件提供自动补全功能,便于操作。

最终可通过UI在"Security"菜单的"Resource roles"和"Row-level roles"中将这些角色分配给用户。更便捷的是使用列表视图中的"Role assignments"按钮,可为列表中的任何人添加两种角色:

拥有这些角色的用户现在可访问Expense列表视图并登记费用。最重要的是,他们无法访问他人的费用。

6. 结论

本文展示了如何轻松创建功能完善且安全的Web应用,突显了Jmix Studio在显著加速开发周期的同时保持高功能性和用户体验标准的潜力。对CRUD操作、基于角色的访问控制和验证的全面支持确保了安全性和易用性。

一如既往,源代码可在GitHub获取。


原始标题:Rapid Web Application Development With Spring Boot and Jmix | Baeldung