转转图书对基于Drools引擎的DMN实践

开发 架构
DMN主要应用在向程序员以外的人员提供决策管理的能力,以求更准确地反映目的,从程序员的角度讲可能和写ifelse没什么不一样,但是其他角色的参与人员可以通过较低的学习成本来上手实现规则,能够减少沟通成本和不同人的理解差异产生的不符合预期的结果。

1 背景介绍

1.1 DMN是什么

DMN全称Decision Model and Notation(决策模型和符号、决策模型和表示法),是一种用于表示业务决策和规则的规范,旨在帮助参与决策的人都能简单快速理解决策过程。

图片

DMN logo

DMN是由OMG(Object Management Group,对象管理组织)管理的一种规范,该组织下比较知名的还有UML等。

图片

DMN示例

如图所示,DMN的表现形式近似于流程图,通过可视化来更直观地体现出流转和处理的过程,而各个节点使用附带逻辑的表格来表现该节点处理数据的方式。

由于DMN是一个规范,所以在应用上,主要依靠各个DMN工具供应商提供具体实现,近似于SQL语句和MySQL、Oracle的关系。

1.2 为什么要用DMN

应用DMN主要目的是为了解决转转图书项目定价逻辑过于复杂的问题。

在引入DMN之前,图书项目使用java代码实现产品人员提供的定价逻辑。但是随着逻辑越来越复杂,定价逻辑的代码也越来越难以阅读和维护。同时,将产品人员的逻辑翻译成代码是一个单向过程,只有程序员能理解实现过程,产品人员只能通过结果反推是否正确合理,在逻辑变得复杂之后,很难快速发现问题。

图片

复杂规则的一部分,区间和系数

而通过DMN,可以可视化的表示出逻辑过程,产品人员也可以直观的看到是否符合预期,还能够进行编辑,从而解决了只有程序员掌握实现而过于依赖程序员的问题,也能减少表述交流的过程产生理解偏差进而导致的错误。

图片

使用DMN实现的规则表现

1.3 什么是Drools

Drools是用java语言编写的开源规则引擎,是DMN规范的一种实现。举例来说DMN规范就是接口,Drools就是实现了接口的其中一个工具类。

图片

Drools logo

Drools属于Kie开源社区,而kie社区由Red Hat赞助,有比较高的社区活跃度。

图片

Kie旗下的产品

1.4 为什么选择Drools

Drools引擎是基于java实现的,这使得不需要为Drools单独部署运行环境,运维成本是0,十分友好。

其次,DMN规范规定实现其规范的软件必须满足三级递增的符合性级别,三级最高,一级最低,满足三级级别时必须同时满足一级和二级级别。而Drools引擎对DMN规范的支持属于三级级别,功能完善。

图片

Drools引擎对DMN各个版本提供支持

同时,Drools引擎社区活跃度高,同时提供了完整的工作组件,可靠性高。

2 Drools引擎应用

以下内容基于7.61.0版本,与8.x及以上的版本存在差异

2.1 官方推荐的最直接的应用方式

首先需要部署运行一个Kie Server用来执行DMN规则,然后部署Drools官方提供的基于Web的工作站软件用来编辑、测试、发布DMN规则。

图片

Drools提供的工作站软件

当部署完成DMN规则之后,可以通过Kie Server REST API来运行规则获取结果,即通过Http的方式请求Kie Server提供的接口。

2.2 转转图书的限制

首先说图书项目并没有直接按照官方推荐的做法部署应用,原因有几个方面:

第一点,转转内部项目部署运行有自有的框架体系,而Kie Server是基于JBoss运行的服务器软件,在现有体系中部署一个外部项目需要额外的运维成本。

其次,Kie Server作为一个第三方工具,当出现问题时仅靠图书项目的人员难以解决,而等待社区反馈对于转转图书这种线上的商业项目难以接收。所以为了尽可能的减少出现问题的概率,节约人力,图书项目倾向于尽可能少的引入外部依赖。

基于以上原因,图书项目在应用Drools的时候选择了另外一种方式。

3 脱离Kie Server的Drools引擎实践

脱离开Kie Server,自然就没有了REST API的http接口,同时也失去了官方工作站的支持,但是相对的,Drools提供了一些对这种场景下的支持。

3.1 在线编辑DMN规则

在Kie旗下有另一款名为Kogito的产品,是一个提供在线编辑BPMN和DMN的服务器软件,同时其中有一个all-in-one的js文件,实现了在线编辑DMN的全部功能。

图片

js提供的接口

