4 个月前
CUBA 7 新特性

三年前,我们宣布了 CUBA 框架的第二个公开的主版本。CUBA 6 是改变游戏规则的版本 - 框架的许可从私有化变成了公开的 Apache2.0。那些日子里,我们甚至猜不到这个变化会最终将框架带向何方。随之而来的是,CUBA社区开始呈指数级增长,从中我们学习到许多开发人员可能使用框架的方法(有时甚至是不可能的方法)。现在我们很高兴的宣布 CUBA 7 的发布,通过这个版本,我们希望那些刚刚开始CUBA和Java之旅的社区成员能更加顺利和快乐的成长为熟练的企业级开发人员或者Java专家。

开发工具

显然,CUBA 的成功很大一部分要依赖于 CUBA Studio。它极大的简化了繁琐的 Java 企业级开发任务,很多地方被简化成只需要在可视化编辑器进行简单的配置即可,不需要了解Persistence API 或者 Gradle,甚至不需要了解 Spring 就能开发出来完整的、功能丰富的CRUD 应用程序。这一切,Studio就能帮你完成。

text

以前,Studio 是一个单独的 web 应用程序,这样会有一些明显的局限:

l 首先,Studio 并不是功能完备的 IDE,所以开发者需要经常在 Studio 和 IntelliJ IDEA 或者Eclipse 之间切换,以便在 IDE 中开发业务逻辑,也能更好的利用 IDE 方便的导航、代码完成功能和其他必要的功能。来回地切换有时候很烦人。

l 其次,Studio 的简单性是建立在大量的源码解析和生成的基础上。所以,要提高代码生成的能力也就意味着要朝着开发功能完备的IDE方向努力 - 这个想法太过雄心勃勃了。

最后我们决定依靠另一位巨人的肩膀来解决这些局限。现在 Studio 跟 JetBrains 开发的IntelliJ IDEA 合并了。现在可以将 Studio 作为 IntelliJ IDEA 的插件安装或者下载单独打包的版本。

text

这个方法为我们开辟了新的视野:

l 能支持其它JVM的开发语言(首先就是Kotlin)

l 提升了热部署的能力

l 整个项目中能更直观的导航

l 更聪明的代码生成和提醒

现在新的Studio正在积极的开发中:我们正在移植旧版本的功能。短期计划还包括使用原生IntelliJ UI重新实现基于 Web 的设计器,并改善项目导航体验。

技术栈升级

跟以前主版本升级一样,这次底层的技术栈也做了升级,比如 Java 8/11,Vaadin 8,Spring 5。

text

By default new projects use Java 8, but you can specify the version of Java by adding the following clause to the build.gradle file:

subprojects {
   sourceCompatibility = JavaVersion.VERSION_11
   targetCompatibility = JavaVersion.VERSION_11
}

升级到Vaadin 8是个不小的挑战,因为Vaadin的数据绑定API发生了很大的破坏性变化。但使用CUBA的开发者很幸运,因为CUBA为开发者提供了统一封装的自有API层,屏蔽了底层Vaadin的内部结构。CUBA开发团队做了大量的工作,重新实现了很多内部逻辑以保持CUBA自有的API不变化。也就是说,这很好的保持了CUBA框架的兼容性,不需要做任何重构就可以直接移植到CUBA 7并享受Vaadin 8带来的好处。

依赖库的完整升级列表可以在官方的 release notes 中找到。

新的界面API

这一小节也可以称为 “第一版界面API”,因为CUBA之前没有任何官方的声明在web客户端层有API存在。界面API基于框架的历史,也基于我们最初的一些假设:

以声明为中心的方法 - 所有可以以声明式描述的,都应该在界面描述文件中声明,而不是在其控制器中编码。

标准界面(浏览和编辑界面)提供具体的通用功能,一般不需要修改。

从最初的一千个成员加入了社区开始,我们就认识到对于“标准” CRUD 界面的需求是有多么广泛,已经超出了最开始我们设计的一组功能了。然而,很长一段时间,即使没有 API 层,我们也能够处理自定义行为的需求,这是因为有另一个第一阶段假设 - 开放继承。有效地进行开放继承意味着可以覆盖基础类的任何公共或保护方法,再根据需要定制其行为。这听起来似乎是所有顽疾的解药,但事实上可能短期都不一定能见效:如果被覆盖的方法被重命名、删除了或者将来版本的框架根本不同这个方法了,该怎么办?

