Java 扩展组件 (IScriptExtension) 开发指南
1. 概述
本指南面向需要在 lovrabet-runtime 独立部署环境中,通过 Java 代码向平台脚本引擎暴露自定义能力的开发者。
平台通过定义一个标准的 IScriptExtension 服务提供者接口(SPI),允许您将任何 Java 逻辑(如操作 Redis、调用内部微服务、执行特定算法等)封装成一个可供 JS 脚本调用的“组件”,而无需关心脚本引擎的内部实现。
2. 核心接口
您需要实现的核心接口是 com.lovrabet.runtime.core.sdk.IScriptExtension。
public interface IScriptExtension {
/**
* 定义您的组件类型名称。
* 这个名称将作为 JS 脚本中 context.client.extension.execute 的第一个参数,是组件的唯一标识符。
* @return 组件类型名 (e.g., "redis", "myCustomLogic")
*/
String getComponentType();
/**
* 执行组件的具体动作。
* @param action JS 调用时传入的“动作”名,用于区分同一组件下的不同操作。
* @param params JS 调用时传入的参数对象,平台会自动将其转换为一个 Map。
* @return 执行结果。任何可序列化的 Java 对象都将被自动转为 JS 对象。
*/
Object invoke(String action, Map<String, Object> params);
}3. 开发步骤
步骤 1: 创建实现类
在您的 Spring Boot 项目中(通常是宿主应用或一个自定义的 starter 包),创建一个新的 Java 类并实现 IScriptExtension 接口。
必须将该类声明为一个 Spring Bean,最简单的方式是使用 @Component 注解。
import org.springframework.stereotype.Component;
import java.util.Map;
@Component // 关键:确保 Spring 能够扫描并注册这个 Bean
public class MyCustomComponent implements IScriptExtension {
@Override
public String getComponentType() {
// ... 见步骤 2
return null;
}
@Override
public Object invoke(String action, Map<String, Object> params) {
// ... 见步骤 3
return null;
}
}步骤 2: 定义组件类型
实现 getComponentType() 方法,返回一个全局唯一的字符串,用于在 JS 中标识您的组件。命名应清晰、简洁,并使用驼峰式命名法(如 myCustomLogic)。
@Override
public String getComponentType() {
return "myCustomLogic"; // JS 中将通过 "myCustomLogic" 调用到这里
}步骤 3: 实现核心逻辑
在 invoke 方法中,根据传入的 action 和 params 参数,编写您的核心业务逻辑。
@Override
public Object invoke(String action, Map<String, Object> params) {
// 使用 action 参数来分发不同的子任务
if ("sayHello".equals(action)) {
// 从 params Map 中安全地获取参数
String name = (String) params.getOrDefault("name", "World");
return "Hello, " + name + "!";
}
if ("calculate".equals(action)) {
Number a = (Number) params.get("a");
Number b = (Number) params.get("b");
if (a == null || b == null) {
throw new IllegalArgumentException("参数 'a' 和 'b' 不能为空");
}
return a.doubleValue() + b.doubleValue();
}
// 如果 action 不被支持,抛出异常
throw new UnsupportedOperationException("不支持的动作: " + action);
}JS 侧调用示例:
// 调用 sayHello
const message = context.client.extension.execute("myCustomLogic", "sayHello", { name: "开发者" });
// message -> "Hello, 开发者!"
// 调用 calculate
const sum = context.client.extension.execute("myCustomLogic", "calculate", { a: 10, b: 20 });
// sum -> 30.04. 关键原则与最佳实践
4.1 无状态与线程安全
由于 @Component 默认创建的是单例 (Singleton) Bean,您的 IScriptExtension 实现类将被多个脚本执行线程并发访问。因此,实现必须是无状态和线程安全的。严禁在类中使用实例变量(成员变量)来存储单个请求的状态。
@Component
public class BadExampleComponent implements IScriptExtension {
private int count = 0; // 错误!这是一个非线程安全的实例变量
public Object invoke(String action, Map<String, Object> params) {
this.count++; // 在并发下会导致数据错乱
return this.count;
}
// ...
}4.2 依赖注入
您可以自由地使用 @Autowired 来注入项目中的任何其他 Spring Bean,如 Service、Repository、RestTemplate 或 RedisTemplate。
@Component
public class UserComponent implements IScriptExtension {
@Autowired
private UserService userService; // 正确:注入其他 Spring Bean
@Override
public String getComponentType() { return "user"; }
@Override
public Object invoke(String action, Map<String, Object> params) {
if ("findById".equals(action)) {
Long userId = ((Number) params.get("id")).longValue();
return userService.findById(userId); // 调用已有业务逻辑
}
// ...
}
}4.3 异常处理
您在 invoke 方法中抛出的任何 Java 异常(无论是 RuntimeException 还是受检异常),都会被平台的核心执行引擎捕获。异常的 message 会被提取出来,并作为错误信息在 JS 侧的 catch 块中抛出。
我们建议您抛出带有清晰业务含义的异常,例如 IllegalArgumentException 或自定义的业务异常。
4.4 关于性能
execute 方法会被一个隔离的、有超时限制的线程池调用。如果您的代码执行时间过长(默认超过3秒),平台会强制中断并向 JS 抛出超时异常。因此,请确保您的实现是高效的,避免长时间的阻塞操作。
5. 部署
您无需进行任何特殊的部署配置。只要您将实现了 IScriptExtension 接口并带有 @Component 注解的类打包到您最终运行的 Spring Boot 应用的 classpath 中(例如,作为一个普通的业务类或包含在某个依赖 JAR 包里),平台的 IScriptExtension 扫描机制就会在启动时自动发现并注册它。