基于SprinBoot3+GraalVM js的企业级低代码接口平台架构设计

0 阅读11分钟

摘要:本文探讨如何基于 Spring Boot 3GraalVM JavaScript 构建现代企业级低代码接口平台。该架构通过 Spring Boot 3 提供高性能的微服务基础,结合 GraalVM 的轻量级 JavaScript 运行时,实现了动态业务逻辑的热部署与脚本化配置。平台采用声明式接口定义,低代码开发模式,同时通过 GraalVM 的 AOT 编译能力显著提升启动速度和运行效率。核心设计包括多数据源和实时脚本引擎,为企业提供灵活、高效且资源消耗低的快速接口开发解决方案。

背景

一、核心技术背景

Spring Boot 3 + GraalJS 技术组合的定位:

这是一个轻量级但高性能的技术组合,专门针对需要快速接口开发动态脚本能力的企业场景。与传统的重量级低代码平台相比,这个组合更加聚焦、精简且高效

解决的问题:

  1. 敏捷接口开发:快速创建和修改RESTful API接口
  2. 动态业务逻辑:支持通过JavaScript脚本动态调整业务规则
  3. 资源效率:在有限资源下实现高性能接口服务
  4. 技术栈简化:避免引入复杂的外部脚本引擎或额外的运行时

系统架构图

image.png

核心业务代码

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) 时才加载数据源
  • ✅ 缓存机制:同一数据源只加载一次
  • ✅ 连接池管理:自动初始化连接池
  • ✅ 事务跟踪:记录所有事务,便于清理

如何扩展

  1. 添加新的数据源类型:实现 DataSource 接口
  2. 添加新的连接池:修改 ConnectionPoolManager
  3. 添加预加载策略:在 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 双向通信

如何扩展

  1. 添加新的全局对象:创建新的 Proxy 类(如 FileProxyCacheProxy
  2. 实现 ProxyObject 接口:定义 JavaScript 可调用的方法
  3. 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 统一处理
  • ✅ 类型转换:自动识别并转换类型
  • ✅ 递归处理:支持嵌套对象和数组

如何扩展

  1. 添加自定义类型转换:在 convertToJavaObject 中添加新的类型判断
  2. 支持更多数据格式:添加 XML、YAML 等格式的转换
  3. 优化性能:使用缓存减少重复转换

扩展点 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
  • ✅ 资源管理:支持关闭连接池

如何扩展

  1. 添加连接池监控:记录连接池状态、活跃连接数
  2. 支持其他连接池:添加 Druid、C3P0 等连接池实现
  3. 动态调整配置:支持运行时调整连接池参数

关键设计模式

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();
}

优势

  • 固定执行流程
  • 扩展点清晰
  • 易于维护

总结

核心业务代码特点

  1. 安全第一:沙箱隔离、参数化查询、自动清理
  2. 性能优先:连接池、懒加载、缓存机制
  3. 易于扩展:代理模式、工厂模式、清晰的扩展点

扩展点设计原则

  1. 开闭原则:对扩展开放,对修改关闭
  2. 单一职责:每个类只负责一个功能
  3. 依赖倒置:依赖抽象而不是具体实现

如何添加新功能

  1. 添加新的全局对象

    • 创建 Proxy 类实现 ProxyObject
    • execute 方法中注入
  2. 添加新的数据源类型

    • 实现 DataSource 接口
    • 修改 ConnectionPoolManager
  3. 添加新的转换策略

    • convertToJavaObject 中添加类型判断
    • 实现转换逻辑

前端界面

个人让AI写就好了,每个人写的也不同。

image.png

结语

快去上手吧