Java 适合承载主程序、并发能力和工程化体系,Lua 则更轻量,适合把变化频繁的逻辑脚本化。很多项目之所以把两者放在一起,并不是单纯“能互调”,而是希望在不重编译主程序的前提下,把功能扩展、规则调整甚至部分更新流程交给脚本层处理。
下面按两条常见路径展开:先看如何在 Java 中执行 Lua,以实现动态扩展和脚本升级;再看如何在 Lua 中访问 Java 对象与类。文中的依赖版本、调用方式和代码示例都保留原始事实,方便你直接对照验证。
为什么项目会同时使用 Java 和 Lua
Lua 在游戏开发、嵌入式系统等场景中应用广泛,优势是体积小、语法简单、嵌入方便;Java 则更擅长承担主程序框架、线程调度、GUI 之外的复杂业务和工程化管理。
把两者结合起来,通常是为了解决两个实际问题:
- 把经常变化的逻辑从 Java 主程序中拆出来,用脚本按需加载。
- 保留 Java 的主体能力,同时让 Lua 接管一部分规则、配置或扩展点。
落地时大致有两种方向:一是“Java 调 Lua”,二是“Lua 调 Java”。前者更常见,适合做动态扩展;后者更适合给脚本层暴露宿主能力。
在 Java 中执行 Lua:从依赖引入到基础调用
LuaJ 是什么,它在这里解决了什么问题
要在 Java 程序里运行 Lua,首先需要一个解释器或桥接层。原文采用的是 LuaJ,它是一个 Java 实现的 Lua 解释器,用来打通 Java 与 Lua 之间的调用。
按照原文描述,LuaJ 的实现可以理解为三部分:
- Lua 编译器:负责把 Lua 代码转换为 Lua 字节码,而不是翻译成 Java 代码。
- Lua 虚拟机:负责执行 Lua 字节码,并处理 Lua 数据类型。
- Java 与 Lua 的桥梁类库:提供
LuaValue、LuaFunction等关键类型,让 Java 可以操作 Lua 值与 Lua 函数。
其中,LuaValue 对应 Lua 中的值,可表示 nil、boolean、number、string 以及 table、function、userdata 等复杂类型;LuaFunction 用于表示可调用的 Lua 函数。
先引入 LuaJ 依赖
原文给出的 Maven 依赖如下:
<dependency>
<groupId>org.luaj</groupId>
<artifactId>luaj-jse</artifactId>
<version>3.0.1</version>
</dependency>
这里的关键信息是库名 luaj-jse 和版本 3.0.1。如果你只是想先把 Lua 嵌入 Java 程序,这已经足够作为起点。
最小示例:在 Java 中直接执行一段 Lua 代码
下面这段代码展示了最基本的调用方式:
import org.luaj.vm2.*;
import org.luaj.vm2.lib.jse.*;
public class HelloWorld {
public static void main (String[] args) {
LuaValue globals = JsePlatform.standardGlobals();
LuaValue chunk = globals.load("print('Hello, World!')");
chunk.call();
}
}
这个过程可以拆成三步理解:
- 通过
JsePlatform.standardGlobals()创建 Lua 全局环境。 - 通过
globals.load()加载 Lua 代码,并编译成可调用对象。 - 执行
chunk.call(),输出Hello, World!。
如果你的目标只是把少量规则、表达式或脚本片段塞进 Java 服务中,这就是最直接的入口。
把 Lua 函数传给 Java 方法时,代码是怎样组织的
实际项目里,常见需求不是只执行一段字符串脚本,而是把 Lua 函数当作“回调”传给 Java,再由 Java 在合适的时机调用它。原文给了一个示例:
import org.luaj.vm2.*;
import org.luaj.vm2.lib.jse.*;
public class LuaJavaExample {
public static void main (String[] args) {
LuaValue globals = JsePlatform.standardGlobals();
// 定义 Lua 函数
LuaValue luaFunction = LuaValue.valueOf(new TwoParametersFunction());
// 调用 Java 方法,并传递 Lua 函数作为参数
invokeJavaMethod(luaFunction);
// 在 Lua 中调用 Java 方法
globals.set("invokeJavaMethod", new InvokeJavaMethodFunction());
globals.load("invokeJavaMethod()").call();
}
public static void invokeJavaMethod (LuaValue luaFunction) {
// 在 Java 方法中调用传递进来的 Lua 函数
luaFunction.call(LuaValue.valueOf("Hello"), LuaValue.valueOf("World"));
}
public static class TwoParametersFunction extends OneArgFunction {
@Override
public LuaValue call(LuaValue arg) {
String firstParameter = arg.checkjstring();
String secondParameter = arg.checkjstring(2);
System.out.println("First parameter: " + firstParameter);
System.out.println("Second parameter: " + secondParameter);
return LuaValue.NIL;
}
}
public static class InvokeJavaMethodFunction extends ZeroArgFunction {
@Override
public LuaValue call() {
// 在 Lua 中调用 Java 方法
invokeJavaMethod(LuaValue.valueOf(new TwoParametersFunction()));
return LuaValue.NIL;
}
}
}
从结构上看,这个示例想表达两件事:
- Java 可以接收一个
LuaValue,再把它当作函数调用。 - Java 还可以把某个方法挂到 Lua 全局环境中,让 Lua 侧主动触发调用。
原文还特别强调,这里形成了一个“循环调用”的结构:Java 调用 Lua,Lua 再回调 Java。这种模式适合做事件分发、规则回调和插件钩子,但也意味着你需要额外留意调用边界、参数约束和错误处理,否则排查问题会比较困难。
如何用 LuaJ 做动态扩展
核心思路:主程序不重编译,扩展逻辑从脚本加载
所谓动态扩展,指的是在不停止或不重新编译 Java 程序的情况下,通过加载并执行 Lua 脚本,为程序增加功能或修改行为。

