QLExpress是阿里巴巴开源的一款轻量级Java动态脚本引擎,专门设计用于处理需要动态逻辑的场景。作为一名长期使用Java进行企业级开发的工程师,我发现这类动态脚本引擎在实际项目中能解决很多棘手的问题。比如当业务规则频繁变更时,传统的硬编码方式往往需要重新部署应用,而使用QLExpress则可以实现规则的动态加载和执行。
这个引擎的核心优势在于其轻量级特性和高性能表现。它不依赖任何第三方库,整个jar包只有几百KB大小,却能提供完整的脚本解析和执行能力。在我的项目经验中,QLExpress的脚本执行速度可以达到原生Java代码的1/10到1/5,这对于大多数业务场景来说已经足够高效。
要在项目中使用QLExpress,首先需要添加Maven依赖。目前最新稳定版本是3.3.4,这也是我在生产环境中验证过的版本:
xml复制<dependency>
<groupId>com.alibaba</groupId>
<artifactId>QLExpress</artifactId>
<version>3.3.4</version>
</dependency>
如果你使用的是Gradle,可以这样配置:
groovy复制implementation 'com.alibaba:QLExpress:3.3.4'
QLExpress的核心类是ExpressRunner,它负责脚本的编译和执行。创建实例时可以通过构造函数参数配置一些重要选项:
java复制// 基本配置
ExpressRunner runner = new ExpressRunner();
// 带参数的配置(推荐)
ExpressRunner runner = new ExpressRunner(true, false);
// 第一个参数:是否开启语法检查
// 第二个参数:是否开启缓存(对性能敏感场景建议开启)
另一个重要类是DefaultContext,用于向脚本传递参数。它本质上是一个Map结构,但做了一些特定于QLExpress的优化:
java复制DefaultContext<String, Object> context = new DefaultContext<>();
context.put("a", 10);
context.put("b", 20);
有了runner和context,就可以执行简单的脚本了:
java复制Object result = runner.execute("a + b", context, null, true, false);
System.out.println(result); // 输出30
execute方法的参数说明:
在实际业务中,我们经常需要在脚本中调用特定的业务逻辑。QLExpress提供了多种方式来实现这一点。
java复制runner.addFunction("isAdult", new Operator() {
@Override
public Object executeInner(Object[] list) {
Integer age = (Integer) list[0];
return age != null && age >= 18;
}
});
// 脚本中使用
Object result = runner.execute("isAdult(20)", context, null, true, false);
// 返回true
java复制// 假设我们有一个StringUtil工具类
public class StringUtil {
public static boolean isEmpty(String str) {
return str == null || str.trim().isEmpty();
}
}
// 注入静态方法
runner.addFunctionOfClassMethod("isEmpty",
StringUtil.class.getName(),
"isEmpty",
new Class[]{String.class},
null);
// 脚本中使用
Object result = runner.execute("isEmpty('')", context, null, true, false);
// 返回true
对于频繁使用的复杂表达式,可以定义为宏来简化脚本编写:
java复制runner.addMacro("高风险交易", "amount > 10000 && (isForeign || isHighRiskCountry)");
定义后,在脚本中可以直接使用"高风险交易"这个宏:
java复制context.put("amount", 15000);
context.put("isForeign", true);
context.put("isHighRiskCountry", false);
Object result = runner.execute("高风险交易", context, null, true, false);
// 返回true
当脚本可能来自外部输入时,安全控制就变得至关重要。QLExpress提供了沙箱模式来限制脚本的能力。
java复制// 开启沙箱模式
QLExpressRunStrategy.setSandBoxMode(true);
// 设置白名单
QLExpressRunStrategy.addSecureMethod(SafeService.class, "safeMethod1");
QLExpressRunStrategy.addSecureMethod(SafeService.class, "safeMethod2");
// 现在脚本只能调用白名单中的方法
对于频繁执行的脚本,预编译可以显著提高性能:
java复制String script = "a + b * c";
IExpress<String> compiledExpress = runner.compile(script, null);
// 后续可以重复使用compiledExpress
for(int i=0; i<1000; i++) {
Object result = compiledExpress.execute(context, null, true, false);
}
DefaultContext的创建也是有开销的,在可能的情况下应该复用:
java复制DefaultContext<String, Object> context = new DefaultContext<>();
for(Transaction tx : transactions) {
context.put("amount", tx.getAmount());
context.put("userId", tx.getUserId());
// ...其他参数
Object result = runner.execute(script, context, null, true, false);
context.clear(); // 清空但不销毁
}
QLExpress本身支持缓存,但对于特别高频的场景,可以考虑在外层再加一层缓存:
java复制// 使用Guava Cache做外层缓存
LoadingCache<String, IExpress<String>> scriptCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(new CacheLoader<String, IExpress<String>>() {
public IExpress<String> load(String script) {
return runner.compile(script, null);
}
});
// 使用缓存
IExpress<String> compiled = scriptCache.get(script);
Object result = compiled.execute(context, null, true, false);
下面展示一个完整的风控规则引擎实现,这是我最近在一个电商项目中实际采用的方案。
java复制@Data
public class RiskRule {
private String ruleId; // 规则唯一标识
private String ruleName; // 规则名称
private String expression; // QLExpress脚本
private String riskLevel; // 风险等级
private Integer priority; // 执行优先级
private Boolean isActive; // 是否激活
private Date createTime; // 创建时间
private Date updateTime; // 更新时间
}
java复制@Service
@Slf4j
public class RiskEngineService {
private final ExpressRunner runner;
private final Map<String, IExpress<String>> ruleCache;
public RiskEngineService() {
this.runner = new ExpressRunner(true, true); // 开启语法检查和缓存
this.ruleCache = new ConcurrentHashMap<>();
// 初始化一些公共函数
initCommonFunctions();
}
private void initCommonFunctions() {
runner.addFunction("isWeekend", new Operator() {
@Override
public Object executeInner(Object[] list) {
Calendar cal = Calendar.getInstance();
int day = cal.get(Calendar.DAY_OF_WEEK);
return day == Calendar.SATURDAY || day == Calendar.SUNDAY;
}
});
}
public RiskResult evaluate(Transaction transaction, List<RiskRule> rules) {
DefaultContext<String, Object> context = createContext(transaction);
// 按优先级排序
List<RiskRule> sortedRules = rules.stream()
.filter(RiskRule::getIsActive)
.sorted(Comparator.comparing(RiskRule::getPriority))
.collect(Collectors.toList());
for (RiskRule rule : sortedRules) {
try {
IExpress<String> compiled = ruleCache.computeIfAbsent(
rule.getRuleId(),
id -> runner.compile(rule.getExpression(), null)
);
Boolean isHit = (Boolean) compiled.execute(context, null, true, false);
if (Boolean.TRUE.equals(isHit)) {
return RiskResult.hit(rule.getRiskLevel(), rule.getRuleName());
}
} catch (Exception e) {
log.error("规则执行失败: {}", rule.getRuleId(), e);
// 继续执行下一条规则
}
}
return RiskResult.pass();
}
private DefaultContext<String, Object> createContext(Transaction tx) {
DefaultContext<String, Object> context = new DefaultContext<>();
context.put("amount", tx.getAmount());
context.put("userId", tx.getUserId());
context.put("ip", tx.getIp());
context.put("deviceId", tx.getDeviceId());
// ...其他字段
return context;
}
}
java复制@RestController
@RequestMapping("/api/risk")
@RequiredArgsConstructor
public class RiskController {
private final RiskEngineService riskEngine;
private final RiskRuleRepository ruleRepo;
@PostMapping("/evaluate")
public ResponseEntity<RiskResult> evaluate(@RequestBody Transaction tx) {
List<RiskRule> rules = ruleRepo.findActiveRules();
RiskResult result = riskEngine.evaluate(tx, rules);
return ResponseEntity.ok(result);
}
@GetMapping("/rules")
public ResponseEntity<List<RiskRule>> getAllRules() {
return ResponseEntity.ok(ruleRepo.findAll());
}
}
在实际项目中,我们通常会开发一个规则管理界面,让业务人员可以方便地维护规则。这里给出一个简化的前端实现思路:
javascript复制// React示例
function RuleEditor({ rule, onSave }) {
const [formData, setFormData] = useState(rule);
const handleSubmit = () => {
// 验证规则表达式语法
try {
// 调用后端验证接口
await validateRule(formData.expression);
onSave(formData);
} catch (error) {
alert(`规则语法错误: ${error.message}`);
}
};
return (
<div>
<input
value={formData.ruleName}
onChange={e => setFormData({...formData, ruleName: e.target.value})}
/>
<textarea
value={formData.expression}
onChange={e => setFormData({...formData, expression: e.target.value})}
/>
<button onClick={handleSubmit}>保存</button>
</div>
);
}
当脚本执行出错时,可以按照以下步骤排查:
java复制runner.setIsTrace(true); // 开启执行轨迹跟踪
QLCompileException:编译时错误,通常是语法问题QLExecuteException:运行时错误,可能是类型不匹配等问题QLTimeOutException:执行超时java复制// 类型不匹配
context.put("a", "10"); // 字符串
Object result = runner.execute("a * 2", context, null, true, false);
// 会抛出异常,因为不能对字符串做乘法
// 解决方案:
context.put("a", 10); // 改为数字
// 或者修改脚本:Integer.parseInt(a) * 2
如果发现脚本执行变慢,可以考虑:
java复制// 获取缓存统计信息
ExpressCache stats = runner.getExpressCache();
log.info("缓存命中率: {}", stats.getHitRate());
java复制// 设置超时时间(毫秒)
runner.setExecuteTimeout(100);
try {
Object result = runner.execute(script, context, null, true, false);
} catch (QLTimeOutException e) {
// 处理超时
}
java复制// 验证脚本是否包含危险关键字
public boolean isScriptSafe(String script) {
String[] blacklist = {"System", "Runtime", "Process", "Class", "JNI"};
for (String keyword : blacklist) {
if (script.contains(keyword)) {
return false;
}
}
return true;
}
java复制// 记录所有脚本执行情况
public Object executeWithAudit(String script, DefaultContext context) {
log.info("执行脚本: {}, 参数: {}", script, context.keySet());
try {
Object result = runner.execute(script, context, null, true, false);
log.info("执行结果: {}", result);
return result;
} catch (Exception e) {
log.error("脚本执行失败", e);
throw e;
}
}
QLExpress不仅适用于风控系统,还可以应用于以下场景:
java复制// 定价规则示例
String priceRule = "if (isVIP) {
basePrice * 0.9
} else if (quantity > 10) {
basePrice * 0.95
} else {
basePrice
}";
context.put("basePrice", 100);
context.put("isVIP", true);
context.put("quantity", 5);
Object price = runner.execute(priceRule, context, null, true, false);
// 返回90
java复制// 活动参与条件
String condition = "age >= 18 &&
(region == '华东' || region == '华北') &&
lastLoginDays < 30";
context.put("age", 20);
context.put("region", "华东");
context.put("lastLoginDays", 15);
Object qualified = runner.execute(condition, context, null, true, false);
// 返回true
java复制// 工作流分支条件
String routeCondition = "amount > 10000 ? '高层审批' :
department == '财务' ? '财务总监审批' :
'部门经理审批'";
context.put("amount", 5000);
context.put("department", "市场");
Object approver = runner.execute(routeCondition, context, null, true, false);
// 返回"部门经理审批"
在选择规则引擎时,QLExpress通常不是唯一选择。下面是与常见替代方案的对比:
| 特性 | QLExpress | Drools | EasyRules |
|---|---|---|---|
| 学习曲线 | 低 | 高 | 中 |
| 性能 | 高 | 中 | 中 |
| 功能丰富度 | 中 | 高 | 低 |
| 动态加载能力 | 强 | 弱 | 中 |
| 适合场景 | 简单规则 | 复杂业务规则 | 简单业务规则 |
| 社区支持 | 中 | 强 | 弱 |
选择建议:
经过多个项目的实践,我总结了以下使用QLExpress的最佳实践:
在实际项目中,我们团队通过遵循这些实践,成功将QLExpress应用到了风控、定价、促销等多个系统中,大大提高了业务的灵活性。特别是在618、双11等大促期间,业务团队可以实时调整规则而不需要开发介入,显著缩短了响应时间。