text

所以,为了响应社区日益增长的需求,我们决定引入新的界面API。API提供了清晰的长期的扩展点,而没有隐藏的声明式暗喻,灵活并且易于使用。

界面声明

在 CUBA 7 里,界面声明异常简单:

@UiController("new-screen") // screen id 
public class NewScreen extends Screen { }

从上面的例子我们可以看到,界面的标识符在控制器类上显式的进行定义。也就是说,现在界面id和控制器类能相互唯一的对应。由此带来的好消息就是,现在界面可以直接通过其控制类来安全访问了(注意下面例子用控制器类来创建确认窗口):

@Inject
private ScreenBuilders screenBuilders;
@Subscribe
private void onBeforeClose(BeforeCloseEvent event) {
screenBuilders.screen(this)
.withScreenClass(SomeConfirmationScreen.class)
.build()
.show();
}

至此,界面描述文件不再是必须的,而成为了一个补充的部分。界面布局可以通过编程的方式创建或者通过 XML 界面描述声明式创建,界面描述通过控制器类的 @UiDescriptor 注解定义。这样能使得控制器和布局更加容易读懂。这个方式跟Android开发中使用的模式非常类似。

之前,需要在web-screens.xml中注册一个界面描述并为其设置一个标识符。在 CUBA 7 中,这个文件只是因为兼容性的考虑被保留下来,用新方法创建界面不需要这种注册了。

界面生命周期
  新的API带来了清晰的自描述的界面生命周期事件:

  • Init
  • AfterInit
  • BeforeShow
  • AfterShow
  • BeforeClose
  • AfterClose

CUBA 7 中所有的界面相关的事件都可以用下面的方式订阅:

@UiController("new-screen")
public class NewScreen extends Screen {
   
   @Subscribe
   private void onInit(InitEvent event) { 
   }
   
   @Subscribe
   private void onBeforeShow(BeforeShowEvent event) {      
   }

}

将新API与旧方法进行比较,可以看到我们没有重写钩子方法,之前这些钩子方法在父类的层次结构中被模糊地调用。现在我们在界面生命周期的明确预定义的点中定义业务逻辑。

事件处理和功能代理

前一小节我们介绍了如何订阅生命周期事件,那么,其他组件呢?我们是否应该像在6.x版本中那样在界面初始化时分散所有必需的监听器?新API非常统一,因此订阅其他事件与生命周期事件完全相似。

我们举一个带有两个UI元素的简单例子,一个按钮和一个货币字段控件,因此它的XML描述符如下所示:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
       caption="msg://caption"
       messagesPack="com.company.demo.web">
   <layout>
       <hbox spacing="true">
           <currencyField id="currencyField" currency="$"
                          currencyLabelPosition="LEFT"/>
           <button id="calcPriceBtn" caption="Calculate Price"/>
       </hbox>
   </layout>
</window>

通过单击按钮我们调用中间件服务返回一个数字,该数字将被写到货币控件中。货币控件需要根据价格的值更改其样式。

@UiController("demo_MyFirstScreen")
@UiDescriptor("my-first-screen.xml")
public class MyFirstScreen extends Screen {

   @Inject
   private PricingService pricingService;

   @Inject
   private CurrencyField<BigDecimal> currencyField;

   @Subscribe("calcPriceBtn")
   private void onCalcPriceBtnClick(Button.ClickEvent event) {
       currencyField.setValue(pricingService.calculatePrice());
   }

   @Subscribe("currencyField")
   private void onPriceChange(HasValue.ValueChangeEvent<BigDecimal> event) {
       BigDecimal price = pricingService.calculatePrice();
       currencyField.setStyleName(getStyleNameByPrice(price));
   }
  
   private String getStyleNameByPrice(BigDecimal price) {
       ...
   }
  
}

在上面的例子中,我们看到有两个事件处理器:一个是按钮按下时调用的,另一个是当货币控件的值发生变化时执行的。就是这么简单。

现在,我们设想一下,如果需要验证价格的值并确保其为一个正数。最直接的方法就是在界面初始化的时候为其添加一个验证器:

@UiController("demo_MyFirstScreen")
@UiDescriptor("my-first-screen.xml")
public class MyFirstScreen extends Screen {

