1. 引言
本文将介绍如何使用Jmix Studio和Jmix框架在IntelliJ IDEA中快速构建全栈应用。我们将开发一个员工费用跟踪系统的最小可行产品(MVP),从项目环境搭建到生成响应式UI,再到实现基于角色的权限控制,展示该框架如何在保持灵活性的同时加速开发。
2. 项目概览与搭建
我们的MVP是一个员工费用登记系统:管理员定义基本费用类型(如午餐、出租车、机票),员工选择类型并填写金额、日期等详细信息。系统包含管理员和员工两种角色,员工只能查看自己的费用记录,而管理员可访问所有用户的费用数据。
通过Jmix Studio的简单操作,我们将获得包含高级搜索过滤和基本表单验证的响应式UI:
2.1 环境准备
使用Jmix需要注册账户,完成免费注册后,安装以下工具完成环境搭建:
- IntelliJ IDEA
- Jmix插件 for IntelliJ
- Java 17或更新版本
2.2 在IntelliJ中创建Jmix项目
工具准备就绪后,打开IntelliJ会发现新增了"Jmix Project"选项:
选择"Full-Stack Application (Java)",点击下一步。设置项目名称和基础包名后等待依赖下载和项目索引完成。
首次创建项目时会出现登录对话框,登录后点击IntelliJ侧边栏的Jmix按钮查看项目结构:
项目结构包含基础功能:代表员工的User实体、几个UI视图、基本认证/授权功能、国际化消息包和CSS主题。 通过Jmix插件访问项目结构时,还提供了创建新视图或实体等操作的快捷入口。
本质上这是个标准的Spring Boot应用,熟悉且易于修改。对Spring Boot新手来说也很友好,可以轻松理解项目结构和工作原理。
3. 创建新实体
创建新实体非常简单:在插件面板点击加号,选择"New JPA Entity..."。输入类名,确保实体类型选择"Entity"。插件默认使用UUID作为ID类型,建议勾选"versioned"选项以启用乐观锁。
创建实体后,可在属性区域点击加号添加新属性。首先创建Expense实体表示费用类型:
由于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查看可用选项:
这些选项同样出现在顶部面板中。
3.1 为实体添加CRUD UI
在实体设计器中,点击"Views" → "Create view" → "Entity list and detail views",这将创建列表/过滤页面和详情/编辑页面。无需编写任何代码即可获得完整的CRUD功能:
向导提供了合理的默认选项,包含几个关键设置:
- 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"选项创建:
然后将ExpenseCategory作为必需属性添加到Expense实体,属性类型选择"ENUM":
由于在创建视图后添加了该属性,可通过"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脚本,出现执行对话框时只需确认即可:
之后在"Application"菜单中查看"Expenses" UI:
注意现在可以管理Expense数据,且UI具有响应式。登录时自动填充凭据,这对测试很有帮助,配置在application.properties中:
ui.login.defaultUsername = admin
ui.login.defaultPassword = admin
3.3 创建唯一约束
为费用名称添加唯一约束是个好主意,避免重复。通过插件添加约束时,会使用*@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=已存在同名费用类型
Jmix识别特定的本地化消息前缀。对于唯一约束违反,消息键必须以"databaseUniqueConstraintViolation."开头,后跟数据库中的约束名IDX_EXPENSE_UNQ。
4. 添加带引用属性的实体
需要创建新实体表示员工的Expense。插件支持添加引用属性,因此创建UserExpense实体,关联到Expense、User和相关属性:
名称 | 属性类型 | 类型 | 基数 | 必需 |
---|---|---|---|---|
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 {
// 标准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。 可点击地球图标添加本地化消息而非硬编码:
此验证阻止用户创建未来日期的费用。添加该注解还会禁用Jmix在视图中创建的日期选择组件的未来日期选择功能。
4.2 创建主从视图
我们为Expense实体创建了列表和详情视图。现在为UserExpense实体尝试Master-detail view,它将两者合并在单个UI中。这次在实体列表/详情的获取计划中,添加expense属性以包含在初始查询中。 仅在详情获取计划中添加user属性,以便打开费用详情时获取所有必要信息:
与之前一样,在"Application"菜单的"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>
获取计划通过在初始查询中包含必要连接来避免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对象中选择name和category属性:
除了数据网格接收新列元素外,获取计划现在包含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 创建新资源角色
资源角色控制对项目中特定对象(如实体、实体属性、UI视图)和通过UI或API请求的操作的访问。由于我们只使用视图,创建名为EmployeeRole的角色:右键点击Jmix中的"Security"节点,选择"New",确保安全范围标记为"UI":
角色初始为空,仅包含将出现在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.list、Expense.list和UserExpense.list的"View"。最后为UserExpense.list勾选"Menu",使其可通过UI访问:
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获取。