原文给出的示例如下:
import org.luaj.vm2.*;
import org.luaj.vm2.lib.jse.*;
public class DynamicExtensionExample {
public static void main (String[] args) {
LuaValue globals = JsePlatform.standardGlobals();
// 加载并执行Lua脚本
globals.loadfile("extension.lua").call();
// 调用Lua函数
LuaValue luaFunction = globals.get("addTwoNumbers");
LuaValue result = luaFunction.call(10, 20);
// 打印结果
System.out.println("Result: " + result.toint());
}
}
这里的逻辑非常直接:
- 先通过
globals.loadfile("extension.lua").call()把外部脚本装载进来。 - 再通过
globals.get("addTwoNumbers")取出脚本中定义的函数。 - 调用
luaFunction.call(10, 20),最后把结果输出出来。
这类做法适合把“变化快、发布频繁、规则明确”的逻辑放到 Lua 中,例如数值计算、业务规则、活动配置处理等。这样一来,Java 主程序保持稳定,变化部分由脚本承接,发布成本通常会更低。
适合脚本化的部分,通常有哪些特征
结合原文示例,可以归纳出几条判断标准:
- 逻辑更新频率高,但对底层基础设施依赖少。
- 输入输出边界清晰,适合通过函数调用交互。
- 允许由主程序统一提供运行环境、变量和可调用接口。
反过来说,涉及复杂并发控制、重度 I/O、核心安全校验的部分,通常还是更适合留在 Java 主程序中。
脚本自动升级怎么做
原理并不复杂,关键在于版本检测和重新加载
如果说动态扩展解决的是“脚本能不能接管功能”,那么脚本自动升级解决的就是“脚本更新后,程序能不能自己发现并装载新版本”。原文给出的示例如下:

