本文目录导读:

- 案例一:基础入门(Maven 依赖 & Hello World)
- 案例二:业务逻辑场景(电商价格计算)
- 案例三:规则引擎实战(风控系统)
- 案例四:高级特性(函数定义 & 宏 & 依赖注入)
- 案例五:性能优化与安全(编译缓存 & 沙箱)
- 总结:QLExpress 适用场景与最佳实践
QLExpress 是阿里巴巴开源的一款轻量级、高性能的规则引擎,主要用于解决业务规则复杂多变、灵活配置的问题,它采用 Java 语法(近似)编写,支持脚本化动态编译执行。
下面我将通过几个从简单到复杂的典型案例,展示 QLExpress 的核心用法、语法特性和实际业务场景。
基础入门(Maven 依赖 & Hello World)
这是最基础的用法,演示如何引入依赖并执行一个简单表达式。
引入 Maven 依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>QLExpress</artifactId>
<version>3.3.1</version> <!-- 请使用最新稳定版本 -->
</dependency>
基础执行代码
import com.ql.util.express.ExpressRunner;
import com.ql.util.express.DefaultContext;
public class HelloWorld {
public static void main(String[] args) throws Exception {
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
// 执行简单的数学运算
Object r1 = runner.execute("1 + 2 * 3", context, null, true, false);
System.out.println("Result: " + r1); // 输出 7
// 带变量的表达式
context.put("a", 10);
context.put("b", 5);
Object r2 = runner.execute("a > b ? 'a更大' : 'b更大'", context, null, true, false);
System.out.println("Result: " + r2); // 输出 a更大
}
}
业务逻辑场景(电商价格计算)
这是最常见的业务场景——根据规则计算商品价格,包含条件分支、循环和方法调用。
业务规则:
- 普通会员:原价 9 折
- VIP 会员:原价 7 折,且满 500 减 50
- 如果商品库存不足,提示错误
import com.ql.util.express.ExpressRunner;
import com.ql.util.express.DefaultContext;
public class PriceCalculator {
// 定义一个业务方法,供表达式调用
public static double calculateDiscount(int level, double price) {
if (level == 1) return price * 0.9;
if (level == 2) return price * 0.7;
return price;
}
public static void main(String[] args) throws Exception {
ExpressRunner runner = new ExpressRunner();
// 将 Java 方法注入到表达式中
runner.addFunction("calcDiscount", new com.ql.util.express.Operator() {
@Override
public Object executeInner(Object[] list) throws Exception {
int level = (Integer) list[0];
double price = (Double) list[1];
return calculateDiscount(level, price);
}
});
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
context.put("memberLevel", 2); // VIP
context.put("goodsPrice", 800.0);
context.put("stock", 10);
// 复杂表达式:包含条件、方法调用
String expression =
"if (stock < 1) { return '库存不足'; }" +
"else { " +
" double finalPrice = calcDiscount(memberLevel, goodsPrice);" +
" if (memberLevel == 2 && finalPrice >= 500) {" +
" finalPrice = finalPrice - 50;" +
" }" +
" return '应付金额: ' + finalPrice;" +
"}";
Object result = runner.execute(expression, context, null, true, false);
System.out.println(result); // 输出: 应付金额: 510.0 (800*0.7 - 50)
}
}
规则引擎实战(风控系统)
这是 QLExpress 最核心的使用场景——动态下发风控规则,无需重启应用即可快速调整规则。
场景:根据用户行为(登录地域、金额、频次)判断是否存在风险。
核心代码(规则加载与执行):
package com.example.rule;
import com.ql.util.express.ExpressRunner;
import com.ql.util.express.DefaultContext;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class RiskControlEngine {
// 缓存规则,key为规则ID,value为规则表达式
private static Map<String, String> ruleCache = new ConcurrentHashMap<>();
private ExpressRunner runner = new ExpressRunner();
// 动态更新规则(通过后台管理系统调用)
public void updateRule(String ruleId, String expression) {
ruleCache.put(ruleId, expression);
System.out.println("规则已更新: " + ruleId);
}
// 执行风控检查
public boolean checkRisk(String ruleId, Map<String, Object> params) throws Exception {
String expression = ruleCache.get(ruleId);
if (expression == null) {
throw new IllegalArgumentException("规则不存在: " + ruleId);
}
DefaultContext<String, Object> context = new DefaultContext<>();
// 将请求参数注入上下文
params.forEach(context::put);
Object result = runner.execute(expression, context, null, true, false);
// 结果转为 boolean
return result instanceof Boolean && (Boolean) result;
}
public static void main(String[] args) throws Exception {
RiskControlEngine engine = new RiskControlEngine();
// 初始规则:如果交易金额 > 5000 且 新用户 且 IP 不在白名单,则风控
String rule1 =
"amount > 5000 && isNewUser && !whiteList.contains(ip) " +
"&& (loginCount < 3 || lastLoginDays > 30)";
engine.updateRule("RULE_HIGH_TRADE", rule1);
// 模拟请求参数
DefaultContext<String, Object> params1 = new DefaultContext<>();
params1.put("amount", 12000);
params1.put("isNewUser", true);
params1.put("ip", "192.168.1.100");
params1.put("loginCount", 2);
params1.put("lastLoginDays", 45);
params1.put("whiteList", java.util.Arrays.asList("127.0.0.1", "10.0.0.1"));
boolean risk1 = engine.checkRisk("RULE_HIGH_TRADE", params1);
System.out.println("是否触发风控: " + risk1); // 输出 true (金额大、新用户、登录少且间隔久)
// 动态修改规则(不重启应用)
String rule1New =
"amount > 10000 && isNewUser && !whiteList.contains(ip) " +
"&& (loginCount < 5 || lastLoginDays > 15)";
engine.updateRule("RULE_HIGH_TRADE", rule1New);
// 再次检测,规则已更新
boolean risk2 = engine.checkRisk("RULE_HIGH_TRADE", params1);
System.out.println("更新规则后是否触发风控: " + risk2);
// 因为 loginCount(2) < 5 依然为true,所以仍为 true
// 模拟一个业务人员调低阈值
DefaultContext<String, Object> params2 = new DefaultContext<>();
params2.put("amount", 8000);
params2.put("isNewUser", false);
params2.put("ip", "10.0.0.5");
params2.put("loginCount", 10);
params2.put("lastLoginDays", 2);
params2.put("whiteList", java.util.Arrays.asList("127.0.0.1"));
boolean risk3 = engine.checkRisk("RULE_HIGH_TRADE", params2);
System.out.println("低风险用户是否触发: " + risk3); // 输出 false (金额未超10000 且 非新用户)
}
}
高级特性(函数定义 & 宏 & 依赖注入)
QLExpress 支持在脚本中定义函数(function)、使用宏(macro)以及通过 Spring 注入 Bean,以下展示其强大扩展能力。
场景:物流运费计算 + 优惠券逻辑。
import com.ql.util.express.ExpressRunner;
import com.ql.util.express.DefaultContext;
public class AdvancedUsage {
public static void main(String[] args) throws Exception {
ExpressRunner runner = new ExpressRunner();
// 1. 定义一个函数:计算运费(按重量阶梯计价)
runner.addFunction("calcFreight", new com.ql.util.express.Operator() {
@Override
public Object executeInner(Object[] list) throws Exception {
double weight = (Double) list[0];
double region = (Double) list[1]; // 地区系数
double base = (Double) list[2];
// 模拟:首重 1kg 内 base,超出后每kg 加 1.5
if (weight <= 1.0) {
return base * region;
} else {
return (base + (weight - 1.0) * 1.5) * region;
}
}
});
// 2. 定义宏(常量替换)
runner.addMacro("FREE_SHIPPING_THRESHOLD", 199.0);
runner.addMacro("VIP_DISCOUNT", 0.85);
// 3. 模拟 Spring Bean 注入(假设有个优惠券服务)
@SuppressWarnings("unused")
class CouponService {
public double getCouponAmount(double price, int couponId) {
return couponId == 1001 ? 20.0 : (couponId == 1002 ? 50.0 : 0.0);
}
}
Object couponBean = new CouponService() {
public Object executeInner(Object[] list) {
// 简化:直接暴露一个方法
return (Double) getCouponAmount((Double) list[0], (Integer) list[1]);
}
};
runner.addFunction("getCoupon", new com.ql.util.express.Operator() {
@Override
public Object executeInner(Object[] list) throws Exception {
return ((CouponService) list[0]).getCouponAmount((Double) list[1], (Integer) list[2]);
}
});
DefaultContext<String, Object> context = new DefaultContext<>();
context.put("weight", 3.5);
context.put("region", 1.2); // 偏远地区
context.put("prodPrice", 250.0);
context.put("couponId", 1001);
context.put("couponService", couponBean);
// 复杂表达式:使用宏、函数、条件
String expression =
// 如果商品价格达到免邮门槛(199),运费为0,否则计算运费
"freight = prodPrice >= FREE_SHIPPING_THRESHOLD ? 0.0 : calcFreight(weight, region, 10);" +
// 应用 VIP 折扣和优惠券(注意:这里演示宏和函数)
"finalPrice = (prodPrice * VIP_DISCOUNT) - getCoupon(couponService, prodPrice, couponId);" +
// 自动取到变量结果
"total = finalPrice + freight;" +
"return '原价: ' + prodPrice + ', 折后: ' + finalPrice + ', 运费: ' + freight + ', 总价: ' + total;";
Object result = runner.execute(expression, context, null, true, false);
System.out.println(result);
// 输出: 原价: 250.0, 折后: 192.5, 运费: 0.0, 总价: 192.5
}
}
性能优化与安全(编译缓存 & 沙箱)
在生产环境中,我们通常希望提前编译表达式并限制脚本能力以保证性能和安全。
import com.ql.util.express.ExpressRunner;
import com.ql.util.express.DefaultContext;
import java.util.ArrayList;
import java.util.List;
public class ProductionReady {
public static void main(String[] args) throws Exception {
// 开启沙箱模式,禁止某些危险操作(如 System.exit, Runtime.exec 等)
ExpressRunner runner = new ExpressRunner(true, true);
// 预编译表达式(提高性能)
String expression = "name == 'Alice' ? '欢迎' : '拒绝'";
com.ql.util.express.InstructionSet instructionSet = runner.parseInstructionSet(expression);
// 多线程池化使用
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int num = i;
Thread t = new Thread(() -> {
try {
DefaultContext<String, Object> context = new DefaultContext<>();
context.put("name", num % 2 == 0 ? "Alice" : "Bob");
// 使用编译好的指令集,避免重复解析
Object result = runner.executeInstructionSet(instructionSet, context, null);
System.out.println(Thread.currentThread().getName() + ": " + result);
} catch (Exception e) {
e.printStackTrace();
}
});
threads.add(t);
t.start();
}
for (Thread t : threads) t.join();
}
}
QLExpress 适用场景与最佳实践
| 场景 | 是否推荐 | 说明 |
|---|---|---|
| 风控规则 | ✅ 强烈推荐 | 规则变更频繁,动态下发 |
| 营销活动(改折扣/满减) | ✅ 推荐 | 业务人员可配置 |
| 复杂计算引擎 | ✅ 推荐 | 如运费、积分、佣金计算 |
| 数据校验 | ✅ 可选 | 支持复杂条件 |
| 简单 if-else 映射 | ❌ 不推荐 | 直接用 Java 或枚举更简单 |
| 超高性能要求(QPS万级以上) | ⚠️ 谨慎 | 虽然支持预编译,但脚本执行仍有开销 |
最佳实践贴士:
- 预编译:在启动时将常用规则
parseInstructionSet,不要每次执行都解析。 - 白名单:在脚本中限制可调用方法,禁止
Runtime、System、Class等危险类。 - 上下文隔离:每次请求传入独立的
DefaultContext,避免并发数据污染。 - 监控:添加执行耗时日志和异常统计。
通过以上案例,你已经掌握了 QLExpress 从基础语法到生产级应用的核心技巧,希望你项目开发顺利!如果还有细节想了解(比如特定版本差异、SPEL 对比),可以继续交流。