ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring AI项目集成MCP协议的三层架构设计与工程实践

2026/8/12 11:51:31 拓冰建站 浏览量
Spring AI项目集成MCP协议的三层架构设计与工程实践 1. 项目缘起从“第一天不写Tool”说起最近在折腾一个基于 Spring AI 的项目想引入 MCPModel Context Protocol来增强 AI 的能力边界。按照常规教程上手第一天大家都会直奔主题去写一个Tool注解的类暴露一个简单的工具函数给大模型调用。这没错是标准操作。但我这次没这么干。我面对的是一个已经有一定规模、模块混杂的“大仓库”如果直接埋头写工具很容易陷入“只见树木不见森林”的困境。工具写出来了但放哪儿谁来管理怎么和现有业务逻辑解耦后续的扩展性怎么保证这些问题在第一天不解决后面就会变成技术债。所以我决定先停下来花点时间在这个大仓库里“划三层”。这不是什么高深的理论而是基于实际工程痛点的、最朴素的架构梳理。这三层我称之为“MCP 集成准备层”它们的目标是在代码侵入最小化的前提下为 MCP Server 的接入提供一个清晰、稳固且可扩展的底座。很多人在搜索“MCP Server 如何集成”、“Spring AI 状态存储”时真正卡住他们的往往不是某个 API 怎么调用而是缺少这么一个全局的、结构化的思考。今天我就把这“三层”的划分逻辑、具体实践和背后的“为什么”彻底讲清楚。2. 第一层基础设施与依赖治理层在动手写任何业务代码之前确保你的“地基”是稳固的。这一层的工作看似琐碎但决定了后续所有环节的顺畅度。很多“找不到 JDK”、“版本冲突”的问题都源于这一层的疏忽。2.1 JDK 版本统一与环境隔离首先就是 JDK。Spring Boot 3.x 和 Spring AI 通常要求 JDK 17 或更高版本。你的大仓库里可能既有老模块用着 JDK 8也有新模块用着 JDK 17。直接上 MCP 和 Spring AI很可能遇到it is configured to use jdk 0, but ide supports compilation using jdk 7 and这类令人困惑的 IDE 报错或者 Maven/Gradle 构建失败。我的做法是强制统一与隔离在项目根目录的构建配置文件如pom.xml或gradle.properties中显式指定 JDK 版本。!-- pom.xml 示例 -- properties java.version17/java.version maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target /properties这确保了整个项目的编译环境一致。对于仍需使用低版本 JDK 的遗留模块考虑是否真的需要与 MCP/Spring AI 新模块强耦合。如果必须可以尝试通过maven-toolchains-plugin进行精细化的多版本管理但这会显著增加复杂度建议优先推动升级或重构。使用 IDE 的 Project SDK 和 Module SDK 设置。在 IntelliJ IDEA 中进入File - Project Structure确保 “Project” 和所有相关 “Modules” 的 SDK 都指向正确的 JDK 17或更高安装路径。很多“找不到符号”的编译错误根源就在这里。本地环境变量检查。虽然项目配置优先级更高但确保JAVA_HOME环境变量指向正确的 JDK 17 路径可以避免命令行操作如mvn spring-boot:run时出现意外。这步是很多“JDK 安装配置详细步骤”教程的终点但对我们来说只是确保工程一致性的起点。注意不要迷信“IDEA 自动检测”。在多人协作的大仓库中依赖 IDE 的自动配置是灾难的开始。必须将 JDK 版本、Spring Boot 版本等关键约束固化在版本控制下的配置文件中。2.2 依赖声明与冲突仲裁接下来是引入 Spring AI 和 MCP SDK 的依赖。这里的关键词是“精准”和“收敛”。引入 Spring AI 依赖。根据你的 Spring Boot 版本选择对应的 Spring AI 版本。例如Spring Boot 3.2.x 通常对应 Spring AI 1.0.x。在父 POM 或核心模块的依赖管理中定义好版本。dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后在需要 AI 能力的模块中引入具体依赖如spring-ai-openai-spring-boot-starter。引入 MCP SDK。目前 MCP 的 Java SDK 可能还不是 Spring 官方的一部分你需要找到可靠的依赖例如来自 modelcontextprotocol 官方仓库。同样建议在依赖管理中统一版本。dependency groupIdcom.modelcontextprotocol/groupId artifactIdmcp-java-sdk/artifactId version0.1.0/version !-- 请使用最新稳定版 -- /dependency关键点立刻检查这个 SDK 的传递依赖Transitive Dependencies是否与你的现有依赖特别是 Spring Boot 的依赖产生冲突。使用mvn dependency:tree命令查看。常见的冲突点包括 Netty 版本、Jackson 版本等。发现冲突后在依赖管理中使用exclusions或统一指定版本号来解决。为 MCP 工具准备专用依赖。你未来的Tool方法可能会需要操作数据库、调用 HTTP 接口、处理文件。不要直接在工具类里随意引入新的DataSource或RestTemplateBean。在这一层你应该预先规划和声明这些“基础设施Bean”。例如如果你知道会有工具需要查询 SQLite那么就在一个通用的配置类中定义好 SQLite 的DataSourceBean 和相关的JdbcTemplateBean并做好连接池配置。这样后续的工具类只需要Autowired注入即可避免了每个工具类都去重复配置也便于统一管理连接资源。这一层做完你的项目应该能干净地编译通过并且所有必要的底层组件JDK、核心依赖、共享 Bean都已就位为 MCP 的接入铺平了道路。这相当于打仗前的粮草和装备整顿虽然不直接杀敌但决定了你能打多远。3. 第二层上下文与状态管理层MCP 的核心价值之一是让 AI 能够理解和使用“上下文”。在 Spring AI 的语境下这常常涉及到“对话历史”、“工具调用结果”、“用户会话数据”的管理。很多关于“Spring AI 状态存储”的搜索其本质就是在寻找这一层的最佳实践。在大仓库里状态管理不能乱。你不能让 MCP 工具直接去操作 HttpSession或者随意读写一个全局的 Map。这会导致状态污染、内存泄漏和难以调试的并发问题。3.1 设计专用的上下文持有者Context Holder我建议抽象出一个McpConversationContext类。这个类的实例代表一次完整的 AI 交互会话可能包含多轮对话和多次工具调用。public class McpConversationContext { private final String conversationId; private final MapString, Object attributes new ConcurrentHashMap(); private final ListChatMessage messageHistory new CopyOnWriteArrayList(); private final ListToolCallResult toolCallHistory new CopyOnWriteArrayList(); // ... getters, setters, 以及添加消息/结果的方法 }为什么这么设计conversationId: 用于唯一标识一次会话可以从请求头、用户 Token 或自动生成。这是串联所有状态的钥匙。ConcurrentHashMap和CopyOnWriteArrayList: 大仓库可能是多线程环境如 Web 服务这些线程安全的集合可以防止状态在并发访问时损坏。分离messageHistory和toolCallHistory: 消息历史用于让 AI 理解对话脉络工具调用历史则可用于审计、调试或作为后续工具的输入例如前一个工具查询的数据后一个工具需要处理。3.2 实现上下文的生命周期管理有了上下文对象下一步是管理它的生命周期何时创建、如何存储、何时销毁。创建与绑定通常在一个 HTTP 请求拦截器Interceptor或 Spring AI 的ChatClient调用入口处根据conversationId创建或获取已有的McpConversationContext。然后将其绑定到当前请求线程。可以使用ThreadLocal但更推荐使用 Spring 的RequestScope或ConversationScope如果自定义来管理这样能与 Spring 的生命周期更好地集成。Component Scope(scopeName conversation, proxyMode ScopedProxyMode.TARGET_CLASS) public class McpConversationContextHolder { private McpConversationContext context; // ... 初始化和获取方法 }存储策略对于简单的、短期的对话将McpConversationContext放在内存中如上述 Holder是可以的。但对于需要持久化、跨服务或长时间运行的会话你必须考虑外部存储。这就是“Spring AI 状态存储”问题的答案。内存存储默认/开发用使用一个MapString, McpConversationContext键为conversationId。需要处理过期清理。Redis 存储生产推荐将McpConversationContext序列化如 JSON后存入 Redis。利用 Redis 的 TTL 实现自动过期。这解决了服务重启或水平扩展时的状态丢失问题。数据库存储如果需要复杂的查询或永久保存可以存入关系型或文档型数据库。表结构可以对应上下文里的各个列表和属性。注入与使用在你的 MCP 工具类即未来的Tool类中你可以直接注入McpConversationContextHolder或McpConversationContext如果 Scope 配置正确从而安全地读取或修改本次会话的上下文。Component public class MyDataQueryTool { Autowired private McpConversationContextHolder contextHolder; Tool(name queryUserData, description 查询当前会话用户的历史数据) public String queryUserData() { McpConversationContext context contextHolder.getContext(); String userId (String) context.getAttribute(userId); // 使用 userId 进行查询... // 可以将查询结果存入 context 的 attributes供后续工具使用 context.setAttribute(lastQueryResult, result); return result; } }这一层的价值它让状态管理从“混乱的全局变量”变成了“有界上下文内的可控操作”。它为 MCP 工具提供了安全、一致的数据访问通道也是实现复杂 AI Agent如具有记忆和规划能力的 Agent的基础。当你看到“Spring AI Alibaba DataAgent 有权限模块吗”这种问题时其底层依赖的正是这样一个清晰的状态管理层权限校验可以作为一个拦截器在上下文创建或工具调用前检查其中的用户属性。4. 第三层工具契约与路由层前两层搭好了台子这一层才是决定“戏怎么唱”的关键。它定义了 MCP 工具如何被声明、如何被发现、以及如何被分派执行。这直接对应了“MCP 协议”的核心部分。4.1 定义工具契约接口不要一上来就写具体的Tool实现类。先定义接口。这符合“依赖倒置”原则能极大地提升代码的灵活性和可测试性。public interface DataQueryTool { ToolResult query(String sql); } public interface FileOperationTool { ToolResult readFile(String path); ToolResult writeFile(String path, String content); } public interface ExternalApiTool { ToolResult callApi(String endpoint, MapString, Object params); }这里我用了统一的ToolResult作为返回类型。这个ToolResult应该是一个包含成功状态、数据、错误信息等字段的通用对象方便序列化返回给 AI也便于工具间传递结果。为什么先定义接口明确边界每个接口代表了一类能力。在大仓库中不同的能力可能由不同的团队或模块负责实现。便于测试和 Mock你可以为这些接口编写 Mock 实现在 AI 逻辑测试时无需启动真实的数据库或外部服务。实现多版本共存未来你可以有DataQueryToolV1和DataQueryToolV2两个实现通过配置决定启用哪个。4.2 实现工具路由与执行引擎这是最核心的部分。你需要一个“路由器”ToolRouter或ToolExecutor它的职责是注册收集所有实现了上述工具契约接口的 Spring Bean。匹配当 MCP Server 收到 AI 的请求如{“name”: “queryUserData”, “arguments”: {...}}时路由器能根据工具名name找到对应的工具 Bean 和方法。执行调用该工具方法并传入解析好的参数。包装与返回将方法的返回值或抛出的异常包装成符合 MCP 协议格式的响应。在 Spring 环境下这可以借助ApplicationContextAware和反射优雅地实现。但更现代、更推荐的方式是利用 Spring AI 已经提供的ToolCallCallback或自定义ChatClient拦截器机制与你的路由器结合。一个简化的路由核心逻辑示例Service public class McpToolRouter { private final MapString, ToolExecutor toolRegistry new ConcurrentHashMap(); PostConstruct public void init() { // 扫描所有实现了特定接口或带有 McpTool 注解的 Bean注册到 registry // key: 工具名, value: 一个能执行该工具方法的执行器 } public ToolExecutionResult execute(ToolCallRequest request) { ToolExecutor executor toolRegistry.get(request.getName()); if (executor null) { return ToolExecutionResult.error(Tool not found: request.getName()); } try { Object result executor.execute(request.getArguments()); return ToolExecutionResult.success(result); } catch (Exception e) { // 这里可以记录日志并将异常信息转化为对 AI 友好的错误描述 return ToolExecutionResult.error(Execution failed: e.getMessage()); } } }4.3 处理工具间的依赖与上下文传递工具不是孤立的。Tool A产生的结果可能是Tool B的输入。这就是我们第二层准备的McpConversationContext发挥作用的地方。在你的ToolExecutor执行具体工具方法前可以将当前的McpConversationContext作为隐含参数注入进去。或者更优雅的做法是使用 Spring 的 AOP面向切面编程在工具方法执行前后自动处理上下文的读取和更新。例如你可以定义一个ToolContext注解加在工具方法的参数上由 AOP 切面自动将当前的上下文对象注入进来。Tool(name analyzeLastQuery) public String analyzeLastQuery(ToolContext McpConversationContext context) { Object lastResult context.getAttribute(lastQueryResult); // 分析 lastResult... return analysis; }这一层的价值它将 MCP 工具的“声明”、“发现”、“调用”、“编排”流程标准化和框架化了。未来无论增加多少新工具你只需要实现契约接口并注册为 Spring Bean路由器就能自动处理。这为集成“搜索类 MCP 服务器如 tavily-mcp、brave-search-mcp”提供了清晰的路径——你只需要为这些外部 MCP 服务编写一个适配器实现对应的工具接口它就能无缝接入你的工具生态被你的 AI 模型调用。5. 三层之后的思考从“划层”到“写工具”完成这三层的梳理和基础搭建后你的大仓库就已经为 MCP 的深度集成准备好了“手术台”。此时再回头去看“第一天该写什么”答案就非常清晰了编写具体的工具实现类现在你可以安心地创建UserDataQueryToolImpl类实现DataQueryTool接口并加上Service和Tool注解。因为它依赖的 JDK 环境、数据源、上下文管理器、工具路由器都已就绪。配置与连接 MCP Server将你的McpToolRouter与 MCP SDK 提供的 Server 实现连接起来。配置传输层Stdio、HTTP、SSE让外部 AI 客户端如 Cursor、Claude Desktop能够连接到你的 Server。测试与迭代启动你的 Spring Boot 应用通过 MCP 客户端测试工具调用是否正常。根据 AI 的使用反馈调整工具的描述description、参数设计或者增加新的工具。这个“先划三层再写工具”的过程本质上是一个“以终为始”的架构设计。它强迫你在动手编码前先想清楚整个系统应该如何协作。这不仅能避免后期的重构痛苦还能让你的 MCP 集成方案在大仓库中更具弹性和可维护性。当别人还在为依赖冲突和状态混乱焦头烂额时你的 AI 功能已经可以稳健地迭代和扩展了。这第一天的“慢”正是为了之后无数天的“快”。