import org.luaj.vm2.*;
import org.luaj.vm2.lib.jse.*;
public class ScriptAutoUpgradeExample {
public static void main (String[] args) {
LuaValue globals = JsePlatform.standardGlobals();
// 加载并执行初始版本的Lua脚本
globals.loadfile("script.lua").call();
// 循环检测是否有新版本的Lua脚本
while (true) {
// 检测是否有新版本的脚本
// 如果有新版本,则加载并执行新版本的Lua脚本
if (hasNewScriptVersion()) {
globals.loadfile("new_script.lua").call();
}
// 执行其他程序逻辑
// 休眠一段时间后再次进行检测
sleep(1000);
}
}
// 检测是否有新版本的脚本
private static boolean hasNewScriptVersion() {
// 实现自己的检测逻辑,如从远程服务器下载新版本脚本进行比对等
// 返回 true 表示有新版本的脚本可用,否则返回 false
return false;
}
// 线程休眠
private static void sleep(long milliseconds) {
try {
Thread.sleep(milliseconds);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这段代码的重点不在循环本身,而在两个可替换点:
hasNewScriptVersion():负责判断是否存在新版本脚本,比如比对远端版本、校验下载文件、检查本地时间戳等。globals.loadfile("new_script.lua").call():在确认有更新后,重新加载新脚本。
按原文的思路,这就可以实现运行过程中的脚本升级。对于规则引擎、活动逻辑或可配置流程来说,这是一个很有吸引力的能力。
真正落地时,重点不只是“加载成功”
从工程角度看,示例已经说明原理,但上线时还需要补几层保护:
- 新脚本是否经过完整校验,避免错误版本直接覆盖运行逻辑。
- 加载失败后是否能回退到旧版本,而不是让主流程中断。
- 升级时是否会影响当前正在执行的脚本上下文。
也就是说,示例适合帮助理解“自动升级”机制,而真正生产化时,还要围绕版本管理、异常处理和回滚策略继续补齐。
反过来:在 Lua 中调用 Java
先看依赖:原文使用的是 JNLua
除了“Java 调 Lua”,很多场景也需要“Lua 调 Java”。比如脚本需要访问宿主对象、调用服务方法,或者直接使用某个 Java 类提供的能力。

原文这里引入的是 JNLua,对应 Maven 依赖如下:
<dependency>
<groupId>com.naef.jnlua</groupId>
<artifactId>jnlua</artifactId>
<version>0.9.0</version>
</dependency>
其中需要保留的关键信息是:
groupId:com.naef.jnluaartifactId:jnluaversion:0.9.0
在 Lua 中访问 Java 对象
原文示例展示了如何把一个 Java 对象放进 Lua 全局环境,再由 Lua 脚本调用它的方法:
import org.luaj.vm2.*;
import org.luaj.vm2.lib.*;
import com.naef.jnlua.*;
public class HelloWorld {
public static void main (String[] args) throws Exception {
LuaState luaState = JNLuaUtil.newState();
luaState.openLibs();
luaState.pushJavaObject(new Hello());
luaState.setGlobal("hello");
luaState.load("hello:sayHello('World')");
luaState.call(0, 0);
}
public static class Hello {
public void sayHello(String name) {
System.out.println("Hello, " + name);
}
}
}
这段代码的调用链可以概括为:
- 创建
LuaState,并执行openLibs()。 - 通过
pushJavaObject(new Hello())把 Java 对象压入 Lua 栈。 - 使用
setGlobal("hello")把它绑定到全局变量hello。 - 在 Lua 脚本里执行
hello:sayHello('World')。
这里特别值得注意的是 Lua 的冒号语法。原文用它来调用对象方法,本质上是在脚本层访问宿主对象暴露出的实例能力。
在 Lua 中访问 Java 类和静态方法
如果需要暴露的不是对象实例,而是一个类本身,原文给了第二个示例:
import org.luaj.vm2.*;
import org.luaj.vm2.lib.*;
import com.naef.jnlua.*;
public class HelloWorld {
public static void main (String[] args) throws Exception {
LuaState luaState = JNLuaUtil.newState();
luaState.openLibs();
luaState.pushJavaClass(Hello.class);
luaState.setGlobal("Hello");
luaState.load("Hello.sayHello('World')");
luaState.call(0, 0);
}
public static class Hello {
public static void sayHello(String name) {
System.out.println("Hello, " + name);
}
}
}
与对象访问相比,这里的差别主要有两点:
- 压入 Lua 栈的是类:
pushJavaClass(Hello.class)。 - 调用方式改为类名加静态方法:
Hello.sayHello('World')。
这种方式适合把一些稳定的工具能力、静态入口或宿主接口直接交给 Lua 使用。
该选哪种集成方式
如果你的目标是让 Java 主程序按需加载脚本、把频繁变化的逻辑外置,那么重点应放在 LuaJ 这条路径上,也就是“在 Java 中执行 Lua”。原文中的动态扩展和脚本自动升级,都是围绕这个方向展开的。
如果你的目标是让 Lua 脚本直接访问宿主能力,比如调用 Java 对象、触发静态方法,那么更适合看“在 Lua 中调用 Java”的方案,也就是原文使用的 JNLua 方式。
简单概括就是:
- Java 主导流程、Lua 负责扩展:优先考虑 Java 调 Lua。
- Lua 作为脚本入口、需要调用宿主能力:优先考虑 Lua 调 Java。
总结
Java 和 Lua 结合使用,核心价值不在于“语法互通”,而在于把稳定部分和变化部分拆开。Java 负责主程序和工程化能力,Lua 负责规则、扩展和更新频繁的脚本逻辑,这样可以明显提高系统的灵活性。
结合本文保留的示例可以看到,LuaJ 3.0.1 适合让 Java 程序加载并执行 Lua,从而实现动态扩展和脚本自动升级;jnlua 0.9.0 则适合让 Lua 访问 Java 对象与类。真正选型时,关键不是“哪一种更强”,而是你的项目到底想让谁主导运行流程,谁来承载变化最快的那部分逻辑。













