背景

对一个基于 Spring Boot 4.0 + MyBatis-Plus + PostgreSQL 的教学管理系统进行技术升级,参考了 spring-ai 企业级项目的架构模式,完成了三项核心优化。


一、Java 17 → 21 + 虚拟线程

改动

pom.xml

1
<java.version>17</java.version><java.version>21</java.version>

application.properties

1
spring.threads.virtual.enabled=true

原理

Spring Boot 4.0 原生支持 Java 21 虚拟线程(Project Loom)。开启后,Tomcat 请求处理线程、@Async 方法、WebFlux 调度线程全部切换为虚拟线程。

虚拟线程由 JVM 管理,不绑定 OS 线程,I/O 阻塞时自动让出 CPU。对于 AI API 调用、数据库查询、Redis 操作等 I/O 密集型场景,并发吞吐显著提升。

兼容性

现有 pom.xml 中所有依赖(MyBatis-Plus 3.5.15、Redisson 3.29.0、DJL 0.28.0 等)均已支持 Java 21,无需升级。


二、Spring AI 2.0 替换手动 HTTP 调用

改前

1
2
3
4
5
6
7
8
9
10
// 手动构造 HTTP 请求 → WebClient 发送 → 手动解析 JSON → 手动处理 SSE
Map<String, Object> requestBody = buildRequest(message, false, sessionId);
return openClawWebClient.post()
.uri("/v1/responses")
.header("x-openclaw-agent-id", agent)
.bodyValue(requestBody)
.retrieve()
.bodyToMono(OpenResponsesResponse.class)
.map(this::extractContent) // 手动解析 JSON 输出
.block();

代码量:约 200 行,包含请求构造、SSE 解析、Agent 路由、流式包装。

改后

1
2
3
4
5
6
7
// ChatClient 链式调用,一行搞定
return chatClientBuilder.build()
.prompt()
.user(message)
.options(OpenAiChatOptions.builder().model("openclaw/" + agent).build())
.call()
.content();

代码量:约 80 行。流式响应、结构化输出、重试容错全部由框架处理。

关键发现

OpenClaw Gateway 原生支持 /v1/chat/completions 端点(默认关闭,需在 config 中启用),与 Spring AI 2.0 的 OpenAI 兼容模式完全对齐。Agent 路由通过 model 字段实现,不再需要自定义 Header。

涉及文件

  • pom.xml:添加 spring-ai-starter-model-openai
  • application.properties:Spring AI 配置替换原 OpenClaw HTTP 配置
  • OpenClawServiceImpl.java:ChatClient 重写(200 行 → 80 行)
  • openclaw.json:启用 chatCompletions 端点
  • 删除 OpenClawConfig.javaOpenResponsesResponse.java(Spring AI 自动配置接管)

三、Redis Lua 滑动窗口限流 AOP

架构

1
2
@RateLimit 注解 → AOP 切面拦截 → Redis Lua 原子执行
声明式配置 多维度路由 滑动窗口计数

实现细节

1. 注解层

1
2
3
4
5
6
7
8
9
@RateLimit(
dimensions = {Dimension.GLOBAL, Dimension.IP}, // 全局限流 + IP 限流
count = 10, // 窗口内最多 10 次
interval = 60, // 时间窗口 60
timeUnit = TimeUnit.SECONDS, // 秒
fallback = "rateLimitFallback" // 超限后调降级方法
)
@GetMapping("/api/chat/stream")
public Result stream() { ... }

2. AOP 切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) {
long intervalMs = calculateIntervalMs(rateLimit.interval(), rateLimit.timeUnit());
List<String> keys = generateKeys(className, methodName, rateLimit.dimensions());

// 每个维度独立调用 Lua,全部通过才算通过
for (String key : keys) {
Long result = redissonClient.getScript(StringCodec.INSTANCE)
.evalSha(READ_WRITE, luaScriptSha, ReturnType.VALUE,
List.of(key), new Object[]{
String.valueOf(System.currentTimeMillis()),
"1", String.valueOf(intervalMs),
String.valueOf((long) rateLimit.count()),
UUID.randomUUID().toString()
});
if (result == null || result == 0) {
return handleRateLimitExceeded(...);
}
}
return joinPoint.proceed();
}

3. Lua 滑动窗口脚本

1
2
3
4
5
6
7
8
9
10
11
-- 清理过期记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 统计窗口内请求数
local count = redis.call('ZCARD', key)
-- 判断是否超限
if count < limit then
redis.call('ZADD', key, now, member)
redis.call('EXPIRE', key, math.ceil(window / 1000) + 1)
return 1 -- 通过
end
return 0 -- 拒绝

为什么用 Lua + Sorted Set

  • 原子性:ZREMRANGEBYSCORE → ZCARD → ZADD 三步必须原子完成,否则并发时计数不准
  • 滑动窗口:Sorted Set 按时间戳 score 精确清理过期记录,避免固定窗口的边界突刺问题
  • Hash Tag:Key 中的 {ClassName:method} 确保同一方法的多维度 Key 落在同一 Redis Cluster Slot

涉及文件

  • common/annotation/RateLimit.java — 注解定义
  • common/aspect/RateLimitAspect.java — AOP 切面
  • common/exception/RateLimitExceededException.java — 限流异常
  • resources/scripts/rate_limit.lua — Lua 脚本
  • ErrorCode.java — 新增 RATE_LIMIT_EXCEEDED(429)
  • ChatController.java — 示例注解

四、后续优化方向

优先级 项目 价值说明
🔴 高 Docker 多服务编排 一键部署 PG + Redis + App,面向学校交付
🔴 高 Flyway 数据库迁移 SQL 版本化管理,替代手动建表
🟡 中 iText PDF 报告导出 作业批改结果导出 PDF 给学生
🟡 中 单元/集成测试 Dashboard 统计逻辑复杂,该有测试兜底
🟢 低 MinIO 对象存储 替换本地磁盘存储文件

五、技术栈总览

类别 技术
框架 Spring Boot 4.0、Spring AI 2.0、Spring Security、Spring AOP
ORM MyBatis-Plus
数据库 PostgreSQL + pgvector(向量检索)
缓存 Redis(Redisson 客户端)
消息队列 Redis Stream
AI 集成 Spring AI ChatClient(OpenClaw Gateway)
Embedding DJL ONNX Runtime(bge-small-zh-v1.5 本地推理)
文档解析 Apache Tika
限流 Redis Lua 滑动窗口 + AOP
Java Java 21 + 虚拟线程
文档 SpringDoc OpenAPI