15-A-解释器模式详解
关键词:解释器、文法树、表达式求值、营销规则、规则引擎、QLExpress、Aviator
解释器模式解决一个问题:给一种"小语言"定义文法,并把表达式解析成可求值的语法树。营销规则"满100减20 AND 会员"、搜索语法"手机 -二手 价格<3000",都是小语言。本篇先讲清解释器模式本身,再诚实回答"生产上到底该不该手写"。
📖 术语速查表
| 术语 | 人话解释 |
|---|---|
| 文法(Grammar) | 语言的规则:什么符号能怎么组合 |
| 抽象表达式(Expression) | 语法树节点的公共接口,核心方法interpret() |
| 终结符表达式 | 叶子节点,如数字、变量(amount > 100) |
| 非终结符表达式 | 枝干节点,如 AND/OR,组合子表达式 |
| 上下文(Context) | 求值时需要的变量环境(订单金额、用户等级) |
| 规则引擎 | 解释器思想的工程化产品:QLExpress/Aviator/Drools |
一、业务场景:营销优惠规则表达式
运营在后台配置优惠条件,要求支持:
amount >= 100 AND vipLevel >= 3 amount >= 200 OR (vipLevel >= 5 AND orderCount >= 10)如果每种组合都写 Java 代码,运营每改一次规则就要发一次版——规则必须变成"数据"(字符串),运行时解析求值,这就是解释器模式的地盘。
二、模式结构
非终结符(AND/OR)持有子表达式——语法树本身就是组合模式(见 08-A 的规则组),解释器 = 组合模式 +interpret()求值语义。
三、完整实现:迷你规则解释器
抽象表达式与上下文
/** 求值上下文:变量环境 */publicclassRuleContext{privatefinalMap<String,Object>vars=newHashMap<>();publicRuleContextset(Stringkey,Objectvalue){vars.put(key,value);returnthis;}publicBigDecimalgetNumber(Stringkey){Objectv=vars.get(key);if(v==null)thrownewIllegalArgumentException("规则变量缺失: "+key);returnnewBigDecimal(v.toString());}}/** 抽象表达式:语法树节点接口 */publicinterfaceRuleExpression{booleaninterpret(RuleContextctx);}终结符表达式:比较条件
/** amount >= 100 这类原子条件 */publicclassCompareExpressionimplementsRuleExpression{privatefinalStringvarName;// amountprivatefinalStringoperator;// >= <= == > <privatefinalBigDecimalvalue;// 100publicCompareExpression(StringvarName,Stringoperator,BigDecimalvalue){this.varName=varName;this.operator=operator;this.value=value;}@Overridepublicbooleaninterpret(RuleContextctx){BigDecimalactual=ctx.getNumber(varName);intcmp=actual.compareTo(value);returnswitch(operator){case">="->cmp>=0;case"<="->cmp<=0;case">"->cmp>0;case"<"->cmp<0;case"=="->cmp==0;default->thrownewIllegalArgumentException("不支持的操作符: "+operator);};}}非终结符表达式:AND / OR
publicclassAndExpressionimplementsRuleExpression{privatefinalRuleExpressionleft;privatefinalRuleExpressionright;publicAndExpression(RuleExpressionleft,RuleExpressionright){this.left=left;this.right=right;}@Overridepublicbooleaninterpret(RuleContextctx){// 短路求值:左边 false 就不用算右边returnleft.interpret(ctx)&&right.interpret(ctx);}}publicclassOrExpressionimplementsRuleExpression{privatefinalRuleExpressionleft;privatefinalRuleExpressionright;publicOrExpression(RuleExpressionleft,RuleExpressionright){this.left=left;this.right=right;}@Overridepublicbooleaninterpret(RuleContextctx){returnleft.interpret(ctx)||right.interpret(ctx);}}解析器:字符串 → 语法树
/** 极简解析器:支持 "cond AND cond"、"cond OR cond"、括号嵌套 */publicclassRuleParser{privatefinalList<String>tokens;privateintpos;publicRuleParser(Stringrule){// 分词:按空格切,括号单独成 token(生产要用真正的词法分析)this.tokens=tokenize(rule);}publicRuleExpressionparse(){RuleExpressionexpr=parseOr();if(pos<tokens.size()){thrownewIllegalArgumentException("规则解析失败,多余 token: "+tokens.get(pos));}returnexpr;}/** 优先级:OR 最低 → AND → 括号/原子条件(递归下降解析) */privateRuleExpressionparseOr(){RuleExpressionleft=parseAnd();while(pos<tokens.size()&&"OR".equals(tokens.get(pos))){pos++;left=newOrExpression(left,parseAnd());}returnleft;}privateRuleExpressionparseAnd(){RuleExpressionleft=parsePrimary();while(pos<tokens.size()&&"AND".equals(tokens.get(pos))){pos++;left=newAndExpression(left,parsePrimary());}returnleft;}privateRuleExpressionparsePrimary(){Stringtoken=tokens.get(pos);if("(".equals(token)){pos++;RuleExpressionexpr=parseOr();expect(")");returnexpr;}// 原子条件:amount >= 100(三个 token)Stringvar=token;Stringop=tokens.get(pos+1);BigDecimalval=newBigDecimal(tokens.get(pos+2));pos+=3;returnnewCompareExpression(var,op,val);}privatevoidexpect(Stringt){if(!t.equals(tokens.get(pos))){thrownewIllegalArgumentException("期望 "+t+" 但遇到 "+tokens.get(pos));}pos++;}privateList<String>tokenize(Stringrule){// 简化版分词,生产建议用 ANTLR 或成熟规则引擎returnArrays.stream(rule.replaceAll("([()])"," $1 ").split("\\s+")).filter(s->!s.isEmpty()).toList();}}使用方
publicclassPromotionService{/** 规则树缓存:同一规则字符串只解析一次(解析比求值贵得多) */privatefinalCache<String,RuleExpression>ruleCache=Caffeine.newBuilder().maximumSize(1000).build();publicbooleanhitPromotion(StringruleText,Orderorder){// (1) 解析(带缓存)RuleExpressiontree=ruleCache.get(ruleText,k->newRuleParser(k).parse());// (2) 构造上下文RuleContextctx=newRuleContext().set("amount",order.getAmount()).set("vipLevel",order.getVipLevel()).set("orderCount",order.getUserOrderCount());// (3) 求值returntree.interpret(ctx);}}// 运营配置的规则字符串Stringrule="amount >= 100 AND ( vipLevel >= 3 OR orderCount >= 10 )";booleanhit=promotionService.hitPromotion(rule,order);四、诚实的建议:生产上别手写解释器
手写解释器的完整成本清单:
- 词法分析(tokenize)要处理引号、转义、中文;
- 语法分析要处理优先级、结合性、括号嵌套;
- 错误提示要能告诉运营"第几个字符写错了";
- 安全:表达式注入、死循环、超大数运算;
- 性能:解析缓存、求值优化。
成熟替代品:
| 方案 | 特点 | 适用 |
|---|---|---|
| Aviator | 轻量、高性能、编译成字节码 | 规则求值首选 |
| QLExpress(阿里) | 支持自定义操作符、宏 | 电商营销规则(淘宝同款) |
| MVEL / SpEL | Spring 生态内置(SpEL) | 简单表达式、注解里用 |
| Drools | 完整规则引擎(RETE 算法) | 规则数量巨大、需要规则流 |
| ANTLR | 语法解析器生成器 | 真要自定义一门语言 |
// Aviator 版:三行顶上面三百行Expressionexp=AviatorEvaluator.compile(ruleText,true);// true = 编译缓存Booleanhit=(Boolean)exp.execute(Map.of("amount",order.getAmount(),"vipLevel",order.getVipLevel()));什么时候才手写:文法极简且固定(如只支持AND连接的等值匹配)、不想引依赖、或教学目的。面试讲解释器模式,重点讲"文法树 + interpret 递归求值"的思想,然后主动说"生产我会用 Aviator/QLExpress"——这是加分而不是减分。
五、框架中的同款实现
| 框架 | 应用 | 说明 |
|---|---|---|
| JDK | java.util.regex.Pattern | 正则就是小语言,Pattern 内部是语法树 |
| Spring | SpEL(ExpressionParser) | @Value("#{...}")、Security 的@PreAuthorize |
| MyBatis | 动态 SQL 的 OGNL 表达式 | <if test="status != null">里的 test |
| SQL 引擎 | 任何 SQL 解析器(Calcite/Druid Parser) | SQL 是最大的"小语言",解析→AST→执行 |
| Drools/Aviator/QLExpress | 规则引擎 | 解释器思想的工程化 |
六、常见误用与生产事故
误用:用解释器模式做"配置化"
只有两三种固定规则形态,却上了完整解释器——维护成本远大于收益。规则形态固定时用策略模式 + 配置参数(见 21-A):策略选逻辑,配置传参数,比自由文法可控得多。
事故一:规则解析无缓存,大促 CPU 打满
每次请求都new RuleParser(rule).parse()——解析(字符串切分 + 建树)比求值贵 50 倍。大促 QPS 上来后 CPU 100%,RT 飙升。
修复:规则树缓存(规则字符串 → 编译好的 Expression),Caffeine 带上限;规则变更时主动失效。"编译一次,执行多次"是所有表达式引擎的铁律。
事故二:运营配错规则,全量用户命中 0 元购
规则字符串amount >= 0(本想写amount >= 100,少打两个字符)——所有订单都命中"满 0 减 50"。
修复:
- 规则保存时语法校验 + 语义预检(用样例订单试算,展示命中结果让运营确认);
- 优惠金额上限硬编码兜底(单笔优惠 ≤ 订单金额的 50%,超出拒绝);
- 规则变更走审批 + 灰度(先 1% 流量验证命中率分布);
- 命中率突变告警(某规则命中率从 5% 跳到 90% 立刻通知)——规则是"改一个字符影响全站"的高危配置(同款教训见 09-04-B-监控告警体系面试与生产事故实战 的配置变更事故)。
七、总结
- 解释器的核心:为小语言定义文法,表达式解析成语法树,
interpret()递归求值。 - 结构本质:语法树 = 组合模式;非终结符组合终结符,求值就是树的递归遍历。
- 生产建议:文法复杂或会演化 → 直接用 Aviator/QLExpress/SpEL;手写只适合极简固定文法。
- 两条铁律:编译一次执行多次(解析必须缓存);规则是高危配置(校验 + 兜底 + 灰度 + 命中率告警)。
- 面试姿势:讲清"文法树 + 递归求值"思想,主动说明生产用规则引擎替代——展示的是判断力。
📌上一篇:14-A-命令模式详解
📌下一篇:16-A-迭代器模式详解