   @Inject
   private CurrencyField<BigDecimal> currencyField;

   @Subscribe
   private void onInit(InitEvent event) {
       currencyField.addValidator(value -> {
           if (value.compareTo(BigDecimal.ZERO) <= 0)
               throw new ValidationException("Price should be greater than zero");
       });
   }

}

在真实的应用程序中,界面的入口点经常会被这种界面元素的初始化方法填满。为了避免这个问题,CUBA提供了一个非常有用的注解 @Install。看看使用这个注解怎么避免这个情况:

@UiController("demo_MyFirstScreen")
@UiDescriptor("my-first-screen.xml")
public class MyFirstScreen extends Screen {

   @Inject
   private CurrencyField<BigDecimal> currencyField;

   @Install(to = "currencyField", subject = "validator")
   private void currencyFieldValidator(BigDecimal value) {
       if (value.compareTo(BigDecimal.ZERO) <= 0)
           throw new ValidationException("Price should be greater than zero");
   }
}

事实上,这里是将货币控件验证的逻辑代理给了界面的 currencyFieldValidator 方法来执行。虽然看上去稍微复杂一点,但是开发人员使用起这个功能来惊人的快速。

界面Builders/通知消息/对话框

text

CUBA 7 还引入了一些新的非常有用的带有流式 API 的组件:

l ScreenBuilders 结合了流式工厂来生成标准的查找、编辑和自定义界面。下面的例子展示了如何从一个界面打开另一个界面。注意,build() 方法能返回正确类型的界面实例,不需要不安全的类型转换。

CurrencyConversions currencyConversions = screenBuilders.screen(this)
       .withScreenClass(CurrencyConversions.class)
       .withLaunchMode(OpenMode.DIALOG)
       .build();
currencyConversions.setBaseCurrency(Currency.EUR);
currencyConversions.show();

