
摘要主工程是 JDK 8 Spring Boot 2.7 的多模块单体而 Spring AI 需要 Spring Boot 3.x JDK 17——升级主框架牵一发动全身二十多个业务模块没人敢动。这篇文章讲我们的解法主系统一个依赖都不升级把 AI 能力做成旁路的独立网关服务主系统用 HTTP 调用流式输出在 JDK 8 的 Servlet 世界里也能丝滑转发。包含架构决策过程、JDK 8 兼容的流式转发代码、以及失败重试与降级的落地细节。关键词Spring Boot 2.7、JDK 8、大模型集成、SSE、老系统改造适合人群被锁死在老版本技术栈上、又想给系统加 AI 能力的 Java 团队一、先面对现实升级 vs 旁路主工程是芋道框架的 JDK 8 Spring Boot 2.7 分支二十多个业务模块跑在上面。想加 AI 能力智能问答、文档摘要、工单自动分类摆在面前的路方案 A升级主框架到 JDK 17 Boot 3.xSpring AI 官方姿势生态最顺但JDK 8 → 17 跨了 javax → jakarta 命名空间迁移所有javax.*引用要改老版本三方依赖某些 SDK、连接池可能没有 JDK 17 兼容版二十多个模块全量回归测试结论为了一个 AI 功能做全量升级风险收益不成比例否决升级可以作为独立项目立项不跟 AI 需求捆绑方案 B旁路集成独立 AI 网关服务AI 能力独立成一个服务主系统 HTTP 调用主系统零升级AI 侧技术栈自由可以用 JDK 17 Spring AI也可以干脆用 Python FastAPI代价多一个服务要部署运维多一次网络跳转我们选了 B。而且这个方案和之前双启动器的架构天然兼容——AI 网关可以作为工程里的第三个启动器共享 Maven 版本管理和 CI 流水线模板。二、AI 网关的职责边界先想清楚什么放网关、什么留主系统主系统JDK 8 AI 网关独立服务 ┌─────────────────┐ ┌──────────────────────┐ │ 业务触发 │ HTTP/SSE │ Prompt 组装 │ │ 权限/租户校验 │ ──────────→ │ 模型调用 重试降级 │ │ 会话历史存储 │ ←────────── │ Token 计量 │ │ 业务数据上下文 │ 流式响应 │ 敏感词过滤 │ └─────────────────┘ └──────────────────────┘原则网关只做和模型打交道的事业务语义这个用户有没有权限问、问了什么业务数据全部留在主系统。这样网关足够薄可以随时重写甚至换语言主系统的业务逻辑也不会散落到网关里。网关技术栈我们用了 Python FastAPI配额管理、流式处理生态更成熟如果团队清一色 Java用 JDK 17 Spring Boot 3 小服务也一样它足够小升级风险可控。三、主系统侧JDK 8 也能流式转发大多数人卡住的地方主系统是 Servlet 栈Tomcat Spring MVC没有 WebFlux怎么转发 SSE 流答案是用 OkHttp 的流式读取 ResponseBody的source()逐行读写回HttpServletResponse。JDK 8 完全可行PostMapping(/ai/chat)publicvoidchat(RequestBodyAiChatReqreq,HttpServletResponseresponse){response.setContentType(text/event-stream);response.setCharacterEncoding(UTF-8);response.setHeader(Cache-Control,no-cache);response.setHeader(X-Accel-Buffering,no);StringpayloadbuildGatewayPayload(req);// 带上用户身份、租户、业务上下文RequestrequestnewRequest.Builder().url(aiGatewayUrl/v1/chat/stream).post(RequestBody.create(payload,MediaType.parse(application/json))).build();try(ResponseupstreamhttpClient.newCall(request).execute()){if(!upstream.isSuccessful()||upstream.body()null){writeSseEvent(response,error,{msg:AI服务不可用});return;}BufferedSourcesourceupstream.body().source();while(!source.exhausted()){Stringlinesource.readUtf8Line();if(linenull)break;response.getWriter().write(line\n);response.getWriter().flush();// 每行必须 flush否则前端收不到}}catch(IOExceptione){log.error([AI转发] 流中断, userId{},SecurityFrameworkUtils.getLoginUserId(),e);}}几个必须注意的点flush()逐行调用Tomcat 默认有响应缓冲不 flush 就变成憋到最后一次性输出超时拆开配OkHttp 的connectTimeout设 5sreadTimeout要给足生成 30s 很正常callTimeout不要设——它会覆盖整个流式过程的生命周期异常时补终止事件和之前 Python 文章里说的一样流中断必须给前端一个明确信号否则生成中会永远转圈租户/用户上下文显式传递旁路调用跨了进程芋道的TenantContextHolder这类 ThreadLocal 不会跟过去必须在 payload 里带上 tenantId/userId网关侧做隔离四、同步场景工单自动分类怎么接不是所有场景都要流式。比如工单创建后自动打标签分类就是普通的同步调用套一层异步化防止拖慢主流程Async(aiExecutor)// 独立线程池别占用 Web 线程publicvoidclassifyTicketAsync(LongticketId){TicketDOticketticketMapper.selectById(ticketId);try{AiClassifyResprespaiClient.classify(buildPrompt(ticket));ticketService.updateAiTags(ticketId,resp.getTags(),resp.getCategory());}catch(Exceptione){// AI 失败不能影响主流程静默降级记录待重试log.warn([AI分类] 失败, ticketId{},ticketId,e);aiRetryQueue.add(ticketId);}}这里的核心纪律AI 是增强能力不是主链路依赖。AI 网关挂了工单创建、审批该怎么样还怎么样。所有 AI 调用都套Async 降级兜底线程池独立命名aiExecutor并限队列长度防止网关无响应时把主系统线程池占满。五、网关侧的鉴权与计量旁路服务最大的风险是裸奔——内网服务忘了加鉴权被任意进程调用刷账单。最低配的安全三件套服务间鉴权主系统调用带固定 API Key配置中心下发定期轮换网关过滤器统一校验按租户/用户计量网关侧记录每次调用的 tenantId、userId、token 用量落表。做成本分摊和限额的数据基础双端限流主系统按用户限流防前端刷网关按租户限流防主系统 bug 产生的雪崩调用六、上线效果与遗留问题上线后的现状智能问答、文档摘要、工单分类三个场景跑在旁路网关上主系统代码只增加了一个AiClient封装类和两个 Controller没有引入任何新依赖没有动任何版本号。遗留问题也坦白说跨进程调用多了一跳网关健康检查和报警要单独配会话历史存在主系统的 MySQL 里将来做 RAG 知识库时网关要独立建向量存储两边数据模型要提前想清楚边界JDK 8 的 OkHttp 老版本对 HTTP/2 支持有限网关如果上了 HTTP/2 要验证兼容性这套方案的本质是用架构手段绕开版本枷锁让新旧技术栈在进程边界上握手。它不酷但让业务这周就能用上 AI——对多数团队来说这比等哪天有空升级框架实在得多。老系统改造的话题可以聊的很多评论区见。有帮助就点个赞 收藏。