前段时间在一个项目重构的过程中,发现很多地方充斥着复杂的条件判断。虽然可以用大量的 if-else 来解决,但当时一股脑想玩点新花样,便转头调研起了规则引擎。
市面上成熟的规则引擎很多,功能强大且性能优异。不过,在做技术选型时,我还是被 URule 吸引住了,主要原因是它基于纯浏览器配置,不需要额外安装沉重的客户端,且可视化规则编辑做得相当不错。经过一番调研后,我顺利将其接入了项目中,便顺手记录下了这次实践。
1. 介绍
规则引擎本质上是一种嵌入在程序中的组件。它能将复杂的业务判断逻辑从业务代码中剥离,使程序只需关注自身的业务流转,而无需陷入繁杂的条件判断中。简单来说,规则引擎接收一组输入数据,通过预先配置好的规则,再输出一组结果。
市面上知名的规则引擎有 Drools、Aviator、EasyRules 等。相比之下,URule 可以运行在 Windows、Linux、Unix 等多种操作系统上,采用纯浏览器编辑模式,直接在网页上完成规则编写与测试。
当然,URule 分为开源版和 Pro 版,两者之间的差异可以通过下面的对比表格了解。



此处原为某后台管理系统项目推广,已略去。
2. 安装使用
在实际应用中,URule Pro 有四种集成方式:嵌入式模式、本地模式、分布式计算模式以及独立服务模式。
不过本文聚焦于开源版,我们在开源版集成 Spring Boot 的基础上进行二次开发。经过搜索,社区已有现成的解决方案,项目模块结构大致如下:

只需自行创建一个空数据库,并在 edas-rule-server 服务中修改对应的数据库连接配置,然后启动服务即可。首次启动时,相关的表结构会自动创建。
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/urule-data?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=mysql
如前所述,URule 完全基于浏览器进行规则编辑。服务启动后,在浏览器中访问 http://localhost:8090/urule/frame,看到如下界面就代表启动成功了。

此处原为某微服务项目推广,已略去。
3. 基础概念
3.1 整体介绍
URule 主要由两部分构成:设计器 和 规则执行引擎。其中,设计器又由库文件和规则文件组成。整体的结构图如下:

3.2 库文件
如上图所示,库文件包含四种:变量库、参数库、常量库和动作库。这类似于我们在 Java 开发中定义的实体对象、枚举、常量和方法。
在 URule 中,所有规则都是可视化配置的。在配置规则的过程中,需要引入这些已经定义好的库文件,并结合业务需求,才能拼装出符合业务场景的规则,因此库文件可以说是无处不在。
3.2.1 变量库文件
在业务开发中,我们会创建许多包含 Getter 和 Setter 的 Java 类(如 PO、VO、BO、DTO、POJO 等),这些对象主要充当数据的载体。
在 URule 中,变量库就是用来映射这些对象的,它使得规则能够与业务数据产生关联,最终实现业务与规则的互动。下图为创建变量库的操作入口:

聊了这么多可视化配置,这算是第一次真正展示配置界面。
在“库”菜单下右键,点击“添加变量库”,然后自定义一个变量库名称即可。需要注意的是,名称仅支持中文或英文,不支持其他特殊字符。

创建完成后,就可以对变量库进行编辑了,这其实就相当于给 POJO 类添加属性。

简单理解:左侧是定义“类”,其中“名称”作为别名,后续配置规则时,将直接使用该别名代替类;右侧则是定义类的具体属性。
最后,在业务系统中创建与之对应的 Java 类,务必保证全限定名与变量库中配置的“类路径”完全一致。
package com.cicada;
import com.bstek.urule.model.Label;
import lombok.Data;
/**
* @author 芋道源码
* @version 1.0
* @date 2023/3/3 15:38
* @description
*/
@Data
public class Stu {
@Label("姓名")
private String name;
@Label("年龄")
private int age;
@Label("班级")
private String classes;
}
这里用到的 @Label 注解由 URule 提供,主要用来描述字段属性,属性值需要与变量库中定义的“标题”一列保持一致。官方介绍说,通过这个注解可实现 POJO 属性与变量库属性之间的双向映射——也就是说,写好 POJO 代码后,对应的变量库无需手动重新构建,甚至可以直接生成。
3.2.2 常量库文件
常量库可以类比为 Java 系统中的常量或枚举,例如性别、对接机构等。
和变量库一样,常量库也可以与系统中的枚举进行映射,这样做的好处是避免了手动输入可能带来的拼写错误。创建常量库同样简单,在“库”菜单下右键选择“添加常量库”即可。
创建后,会弹出如下编辑页面:

