JVM 嵌入指南
Aria 可以轻松嵌入到任何 JVM 应用中。本章介绍如何在 Java 项目中集成 Aria 引擎。
依赖配置
Aria 发布在 ArcartX Maven 仓库,需要先添加仓库地址。
仓库配置
Gradle (Kotlin DSL):
repositories {
maven {
name = "arcartx-repo"
url = uri("https://repo.arcartx.com/repository/maven-public/")
}
}
Gradle (Groovy DSL):
repositories {
maven {
name = 'arcartx-repo'
url = 'https://repo.arcartx.com/repository/maven-public/'
}
}
Maven:
<repositories>
<repository>
<id>arcartx-repo</id>
<url>https://repo.arcartx.com/repository/maven-public/</url>
</repository>
</repositories>
Gradle (Kotlin DSL)
dependencies {
implementation("priv.seventeen.artist.aria:aria:1.0.0")
// 可选:数据库模块
implementation("priv.seventeen.artist.aria:aria-db:1.0.0")
}
Gradle (Groovy DSL)
dependencies {
implementation 'priv.seventeen.artist.aria:aria:1.0.0'
// 可选:数据库模块
implementation 'priv.seventeen.artist.aria:aria-db:1.0.0'
}
Maven
<dependency>
<groupId>priv.seventeen.artist.aria</groupId>
<artifactId>aria</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 可选:数据库模块 -->
<dependency>
<groupId>priv.seventeen.artist.aria</groupId>
<artifactId>aria-db</artifactId>
<version>1.0.0</version>
</dependency>
要求 Java 17+。Aria 依赖 ASM 9.6 进行字节码生成(JIT 编译)。
快速开始
最简单的用法 — 一行代码执行脚本:
import priv.seventeen.artist.aria.Aria;
import priv.seventeen.artist.aria.context.Context;
import priv.seventeen.artist.aria.value.IValue;
public class QuickStart {
public static void main(String[] args) throws Exception {
Context ctx = Aria.createContext();
IValue<?> result = Aria.eval("return 1 + 2", ctx);
System.out.println(result.numberValue()); // 3.0
}
}
Aria 类在首次使用时会自动初始化默认引擎(注册所有内置函数和服务)。
运行时错误以 AriaException 抛出,消息包含编译单元名、行号与出错源码行
(单元: [名字] 运行时错误, 位于第 X 行…),详见异常处理。
Aria.eval — 一步执行
public static IValue<?> eval(String code, Context context) throws AriaException
编译并立即执行代码,返回结果值。内部流程:解析 → 编译 IR → 优化 → 解释执行。
Context ctx = Aria.createContext();
// 执行表达式
IValue<?> result = Aria.eval("return math.sqrt(16)", ctx);
System.out.println(result.numberValue()); // 4.0
// 执行多行代码
IValue<?> result2 = Aria.eval("""
list = [1, 2, 3, 4, 5]
sum = list.reduce(-> { return args[0] + args[1] }, 0)
return sum
""", ctx);
System.out.println(result2.numberValue()); // 15.0
沙箱模式执行
public static IValue<?> eval(String code, Context context, SandboxConfig sandbox) throws AriaException
在沙箱限制下执行代码:
import priv.seventeen.artist.aria.runtime.SandboxConfig;
SandboxConfig sandbox = SandboxConfig.builder()
.maxExecutionTime(5000)
.maxCallDepth(100)
.allowFileSystem(false)
.allowNetwork(false)
.build();
IValue<?> result = Aria.eval("return 1 + 1", ctx, sandbox);
Aria.compile — 编译 API
提供两种编译重载,均可追加 boolean lenient 参数启用宽容编译(见下文)。
编译并绑定上下文
public static AriaCompilationUnit compile(String name, Context context, String code) throws CompileException
返回 AriaCompilationUnit,绑定了 Context,可直接执行:
Context ctx = Aria.createContext();
AriaCompilationUnit unit = Aria.compile("myScript", ctx, """
x = 10
y = 20
return x + y
""");
IValue<?> result = unit.execute();
System.out.println(result.numberValue()); // 30.0
// 可以多次执行(共享同一个 Context)
IValue<?> result2 = unit.execute();
AriaCompilationUnit 提供的方法:
execute()— 执行并返回结果getName()— 编译单元名称getProgram()— 获取 IR 程序getContext()— 获取绑定的 ContextgetTracker()— 获取源码追踪器getWarnings()— lenient 编译产生的警告列表(严格模式恒为空)
编译为预编译例程(不绑定上下文)
public static AriaCompiledRoutine compile(String name, String code) throws CompileException
返回 AriaCompiledRoutine,执行时需要外部传入 Context:
// 预编译(可复用)
AriaCompiledRoutine routine = Aria.compile("template", """
greeting = "Hello, " + args[0] + "!"
return greeting
""");
// 每次执行使用不同的 Context
Context ctx1 = Aria.createContext();
ctx1.setArgs(new IValue<?>[]{ new StringValue("Alice") });
IValue<?> r1 = routine.execute(ctx1); // "Hello, Alice!"
Context ctx2 = Aria.createContext();
ctx2.setArgs(new IValue<?>[]{ new StringValue("Bob") });
IValue<?> r2 = routine.execute(ctx2); // "Hello, Bob!"
AriaCompiledRoutine 提供的方法:
execute(Context context)— 使用指定 Context 执行getName()— 例程名称getProgram()— 获取 IR 程序getTracker()— 获取源码追踪器getWarnings()— lenient 编译产生的警告列表(严格模式恒为空)
lenient 宽容编译
public static AriaCompilationUnit compile(String name, Context context, String code, boolean lenient)
public static AriaCompiledRoutine compile(String name, String code, boolean lenient)
默认(lenient = false)严格模式下,任何解析错误立即抛出 CompileException(fail-fast)。
lenient = true 时恢复 Shimmer 式宽容语义:解析出错的语句及其之后的所有内容被丢弃,
保留出错前的语句正常编译执行;每个被丢弃的错误记录为一条警告:
Context ctx = Aria.createContext();
AriaCompilationUnit unit = Aria.compile("demo", ctx,
"var.a = 1\nvar.b = {bad\nvar.a = 99\nreturn var.a\n", true);
unit.execute().numberValue(); // 1.0 —— 第 2 行起全部被截断,var.a = 99 未执行
unit.getWarnings();
// [ "[lenient] 语句解析失败,该语句及其后内容被丢弃: Expected COLON, got NEWLINE ..." ]
lenient 只处理语法错误;运行时错误不受影响。
AriaEngine 自定义
AriaEngine 负责引擎初始化和 Context 工厂。
import priv.seventeen.artist.aria.api.AriaEngine;
import priv.seventeen.artist.aria.context.GlobalStorage;
// 使用默认引擎
AriaEngine engine = Aria.getEngine();
// 创建自定义引擎(独立的 GlobalStorage)
GlobalStorage customGlobal = new GlobalStorage();
AriaEngine customEngine = new AriaEngine(customGlobal);
customEngine.initialize(); // 注册所有内置函数
// 通过引擎创建 Context
Context ctx = customEngine.createContext();
initialize() 方法会注册:
- 所有内置函数(math、console、type、string、list、map 等)
- 对象构造器(Range 等)
- Java 互操作(Java 命名空间)
- 动画对象(aria-animations 在 classpath 中时自动通过反射加载)
- 服务层(fs、net、event、json、serial、scheduler、template、crypto、datetime、regex)
- 数据库模块(aria-db 在 classpath 中时自动通过反射加载,否则静默跳过)
- 模块加载函数(
__import__)
initialize() 是幂等的,多次调用只会执行一次。
Context 创建和配置
Context 是脚本执行的上下文环境,包含三层存储和作用域栈。
import priv.seventeen.artist.aria.context.Context;
import priv.seventeen.artist.aria.context.GlobalStorage;
import priv.seventeen.artist.aria.context.VariableKey;
import priv.seventeen.artist.aria.value.*;
// 通过 Aria 创建(使用默认引擎的 GlobalStorage)
Context ctx = Aria.createContext();
// 通过引擎创建
Context ctx2 = engine.createContext();
// 设置全局变量
ctx.getGlobalStorage().getGlobalVariable(VariableKey.of("config"))
.setValue(new StringValue("production"));
// 设置 self 和 args
ctx.setSelf(someValue);
ctx.setArgs(new IValue<?>[]{ new StringValue("arg1"), new NumberValue(42) });
Context 提供的变量访问层级:
getGlobalVariable(key)— 全局变量(global.xxx)getClientVariable(key)— 客户端变量(client.xxx)getServerVariable(key)— 服务端变量(server.xxx)getLocalVariable(key)— 局部变量(var.xxx)getLocalValue(key)— val 只读槽(val.xxx,宿主经 forceSetValue 写入)getScopeVariable(key)— 作用域变量
Context 还支持创建派生上下文:
createAsyncContext()— 异步上下文(共享 globalStorage 和 localStorage,独立 scopeStack)snapshotForClosure()— 闭包快照createCallContext(self, args)— 函数调用上下文(全新 ScopeStack,裸名与调用方隔离)createSharedCallContext(self, args)— 共享调用上下文(复用调用方同一 ScopeStack,仅替换 self/args)
createSharedCallContext — 跨调用共享裸名作用域
createSharedCallContext :被调脚本
对裸名临时变量的读写与调用方完全互通(同一 ScopeStack、同一 local/global 存储),
只有 self / args 被替换。适合宿主回调场景(如 UI 的多个 action 脚本共享一批临时变量)。
配套契约:每次执行结束时作用域栈会弹回执行前的深度(裸名不跨 eval 残留)。宿主要维持
跨多次执行的共享层,需要自行 pushScope() 持有一层常驻作用域:
Context caller = Aria.createContext();
caller.pushScope(); // 宿主常驻层:执行边界只弹回到该深度,此层长驻
// 预置/读取常驻层里的裸名变量
caller.getScopeStack().get(VariableKey.of("flag")).setValue(new NumberValue(1));
// 派生共享上下文执行脚本:脚本里的裸名 flag 与调用方是同一个绑定
Context shared = caller.createSharedCallContext(selfValue, argsArray);
Aria.eval("flag = 9", shared);
Aria.eval("return flag", caller).numberValue(); // 9.0 —— 写回对调用方可见
val 变量注入
val. 是宿主注入的只读槽:脚本对 val 的写入是静默 no-op,引用上的普通
setValue 同样是 no-op。宿主注入必须用 forceSetLocalValue(或引用上的
forceSetValue):
// Java 端注入/覆写 val 变量(脚本中经 val.config 读取)
ctx.forceSetLocalValue(VariableKey.of("config"), new StringValue("production"));
// 无效:普通 setValue 对 val 引用是 no-op
ctx.getLocalValue(VariableKey.of("config")).setValue(new StringValue("ignored"));
注解注册表(AnnotationRegistry)
AnnotationRegistry registry = Aria.getEngine().getAnnotationRegistry();
// 注册处理器:脚本执行时遇到 @route 注解自动回调
registry.onAnnotation("route", (annotation, target) -> {
String path = annotation.getArg(0).stringValue();
String method = target.name();
httpServer.register(path, method);
});
// 执行脚本
Aria.eval("""
class API {
@route('/api/users')
getUsers = -> { return 'users' }
}
""", ctx);
// 手动查询
List<AnnotatedTarget> routes = registry.findByAnnotation("route");
List<AnnotatedTarget> services = registry.findClassesByAnnotation("service");
AnnotationRegistry API:
onAnnotation(name, handler)— 注册注解处理器findByAnnotation(name)— 查找所有带指定注解的目标findClassesByAnnotation(name)— 只查类findFunctionsByAnnotation(name)— 只查函数/方法getAll()— 获取所有注解目标clear()— 清空注册表
JIT 与 Context 变量访问
JIT 编译器对变量系统的处理方式:
| 命名空间 | JIT 是否处理 | 说明 |
|---|---|---|
| var.xxx | 是 | fastDoubleVars 路径中复制为 double 局部变量,执行完毕后写回;运行时守卫在 var 值非数字时回退解释路径 |
| val.xxx | 否 | 包含 LOAD_VAL 的代码不会被 JIT 编译,始终走解释器 |
| global.xxx | 读是 / 写否 | LOAD_GLOBAL 可编译(每次经 GlobalStorage 读取);写入走解释器 |
| server.xxx | 读是 / 写否 | 同上(读取仍会触发 listener) |
| client.xxx | 读是 / 写否 | 同上 |
var 变量的并发风险: 当 JIT 编译的代码正在执行时,var 变量被复制到 JVM 局部变量中运算,执行完毕后才写回 Context。如果 Java 端在 JIT 执行期间通过 Context 修改 var 变量,JIT 代码不会看到这个修改。这与 Java 自身的 JIT 行为一致。
函数内联的假设: JIT 会将简单的用户自定义函数(如 var.inc = -> { return args[0] + 1 })内联为纯算术运算。内联基于编译期的函数体 IR,不会在运行时重新检查 var.inc 是否被重新赋值。如果脚本在循环中重新定义了同名函数,JIT 代码仍然执行旧的内联逻辑。这是一个已知的限制——JIT 优化的代码假设函数定义在热循环中不会改变。
常量折叠: 只折叠字面量常量(LOAD_CONST),不涉及任何命名空间变量。val.PI 的读取不会被折叠——forceSetLocalValue 随时覆写 val 都是安全的。
建议:
- 避免在脚本执行期间从另一个线程修改同一个 Context 的 var 变量
- 如果需要跨线程通信,使用
global.xxx(GlobalStorage 是线程安全的,且不被 JIT 处理) - 不要在热循环中重新定义已被 JIT 内联的函数
沙箱配置
SandboxConfig 通过 Builder 模式配置脚本执行的资源限制和能力权限。
import priv.seventeen.artist.aria.runtime.SandboxConfig;
SandboxConfig sandbox = SandboxConfig.builder()
.maxExecutionTime(5000) // 最大执行时间(毫秒),0 = 无限制
.maxCallDepth(256) // 最大调用深度,默认 512
.maxInstructions(1000000) // 最大指令数,0 = 无限制
.allowFileSystem(false) // 禁止文件系统访问(fs 命名空间)
.allowNetwork(false) // 禁止网络访问(net 命名空间)
.allowJavaInterop(false) // 禁止 Java 互操作(Java 命名空间)
.allowedNamespaces("math", "type", "console", "json") // 白名单命名空间
.build();
// 使用沙箱执行
Context ctx = Aria.createContext();
IValue<?> result = Aria.eval(code, ctx, sandbox);
配置项说明
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
maxExecutionTime | long | 0(无限制) | 最大执行时间(毫秒) |
maxCallDepth | int | 512 | 最大函数调用深度(非沙箱模式下解释器内建上限为 2048) |
maxInstructions | long | 0(无限制) | 最大执行指令数 |
allowFileSystem | boolean | true | 是否允许 fs 命名空间 |
allowNetwork | boolean | true | 是否允许 net/http 命名空间 |
allowJavaInterop | boolean | true | 是否允许 Java 命名空间 |
allowedNamespaces | String... | null(全部允许) | 命名空间白名单 |
预定义配置:
SandboxConfig.UNRESTRICTED— 无任何限制(默认)
运行不可信脚本的建议
要安全运行不可信代码,建议组合使用:
allowJavaInterop(false):从源头(use/Java.type/Java.*)封死 Java 互操作,杜绝经use('java.lang.Runtime')等的任意代码执行。allowedNamespaces(...)白名单:只放行明确需要的命名空间(能力开关allowFileSystem/allowNetwork/allowJavaInterop即使在无白名单时也会拦截对应命名空间)。- 资源限制:设置
maxInstructions/maxExecutionTime/maxCallDepth,防止 DoS。 - 沙箱激活时,
import已禁止路径穿越(..)与绝对路径,模块代码也继承沙箱;命名空间检查不会被指令缓存绕过。
注意:
JavaInterop.setClassFilter(...)是全局类过滤器,不随单个SandboxConfig实例变化。若要做细粒度的 Java 类白名单(而非整体allowJavaInterop(false)),需在进程级设置全局ClassFilter。
模块加载器配置
ModuleResolver — 路径解析
import priv.seventeen.artist.aria.module.ModuleResolver;
ModuleResolver resolver = new ModuleResolver();
// 默认搜索当前目录 "."
resolver.addSearchPath(Path.of("/path/to/modules"));
resolver.addSearchPath(Path.of("/another/path"));
// 解析模块路径(按 .ariac → .aria → 无扩展名 顺序查找;点号映射子目录)
Path resolved = resolver.resolve("myModule");
// 搜索顺序:./myModule.ariac → ./myModule.aria → ./myModule → /path/to/modules/myModule.ariac → ...
ModuleLoader — 模块加载
import priv.seventeen.artist.aria.module.ModuleLoader;
import priv.seventeen.artist.aria.module.ModuleResolver;
import priv.seventeen.artist.aria.module.ModuleCache;
// 使用默认配置
ModuleLoader loader = new ModuleLoader();
// 自定义 resolver 和 cache
ModuleResolver resolver = new ModuleResolver();
resolver.addSearchPath(Path.of("src/scripts"));
ModuleCache cache = new ModuleCache();
ModuleLoader customLoader = new ModuleLoader(resolver, cache);
// 加载单个模块
IRProgram program = loader.load("utils");
// 并行加载多个模块
Map<String, IRProgram> modules = loader.loadAll(List.of("utils", "config", "routes"));
ModuleLoader 特性:
- 内存缓存:已加载的模块不会重复编译
- 增量编译:源码哈希未变时跳过重新编译
- 并行编译:
loadAll使用线程池并行加载 - 支持
.aria预编译文件和源码文件
通过引擎配置模块搜索路径:
AriaEngine engine = Aria.getEngine();
engine.getModuleLoader().getResolver().addSearchPath(Path.of("scripts"));
预编译例程
AriaCompiledRoutine 适用于需要多次执行同一段代码的场景(如模板渲染、规则引擎):
// 编译一次
AriaCompiledRoutine routine = Aria.compile("rule", """
score = args[0]
if (score >= 90) {
return "A"
} elif (score >= 80) {
return "B"
} elif (score >= 70) {
return "C"
} else {
return "D"
}
""");
// 多次执行,每次传入不同参数
for (int score : new int[]{95, 82, 71, 55}) {
Context ctx = Aria.createContext();
ctx.setArgs(new IValue<?>[]{ new NumberValue(score) });
IValue<?> grade = routine.execute(ctx);
System.out.println(score + " → " + grade.stringValue());
}
// 输出:
// 95 → A
// 82 → B
// 71 → C
// 55 → D
完整嵌入示例
import priv.seventeen.artist.aria.Aria;
import priv.seventeen.artist.aria.api.AriaCompiledRoutine;
import priv.seventeen.artist.aria.api.AriaEngine;
import priv.seventeen.artist.aria.callable.CallableManager;
import priv.seventeen.artist.aria.context.Context;
import priv.seventeen.artist.aria.context.VariableKey;
import priv.seventeen.artist.aria.interop.JavaInterop;
import priv.seventeen.artist.aria.runtime.SandboxConfig;
import priv.seventeen.artist.aria.value.*;
public class EmbeddingExample {
public static void main(String[] args) throws Exception {
JavaInterop.setClassFilter(className ->
className.startsWith("java.util.") ||
className.startsWith("java.lang.Math")
);
CallableManager manager = CallableManager.INSTANCE;
manager.registerStaticFunction("app", "getUser", data -> {
String id = data.get(0).stringValue();
// 模拟数据库查询
return new StringValue("User-" + id);
});
AriaEngine engine = Aria.getEngine();
engine.getModuleLoader().getResolver()
.addSearchPath(java.nio.file.Path.of("scripts"));
Context ctx = Aria.createContext();
IValue<?> result = Aria.eval("""
user = app.getUser("123")
return "Hello, " + user
""", ctx);
System.out.println(result.stringValue()); // "Hello, User-123"
AriaCompiledRoutine routine = Aria.compile("sandbox-test", """
sq = math.pow(args[0], 2) + math.pow(args[1], 2)
return math.sqrt(sq)
""");
SandboxConfig sandbox = SandboxConfig.builder()
.maxExecutionTime(1000)
.maxCallDepth(50)
.allowFileSystem(false)
.allowNetwork(false)
.allowJavaInterop(false)
.allowedNamespaces("math", "type")
.build();
Context sandboxCtx = Aria.createContext();
sandboxCtx.setArgs(new IValue<?>[]{ new NumberValue(3), new NumberValue(4) });
IValue<?> distance = Aria.eval(
"return math.sqrt(math.pow(args[0], 2) + math.pow(args[1], 2))",
sandboxCtx, sandbox
);
System.out.println(distance.numberValue()); // 5.0
Context ctx1 = Aria.createContext();
Aria.eval("global.counter = 0", ctx1);
Context ctx2 = Aria.createContext();
Aria.eval("global.counter = global.counter + 1", ctx2);
Context ctx3 = Aria.createContext();
IValue<?> counter = Aria.eval("return global.counter", ctx3);
System.out.println(counter.numberValue()); // 1.0
}
}