l Screens 组件相对于 ScreenBuilders 来说提供了更底层的抽象,用来显示和创建界面。并且提供了访问 CUBA 应用程序中所有已打开界面信息的方法(Screens#getOpenedScreens),如果需要遍历这些界面,这个方法很有用。

l Notifications和Dialogs 组件均提供了自描述的方便接口。这里有个例子创建对话框和消息通知:

dialogs.createOptionDialog()
       .withCaption("My first dialog")
       .withMessage("Would you like to thank CUBA team?")
.withActions(
       new DialogAction(DialogAction.Type.YES).withHandler(e -> 
notifications.create()
               .withCaption("Thank you!")
               .withDescription("We appreciate all community members")
               .withPosition(Notifications.Position.MIDDLE_CENTER)
               .withHideDelayMs(3000)
               .show()),
       new DialogAction(DialogAction.Type.CANCEL)
)
       .show();

数据绑定

CUBA 之所以可以做到后台UI的快速开发,不仅仅是因为提供了可以生成大部分代码的可视化工具,还因为提供了大量开箱即用的具有数据感知能力的组件。 这些组件只需要知道要使用哪些数据,其余事情会自动管理。例如, 查找列表、选择器字段、具有 CRUD 操作的各种网格等。

在版本 7 之前,数据绑定是通过称为数据源的对象实现的,数据源包装单个实体或实体集合、与数据感知组件绑定,然后响应数据感知组件的数据变化。 这种方法非常有效,但是是以一个整块的方式实现的。 整块石头似的架构通常在可定制性方面会有问题。因此在 CUBA 7 中,这块坚固的巨石被分成 3 个数据组件:

Data Loader (数据加载器) 是数据容器的数据提供者。 数据加载器不保存数据,它们只是将所有必需的查询参数传递给数据存储,并将结果数据集提供给数据容器。

l **Data container (数据容器)  **保留加载的数据(单个实体或多个实体)并以响应式的方式将数据提供给数据感知组件:被包装实体的所有更改都会暴露给相应的UI组件,反之亦然,UI组件内的所有更改都会引起数据容器作出相应更改。

Data context  (数据上下文)是一个强大的数据更改管理器,可跟踪更改并提交所有已修改的实体。 一个实体可以合并到一个数据上下文中,合并后会得到一个原始实体的副本,这个副本与原始实体有一个唯一但非常重要的区别:对副本实体及其引用的所有实体(包括集合)的所有修改都将被跟踪、存储和提交。

数据组件可以在界面描述符中声明,也可以使用专门的工厂类 - DataComponents 以编程的方式创建。

其它

上面介绍了新的界面API中最重要的部分,所以剩下的部分我简要列出 Web客户端层中的其他重要功能:

**  URL 历史记录和导航**。此功能解决了在 WEB 浏览器中具带有“后退”按钮的 SPA 应用程序存在的一个普遍问题,提供了一种简单地为应用程序界面分配路径的方法,同时使 API 能够在URL中反映界面的当前状态。

**  使用 Form 代替 FieldGroup**。 FieldGroup 是一个数据感知组件,用于显示和修改单个实体的字段。它在运行时推断出用于显示字段的实际UI组件。也就是说,如果你的实体中有一个日期类型的字段,它将使用 DateField 组件来显示 。但是,如果你希望以编程方式使用此组件,则需要将此组件注入到界面控制器并手动将其转换为正确的类型(在我们的示例中为DateField)。过了一段时间,可能会字段类型更改为其他类型,这时应用程序就是崩溃。表单通过显式声明组件类型解决此问题。关于 Form 的更多信息请参阅这里

**  显著地简化了第三方 JavaScript 组件的集成**,可参考这个文档将自定义 JavaScript 组件嵌入到CUBA 应用程序中。

**  现在可以在 XML 界面描述中轻松定义 HTML/CSS属性**,也可以通过编程方式设置。详细信息请参阅这里

上篇我们主要介绍了 CUBA 7 中前端的一些主要功能。这篇我们介绍一下中间件的一变化和新特性。

中间件功能

前面关于新的界面 API 的描述内容比我预期的要多许多,所以在这一节,我会尽量简单点说!

实体更改事件

实体更改事件是一个Spring 应用程序事件。在实体已经进入数据存储、已物理插入且马上要提交事务时触发。这时,可以进行一些额外的检查(例如,在确认订单之前检查库存中的产品可用性)并在其他事务可见前对数据进行一些修改(例如重新计算总数)(显然需要使用“读已提交”事务隔离级别)。在这个事件处理器中你还可以通过抛出异常来中断事务,这是中断事务的最后一个机会,在某些极端情况下可能很有用。

还有一种方法可以在事务提交后捕获实体更改事件。

按照这个文档的描述查看示例。

事务型数据管理器

在开发应用程序时,我们通常使用分离的实体 ,这种实体不受任何事务管理。但是,在有些情况下使用分离的实体并不可行,特别是在需要满足 ACID 要求时 ,这时你就需要使用事务型数据管理器。它看起来与普通的数据管理器非常相似,但在以下方面有所不同:

l 它可以嵌入现有事务(如果在事务上下文中调用它)或创建自己的事务。

l 它没有 commit  方法,但是有 save 方法,save 方法不会立即提交事务,待附加的事务提交时才提交。

这里有相关示例。

JPA生命周期回调

最后,CUBA 7 支持 JPA 生命周期回调。 对于生命周期回调我们在文档中有精细的描述,这里我就不再重复了。可以在这里找到详细的描述。

兼容性怎么样?

任何重大版本的发布都是一个需要认真对待问题,特别有这么多看起来具有破坏性的变化时! 我们设计所有这些新功能和API,同时考虑到向后兼容性:

l CUBA 7 支持旧的界面 API,同时在底层使用新的 API 实现这些旧的 API 功能。

l 我们还为旧的数据绑定提供了适配器,这些适配器继续适用于旧的界面。

所以,好消息是,从版本 6 到 7 的迁移应该非常简单。

总结

在结束这个技术概述时,我想提一下,还有其他重要的更新,特别是在许可方面:

l Studio 已取消10个实体的限制。

l 报表、BPM(业务流程管理)、图表和地图以及全文搜索扩展现在都免费并开源。

l Studio 的商业版致力于通过可视化设计器来提升实体、界面、菜单和其他平台元素的开发体验,而在免费版中主要通过编码的方式实现这些。

l 请注意,对于6.x 及更早版本的 Platform 和 Studio ,许可条款保持不变!

最后,让我再次感谢社区成员的所有支持和反馈。 希望你们会喜欢第 7 版! 根据传统,发行说明中提供了完整的更改列表。