第5章 - SpringBoot + Lua 整合实战 ⭐
嗨,朋友!我是长安。
这一章是整个教程的重头戏!作为 Java 程序员,你最关心的肯定是:怎么在 SpringBoot 项目中使用 Lua? 这一章我就手把手教你。
🤔 为什么要在 Java 中嵌入 Lua?
| 场景 | 传统方案 | Lua 方案 |
|---|---|---|
| 业务规则变更 | 修改 Java 代码 → 编译 → 打包 → 重启 | 修改 Lua 脚本 → 热更新 |
| 营销活动规则 | 硬编码或 Drools | Lua 脚本动态加载 |
| 风控策略 | 重新部署 | Lua 脚本实时更新 |
| 计算公式 | 代码写死 | Lua 脚本可配置 |
核心优势:不用重启服务就能修改业务逻辑!
📦 技术选型
在 Java 中运行 Lua 有几种方案:
| 方案 | 说明 | 推荐度 |
|---|---|---|
| LuaJ | 纯 Java 实现的 Lua 解释器 | ⭐⭐⭐⭐⭐ |
| LuaJava | 通过 JNI 调用原生 Lua | ⭐⭐⭐ |
| javax.script + Luaj | JSR-223 脚本引擎 | ⭐⭐⭐⭐ |
| GraalVM | 多语言运行时 | ⭐⭐⭐ |
本教程使用 LuaJ,它是纯 Java 实现,无需安装原生 Lua,集成最简单。
🚀 快速开始:创建 SpringBoot 项目
1. 添加依赖
<!-- pom.xml -->
<dependencies>
<!-- SpringBoot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- LuaJ - Java 版 Lua 解释器 -->
<dependency>
<groupId>org.luaj</groupId>
<artifactId>luaj-jse</artifactId>
<version>3.0.1</version>
</dependency>
</dependencies>
2. 基本使用:执行 Lua 脚本
import org.luaj.vm2.*;
import org.luaj.vm2.lib.jse.*;
@Service
public class LuaService {
/**
* 执行 Lua 脚本字符串
*/
public String executeLuaScript(String script) {
// 创建 Lua 全局环境
Globals globals = JsePlatform.standardGlobals();
// 执行脚本
LuaValue result = globals.load(script).call();
return result.tojstring();
}
}
// 使用示例
@RestController
@RequestMapping("/api/lua")
public class LuaController {
@Autowired
private LuaService luaService;
@PostMapping("/execute")
public String execute(@RequestBody String script) {
return luaService.executeLuaScript(script);
}
}
# 测试
curl -X POST http://localhost:8080/api/lua/execute \
-H "Content-Type: text/plain" \
-d 'return 1 + 2'
# 返回:3
🔧 Java 调用 Lua 函数
@Service
public class LuaService {
/**
* 调用 Lua 中定义的函数
*/
public Object callLuaFunction(String script, String funcName, Object... args) {
Globals globals = JsePlatform.standardGlobals();
// 加载并执行脚本(定义函数)
globals.load(script).call();
// 获取函数
LuaValue func = globals.get(funcName);
// 转换参数
LuaValue[] luaArgs = new LuaValue[args.length];
for (int i = 0; i < args.length; i++) {
luaArgs[i] = CoerceJavaToLua.coerce(args[i]);
}
// 调用函数
Varargs result = func.invoke(LuaValue.varargsOf(luaArgs));
return result.arg1().tojstring();
}
}
// 使用示例
String luaScript = """
function calculatePrice(basePrice, discount, quantity)
local total = basePrice * quantity
local finalPrice = total * (1 - discount / 100)
return finalPrice
end
""";
Object result = luaService.callLuaFunction(
luaScript, "calculatePrice", 100.0, 20.0, 3
);
System.out.println("最终价格: " + result); // 最终价格: 240.0
🌟 实战场景一:动态规则引擎
这是 Lua + SpringBoot 最常见的应用场景!
Lua 规则脚本
-- discount_rule.lua
-- 折扣计算规则(可以随时修改,不用重启服务!)
function calculateDiscount(userLevel, orderAmount, isHoliday)
local discount = 0
-- VIP 等级折扣
if userLevel == "VIP3" then
discount = 20 -- 8折
elseif userLevel == "VIP2" then
discount = 15 -- 85折
elseif userLevel == "VIP1" then
discount = 10 -- 9折
end
-- 大额订单额外折扣
if orderAmount >= 1000 then
discount = discount + 5
elseif orderAmount >= 500 then
discount = discount + 3
end
-- 节假日活动
if isHoliday then
discount = discount + 10
end
-- 最大折扣不超过 40%
if discount > 40 then
discount = 40
end
return discount
end
Java 端集成
@Service
public class DiscountRuleEngine {
private Globals globals;
@PostConstruct
public void init() {
globals = JsePlatform.standardGlobals();
}
/**
* 从文件加载 Lua 规则脚本
*/
public void loadRule(String scriptPath) {
LuaValue chunk = globals.loadfile(scriptPath);
chunk.call();
}
/**
* 计算折扣
*/
public double calculateDiscount(String userLevel, double orderAmount, boolean isHoliday) {
LuaValue func = globals.get("calculateDiscount");
Varargs result = func.invoke(new LuaValue[]{
LuaValue.valueOf(userLevel),
LuaValue.valueOf(orderAmount),
LuaValue.valueOf(isHoliday)
});
return result.arg1().todouble();
}
/**
* 热更新:重新加载规则(不用重启服务!)
*/
public void reloadRule(String scriptPath) {
globals = JsePlatform.standardGlobals();
loadRule(scriptPath);
}
}
@RestController
@RequestMapping("/api/discount")
public class DiscountController {
@Autowired
private DiscountRuleEngine ruleEngine;
@PostMapping("/calculate")
public Map<String, Object> calculate(
@RequestParam String userLevel,
@RequestParam double orderAmount,
@RequestParam(defaultValue = "false") boolean isHoliday) {
double discount = ruleEngine.calculateDiscount(userLevel, orderAmount, isHoliday);
double finalPrice = orderAmount * (1 - discount / 100);
Map<String, Object> result = new HashMap<>();
result.put("userLevel", userLevel);
result.put("orderAmount", orderAmount);
result.put("discount", discount + "%");
result.put("finalPrice", finalPrice);
return result;
}
@PostMapping("/reload")
public String reload() {
ruleEngine.reloadRule("scripts/discount_rule.lua");
return "规则已热更新!";
}
}
热更新的威力
当运营说"双十一要临时加大折扣力度"时,你只需要修改 Lua 文件,调用 /reload 接口,零重启完成规则变更!
🌟 实战场景二:动态数据校验
-- validation_rule.lua
-- 数据校验规则
function validateUser(data)
local errors = {}
-- 用户名校验
if not data.username or #data.username < 3 then
table.insert(errors, "用户名至少3个字符")
end
if #data.username > 20 then
table.insert(errors, "用户名不能超过20个字符")
end
-- 年龄校验
if not data.age or data.age < 0 or data.age > 150 then
table.insert(errors, "年龄不合法")
end
-- 邮箱校验(简单版)
if data.email and not string.match(data.email, "%w+@%w+%.%w+") then
table.insert(errors, "邮箱格式不正确")
end
return #errors == 0, errors
end
@Service
public class LuaValidationService {
public ValidationResult validate(Map<String, Object> data) {
Globals globals = JsePlatform.standardGlobals();
globals.loadfile("scripts/validation_rule.lua").call();
// 将 Java Map 转换为 Lua Table
LuaTable luaData = new LuaTable();
for (Map.Entry<String, Object> entry : data.entrySet()) {
luaData.set(entry.getKey(),
CoerceJavaToLua.coerce(entry.getValue()));
}
// 调用验证函数
LuaValue func = globals.get("validateUser");
Varargs result = func.invoke(luaData);
boolean isValid = result.arg1().toboolean();
List<String> errors = new ArrayList<>();
if (!isValid) {
LuaTable errTable = result.arg(2).checktable();
for (int i = 1; i <= errTable.length(); i++) {
errors.add(errTable.get(i).tojstring());
}
}
return new ValidationResult(isValid, errors);
}
}
🌟 实战场景三:Java 和 Lua 互调
Java 向 Lua 注入方法
/**
* 在 Lua 中调用 Java 方法
*/
public Globals createLuaEnvWithJavaFunctions() {
Globals globals = JsePlatform.standardGlobals();
// 注入 Java 方法供 Lua 调用
globals.set("getSystemTime", new ZeroArgFunction() {
@Override
public LuaValue call() {
return LuaValue.valueOf(System.currentTimeMillis());
}
});
globals.set("log", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
System.out.println("[Lua日志] " + arg.tojstring());
return LuaValue.NIL;
}
});
globals.set("queryDatabase", new OneArgFunction() {
@Override
public LuaValue call(LuaValue sql) {
// 这里调用你的 DAO 查询数据库
String result = myDao.query(sql.tojstring());
return LuaValue.valueOf(result);
}
});
return globals;
}
-- 在 Lua 中使用 Java 注入的函数
log("开始执行业务逻辑")
local currentTime = getSystemTime()
log("当前时间戳: " .. currentTime)
local userData = queryDatabase("SELECT * FROM users WHERE id = 1")
log("查询结果: " .. userData)
🔒 安全性考虑
安全警告
在生产环境中使用 Lua 脚本引擎,必须注意安全性!
/**
* 创建安全的 Lua 环境(限制危险操作)
*/
public Globals createSandbox() {
// 使用受限的标准库(不包含 os、io 等危险库)
Globals globals = JsePlatform.standardGlobals();
// 移除危险的全局函数
globals.set("os", LuaValue.NIL); // 禁用系统调用
globals.set("io", LuaValue.NIL); // 禁用文件操作
globals.set("loadfile", LuaValue.NIL); // 禁用加载文件
globals.set("dofile", LuaValue.NIL); // 禁用执行文件
globals.set("require", LuaValue.NIL); // 禁用模块加载
// 设置执行超时(防止死循环)
globals.set("debug", LuaValue.NIL); // 禁用 debug 库
return globals;
}
脚本执行超时控制
/**
* 带超时控制的脚本执行
*/
public String executeWithTimeout(String script, long timeoutMs) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Globals globals = createSandbox();
LuaValue result = globals.load(script).call();
return result.tojstring();
});
try {
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true);
throw new RuntimeException("Lua 脚本执行超时!");
} catch (Exception e) {
throw new RuntimeException("Lua 脚本执行错误: " + e.getMessage());
} finally {
executor.shutdown();
}
}
📋 完整项目结构
springboot-lua-demo/
├── src/main/java/com/example/
│ ├── Application.java
│ ├── controller/
│ │ └── LuaController.java
│ ├── service/
│ │ ├── LuaService.java
│ │ └── DiscountRuleEngine.java
│ └── config/
│ └── LuaSandboxConfig.java
├── src/main/resources/
│ ├── application.yml
│ └── scripts/ # Lua 脚本目录
│ ├── discount_rule.lua
│ ├── validation_rule.lua
│ └── scoring_rule.lua
└── pom.xml
💡 最佳实践
- 脚本沙箱化 — 生产环境一定要限制 Lua 的权限
- 超时控制 — 设置脚本执行超时,防止死循环
- 脚本缓存 — 编译后的脚本可以缓存,避免重复编译
- 错误处理 — 对 Lua 脚本的错误要优雅处理,不能影响主服务
- 版本管理 — Lua 脚本也要纳入版本控制(Git)
- 监控告警 — 监控脚本执行耗时和错误率
📝 小结
- LuaJ 是在 Java 中运行 Lua 的最佳选择(纯 Java 实现)
- 核心用途:动态规则引擎、热更新业务逻辑
- Java 可以向 Lua 注入方法,Lua 也可以调用 Java 方法
- 生产环境必须做好安全沙箱和超时控制
- 规则变更无需重启服务,大幅提升运维效率
➡️ 下一步
SpringBoot + Lua 掌握了!下一章来学习另一个超实用的场景——Redis + Lua 脚本。
💪 练习题
- 创建一个 SpringBoot 项目,集成 LuaJ,实现一个简单的计算器 API。
- 实现一个动态折扣规则引擎,支持通过 API 热更新规则。
- 给 Lua 环境注入一个
httpGet(url)方法,让 Lua 可以发 HTTP 请求。 - 如何防止用户提交的 Lua 脚本执行恶意操作?
答案提示
- 添加
luaj-jse依赖 → 创建 LuaService → 创建 REST API - 参考本章的
DiscountRuleEngine示例 - 使用
OneArgFunction注入,内部用RestTemplate发请求 - 创建沙箱环境:移除
os、io、loadfile等危险模块,设置执行超时
