Micronaut 集成 SPL 实现微服务
微服务架构作为现代企业应用系统的主流趋势,常因其部署灵活、可扩展性强而备受推崇。然而,在实际落地中却面临“叫好不叫座”的尴尬局面。根源在于传统Java实现方式下,微服务代码冗长、业务逻辑难以解耦、更新困难,甚至很多企业最终不得不将复杂业务逻辑回写至数据库层,导致系统耦合度反而升高。
SPL(Structured Process Language)为这一困境提供了解法。SPL是纯Java实现的解释执行型脚本语言,擅长复杂数据逻辑处理,具备热切换能力,能够在不重启系统、不重新部署的前提下快速变更业务逻辑,天然适合作为微服务中的业务计算引擎。它与Micronaut这样的现代Java微服务框架结合,既不破坏微服务的原生特性,又提升了开发与运维效率。
微服务与SPL集成的架构图
以下将通过一个案例来演示SPL和Micronaut集成实现微服务的过程。
Micronaut环境准备
Micronaut CLI的安装可以参考官方文档(https://micronaut-projects.github.io/micronaut-starter/latest/guide/#installation),这里不再赘述。
创建项目
使用 Micronaut CLI可快速创建基于 Maven 的Micronaut Java项目,并在项目中启用服务发现(Consul)。
分别创建服务提供者和服务消费者:
mn create-app micronaut-pvovider --build maven --features discovery-consul
mn create-app micronaut-consumer --build maven --features discovery-consul
创建好后,在集成开发环境(本文使用IDEA)中打开之前创建的micronaut-pvovider项目
启动应用
启动应用之前,需要先启动注册中心(Consul)的服务端,安装运行Consul可以参考官方文档:https://developer.hashicorp.com/consul/docs/fundamentals/install。为了方便起见,本文使用docker镜像启动Consul,命令如下:
docker run -p 8500:8500 consul:1.15.4
启动Micronaut应用,可以直接run main calss,也可以在项目的根目录下通过命令启动。
./mvnw mn:run
微服务开发与部署
集成SPL
引入依赖(pom.xml)
<dependency>
<groupId>com.scudata.esproc</groupId>
<artifactId>esproc</artifactId>
<version>20250605</version>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.7.4</version>
</dependency>
实战中,由于中央仓库的代码更新频率较低,如需要包含最全最新功能的SPL版本,建议从官网下载标准版,并通过“私有Maven仓库”方案来同步更新jar。可以从[SPL 下载地址]下载标准版,安装。
需要用到两个 jar 包都可以在 [安装目录]\esProc\lib 目录下找到。
esproc-bin-xxxx.jar //集算器计算引擎及JDBC驱动包
icu4j-60.3.jar //处理国际化
部署SPL配置文件
raqsoftConfig.xml 是 SPL 的主要配置文件,数据源、主目录、SPL 脚本寻址路径等信息都在该文件中配置。将其复制到应用的类路径下即可。配置数据源部分的示例如下:
……
<DBList encryptLevel="0">
<DB name="hsql">
<property name="url" value="jdbc:hsqldb:hsql://127.0.01:9001/demo"/>
<property name="driver" value="org.hsqldb.jdbcDriver"/>
<property name="type" value="13"/>
<property name="user" value="sa"/>
<property name="password"/>
<property name="batchSize" value="0"/>
<property name="autoConnect" value="false"/>
<property name="useSchema" value="false"/>
<property name="addTilde" value="false"/>
<property name="caseSentence" value="false"/>
</DB>
</DBList>
……
服务提供者端
通过SPL提供的JDBC驱动实现对SPL脚本的调用。
package micronaut.pvovider;
import io.micronaut.http.annotation.*;
import io.micronaut.http.MediaType;
import java.sql.*;
import java.util.*;
@Controller("/spl")
public class SPLProviderController {
@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://");
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<>();
int resultSetCount = 0;
do {
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 HashMap<>();
for (int i = 1; i <= columnCount; i++) {
row.put(metaData.getColumnLabel(i), rs.getObject(i));
}
currentResult.add(row);
}
}
if (!currentResult.isEmpty()) {
allResults.add(currentResult);
resultSetCount++;
}
}
}
} while ((hasResult = st.getMoreResults()) || st.getUpdateCount() != -1);
if (resultSetCount > 0) {
response.put("code", 200);
response.put("message", "success");
response.put("data", resultSetCount == 1 ? allResults.get(0) : allResults);
} else {
response.put("code", 404);
response.put("message", "No result data");
}
}
} catch (Exception e) {
response.put("code", 500);
response.put("message", "Execution failed: " + e.getMessage());
}
return response;
}
}
服务消费者端
打开之前创建的micronaut-consumer项目。因为与服务提供者一样,都是本地启动,所以在配置文件(application.properties)中改下应用的端口为8081(默认为8080),避免冲突
micronaut.server.port=${SERVER_PORT:8081}
package micronaut.consumer;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.*;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import java.util.Map;
@Controller("/consumer")
@Produces(MediaType.APPLICATION_JSON)
@ExecuteOn(TaskExecutors.BLOCKING)
public class SPLConsumerController {
private final HttpClient httpClient;
public SPLConsumerController(@Client("micronaut-pvovider") HttpClient httpClient) {
this.httpClient = httpClient;
}
@Post("/call-spl")
public Map<String, Object> callSPL(@Body Map<String, Object> body) {
return httpClient.toBlocking().retrieve(
HttpRequest.POST("/spl/call", body),
Argument.of(Map.class)
);
}
}
至此,SPL 与 Micronaut 的集成已完整搭建完毕。服务侧代码和配置可作为稳定通用的底层执行框架,实际的业务计算将完全由可热更新的 SPL 脚本驱动,实现逻辑与服务解耦、灵活可控。
通过 SPL 实现微服务中的业务计算逻辑
业务用例示例:识别年度大客户
列出某年内销售额排名前若干的客户,其销售额累计达到全公司一半者,视为“大客户”。
SPL脚本名为top-customer-ids.splx,脚本内容如下:
A |
B |
|
1 |
=year=json(jsonParam).year |
/Parse the input parameter jsonParam as a JSON object and extract the year field |
2 |
=connect@l("hsql") |
/Establish a connection to the local HSQL database |
3 |
=A2.query@x("select customer_id,order_amount from orders where year(order_date) = ?",year) |
/Execute a parameterized SQL query to fetch customer_id and order_amount for the specified year |
4 |
=A3.groups(customer_id;sum(order_amount):total_amount).sort(total_amount:-1) |
/Group the results by customer_id, calculate total order_amount as total_amount, and sort in descending order |
5 |
=a=0,half=A4.sum(total_amount)*0.5,A4.pselect((a+=total_amount,a>=half)) |
/Compute half of the total sales and find the index where the cumulative sum first reaches or exceeds this half |
其中jsonParam为脚本参数,格式为:{year:2012}
这个代码只有短短5行,读者可以自行脑补Java实现同样功能的复杂度。
脚本要先用SPL的IDE来编写调试,但这不是本文的重点,感兴趣的读者可以去参考SPL的官方文档。
编写好后的SPL脚本需要放置于raqsoftConfig.xml 中设置的SPL 脚本寻址路径下,例如:
<splPathList>
<splPath>/home/raqsoft/SPL</splPath>
</splPathList>
服务消费者端请求测试
请求行
POST http://localhost:8081/consumer/call-spl
Content-Type: application/json
请求体
{
"splxName": "top-customer-ids",
"jsonParam": "{year:2012}"
}
返回
{
"code": 200,
"data": [
"ERNSH",
"BLONP",
"QUICK",
"QUEEN",
"RATTC",
"PICCO",
"FRANK",
"SPLIR",
"SAVEA",
"SUPRD"
],
"message": "success"
}
应对业务逻辑变更
我们将“大客户”判定标准从销售额占比50%提升至80%,以更好满足精细化客户分析与服务需求。
在 SPL 中,业务逻辑脚本的修改非常简单,只需将 A5 单元格中的 0.5 改为 0.8 即可。更重要的是,SPL 脚本采用解释执行机制,支持运行时热更新——只需将修改后的脚本重新上传至服务端,再次发起请求即可获得新的计算结果,无需编译,也无需重启应用,变更即时生效。
相比之下,Java 的灵活性明显不足。由于销售占比门槛等规则通常是硬编码在应用逻辑中的,哪怕只是一个参数调整,也必须重新编译应用并重启服务,部署过程繁琐,响应慢,难以适应频繁变化的业务需求。
总结
通过本案例可以看出,将SPL融入Micronaut微服务架构,能够有效解决传统Java微服务在复杂业务逻辑处理方面的诸多痛点。得益于SPL的解释执行特性,业务计算逻辑可以实现热更新,无需重启服务即可动态调整计算规则,极大提升了系统的灵活性与运维效率。
相比之下,传统Java实现将规则硬编码于服务中,任何调整都需重新编译和部署,导致开发成本高、响应周期长,不利于快速变化的业务场景。
SPL作为一个专注于结构化数据处理的脚本语言,不仅具备强大的计算表达能力,还能够与Micronaut等现代微服务框架无缝协作,实现“计算逻辑脚本化、服务框架标准化、部署运维最小化”的现代微服务设计理念。
在实际项目中,采用SPL驱动微服务计算逻辑,不仅提升了开发效率,也为业务规则的快速变更提供了高效通道,是面向灵活敏捷业务需求的理想解决方案。