前言
在数字化转型持续加速的背景下,企业越来越多地将业务逻辑以服务化方式部署至云端。阿里云函数计算(Function Compute,简称FC)作为一种无服务器计算平台,屏蔽了底层资源运维的复杂性,使开发者能够专注于核心逻辑的开发与交付。只需上传代码,即可通过HTTP请求或事件驱动灵活触发函数执行,实现弹性伸缩、按需计费的函数式计算能力。
在实际应用中,函数计算往往需要处理复杂的数据处理任务,如多源数据的汇聚、清洗与分析。然而,使用传统Java编写这类逻辑不仅代码冗长、调试困难,还缺乏灵活性,微小的逻辑变更通常也需要重新构建、打包并上传整个函数代码包。
SPL(Structured Process Language)专注于结构化数据处理,具备简洁的语法、丰富的计算函数,特别适合表达各类复杂的数据逻辑。更重要的是,SPL支持将数据处理逻辑以外部脚本形式运行,修改逻辑时只需替换脚本文件,无需重新构建或部署函数代码包,即可在下一次调用时自动生效。这种“轻量级热切换”特性,非常契合函数计算轻量、快速迭代的特性,有效提升了云端函数逻辑的可维护性与敏捷性。
此外,SPL原生支持多种数据源(如支持JDBC的数据源、NAS文件、JSON等),并具备可视化分步调试能力,是Serverless架构中应对动态数据逻辑、数据服务编排的高效利器。
本文将介绍如何结合Micronaut框架与SPL脚本,以Fat-JAR方式部署至阿里云FC2.0,构建一个通过RESTful API调用、支持逻辑热更新的无服务器计算服务。示例将展示SPL如何访问并分析MySQL数据库和NAS文件系统,帮助读者快速构建可扩展、易维护的Serverless数据服务方案。
SPL在阿里云FC中的架构图
函数开发与部署
创建项目
使用 Micronaut CLI 快速创建 Maven 项目:
mn create-app micronaut-spl --build maven
创建完成后,可在集成开发环境(如 IntelliJ IDEA)中打开项目继续开发。
集成SPL
添加依赖
在 pom.xml 中引入 SPL 与数据库驱动:
<dependency>
<groupId>com.scudata.esproc</groupId>
<artifactId>esproc</artifactId>
<version>20250605</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
提示:中央仓库的更新频率有限,推荐从[SPL 下载地址]下载标准版,并通过私有Maven仓库同步更新。
相关jar包位于安装目录esProc/lib/,其中两个基础jar包为:
· esproc-bin-xxxx.jar:SPL 引擎及 JDBC 驱动
· icu4j-60.3.jar:国际化支持库
准备SPL配置文件
raqsoftConfig.xml 为 SPL 的核心配置文件,负责数据源定义、脚本路径管理等。本文中RDS MySQL数据源与脚本路径的示例配置如下:
……
<DBList encryptLevel="0">
<DB name="rds_mysql">
<property name="url" value="jdbc:mysql://rm-xxx.mysql.rds.aliyuncs.com:3306/spltest?useCursorFetch=true"></property>
<property name="driver" value="com.mysql.cj.jdbc.Driver"></property>
<property name="type" value="10"></property>
<property name="user" value="dms_user"></property>
<property name="password" value="password"></property>
<property name="batchSize" value="0"></property>
<property name="autoConnect" value="true"></property>
<property name="useSchema" value="false"></property>
<property name="addTilde" value="false"></property>
<property name="caseSentence" value="false"></property>
</DB>
</DBList>
<esProc>
……
<splPathList>
<splPath>/opt</splPath>
</splPathList>
……
</esProc>
……
特别注意:需加上useCursorFetch=true,否则JDBC驱动可能将整个结果集加载至内存,容易在 FC 的内存限制下导致连接断开或错误。
与传统项目部署方式不同,这里我们先将raqsoftConfig.xml打成zip包(raqsoftConfig.xml.zip),以供后续作为自定义层上传使用。
SPL 通用接口实现
使用 SPL 提供的 JDBC 接口,在 Micronaut 中构建统一的函数调用入口。通过 HTTP POST 请求传入参数,执行指定 SPL 脚本,并将结果以 JSON 格式返回。
该接口接收两个参数:
· splxName:脚本文件名(不带扩展名)
· jsonParam:SPL脚本所需参数(JSON字符串)
返回值中包括状态码、消息提示及脚本执行结果数据。
package micronaut.spl;
import io.micronaut.http.annotation.*;
import io.micronaut.http.MediaType;
import java.sql.*;
import java.util.*;
@Controller("/spl")
public class SPLExecutionController {
@Post(uri = "/call", consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON)
public Map<String, Object> execute(@Body Map<String, String> request) {
Map<String, Object> response = new HashMap<>();
String splxName = request.get("splxName");
String jsonParam = request.get("jsonParam");
if (splxName == null || splxName.isBlank()) {
response.put("code", 400);
response.put("message", "Missing splxName");
return response;
}
try {
Class.forName("com.esproc.jdbc.InternalDriver");
try (Connection con = DriverManager.getConnection("jdbc:esproc:local://?config=/opt/raqsoftConfig.xml");
CallableStatement st = con.prepareCall("call " + splxName + "(?)")) {
if (jsonParam != null && !jsonParam.isEmpty()) {
st.setString(1, jsonParam);
} else {
st.setNull(1, Types.VARCHAR);
}
boolean hasResult = st.execute();
List<List<Object>> allResults = new ArrayList<>();
while (true) {
if (hasResult) {
try (ResultSet rs = st.getResultSet()) {
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
List<Object> currentResult = new ArrayList<>();
while (rs.next()) {
if (columnCount == 1) {
currentResult.add(rs.getObject(1));
} else {
Map<String, Object> row = new LinkedHashMap<>();
for (int i = 1; i <= columnCount; i++) {
row.put(metaData.getColumnLabel(i), rs.getObject(i));
}
currentResult.add(row);
}
}
if (!currentResult.isEmpty()) {
allResults.add(currentResult);
}
}
}
if (!st.getMoreResults() && st.getUpdateCount() == -1) {
break;
}
hasResult = true;
}
if (!allResults.isEmpty()) {
response.put("code", 200);
response.put("message", "success");
response.put("data", allResults.size() == 1 ? allResults.get(0) : allResults);
} else {
response.put("code", 404);
response.put("message", "No result data");
}
}
} catch (Exception e) {
e.printStackTrace();
response.put("code", 500);
response.put("message", "Execution failed: " + e.getMessage());
}
return response;
}
}
打包项目后,再打成zip包(micronaut-spl-0.1.jar.zip)
上传部署Fat-JAR
在阿里云 FC 控制台中创建函数,推荐配置如下:
· 创建方式:使用自定义运行时创建
· 运行环境:Java 21
· 上传方式:ZIP 上传(micronaut-spl-0.1.jar.zip)
· 启动命令:java -jar micronaut-spl-0.1.jar
· 认证方式:无需认证(测试用)
点击创建后,即可完成函数部署。
添加配置层
在函数详情中点击“编辑层”,上传打包好的 raqsoftConfig.xml.zip,新建名为 config 的自定义层。
编写SPL脚本
使用 MySQL 表数据计算
在阿里云 RDS 中创建 MySQL 实例及 orders 表,导入 TPC-H 示例数据(建库建表和导入过程略)。以下脚本 rds-mysql.splx 演示如何按订单年份与状态分组统计订单总额:
A | |
1 | =connect("rds_mysql") |
2 | =A1.cursor@x("SELECT O_ORDERDATE, O_ORDERSTATUS, O_TOTALPRICE FROM ORDERS") |
3 | =A2.groups(year(O_ORDERDATE):year,O_ORDERSTATUS:status;sum(O_TOTALPRICE):amount) |
将脚本打zip包后,添加并创建自定义层(splx),即可在/opt下访问到该脚本。
调用示例
请求行
POST https://sss-fff-xxx.cn-xxx.fcapp.run/spl/call
Content-Type: application/json
请求体
{
"splxName": "rds-mysql",
"jsonParam": ""
}
返回
{
"code": 200,
"data": [
{
"year": 1992,
"status": "F",
"amount": 34330674052.43
},
{
"year": 1993,
"status": "F",
"amount": 34340410079.03
},
{
"year": 1994,
"status": "F",
"amount": 34416369052.97
},
{
"year": 1995,
"status": "F",
"amount": 6614961429.26
},
{
"year": 1995,
"status": "O",
"amount": 20822054361.33
},
{
"year": 1995,
"status": "P",
"amount": 7109117393.01
},
{
"year": 1996,
"status": "O",
"amount": 34609364760.86
},
{
"year": 1997,
"status": "O",
"amount": 34373633413.04
},
{
"year": 1998,
"status": "O",
"amount": 20212721905.53
}
],
"message": "success"
}
使用 NAS 文件系统
通过阿里云函数配置挂载 NAS 路径(如 /mnt/nas),可在 SPL 中直接读写该路径下的文件,实现持久化与共享。
以下为 w.splx(写)和 r.splx(读)两个脚本,模拟数据生成与读取处理过程。
w.splx如下:
A | |
1 | =path=json(jsonParams).path |
2 | =connect("rds_mysql") |
3 | =A2.cursor@x("SELECT O_ORDERDATE, O_ORDERSTATUS, O_TOTALPRICE FROM ORDERS") |
4 | =file(path/"orders.btx").export@b(A3) |
5 | return "btx exported." |
r.splx如下:
A | |
1 | >p=json(jsonParam),path=p.path,n=p.n,m=p.m |
2 | =file(path/"orders.btx").cursor@b() |
3 | =A2.skip(n) |
4 | =A2.fetch(m) |
5 | >A2.close() |
6 | return A4 |
写脚本,通过数据库游标读取MySQL实例中的orders表,并写入orders.btx文件(path可使用配置的NAS路径,实现持久化或与其他服务共享数据)。
读脚本,通过读取指定路径文件的游标,跳过前n条记录,展示接下来的m条记录。
也可将脚本文件本身存放在 NAS 中,配合 raqsoftConfig.xml 中增加路径:
…
<splPathList>
<splPath>/opt;/mnt/nas</splPath>
</splPathList>
…
写入调用示例
请求行
POST https://sss-fff-xxx.cn-xxx.fcapp.run/spl/call
Content-Type: application/json
请求体
POST https://sss-fff-xxx.cn-xxx.fcapp.run/spl/call
Content-Type: application/json
返回
{
"splxName": "w",
"jsonParam": "{path:/mnt/nas/}"
}
读取调用示例
请求行
POST https://sss-fff-xxx.cn-xxx.fcapp.run/spl/call
Content-Type: application/json
请求体
{
"splxName": "r",
"jsonParam": "{path:/mnt/nas/,n:99,m:2}"
}
返回
{
"code": 200,
"data": [
{
"O_ORDERDATE": "1992-12-16",
"O_ORDERSTATUS": "F",
"O_TOTALPRICE": 198800.71
},
{
"O_ORDERDATE": "1994-02-17",
"O_ORDERSTATUS": "F",
"O_TOTALPRICE": 2519.40
}
],
"message": "success"
}
业务逻辑变更
我们将“使用 MySQL 表数据计算”的“按订单年份与状态分组统计订单总额”改为“按订单年月与状态分组统计订单总额”,只需要将 A3 中的 year(O_ORDERDATE):year 改为 month@y(O_ORDERDATE):YearMonth 即可。由于 SPL 支持将数据逻辑以外部脚本形式运行,此类修改无需重新构建和部署函数代码包,只需更新脚本内容即可在下一次函数调用时自动生效。这种“轻量级热切换”方式完美契合 Serverless 架构短生命周期、按需执行的特点,显著提升了云端数据服务的响应速度与维护效率,尤其适合频繁调整的数据分析类场景。
总结
借助 Micronaut 的轻量级框架特性与 SPL 的灵活脚本执行能力,我们成功构建了一个具备结构化数据处理能力的 Serverless 应用,并以 Fat-JAR 方式部署至阿里云函数计算 FC2.0。函数启动后可通过 REST 接口按需触发,访问 MySQL 或 NAS 等多种数据源,执行复杂的计算逻辑。
相比传统 Java 开发,SPL 显著简化了数据处理过程,使业务逻辑更清晰、更易维护。尤其在 Serverless 架构下,SPL 的“脚本热切换”能力无需重构或重新部署函数,即可在下次调用中自动生效,极大提升了系统的灵活性和运维效率。结合 NAS 文件系统,还能实现函数之间的数据共享与持久化,满足更多元的业务场景需求。
这种架构适用于需要快速迭代、规则频繁调整的数据服务场景,是企业在 Serverless 架构下构建高效、可扩展数据计算服务的有力方案。