3.2.3 参数库文件
参数库是 URule 规则中的临时变量,变量的类型和数量都不固定。可以把它想象成一个 Map,实际上,其底层存储也确实用到了 Map。
沿用老套路,在“库”菜单下右键选择“添加参数库”。

可以看到,参数库省去了左侧的分类树,直接添加参数并选择类型即可,配置起来相对简单了很多。这里的“名称”一列使用英文,相当于 Map 中的 key;“标题”一列则用于规则配置时的显示,中文标识让阅读更加直观。
需要注意的是,定义的“名称”必须保证唯一性,因为 Map 的 key 是唯一的,同名的 key 会造成覆盖。
3.2.4 动作库文件
动作库用于映射注册在 Spring 容器中 Bean 的方法,映射后即可在规则中直接调用这些方法。同样的,在“库”菜单下右键,选择“添加动作库”。

接着,我在系统中创建了一个名为 Action 的普通类,使用 @Component 注解将其交给 Spring 容器管理。类中的方法如果想要被动作库读取,就需要打上 URule 提供的 @ExposeAction 注解。
package com.bstek.urule.cicada;
import com.bstek.urule.action.ActionId;
import com.bstek.urule.model.ExposeAction;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author 芋道源码
* @version 1.0
* @date 2023/3/10 13:59
* @description
*/
@Component("action")
public class Action {
@ActionId("Hello")
public String hello() {
return "hello";
}
@ExposeAction(value="方法1")
public boolean evalTest(String username) {
if(username == null) {
return false;
} else if(username.equals("张三")) {
return true;
}
return false;
}
@ExposeAction(value="测试Int")
public int testInt(int a, int b) {
return a + b;
}
@ExposeAction(value="打印内容")
public void printContent(String username, Date birthday) {
SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
if(birthday != null) {
System.out.println(username + "今年已经" + sd.format(birthday) + "岁了!");
} else {
System.out.println("Hello " + username + "");
}
}
@ExposeAction(value="打印Stu")
public void printUser(Stu m) {
System.out.println("Hello " + m.getName() + ", is age:" + m.getAge());
}
}
在动作库配置页面,需要添加 Bean,“Bean Id”一列对应 Spring 容器中的 Bean 名称,这里填入 action。然后点击操作列中的小手图标,系统便会自动检索出所有标记了 @ExposeAction 的方法。选择一个需要引入的方法,其参数列表也会被自动加载进来。


最后要提一句,变量库、参数库、动作库、常量库这些基础库文件一旦被某个规则文件引用,就不要再随意修改了。
3.3 规则集
规则集,顾名思义,就是用来配置具体规则的。前面定义的各种库文件,正是需要导入到规则集中才能派上用场。这也是日常使用频率最高的一种业务规则实现方式。
规则集由三个核心部分组成:如果、那么、否则。
在定义方式上,URule 提供了两种选择:向导式和脚本式。
- 向导式规则集:完全基于页面鼠标点击进行高度可视化配置,即便不懂代码的业务人员也能快速上手,这也是 URule 极具吸引力的核心亮点。
- 脚本式规则集:顾名思义,需要编写脚本,这在一定程度上拉高了配置门槛,适合具备一定编码能力的人员。
3.3.1 向导式规则集
新建步骤同样简单:在“决策集”菜单上右键,点击“添加向导式决策集”,一个新的规则集就创建好了。

在开始配置规则之前,建议先导入前面定义好的库文件。例如,在页面上点击“变量库”,再选择目标变量库文件即可。

一切准备就绪后,就可以愉快地配置规则了。向导式配置没什么门槛,全程可视化界面,鼠标点点就能完成。以下是我配置好的一个简单规则集:

