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 设计层 可读性优先 · 最小表达力 · 可组合性 配置格式层 JSON(高性能) · XML(复杂场景) · YAML(可读性) 规则解析器 · Schema校验 · 版本管理 脚本集成层 Groovy(灵活 · 沙箱隔离) · SpEL(安全 · 高性能) 上下文绑定 · 超时控制 · 资源限制 规则执行引擎 AST编译 · 缓存优化 · 并行执行 抽象程度递减 · 执行效率递增

从这张图你能看到,规则定义语言不是单一的技术选型,而是一个分层体系。顶层DSL负责「好写」,中间配置格式负责「好存」,脚本集成负责「好扩展」,底层引擎负责「好快」。每一层都有各自的关注点,不能混为一谈。

总结一下我的经验:

  • 80%的规则用YAML配置就够了,别动不动就上脚本
  • 脚本只用来处理那20%的复杂场景
  • 不管用什么格式,一定要有版本管理和灰度发布机制
  • 规则引擎的性能瓶颈往往不在语言本身,而在解析和编译的开销——记得加缓存

嗯,规则定义语言这块就聊到这儿。说白了,没有银弹。你得根据团队的技术栈、业务复杂度、性能要求来选。但有一条铁律:规则是写给业务看的,不是写给机器看的。记住了这条,设计就不会跑偏。


公众号:蓝海资料掘金营,微信deep3321