图书项目基于这个js文件进行了包装,增加易用性,使得DMN编辑功能融入到现有的工作后台之中。

图片

包装页面,下方是js提供的功能区

同时增加编辑记录列表用来便于管理和回滚。

图片

编辑列表

js工具编辑后的DMN规则内容是xml格式的字符串,可以使用js提供的getContent()接口导出。

3.2 使用Drools引擎执行DMN规则

没有了Kie Server的支持之后,需要通过代码的方式运行Drools引擎。

首先在项目中引入Drools引擎的组件

图片

引入Drools引擎组件

然后在项目中创建Drools引擎的引用并执行。

// 初始化KieServices
KieServices kieServices = KieServices.Factory.get();
// 通过指定的maven依赖创建Kie容器
ReleaseId releaseId = kieServices.newReleaseId( "com", "my-kjar", "1.0.0" );
KieContainer kieContainer = kieServices.newKieContainer( releaseId );
// 通过容器获取DMN运行时
DMNRuntime dmnRuntime = KieRuntimeFactory.of(kieContainer.getKieBase()).get(DMNRuntime.class);
// 通过DMN运行时获取需要执行的DMNModel,其中包含了DMN规则
String namespace = "my-namespace";
String modelName = "dmn-model-name";
DMNModel dmnModel = dmnRuntime.getModel(namespace, modelName);
// 获取上下文,并传入DMN规则需要用到的数据
DMNContext dmnContext = dmnRuntime.newContext();
dmnContext.set("inputData", "123");
// 执行规则获取结果
DMNResult dmnResult = dmnRuntime.evaluateAll(dmnModel, dmnContext);
for (DMNDecisionResult dr : dmnResult.getDecisionResults()) {
log.info("Decision: '" + dr.getDecisionName() + "', " + "Result: " + dr.getResult());
}

由于Kie容器使用maven作为读取DMN配置的手段,所以要求DMN规则内容需要打包到Kie容器能够识别的jar包里,并且部署到项目可访问的maven环境中。

3.3 完善处理流程

按照上述流程就可以实现脱离Kie server运行Drools引擎。但是距离落地上线还有一小点距离。

官方的工作站可以提供DMN规则上线前的验证和测试功能,但是all-in-one的js文件没有,所以为了保证准确性和稳定性,需要额外实现DMN规则的验证动作。

这里可以使用KieContainer提供的verify接口来触发Drools引擎的验证动作并获取结果。

Results verify = kieContainer.verify();
for (Message message : verify.getMessages()) {
log.info(message.getLevel().name() + ": " + message.getText());
}

将验证流程放到保存DMN规则的时候,就可以快速发现是否存在编写错误。

3.4 DLC - 脱离maven环境运行Drools引擎

前面说了Drools引擎需要使用maven获取运行的DMNjar文件,但是在实际应用中,线上环境不一定会部署maven。例如图书项目需要在公司内部公用的spark集群上运行Drools,但是spark集群上没有部署maven环境,按照上述流程运行就会报错。

所以需要一个能够使Drools脱离maven环境的手段。

通过分析源码可以发现,在创建KieContainer的时候,会使用KieServices.getRepository()方法获取数据源,并通过maven坐标查找其中的KieModule。所以需要做的就是直接我们需要的jar写入KieServices的数据源中:

