摘要:本文探讨如何基于 Spring Boot 3 和 GraalVM JavaScript 构建现代企业级低代码接口平台。该架构通过 Spring Boot 3 提供高性能的微服务基础,结合 GraalVM 的轻量级 JavaScript 运行时,实现了动态业务逻辑的热部署与脚本化配置。平台采用声明式接口定义,低代码开发模式,同时通过 GraalVM 的 AOT 编译能力显著提升启动速度和运行效率。核心设计包括多数据源和实时脚本引擎,为企业提供灵活、高效且资源消耗低的快速接口开发解决方案。
背景
一、核心技术背景
Spring Boot 3 + GraalJS 技术组合的定位:
这是一个轻量级但高性能的技术组合,专门针对需要快速接口开发和动态脚本能力的企业场景。与传统的重量级低代码平台相比,这个组合更加聚焦、精简且高效。
解决的问题:
- 敏捷接口开发:快速创建和修改RESTful API接口
- 动态业务逻辑:支持通过JavaScript脚本动态调整业务规则
- 资源效率:在有限资源下实现高性能接口服务
- 技术栈简化:避免引入复杂的外部脚本引擎或额外的运行时
系统架构图
核心业务代码
1. 脚本执行引擎 (GraalVMScriptEngine)
核心职责:管理 GraalVM Context,执行 JavaScript 代码,注入全局对象
@Component
public class GraalVMScriptEngine {
@Autowired
private ConnectionPoolManager poolManager;
/**
* 执行脚本的核心方法
*/
public ScriptExecutionResult execute(
Script script,
Map<String, Object> params,
Integer timeoutSeconds,
boolean enableConsoleLog,
DataSourceService dataSourceService
) {
Context context = null;
List<String> consoleLogs = enableConsoleLog ? new ArrayList<>() : null;
try {
// 1. 创建安全的 GraalVM Context
context = createSecureContext();
// 2. 获取 JavaScript 绑定
Value bindings = context.getBindings("js");
// 3. 注入 console 对象(可选)
if (enableConsoleLog) {
createConsoleObject(context, bindings, consoleLogs);
}
// 4. 注入 HTTP 对象
injectHttpObject(context, bindings);
// 5. 注入数据库对象(懒加载)
if (dataSourceService != null) {
injectDatabaseObjectsLazy(context, bindings, dataSourceService);
}
// 6. 注入参数对象
injectParams(context, bindings, params);
// 7. 执行脚本(带超时控制)
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<ScriptExecutionResult> future = executor.submit(() -> {
long startTime = System.currentTimeMillis();
Value result = context.eval("js", script.getCode());
long executionTime = System.currentTimeMillis() - startTime;
// 8. 清理未完成的事务
cleanupTransactions(context);
return buildSuccessResult(script, result, executionTime, consoleLogs);
});
// 9. 等待执行完成(带超时)
int timeout = timeoutSeconds != null ? timeoutSeconds : 30;
return future.get(timeout, TimeUnit.SECONDS);
} catch (TimeoutException e) {
return buildTimeoutResult(script, consoleLogs);
} catch (Exception e) {
return handleExecutionError(script, e, consoleLogs);
} finally {
if (context != null) {
context.close();
}
}
}
/**
* 创建安全的 GraalVM Context(沙箱隔离)
*/
private Context createSecureContext() {
return Context.newBuilder("js")
.allowAllAccess(false) // 禁止所有访问
.allowIO(IOAccess.NONE) // 禁止 IO 操作
.allowHostAccess(HostAccess.newBuilder()
.allowListAccess(true) // 允许访问 List
.allowMapAccess(true) // 允许访问 Map
.allowArrayAccess(true) // 允许访问数组
.allowPublicAccess(true) // 允许访问公共成员
.build())
.allowHostClassLookup(s -> false) // 禁止类查找
.allowCreateThread(false) // 禁止创建线程
.allowNativeAccess(false) // 禁止本地访问
.allowPolyglotAccess(PolyglotAccess.NONE) // 禁止多语言访问
.build();
}
}
关键点:
- ✅ 沙箱隔离:禁止 IO、线程、本地访问
- ✅ 超时控制:使用 ExecutorService 实现超时
- ✅ 资源清理:finally 块确保 Context 关闭
- ✅ 事务清理:脚本执行完成后自动清理未完成的事务
2. 数据库代理 (DBProxy)
核心职责:为 JavaScript 提供数据库操作接口
public class DBProxy implements ProxyObject {
private final DataSource dataSource;
private final ConnectionPoolManager poolManager;
private final Context context;
private final List<TransactionProxy> activeTransactions;
@Override
public Object getMember(String key) {
switch (key) {
case "query":
return (ProxyExecutable) arguments ->
query(arguments[0].asString(), arguments[1]);
case "execute":
return (ProxyExecutable) arguments ->
execute(arguments[0].asString(), arguments[1]);
case "executeAndGetKey":
return (ProxyExecutable) arguments ->
executeAndGetKey(arguments[0].asString(), arguments[1]);
case "beginTransaction":
return (ProxyExecutable) arguments -> beginTransaction();
default:
return null;
}
}
/**
* 查询数据(SELECT)
*/
private Object query(String sql, Object params) {
try (Connection conn = poolManager.getConnection(dataSource.getId());
PreparedStatement stmt = conn.prepareStatement(sql)) {
// 设置参数
setParameters(stmt, params);
// 执行查询
ResultSet rs = stmt.executeQuery();
// 转换结果为 JavaScript 数组
List<Map<String, Object>> results = convertResultSet(rs);
return convertToJavaScriptArray(results);
} catch (SQLException e) {
throw new DatabaseOperationException("查询失败: " + e.getMessage(), e);
}
}
/**
* 执行更新(INSERT/UPDATE/DELETE)
*/
private int execute(String sql, Object params) {
try (Connection conn = poolManager.getConnection(dataSource.getId());
PreparedStatement stmt = conn.prepareStatement(sql)) {
setParameters(stmt, params);
return stmt.executeUpdate();
} catch (SQLException e) {
throw new DatabaseOperationException("执行失败: " + e.getMessage(), e);
}
}
/**
* 开启事务
*/
private TransactionProxy beginTransaction() {
try {
Connection conn = poolManager.getConnection(dataSource.getId());
conn.setAutoCommit(false);
TransactionProxy tx = new TransactionProxy(conn, context);
activeTransactions.add(tx); // 跟踪事务
return tx;
} catch (SQLException e) {
throw new TransactionException("开启事务失败: " + e.getMessage(), e);
}
}
}
关键点:
- ✅ 实现 ProxyObject:JavaScript 可以直接调用
- ✅ 参数化查询:防止 SQL 注入
- ✅ 自动资源管理:try-with-resources 自动关闭连接
- ✅ 事务跟踪:记录所有创建的事务,便于清理
3. 事务代理 (TransactionProxy)
核心职责:管理数据库事务,支持自动清理
public class TransactionProxy implements ProxyObject, AutoCloseable {
private final Connection connection;
private final Context context;
private boolean completed = false;
@Override
public Object getMember(String key) {
switch (key) {
case "query":
return (ProxyExecutable) arguments ->
query(arguments[0].asString(), arguments[1]);
case "execute":
return (ProxyExecutable) arguments ->
execute(arguments[0].asString(), arguments[1]);
case "executeAndGetKey":
return (ProxyExecutable) arguments ->
executeAndGetKey(arguments[0].asString(), arguments[1]);
case "commit":
return (ProxyExecutable) arguments -> {
commit();
return null;
};
case "rollback":
return (ProxyExecutable) arguments -> {
rollback();
return null;
};
default:
return null;
}
}
/**
* 提交事务
*/
public void commit() throws SQLException {
if (!completed) {
connection.commit();
completed = true;
closeConnection();
}
}
/**
* 回滚事务
*/
public void rollback() throws SQLException {
if (!completed) {
connection.rollback();
completed = true;
closeConnection();
}
}
/**
* 自动清理(实现 AutoCloseable)
* 如果事务未完成,自动回滚
*/
@Override
public void close() {
try {
if (!completed) {
connection.rollback();
completed = true;
}
closeConnection();
} catch (SQLException e) {
// 忽略清理异常
}
}
}
关键点:
- ✅ 实现 AutoCloseable:支持自动清理
- ✅ 自动回滚:未提交的事务自动回滚
- ✅ 防止重复操作:completed 标志防止重复提交/回滚
- ✅ 安全优先:默认回滚而不是提交
4. HTTP 代理 (HttpProxy)
核心职责:为 JavaScript 提供 HTTP 请求接口
public class HttpProxy implements ProxyObject {
private final Context context;
@Override
public Object getMember(String key) {
switch (key) {
case "get":
return (ProxyExecutable) arguments -> {
String url = arguments[0].asString();
Map<String, String> headers = arguments.length > 1 && !arguments[1].isNull()
? extractHeaders(arguments[1]) : new HashMap<>();
int timeout = arguments.length > 2 && !arguments[2].isNull()
? arguments[2].asInt() : 30000;
return get(url, headers, timeout);
};
case "post":
return (ProxyExecutable) arguments -> {
String url = arguments[0].asString();
Object body = arguments.length > 1 ? extractBody(arguments[1]) : null;
Map<String, String> headers = arguments.length > 2 && !arguments[2].isNull()
? extractHeaders(arguments[2]) : new HashMap<>();
int timeout = arguments.length > 3 && !arguments[3].isNull()
? arguments[3].asInt() : 30000;
return post(url, body, headers, timeout);
};
default:
return null;
}
}
/**
* 执行 GET 请求
*/
private Object get(String url, Map<String, String> headers, int timeout) {
HttpRequest request = HttpRequest.get(url);
headers.forEach(request::header);
request.timeout(timeout);
HttpResponse response = request.execute();
return buildResponse(response);
}
/**
* 执行 POST 请求
*/
private Object post(String url, Object body, Map<String, String> headers, int timeout) {
HttpRequest request = HttpRequest.post(url);
headers.forEach(request::header);
request.timeout(timeout);
// 设置请求体
if (body instanceof Map) {
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(body);
request.body(json);
if (!headers.containsKey("Content-Type")) {
request.header("Content-Type", "application/json");
}
} else if (body != null) {
request.body(body.toString());
}
HttpResponse response = request.execute();
return buildResponse(response);
}
/**
* 提取请求头(使用 JSON.stringify 转换)
*/
private Map<String, String> extractHeaders(Value value) {
// 使用 JavaScript 的 JSON.stringify 转换为字符串
Value stringifyFunc = context.eval("js", "(function(obj) { return JSON.stringify(obj); })");
Value jsonString = stringifyFunc.execute(value);
String json = jsonString.asString();
// 使用 Jackson 将 JSON 字符串转换为 Map
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = mapper.readValue(json, Map.class);
// 转换为 String 类型的 Map
Map<String, String> headers = new HashMap<>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
headers.put(entry.getKey(), String.valueOf(entry.getValue()));
}
return headers;
}
}
关键点:
- ✅ 基于 Hutool:简单可靠的 HTTP 客户端
- ✅ JSON 自动转换:JavaScript 对象自动转为 JSON
- ✅ 使用 JSON.stringify:简化参数转换逻辑
- ✅ 响应自动解析:自动解析 JSON 响应
扩展点实现
扩展点 1:懒加载数据源
设计目标:按需加载数据源,减少资源占用
/**
* 注入数据库对象到 JavaScript 上下文(懒加载方式)
*/
private void injectDatabaseObjectsLazy(
Context context,
Value bindings,
DataSourceService dataSourceService
) {
// 缓存已加载的 DBProxy
Map<Long, DBProxy> dbProxyCache = new HashMap<>();
// 跟踪所有创建的事务
List<TransactionProxy> activeTransactions = new ArrayList<>();
// 注入 getDB(id) 函数
bindings.putMember("getDB", (ProxyExecutable) arguments -> {
long dsId = extractDataSourceId(arguments[0]);
// 检查缓存
DBProxy proxy = dbProxyCache.get(dsId);
if (proxy != null) {
return proxy;
}
// 懒加载:只在需要时才加载数据源
DataSource dataSource = dataSourceService.getDataSourceWithPassword(dsId);
// 初始化连接池(如果还没有初始化)
poolManager.initializePoolIfNeeded(dataSource);
// 创建 DBProxy
proxy = new DBProxy(dataSource, poolManager, context, activeTransactions);
dbProxyCache.put(dsId, proxy);
return proxy;
});
// 注册清理钩子
context.getPolyglotBindings().putMember("__cleanupTransactions",
(ProxyExecutable) arguments -> {
for (TransactionProxy tx : activeTransactions) {
try {
tx.close(); // 自动回滚未完成的事务
} catch (Exception e) {
// 忽略清理异常
}
}
activeTransactions.clear();
return null;
}
);
}
扩展点特性:
- ✅ 按需加载:只在调用
getDB(id)时才加载数据源 - ✅ 缓存机制:同一数据源只加载一次
- ✅ 连接池管理:自动初始化连接池
- ✅ 事务跟踪:记录所有事务,便于清理
如何扩展:
- 添加新的数据源类型:实现
DataSource接口 - 添加新的连接池:修改
ConnectionPoolManager - 添加预加载策略:在
injectDatabaseObjectsLazy中添加预加载逻辑
扩展点 2:注入全局对象
设计目标:灵活注入全局对象到 JavaScript 环境
/**
* 注入 HTTP 对象到 JavaScript 上下文
*/
private void injectHttpObject(Context context, Value bindings) {
HttpProxy httpProxy = new HttpProxy(context);
bindings.putMember("http", httpProxy);
}
/**
* 注入 console 对象到 JavaScript 上下文
*/
private void createConsoleObject(Context context, Value bindings, List<String> consoleLogs) {
// 创建 console 对象
String consoleScript =
"(function() {" +
" var console = {" +
" log: function() {" +
" var args = Array.prototype.slice.call(arguments);" +
" var message = args.map(function(arg) {" +
" if (typeof arg === 'object') {" +
" try { return JSON.stringify(arg); }" +
" catch(e) { return String(arg); }" +
" }" +
" return String(arg);" +
" }).join(' ');" +
" __logToJava(message);" +
" }" +
" };" +
" return console;" +
"})()";
// 创建 Java 回调函数
bindings.putMember("__logToJava", (ProxyExecutable) arguments -> {
if (arguments.length > 0) {
String message = arguments[0].asString();
consoleLogs.add(message);
}
return null;
});
// 创建并设置 console 对象
Value consoleObject = context.eval("js", consoleScript);
bindings.putMember("console", consoleObject);
}
扩展点特性:
- ✅ 模块化注入:每个全局对象独立注入
- ✅ 可选注入:根据配置决定是否注入
- ✅ 回调机制:Java 和 JavaScript 双向通信
如何扩展:
- 添加新的全局对象:创建新的 Proxy 类(如
FileProxy、CacheProxy) - 实现
ProxyObject接口:定义 JavaScript 可调用的方法 - 在
execute方法中注入:调用bindings.putMember()
示例:添加文件操作对象
// 1. 创建 FileProxy
public class FileProxy implements ProxyObject {
@Override
public Object getMember(String key) {
switch (key) {
case "read":
return (ProxyExecutable) arguments ->
readFile(arguments[0].asString());
case "write":
return (ProxyExecutable) arguments ->
writeFile(arguments[0].asString(), arguments[1].asString());
default:
return null;
}
}
}
// 2. 注入到 JavaScript
private void injectFileObject(Context context, Value bindings) {
FileProxy fileProxy = new FileProxy();
bindings.putMember("file", fileProxy);
}
// 3. 在 execute 方法中调用
injectFileObject(context, bindings);
扩展点 3:参数转换机制
设计目标:灵活转换 Java 和 JavaScript 对象
/**
* 注入参数对象(Java Map -> JavaScript Object)
*/
private void injectParams(Context context, Value bindings, Map<String, Object> params) {
if (params != null && !params.isEmpty()) {
// 使用 Jackson 将 Map 转换为 JSON 字符串
ObjectMapper mapper = new ObjectMapper();
String jsonParams = mapper.writeValueAsString(params);
// 在 JavaScript 中解析 JSON
Value paramsObject = context.eval("js", "(" + jsonParams + ")");
bindings.putMember("params", paramsObject);
} else {
// 创建空对象
Value emptyObject = context.eval("js", "({})");
bindings.putMember("params", emptyObject);
}
}
/**
* 转换 JavaScript 结果为 Java 对象
*/
private Object convertToJavaObject(Value value) {
if (value == null || value.isNull()) {
return null;
}
if (value.isBoolean()) {
return value.asBoolean();
}
if (value.isNumber()) {
if (value.fitsInInt()) {
return value.asInt();
} else if (value.fitsInLong()) {
return value.asLong();
} else {
return value.asDouble();
}
}
if (value.isString()) {
return value.asString();
}
if (value.hasArrayElements()) {
List<Object> list = new ArrayList<>();
long size = value.getArraySize();
for (long i = 0; i < size; i++) {
list.add(convertToJavaObject(value.getArrayElement(i)));
}
return list;
}
if (value.hasMembers()) {
Map<String, Object> map = new HashMap<>();
for (String key : value.getMemberKeys()) {
map.put(key, convertToJavaObject(value.getMember(key)));
}
return map;
}
return value.toString();
}
扩展点特性:
- ✅ JSON 序列化:使用 Jackson 统一处理
- ✅ 类型转换:自动识别并转换类型
- ✅ 递归处理:支持嵌套对象和数组
如何扩展:
- 添加自定义类型转换:在
convertToJavaObject中添加新的类型判断 - 支持更多数据格式:添加 XML、YAML 等格式的转换
- 优化性能:使用缓存减少重复转换
扩展点 4:连接池管理
设计目标:灵活管理多个数据源的连接池
@Component
public class ConnectionPoolManager {
// 存储所有连接池
private final Map<Long, HikariDataSource> pools = new ConcurrentHashMap<>();
/**
* 按需初始化连接池
*/
public void initializePoolIfNeeded(DataSource dataSource) {
if (!pools.containsKey(dataSource.getId())) {
synchronized (this) {
if (!pools.containsKey(dataSource.getId())) {
initializePool(dataSource);
}
}
}
}
/**
* 初始化连接池
*/
public void initializePool(DataSource dataSource) {
HikariConfig config = new HikariConfig();
// 基本配置
config.setJdbcUrl(buildJdbcUrl(dataSource));
config.setUsername(dataSource.getUsername());
config.setPassword(PasswordEncryptionUtil.decrypt(dataSource.getPassword()));
// 连接池配置
config.setMinimumIdle(dataSource.getMinPoolSize());
config.setMaximumPoolSize(dataSource.getMaxPoolSize());
config.setConnectionTimeout(dataSource.getConnectionTimeout());
config.setIdleTimeout(dataSource.getIdleTimeout());
// 创建连接池
HikariDataSource hikariDataSource = new HikariDataSource(config);
pools.put(dataSource.getId(), hikariDataSource);
}
/**
* 获取连接
*/
public Connection getConnection(Long dataSourceId) throws SQLException {
HikariDataSource pool = pools.get(dataSourceId);
if (pool == null) {
throw new DataSourceNotFoundException("数据源不存在: " + dataSourceId);
}
return pool.getConnection();
}
/**
* 关闭连接池
*/
public void closePool(Long dataSourceId) {
HikariDataSource pool = pools.remove(dataSourceId);
if (pool != null) {
pool.close();
}
}
}
扩展点特性:
- ✅ 多数据源支持:每个数据源独立连接池
- ✅ 懒加载:按需初始化连接池
- ✅ 线程安全:使用 ConcurrentHashMap 和 synchronized
- ✅ 资源管理:支持关闭连接池
如何扩展:
- 添加连接池监控:记录连接池状态、活跃连接数
- 支持其他连接池:添加 Druid、C3P0 等连接池实现
- 动态调整配置:支持运行时调整连接池参数
关键设计模式
1. 代理模式 (Proxy Pattern)
应用场景:DBProxy、HttpProxy、TransactionProxy
// JavaScript 调用
db.query('SELECT * FROM users', []);
// 实际执行
DBProxy.getMember("query") -> ProxyExecutable -> query(sql, params)
优势:
- 隔离 Java 和 JavaScript
- 控制访问权限
- 添加额外逻辑(日志、验证)
2. 工厂模式 (Factory Pattern)
应用场景:ConnectionPoolManager
// 根据数据源配置创建连接池
public void initializePool(DataSource dataSource) {
HikariConfig config = new HikariConfig();
// 配置连接池...
HikariDataSource hikariDataSource = new HikariDataSource(config);
pools.put(dataSource.getId(), hikariDataSource);
}
优势:
- 封装创建逻辑
- 统一管理连接池
- 易于扩展新的连接池类型
3. 策略模式 (Strategy Pattern)
应用场景:参数转换
// 不同类型使用不同的转换策略
if (value.isBoolean()) {
return value.asBoolean();
} else if (value.isNumber()) {
return convertNumber(value);
} else if (value.isString()) {
return value.asString();
}
优势:
- 灵活处理不同类型
- 易于添加新的转换策略
4. 模板方法模式 (Template Method Pattern)
应用场景:脚本执行流程
public ScriptExecutionResult execute(...) {
// 1. 创建 Context
context = createSecureContext();
// 2. 注入全局对象(可扩展)
injectHttpObject(context, bindings);
injectDatabaseObjectsLazy(context, bindings, dataSourceService);
// 3. 执行脚本
Value result = context.eval("js", script.getCode());
// 4. 清理资源
cleanupTransactions(context);
context.close();
}
优势:
- 固定执行流程
- 扩展点清晰
- 易于维护
总结
核心业务代码特点
- 安全第一:沙箱隔离、参数化查询、自动清理
- 性能优先:连接池、懒加载、缓存机制
- 易于扩展:代理模式、工厂模式、清晰的扩展点
扩展点设计原则
- 开闭原则:对扩展开放,对修改关闭
- 单一职责:每个类只负责一个功能
- 依赖倒置:依赖抽象而不是具体实现
如何添加新功能
-
添加新的全局对象:
- 创建 Proxy 类实现
ProxyObject - 在
execute方法中注入
- 创建 Proxy 类实现
-
添加新的数据源类型:
- 实现
DataSource接口 - 修改
ConnectionPoolManager
- 实现
-
添加新的转换策略:
- 在
convertToJavaObject中添加类型判断 - 实现转换逻辑
- 在
前端界面
个人让
AI写就好了,每个人写的也不同。
结语
快去上手吧