使用应用程序组件进行产品定制
在本指南中,您将学习如何基于标准产品为创建个性化解决方案。我们将利用 CUBA 应用程序组件功能来定制项目并添个性化功能。
将要构建的内容
本指南对 CUBA 宠物诊所 示例进行了增强,展示了两个个性化项目: Kanto-Petclinic
和 Alabastia-Petclinic
的构建过程,以此来演示 CUBA 应用程序的扩展能力。
-
将 Petclinic 项目转换为 CUBA 应用程序组件
-
基于 CUBA Petclinic 创建两个 CUBA 应用程序:
Kanto-Petclinic
和Alabastia-Petclinic
Kanto-Petclinic
Kanto Petclinic 对原始项目进行了扩展,添加了以下个性化行为:
-
给 Visit 实体添加了属性
previousIllnesses
-
允许用户从 “宠物浏览” 界面创建当日的就诊 (Visit)
Alabastia-Petclinic
Alabastia Petclinic 对原始项目进行了以下扩展:
-
使用户能够为 Visit(就诊) 分配治疗室
-
创建新的 Visit(就诊)时,会根据当前用户自动预选一个护士
-
对新的就诊自动预选护士治疗室
开发环境要求
您的开发环境需要满足以下条件:
-
文件编辑器或者IDE (推荐使用 IntelliJ IDEA )
-
独立运行版或者IDEA插件版的 CUBA Studio (可选)
-
CUBA CLI (可选)
下载 并解压本指南的源码,或者使用 git 克隆下来:
概述
CUBA 中的应用程序组件可用来创建功能可重用的组件,这些组件可用在其他 CUBA 应用程序中。应该程序组件可以只包含少量功能,类似于一个小插件的形式,用来对应用程序的某一方面进行补充。
但是应用程序组件也可以是完整的应用程序。这时,应用程序组件包含了构成标准应用程序的所有要素:实体、界面和业务逻辑。然后,最终的应用程序可以基于这种应用程序组件来创建,只是按需要对其稍作调整。
第二种形式是本指南要重点介绍的内容。在示例中,我们会把 Petclinic 应用程序转换为应用程序组件。然后,我们将创建两个基于此应用程序组件的应用程序,并对其调整以满足客户的个性化需求。
通过这种方法,我们可以满足客户对标准产品(Petclinic)按需调整的要求。应用程序组件为我们提供了一种针对这种情况的编译时扩展机制。
各种扩展机制
在深入研究 Petclinic 的具体示例之前,让我们研究一下编译时扩展和运行时扩展之间的区别。 根据所需的功能、产品的部署和分发机制,选择一个更合适的扩展方式。
运行时扩展: 插件、脚本和配置
CUBA 应用程序的第一种扩展机制是运行时扩展。此扩展的主要特征是在构建过程中创建了一个类似 war
二进制文件的应用程序制件。相同的二进制文件会分发给所有的客户。应用程序的扩展部分不更改原始二进制文件,比如替换某些类。而是在应用程序的运行时或部署时添加这些扩展。
有几种方式可以做到这点,比如插件机制,使用这种方式,会生成一些包含插件实现的 jar 文件,这些 jar 文件会被放置到应用程序服务器中。通常,需要重新启动应用程序才能使用这种插件生效。
这种 jar 文件形式的插件可以包含源代码。通常,它用于实现在主应用程序中预定义的某些接口,以实现或替换特定功能。
有一篇文章专门介绍了这种方式: 用脚本应对业务不清晰的情况 |
另外一种方式是脚本机制,这种机制能力稍弱。在 CUBA 中,有几种内置的脚本机制和示例。脚本允许我们以编程方式定义应用程序中某些预定义的功能区域。 CUBA 内置的安全组管理功能中的约束定义可以看作是一个脚本机制的范例。在约束定义中可以定义一个 Groovy 脚本,该脚本用于确定特定用户是否有权访问诸如实体记录之类的特定资源。
通常,脚本与配置(configuration)结合使用。 以 “约束配置(Constraint configuration)” 为例:Groovy 脚本只是动态配置的一部分,其它的配置还有实体链接、操作类型等。
配置也可以扩展某些功能。但通常而言,与插件相比,这些扩展点对整体的影响范围更小。
您可以从博客文章: 如何开发高度可定制的产品 找到更多信息. |
编译时扩展: 应用程序组件
CUBA 应用程序的第二种扩展机制是编译时扩展,这种扩展在软件生产过程中实施。可能是开发阶实现针对特定用户实现的个性化功能或对标准功能的调整。也可能在二进制打包时创建,比如添加某些额外的 jar
文件到二进制包。
这种方式的扩展几乎可以更改所有内容,因为它们在软件生产过程的比较早的阶段。这时可以创建额外的实体、可以覆盖业务逻辑,也可以创建特定于客户的 UI。
将 Petclinic 实现为产品线
考虑上述的 Petlinic 示例,在本指南中,我们将使 Petclinic 产品不再是独立的软件,而是产品线。基本产品是我们已知的 Petclinic 示例。此外,我们将为特定类型的客户甚至单个客户创建定制化的项目或产品。
这些扩展是将 Petclinic 以应用程序组件的方式作为基本产品的 CUBA 应用程序(在我们的示例中是 Kanto-Petclinic
和 ` Alabastia-Petclinic` )。 这样,我们将利用 CUBA 应用程序的编译时扩展机制。
将 Petclinic 项目调整为应用程序组件
要将 Petclinic 项目用作产品线,我们需要了解 CUBA 应用程序组件的构成,以及它与常规 CUBA 应用程序的不同之处。实际上,差别不是很大 - 只有一个特殊的文件:app-component.xml
,它是应用程序组件描述文件。这个文件标记一个 CUBA 应用程序可以用作应用程序组件 - 即表示它可以被其他 CUBA 应用程序使用。
为了避免应用程序组件和使用它的应用程序之间的模块命名冲突,需要通过 CUBA Studio中的 “Project Properties” 窗口将 Petclinic 项目的模块前缀由 app 改为 petclinic 。
|
除此之外,不需要做其它更改。该应用程序组件仍可以用作常规 CUBA 应用程序。它可以在 CUBA Studio 中启动、可以创建业务逻辑、可以生成 UI 等。
在这种情况下,“可以被其它 CUBA 应用程序使用” 不仅意味着应用程序组件的功能会显示在生成的应用程序中。同时,也可以在目标应用程序内部与应用程序组件的任何部分进行交互。可以引用实体 (例如 Pet
实体)、可以调用或覆盖 Spring Bean、可以扩展或替换 UI 界面,等等。
我们来深入研究一下 Petclinic 示例中的 app-component.xml
:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<app-component xmlns="http://schemas.haulmont.com/cuba/app-component.xsd"
dependsOn="com.haulmont.cuba, com.haulmont.addon.helium">
<module name="global" blocks="*">
<artifact name="petclinic-global" appJar="true"/>
<property name="cuba.persistenceConfig" value="+com/haulmont/sample/petclinic/persistence.xml"/>
<property name="cuba.metadataConfig" value="+com/haulmont/sample/petclinic/metadata.xml"/>
<property name="cuba.viewsConfig" value="+com/haulmont/sample/petclinic/views.xml"/>
</module>
<module name="core" dependsOn="global" blocks="core">
<artifact name="petclinic-core" appJar="true"/>
<property name="cuba.mainMessagePack" value="+com.haulmont.sample.petclinic.core"/>
<!-- ... -->
</module>
<!-- ... -->
</app-component>
该文件包含了对应用程序组件结构描述信息。它暴露出几个 module
元素。 可以在 module
元素中定义该应用程序组件的 artifact
和 property
元素。
CUBA 使用此信息来正确地配置的应用程序属性(和值),并定义将哪些制件(artifact) 打包到每个应用程序块(core/web)中。 通常,此文件不需要手动更改,因为它仅包含有关应用程序组件结构的静态信息。
在 CUBA Studio 中,可以通过菜单 CUBA > Advanced > App Component Descriptor 来生成应用程序组件描述。
|
在应用程序中使用 Petclinic 应用程序组件
在使 Petclinic 项目成为应用程序组件之后,需要将其安装到 Maven 仓库中,以便 CUBA (特别是 Gradle) 的依赖机制可以将应用程序组件下载到目标应用程序。
对于组件的安装,在应用程序组件项目中执行一个专门的 gradle 任务:./gradlew install
即可。(CUBA Studio 中等效菜单项为 CUBA > Advanced > Install App Component
)。
通过此任务,将构建应用程序组件并将其放置到本地 Maven 仓库中,即用户主目录下的 .m2
目录:

安装应用程序组件后,可以在目标应用程序中使用它。要使用本地 maven 仓库中的应用程序组件,需要在 build.gradle
文件的 repositories
块内添加 maven 本地仓库:
repositories {
mavenLocal()
//...
}
要使用应用程序组件,可以通过 CUBA Studio 安装依赖。 可以从 CUBA > Marketplace…
找到相应的应用程序组件。当前,Petclinic 应用程序组件还没发布到公共 CUBA 市场中,只是在我们的本地计算机中。 因此,它不会出现在列表中。 Marketplace 界面右上角有一个 Install add-on manually
按钮,可以通过点击此按钮然后输入应用程序组件的坐标:com.haulmont.sample.petclinic:petclinic-global:2.0
。
在一个团队中进行开发时,处理这种依赖的常用方法是使用专用的 Maven 仓库,例如 Nexus、Artifactory 或 Github Packages,可以用它们存储应用程序组件二进制文件。这样,就不需要将应用程序组件安装到每个人的计算机上。 |
满足了这些前提条件,目标应用程序就可以开始使用应用程序组件了。
Petclinic 扩展
如开头所述,我们将对不同的客户及其专用应用程序进行某些扩展。我们从 Kanto-Petclinic 开始。
Kanto-Petclinic
该项目仅需对 Petclinic 的核心进行一些微调。需要对就诊增加额外的信息,用于对疾病进行预先描述。另外,在 Kanto-Petlinic 有很多紧急就诊。这需要护士在当天创建特定类型的就诊。为了支持这种用例,需要提供一种快捷的方式去创建这种就诊。
扩展 Visit 实体
要满足第一个需求:记录曾经的患病记录,需要扩展 Visit
实体。CUBA 允许通过对 JPA 实体添加 @Extends
注解来扩展应用程序组件或框架本身的实体。对于 Kanto-Petclinic,添加如下属性:
@Extends(Visit.class)
@Entity(name = "kantopetclinic_KantoVisit")
@DiscriminatorValue("KantoVisit")
public class KantoVisit extends Visit {
@Column(name = "PREVIOUS_ILLNESSES", length = 4000)
private String previousIllnesses;
public String getPreviousIllnesses() {
return previousIllnesses;
}
public void setPreviousIllnesses(String previousIllnesses) {
this.previousIllnesses = previousIllnesses;
}
}
@Extends
注解将会使系统使用新的扩展实体 KantoVisit
替换所有到 Visit
的地方。 由于子类仍然可以视作一个 Visit
,所以 ,所有应用程序组件内已有的 逻辑/界面 不会受到影响,可以继续正常运行。
添加了 @Extends
注解后,CUBA Studio 将生成相应的数据库更新脚本。这对对原数据表进行更改,而不定义新表。
有关实体扩展机制的更多信息,请参阅 CUBA 文档 实体扩展 。
这个功能需求的第二部分是:可以在 UI 中管理属性 previousIllnesses
。
该功能要求的第二部分是还可以在UI中管理属性“previousIllnesses”。 为此,我们将扩展现有就诊 UI 界面。 在 CUBA Studio 通过 Screens> New > Screen > Extend an existing Screen
完成扩展界面的过程。该字段只需要添加到实体编辑界面,因此只扩展 VisitEdit
就行了。
要给已有的 form
组件添加字段,必须在新的扩展界面 XML 描述中增加 “增量(delta)” XML(含有所有新的属性)。下面是扩展就诊编辑界面以及向其中添加 previousIllnesses
字段的具体代码:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
messagesPack="com.kanto.petclinic.web.screens.visit"
extends="com/haulmont/sample/petclinic/web/screens/visit/visit-edit.xml"> (1)
<layout>
<form id="form"> (2)
<column id="column1">
<textArea
id="previousIllnessesField"
rows="5"
property="previousIllnesses"
colspan="2"
width="100%"/>
</column>
</form>
</layout>
</window>
1 | extends 属性声明此界面扩展基于哪个界面 |
2 | 添加另一个具有相同 ID 的 form 的表单组件,该组件将与父组件合并 |
在这种情况下,新字段的文本区域将添加到 ID 为 form
的表单组件的底部。
从 Pet 浏览界面创建当日的就诊记录
Kanto-Petclinic 需要的第二个扩展是为创建当日 就诊
提供快捷方式。
可以通过为 Pet 浏览界面创建另一个界面扩展来实现。该扩展给 Pet 表格添加一个名为 Creat Visit
的操作。 XML 界面描述中的界面扩展如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
messagesPack="com.kanto.petclinic.web.screens.pet"
extends="com/haulmont/sample/petclinic/web/pet/pet/pet-browse.xml">
<facets>
<inputDialog (1)
id="createVisitForPetDialog"
defaultActions="OK_CANCEL"
caption="msg://createVisitForPetCaption"
onAction="petTable.createVisit"
>
<parameters>
<enumParameter
id="visitType"
enumClass="com.haulmont.sample.petclinic.entity.visit.VisitType"
caption="msg://com.kanto.petclinic.web.screens.pet/visitType"
/>
</parameters>
</inputDialog>
<notification id="visitCreatedNotification"
type="TRAY"
caption="msg://visitCreated" />
</facets>
<layout>
<groupTable id="petsTable"> (2)
<actions>
<action id="createVisit"
trackSelection="true"
caption="msg://createVisit"
icon="USER_MD"
/>
</actions>
<buttonsPanel id="buttonsPanel">
<button id="createVisitBtn" action="petsTable.createVisit"/> (3)
</buttonsPanel>
</groupTable>
</layout>
</window>
1 | 一个输入对话框,用于在创建就诊记录之前获取就诊类型 |
2 | 通过使用与父界面 petsTable 相同的 ID 重新定义 groupTable ,两个界面描述会合并 |
3 | 该按钮将追加到现有的 buttonsPanel 组件中 |
界面的业务逻辑也相应地扩展了,控制器订阅了输入对话框的关闭事件。
@UiController("petclinic_Pet.browse")
@UiDescriptor("kanto-pet-browse.xml")
public class KantoPetBrowse extends PetBrowse {
@Inject
protected GroupTable<Pet> petsTable;
@Inject
protected Notifications notifications;
@Inject
protected VisitCreationService visitCreationService;
@Inject
protected ScreenBuilders screenBuilders;
@Inject
protected MessageBundle messageBundle;
@Inject
protected NotificationFacet visitCreatedNotification;
@Subscribe("createVisitForPetDialog")
protected void onCreateVisitForPetDialogClose(CloseEvent event) {
if (event.getCloseAction().equals(InputDialog.INPUT_DIALOG_OK_ACTION)) {
final VisitType visitType = (VisitType) event.getValues().get("visitType");
final Visit createdVisit = visitCreationService
.createVisitForPet(petsTable.getSingleSelected(), visitType); (1)
visitCreatedNotification.show();
screenBuilders.editor(Visit.class, this)
.editEntity(createdVisit)
.withOpenMode(OpenMode.DIALOG)
.show();
}
}
}
1 | 将 Visit 的创建委托给 VisitCreationService ,它是 Kanto-Petclinic 项目的一部分。 |
VisitCreationService
是新的 CUBA 服务,不属于原始 Petclinic 平台。我们专门为 Kanto-Petclinic 项目创建了此服务:
@Service(VisitCreationService.NAME)
public class VisitCreationServiceBean implements VisitCreationService {
@Inject
protected DataManager dataManager;
@Inject
protected TimeSource timeSource;
@Override
public Visit createVisitForPet(Pet pet, VisitType visitType) {
final Visit visitForPet = dataManager.create(Visit.class);
visitForPet.setPet(pet);
visitForPet.setType(visitType);
final LocalDateTime now = timeSource.now().toLocalDateTime();
visitForPet.setVisitStart(now);
visitForPet.setVisitEnd(now.plusHours(1));
return dataManager.commit(visitForPet);
}
}
使用这些代码,完成了 Kanto-Petclinic 的个性化需求。现在可以将应用程序交付给客户。让我们看看第二个定制项目 “Alabastia-Petclinic”,以便了解其他一些扩展。
Alabastia-Petclinic
Alabastia Petclinic 对原始项目进行了以下定制化扩展:
-
使用户能够为就诊分配治疗室
-
创建新的就诊时,会根据当前用户自动预选分配的护士
-
为新的诠注自动预选护士治疗室
这些变化需要对 Petclinic 产品进行更多侵入性改变。治疗室的分配需要存储在两个为此项目新创建的实体中。 此外,它需要进行一些 UI 调整,以进行自动预选。
就诊的治疗室
要给就诊分配治疗室,治疗室首先得通过专门的实体管理起来:
@Table(name = "ALABASTIAPETCLINIC_TREATMENT_ROOM")
@Entity(name = "alabastiapetclinic_TreatmentRoom")
public class TreatmentRoom extends NamedEntity {
@Column(name = "ROOM_NUMBER", nullable = false)
private String roomNumber;
}
在实体注解中将 'TreatmentRoom' 实体分配给 Alabastia Petclinic 项目的名称空间,这有助于区分 Petclinic 核心实体和其他实体。
但是,仍然可以与 Petclinic 平台项目的实体混合配对使用。在这里,TreatmentRoom
实体扩展了 Petclinic 平台中定义的实体: NamedEntity
。
存储房间的信息后,需要为每个护士分配默认的治疗室信息。因此,我们将创建一个名为 DefaultTreatmentRoom
的映射实体,该实体在治疗室和用户实体之间建立关联 :
@Entity(name = "alabastiapetclinic_DefaultTreatmentRoom")
public class DefaultTreatmentRoom extends StandardEntity {
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "USER_ID")
private User user;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "TREATMENT_ROOM_ID")
private TreatmentRoom treatmentRoom;
}
现在,需要进行的最后一个实体扩展是将 Visit
链接到治疗室。这可以通过实体扩展来完成,就像我们在 Kanto-Petclinic 示例中已经看到的那样:
@Extends(Visit.class)
@Entity(name = "alabastiapetclinic_AlabastiaVisit")
@DiscriminatorValue("AlabastiaVisit")
public class AlabastiaVisit extends Visit {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TREATMENT_ROOM_ID")
private TreatmentRoom treatmentRoom;
public TreatmentRoom getTreatmentRoom() {
return treatmentRoom;
}
public void setTreatmentRoom(TreatmentRoom treatmentRoom) {
this.treatmentRoom = treatmentRoom;
}
}
有了这些实体更改以及新实体的相应管理界面,我们就完全可以在 UI 上为 assignedNurse
和 treatmentRoom
填充正确的值。
分配的护士时使用当前用户,treatmentRoom
由在 DefaultTreatmentRoom
中为用户分配的房间确定。
@UiController("petclinic_Visit.edit")
@UiDescriptor("alabastia-visit-edit.xml")
public class AlabastiaVisitEdit extends VisitEdit {
@Inject
protected DataManager dataManager;
@Inject
protected UserSession userSession;
@Inject
protected EntityStates entityStates;
@Subscribe
protected void onAfterShow(AfterShowEvent event) {
if (entityStates.isNew(getEditedEntity())) {
getEditedEntity().setAssignedNurse(
currentUser()
);
defaultTreatmentRoomFor(
currentUser()
).ifPresent(defaultTreatmentRoom -> (1)
initTreatmentRoom(defaultTreatmentRoom.getTreatmentRoom())
);
}
}
private void initTreatmentRoom(TreatmentRoom treatmentRoom) {
final AlabastiaVisit visit = (AlabastiaVisit) getEditedEntity(); (2)
visit.setTreatmentRoom(treatmentRoom);
}
private User currentUser() {
return userSession.getCurrentOrSubstitutedUser();
}
private Optional<DefaultTreatmentRoom> defaultTreatmentRoomFor(User user) {
return dataManager
.load(DefaultTreatmentRoom.class)
.query("e.user = ?1", user)
.view(viewBuilder ->
viewBuilder.add("treatmentRoom", View.MINIMAL)
)
.optional();
}
}
1 | 通过 dataManager 查找默认治疗室。如果为当前用户定义了映射,则将为就诊记录设置房间 |
2 | 要使用扩展方法,必须将 Visit 实体强制转换为 AlabastiaVisit 。在扩展实体后,我们就可以确定 CUBA 将始终使用 AlabastiaVisit 而不是 Visit 。 因此,在此处进行强制类型转换是安全的。 |
要在 Visit 编辑器中看到新的属性 treatmentRoom
,我们还应该扩展界面描述:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
messagesPack="com.alabastia.petclinic.web.screens.visit"
xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"
extends="com/haulmont/sample/petclinic/web/screens/visit/visit-edit.xml">
<data>
<collection id="treatmentRoomsDc" class="com.alabastia.petclinic.entity.TreatmentRoom"
view="_minimal">
<loader>
<query>
<![CDATA[select e from alabastiapetclinic_TreatmentRoom e]]>
</query>
</loader>
</collection>
</data>
<layout>
<form id="form">
<column id="column2">
<lookupField
ext:index="2" (1)
id="treatmentRoomField"
property="treatmentRoom"
optionsContainer="treatmentRoomsDc"/>
</column>
</form>
</layout>
</window>
1 | ext:index 确定元素在 form 组件第二列中的位置 |
要使用 ext 属性,必须在 XML 文件的开头定义名称空间:xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd" 。 通过可视化界面设计器放置 lookupField 时,CUBA Studio 会自动配置索引值。
|
现在打开界面时,我们将看到对于属性 treatmentRoom
会出现 unfetched attribute
错误。 原因是视图 visit-with-pet
(定义并用于 Visit 实体)不包含 treatmentRoom
属性。 CUBA 允许覆盖实体扩展的视图定义。 要让 AlabastiaVisit
实体加载 treatmentRoom
,我们需要扩展视图定义:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<views xmlns="http://schemas.haulmont.com/cuba/view.xsd">
<view class="com.alabastia.petclinic.entity.AlabastiaVisit" (1)
name="visit-with-pet" extends="visit-with-pet"> (2)
<property name="treatmentRoom" view="_minimal"/>
</view>
</views>
1 | 视图定义针对扩展类 AlabastiaVisit |
2 | 视图的名称必须与原始名称匹配,并且还要表明它扩展了原始视图 |
进行这些调整后,打开界面时将查询扩展视图,unfetched attribute
错误也会消失。
总结
在本指南中,我们学习了如何通过不同扩展机制实现产品定制。实现定制化的方式主要有两类:运行时扩展以及编译时扩展。
CUBA 应用程序组件属于编译时扩展,在整个 Petclinic 示例中我们使用了这种方式。这两种方式在对原始系统的调整能力方面有很大的差别,需要在定制能力和部署灵活性之间进行权衡。运行时扩展可以在部署/运行时更改,而编译时扩展则不能这样做。
在 Petclinic 的具体示例中,我们研究了如何使 Petclinic 成为应用程序组件,以便将其用于项目定制。
然后,我们研究了扩展现有数据模型或更改 Petclinic 项目的现有UI的几个用例。此外,我们也学习了如何创建独立于原始解决方案的额外业务逻辑。
CUBA 为 UI 层提供的扩展机制允许对产品进行高度定制,而无需重复大量源代码。使用扩展 XML 来调整现有界面定义以及使用 ext:index
属性的定位机制使界面的深度定制成为可能。
在全栈框架领域,扩展(和替换)实体的能力非常重要。它允许开发人员在整个应用程序范围内替换某些实体的使用,并为其添加其他属性。与普通实体一样,CUBA Studio 也会为扩展实体创建相应的数据库更新脚本。此特性对于产品定制非常有用,它允许共享现有可用的通用数据模型,然后根据客户的特定需求进行调整。