// 首先通过JarOutputStream生成包含DMN规则字符串,且符合Drools规范的jar二进制流
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
JarOutputStream jos = new JarOutputStream(outputStream);
// kmodule.xml
jos.putNextEntry(new JarEntry("/META-INF/kmodule.xml"));
jos.write("<?xml versinotallow=\"1.0\" encoding=\"UTF-8\"?><kmodule xmlns=\"http://www.drools.org/xsd/kmodule\"><kbase name=\"kbase_1\" packages=\"rules\" default=\"true\"><ksession name=\"ksession_1\" default=\"true\"/></kbase></kmodule>".getBytes(StandardCharsets.UTF_8));
// pom.properties
jos.putNextEntry(new JarEntry("/META-INF/maven/com.myspace/test/pom.properties"));
jos.write("groupId=com.myspace\nartifactId=test\nversinotallow=1.0.0".getBytes(StandardCharsets.UTF_8));
// pom.xml
jos.putNextEntry(new JarEntry("/META-INF/maven/com.myspace/test/pom.xml"));
jos.write("<?xml versinotallow=\"1.0\" encoding=\"UTF-8\"?><project xsi:schemaLocatinotallow=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\" xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"> <modelVersion>4.0.0</modelVersion> <groupId>com.myspace</groupId> <artifactId>test</artifactId> <version>1.0.0</version> <packaging>kjar</packaging> <name>test</name> <description></description></project>".getBytes(StandardCharsets.UTF_8));
// dmn
jos.putNextEntry(new JarEntry("/config/rules/test.dmn"));
jos.write(dmn.getBytes(StandardCharsets.UTF_8));
jos.finish();
ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
// 初始化KieServices
KieServices kieServices = KieServices.Factory.get();
// 获取KieServices的数据源
KieRepository repository = kieServices.getRepository();
// 向数据源中加入基于生成二进制流构造的KeiModule
KieModule kieModule = repository.addKieModule(kieServices.getResources().newInputStreamResource(inputStream));

如上就可以实现在没有maven环境的时候仍然能够让KieServices获取到我们需要DMN规则。

但是仅仅如此还不够,在创建KieContainer的时候,内部会生成一个KieProject实例,KieProject在实例化的过程中会默认生成一个基于maven的MavenClassLoaderResolver用于查找jar,而缺少maven环境的情况下在生成MavenClassLoaderResolver的时候也会报错。为此,我们需要一个取代MavenClassLoaderResolver的办法。

通过分析Drools源码可以发现,在源码中,某些时候会使用ProjectClassLoader.findParentClassLoader()来获取基于当前运行环境的ClassLoader。

所以,需要在创建KieContainer的时候使用ProjectClassLoader.findParentClassLoader()生成的ClassLoader来取代默认的MavenClassLoaderResolver:

// 使用另外一个接收ClassLoader的接口来生成KieContainer
KieContainer kieContainer = kieServices.newKieContainer(kieModule.getReleaseId(), ProjectClassLoader.findParentClassLoader());

如此一来,就可以完全脱离maven环境来使用Drools引擎了,而Drools引擎也可以融入到任意的java项目之中,部署到任意的java环境之下。

4 总结

DMN在实际应用中有比较明显的优点:

  • 可以在不修改代码的情况下更新逻辑,减少上线过程中可能产生的问题
  • 可以通过一套代码实现各种场景的需求
  • 流程可视化,便于理解逻辑的运行
  • 能够让一部分不熟悉程序代码的人员也可以参与编辑,避免流程全部只由程序员了解

但是相对的也存在一些弊端或者表现不够好的地方:

  • 流程复杂时图形化表现比较杂乱,尤其是多个决策节点依赖相同的上游时
  • 编辑规则时还是会有一部分场景条件需要写代码才能实现,例如列表包含(可以使用DMN专用代码FEEL实现,或者引用已经实现的java方法实现)
  • 运行规则的时间相较于使用java代码实现要慢,因为中间涉及规则文件解析和各个节点的计算,而java代码可以更直接的实现

图片

复杂规则示例

总体而言,DMN主要应用在向程序员以外的人员提供决策管理的能力,以求更准确地反映目的,从程序员的角度讲可能和写ifelse没什么不一样,但是其他角色的参与人员可以通过较低的学习成本来上手实现规则,能够减少沟通成本和不同人的理解差异产生的不符合预期的结果。

5 参考资料

Drools官方网站:https://www.drools.org/

Drools官方的DMN教学:https://www.drools.org/learn/dmn.html

Drools官方的15分钟简易教学:https://learn-dmn-in-15-minutes.com/learn/introduction

Kogito在线编辑网页:https://sandbox.kie.org/#/

Kie官方网站:https://www.kie.org/

OMG官方网站的DMN页:https://www.omg.org/dmn/

作者简介

项赢,转转资深java工程师。长期服务于转转图书项目。

责任编辑:武晓燕 来源: 转转技术
相关推荐

2023-06-07 08:32:32

引擎技术while

2024-07-31 20:45:45

2023-10-16 17:41:40

Drools

2023-07-12 08:33:34

引擎LiteFlow编排

2023-11-01 07:44:29

转转Flutter业务

2022-11-07 14:45:26

转转价格DDD

2023-12-27 19:12:42

OLAP自助分析

2022-10-28 09:15:02

2023-03-02 08:54:32

2023-03-22 08:32:35

2023-03-02 08:32:41

2022-10-28 08:31:43

2023-02-08 09:42:30

策略方式容量

2022-12-15 08:35:01

用户画像平台

2024-06-06 08:18:42

回收业务

2023-04-19 13:18:41

动态线程池平台

2024-07-18 08:40:28

2023-01-04 08:31:10

转转测试环境

2024-10-16 21:49:24

2024-09-11 19:36:24

点赞
收藏

51CTO技术栈公众号