可以看到,规则由三部分组成:
- 如果:设置规则触发的条件。
- 那么:条件满足后要执行的动作,通常以“变量赋值”为主。
- 否则:条件不满足时执行的动作。
规则配置完成后,就可以通过代码来执行它了:
package com.cicada;
import cn.hutool.core.bean.BeanUtil;
import com.Result;
import com.bstek.urule.Utils;
import com.bstek.urule.runtime.KnowledgePackage;
import com.bstek.urule.runtime.KnowledgeSession;
import com.bstek.urule.runtime.KnowledgeSessionFactory;
import com.bstek.urule.runtime.service.KnowledgeService;
import com.cicada.req.StuReq;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
/**
* @author 芋道源码
* @version 1.0
* @date 2023/3/10 16:47
* @description
*/
@RestController
@RequestMapping("/rule")
public class RuleDataController {
@PostMapping("/stu")
public Result rule(@RequestBody StuReq stuReq) throws IOException {
KnowledgeService knowledgeService = (KnowledgeService) Utils.getApplicationContext().getBean(KnowledgeService.BEAN_ID);
KnowledgePackage knowledgePackage = knowledgeService.getKnowledge("xxx/xxx");
KnowledgeSession knowledgeSession = KnowledgeSessionFactory.newKnowledgeSession(knowledgePackage);
Stu stu = BeanUtil.copyProperties(stuReq, Stu.class);
knowledgeSession.insert(stu);
knowledgeSession.fireRules();
return Result.success(stu.getTeacher());
}
}
调用接口后,传入的参数一旦符合配置的条件,接口就会返回“那么”分支中配置的结果。

3.3.2 脚本式规则集
脚本式规则集的工作原理与向导式完全一致,只不过它是通过编写脚本来实现规则配置。这里就不作过多展开了。
3.4 决策表
接下来说说决策表,其实可以把它看作是规则集的另一种呈现形式。相比规则集,我个人更偏爱用决策表来配置规则,因为它以二维表格的形式呈现,看起来更加直观,便于理解。但本质上,它和规则集没有什么区别。
这里直接放上一张配置好的决策表截图,一目了然:

3.5 其他
当然,URule 还有其他概念和功能,如交叉决策表、评分卡、复杂评分卡、决策树、规则流等,不过其中部分为 Pro 版的功能。上面介绍的这些已经是最常用到的了,有兴趣的朋友可以自行深入了解。
4. 运用场景
最近在开发一个大版本需求时,就遇到了这样一个场景:每个参与购买订单的用户都有自己对应的职级(也可以理解为角色),目前共有三种角色:普通用户、会员、精英会员。每个月初系统会对用户进行一次晋升处理,普通用户在满足条件后可晋升为会员,会员满足条件后可晋升为精英会员。
不同层级的晋升,触发的规则也不同:
- 普通用户 -> 会员:3 个月内帮注册人数达到 3 人;3 个月内个人及团队下单金额超过 1 万;个人订单继续率超过 80%。
- 会员 -> 精英会员:3 个月内帮注册人数达到 6 人;3 个月内个人及团队下单金额超过 5 万;个人订单继续率超过 90%。
- 不可跨级晋升:普通用户最高只能晋升为会员,之后才能向精英会员晋升。
这只是一个经过简化和略微改动的需求,真实的需求场景要复杂得多。
下面,我通过决策表对这个晋升规则进行配置。在配置前,需要先定义好变量库和常量库:


接着,新建一个决策表,并根据业务需求进行规则配置:

从表格中可以清晰地看到,前四列属于规则条件,最后一列则是满足这些条件后输出的晋升结果。这种表格化的呈现方式非常清晰,哪怕是毫无技术背景的业务人员,也能轻松看懂其中蕴含的规则逻辑。
5. 总结
规则引擎对于我们的系统而言,并非刚性必需品,但它确实能起到锦上添花的作用,帮助我们剥离出业务中大量重复或复杂的判断场景。当然,这种规则的剥离,依赖于开发人员对需求的深刻理解,更需要在理解的基础上对抽象概念进行具象化。这何尝不是整个编程道路上的一条必经之路呢?
在企业级应用的复杂规则场景中,轮子的设计和选型往往是项目成败的关键。如果你也对 开源实战 中的项目选型与源码分析感兴趣,不妨在云栈社区参与更多技术思想的碰撞。