3. 规则定义语言设计:DSL设计原则、JSON/XML/YAML配置规则、自定义脚本语言(Groovy/SpEL)集成
规则引擎的核心,说白了就是「怎么把业务规则写清楚、跑得快」。我做了这么多年风控,见过太多团队在规则表达上栽跟头。有的用硬编码,改一条规则要重启服务;有的用Excel,版本管理一团糟。今天咱们聊聊规则定义语言的设计,这是整个引擎的「地基」。
3.1 DSL设计原则:别让业务同学看不懂
DSL,领域特定语言。听起来高大上,其实就一个目标:让懂业务的人能直接写规则。我见过最失败的DSL,是技术同学自己嗨,写出来的东西跟天书一样。业务同学看一眼就跑了,最后还是技术来写——那要DSL干嘛?
核心原则就三条:
- 可读性优先:规则文本应该接近自然语言。比如
if amount > 10000 then reject,而不是if(a.gt(b, 10000)) - 最小表达力:只提供业务需要的操作符和函数,别把编程语言那套全搬过来
- 可组合性:规则之间能嵌套、能引用,别搞成孤岛
举个例子。我之前在金融项目里设计过一个简单的DSL:
rule "大额交易拦截" {
when {
transaction.amount > 50000 &&
transaction.channel == "online" &&
user.risk_level == "high"
}
then {
action: "reject"
reason: "大额高风险交易"
}
}
你看,业务同学一看就懂。不需要知道什么是AST,什么是解释器。这就是DSL的价值。
我的经验:DSL设计初期,一定要拉上业务同学一起评审。让他们读一遍规则,看能不能理解。我曾经设计过一个DSL,自认为很优雅,结果业务同学说「这括号太多了,我眼晕」——嗯,后来我改成了缩进风格。
3.2 JSON/XML/YAML配置规则:三种格式怎么选?
这三种格式,我全用过。各有各的坑,也各有各的好。咱们一个一个说。
JSON:最通用,但别写太深
JSON是风控系统里最常见的规则格式。为什么?因为解析快,生态好,几乎所有语言都原生支持。我在项目中用JSON定义规则,大概长这样:
{
"ruleId": "R001",
"name": "高频交易检测",
"condition": {
"operator": "and",
"children": [
{ "field": "transaction.count", "op": "gt", "value": 10 },
{ "field": "transaction.timeWindow", "op": "lte", "value": 60 }
]
},
"action": {
"type": "block",
"duration": 3600
}
}
JSON的问题是什么?没有注释。你想想看,一条复杂的规则,几百行JSON,没有注释,三个月后你自己都看不懂。我的做法是:在JSON里加一个 description 字段,写清楚这条规则是干嘛的。
避坑指南:JSON嵌套层级不要太深。我曾经接手过一个项目,规则嵌套了8层,解析的时候递归栈都爆了。后来我规定:最多嵌套3层,超过的必须拆成子规则。
XML:啰嗦,但适合复杂场景
XML现在用得少了,但有些场景它确实合适。比如规则需要带元数据、需要命名空间、需要Schema校验。XML的标签结构天然适合表达树形规则。
<rule id="R002" name="设备指纹校验">
<condition operator="or">
<condition operator="and">
<test field="device.fingerprint" op="is_null" />
<test field="user.risk_level" op="eq" value="high" />
</condition>
<test field="device.emulator" op="eq" value="true" />
</condition>
<action type="review" priority="high" />
</rule>
XML的好处是自带注释,而且可以用XPath做规则查询。但缺点也很明显——太啰嗦了。写一条规则,一半都是标签。我个人现在只在需要强Schema校验的场景用XML,比如规则模板的版本管理。
YAML:简洁,但小心缩进
YAML是我现在最常用的规则格式。为什么?因为它可读性最好。同样的规则,YAML写出来比JSON少一半字符:
rule:
id: R003
name: 异地登录检测
condition:
operator: and
children:
- field: login.ip_city
op: neq
value: user.registered_city
- field: login.time
op: between
value: ["02:00", "05:00"]
action:
type: challenge
channel: sms
YAML支持注释,支持锚点引用,非常适合写复杂的规则配置。但有个大坑——缩进敏感。我曾经因为一个空格没对齐,排查了整整两个小时。后来我定了个规矩:所有YAML规则必须用4个空格缩进,禁止用Tab。
| 格式 | 可读性 | 解析性能 | 注释支持 | 适用场景 |
|---|---|---|---|---|
| JSON | 中等 | 高 | 不支持 | 高性能场景、API交互 |
| XML | 低 | 中等 | 支持 | 复杂规则、Schema校验 |
| YAML | 高 | 中等 | 支持 | 配置管理、人工编写 |
3.3 自定义脚本语言集成:Groovy和SpEL
配置规则总有不够用的时候。比如你需要写一段复杂的计算逻辑,或者调用外部服务。这时候就需要脚本语言了。我主要用两种:Groovy和SpEL。
Groovy:灵活,但要管好沙箱
Groovy和Java无缝集成,语法又灵活。我在规则引擎里集成Groovy,主要是为了处理那些「配置规则搞不定」的场景。比如:
// Groovy脚本:计算用户综合风险分
def riskScore = 0
if (user.orderCount > 10) {
riskScore += 20
}
if (user.returnRate > 0.3) {
riskScore += 30
}
// 调用外部黑名单服务
def blacklistResult = blacklistService.check(user.phone)
if (blacklistResult.hit) {
riskScore += 50
}
return riskScore
我曾经踩过的坑:Groovy默认没有沙箱,用户写的脚本可以执行任何Java代码。有一次业务同学在脚本里写了 System.exit(0),整个服务挂了。后来我强制开启了Groovy的沙箱模式,只允许调用白名单里的类和方法。
集成Groovy的代码也很简单:
import groovy.lang.GroovyShell;
import groovy.lang.Binding;
Binding binding = new Binding();
binding.setVariable("user", userData);
binding.setVariable("transaction", transactionData);
GroovyShell shell = new GroovyShell(binding);
Object result = shell.evaluate(scriptContent);
SpEL:Spring生态的首选
如果你的系统用了Spring,SpEL(Spring Expression Language)是个好选择。它比Groovy轻量,语法也更安全。SpEL天然支持属性访问、方法调用、集合操作:
// SpEL表达式示例
// 判断用户是否在VIP名单中
#user.vipLevel >= 3 && #transaction.amount < 10000
// 计算折扣
#transaction.amount * (#user.memberDiscount ?: 1.0)
// 集合操作
#blacklist.contains(#user.phone) ? 'reject' : 'pass'
SpEL的好处是天生安全——它不能执行任意代码,只能访问你暴露的上下文变量。而且SpEL支持编译模式,性能比Groovy好不少。
我的建议:如果只是简单的条件判断和计算,用SpEL就够了。如果需要复杂的流程控制(循环、异常处理),那就上Groovy。但不管用哪个,一定要做三件事:限制执行时间、限制资源消耗、记录执行日志。
3.4 规则语言设计架构图
下面这张图展示了规则定义语言的整体架构。从顶层DSL到底层执行引擎,每一层都有明确的职责:
从这张图你能看到,规则定义语言不是单一的技术选型,而是一个分层体系。顶层DSL负责「好写」,中间配置格式负责「好存」,脚本集成负责「好扩展」,底层引擎负责「好快」。每一层都有各自的关注点,不能混为一谈。
总结一下我的经验:
- 80%的规则用YAML配置就够了,别动不动就上脚本
- 脚本只用来处理那20%的复杂场景
- 不管用什么格式,一定要有版本管理和灰度发布机制
- 规则引擎的性能瓶颈往往不在语言本身,而在解析和编译的开销——记得加缓存
嗯,规则定义语言这块就聊到这儿。说白了,没有银弹。你得根据团队的技术栈、业务复杂度、性能要求来选。但有一条铁律:规则是写给业务看的,不是写给机器看的。记住了这条,设计就不会跑偏。
公众号:蓝海资料掘金营,微信deep3321