☰
15-A-解释器模式详解
2026/10/18 1:56:24 网站建设 项目流程

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 代码,运营每改一次规则就要发一次版——规则必须变成"数据"(字符串),运行时解析求值,这就是解释器模式的地盘。


二、模式结构

组合(左右子表达式)

组合(左右子表达式)

Expression 接口
interpret(ctx)

AndExpression
非终结符

OrExpression
非终结符

CompareExpression
终结符
amount >= 100

非终结符(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);

四、诚实的建议:生产上别手写解释器

手写解释器的完整成本清单:

  1. 词法分析(tokenize)要处理引号、转义、中文;
  2. 语法分析要处理优先级、结合性、括号嵌套;
  3. 错误提示要能告诉运营"第几个字符写错了";
  4. 安全:表达式注入、死循环、超大数运算;
  5. 性能:解析缓存、求值优化。

成熟替代品:

方案特点适用
Aviator轻量、高性能、编译成字节码规则求值首选
QLExpress(阿里)支持自定义操作符、宏电商营销规则(淘宝同款)
MVEL / SpELSpring 生态内置(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"——这是加分而不是减分。


五、框架中的同款实现

框架应用说明
JDKjava.util.regex.Pattern正则就是小语言,Pattern 内部是语法树
SpringSpEL(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"。

修复:

  1. 规则保存时语法校验 + 语义预检(用样例订单试算,展示命中结果让运营确认);
  2. 优惠金额上限硬编码兜底(单笔优惠 ≤ 订单金额的 50%,超出拒绝);
  3. 规则变更走审批 + 灰度(先 1% 流量验证命中率分布);
  4. 命中率突变告警(某规则命中率从 5% 跳到 90% 立刻通知)——规则是"改一个字符影响全站"的高危配置(同款教训见 09-04-B-监控告警体系面试与生产事故实战 的配置变更事故)。

七、总结

  1. 解释器的核心:为小语言定义文法,表达式解析成语法树,interpret()递归求值。
  2. 结构本质:语法树 = 组合模式;非终结符组合终结符,求值就是树的递归遍历。
  3. 生产建议:文法复杂或会演化 → 直接用 Aviator/QLExpress/SpEL;手写只适合极简固定文法。
  4. 两条铁律:编译一次执行多次(解析必须缓存);规则是高危配置(校验 + 兜底 + 灰度 + 命中率告警)。
  5. 面试姿势:讲清"文法树 + 递归求值"思想,主动说明生产用规则引擎替代——展示的是判断力。

📌上一篇:14-A-命令模式详解

📌下一篇:16-A-迭代器模式详解

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询