Lua 小白入门教程Lua 小白入门教程
首页
基础教程
实战进阶
编程指南
首页
基础教程
实战进阶
编程指南
  • 实战进阶

    • 🚀 实战进阶
    • 第1章 - 模块与包管理
    • 第2章 - 文件 I/O 操作
    • 第3章 - 错误处理
    • 第4章 - 面向对象编程
    • 第5章 - SpringBoot + Lua 整合实战 ⭐
    • 第6章 - Redis + Lua 脚本 ⭐
    • 第7章 - OpenResty 入门
    • 第8章 - 最佳实践与常见坑

第5章 - SpringBoot + Lua 整合实战 ⭐

嗨,朋友!我是长安。

这一章是整个教程的重头戏!作为 Java 程序员,你最关心的肯定是:怎么在 SpringBoot 项目中使用 Lua? 这一章我就手把手教你。

🤔 为什么要在 Java 中嵌入 Lua?

场景传统方案Lua 方案
业务规则变更修改 Java 代码 → 编译 → 打包 → 重启修改 Lua 脚本 → 热更新
营销活动规则硬编码或 DroolsLua 脚本动态加载
风控策略重新部署Lua 脚本实时更新
计算公式代码写死Lua 脚本可配置

核心优势:不用重启服务就能修改业务逻辑!

📦 技术选型

在 Java 中运行 Lua 有几种方案:

方案说明推荐度
LuaJ纯 Java 实现的 Lua 解释器⭐⭐⭐⭐⭐
LuaJava通过 JNI 调用原生 Lua⭐⭐⭐
javax.script + LuajJSR-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

💡 最佳实践

  1. 脚本沙箱化 — 生产环境一定要限制 Lua 的权限
  2. 超时控制 — 设置脚本执行超时,防止死循环
  3. 脚本缓存 — 编译后的脚本可以缓存,避免重复编译
  4. 错误处理 — 对 Lua 脚本的错误要优雅处理,不能影响主服务
  5. 版本管理 — Lua 脚本也要纳入版本控制(Git)
  6. 监控告警 — 监控脚本执行耗时和错误率

📝 小结

  • LuaJ 是在 Java 中运行 Lua 的最佳选择(纯 Java 实现)
  • 核心用途:动态规则引擎、热更新业务逻辑
  • Java 可以向 Lua 注入方法,Lua 也可以调用 Java 方法
  • 生产环境必须做好安全沙箱和超时控制
  • 规则变更无需重启服务,大幅提升运维效率

➡️ 下一步

SpringBoot + Lua 掌握了!下一章来学习另一个超实用的场景——Redis + Lua 脚本。

💪 练习题

  1. 创建一个 SpringBoot 项目,集成 LuaJ,实现一个简单的计算器 API。
  2. 实现一个动态折扣规则引擎,支持通过 API 热更新规则。
  3. 给 Lua 环境注入一个 httpGet(url) 方法,让 Lua 可以发 HTTP 请求。
  4. 如何防止用户提交的 Lua 脚本执行恶意操作?

答案提示

  1. 添加 luaj-jse 依赖 → 创建 LuaService → 创建 REST API
  2. 参考本章的 DiscountRuleEngine 示例
  3. 使用 OneArgFunction 注入,内部用 RestTemplate 发请求
  4. 创建沙箱环境:移除 os、io、loadfile 等危险模块,设置执行超时
最近更新: 2026/2/27 17:54
Contributors: 王长安
Prev
第4章 - 面向对象编程
Next
第6章 - Redis + Lua 脚本 ⭐