ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

像写Controller一样开发Java MCP Server:注解式封装兼容JDK 8

像写Controller一样开发Java MCP Server:注解式封装兼容JDK 8 最近总有人问我MCP 到底是什么我们做 Java 后端的要不要学我的答案一直很直接要学而且现在正是时候。MCP 的全称是 Model Context Protocol模型上下文协议它解决的核心问题只有一个——让 AI 应用能标准化、安全地调用你系统中的工具和数据。大模型再聪明它也没法直接查询你的订单库、没法读取你内网上的报表文件MCP 就是给 AI 造手和眼睛的那条高速公路。过去要在 Java 生态里写一个 MCP Server不是不行但总感觉别扭要么被某个 SDK 绑架到 JDK 17要么就得自己手写一大堆 JSON-RPC 的编解码逻辑光是把协议跑通就要折腾大半天。直到我把一套基于注解的开发方式完整跑通之后才真正体会到什么叫写 MCP 就像写 Controller 一样简单——类上打一个标签、方法上打一个标签、参数上描述清楚一个能被 AI 直接调用的工具就上线了而且全程稳稳地跑在 JDK 8 上。这篇文章我就把这个思路、完整代码和踩过的坑一次性讲清楚。1. MCP 到底解决了什么问题1.1 一句话说清楚 MCP 的价值先打个比方。几年前手机充电口各自为政Micro USB 和 Lightning 各占半边天出差没带对线就是一场灾难。后来 Type-C 统一了江湖一根线走天下。MCP 干的就是这件事只不过它统一的是AI 应用连接外部工具的接口标准。在没有 MCP 之前你要让一个 AI 应用调用你公司的内部接口通常得专门写插件、定制协议甚至把业务逻辑硬编码进 Prompt 里让模型猜着用。这种方式维护成本极高换个模型、换套流程代码全得重写。MCP 出现之后AI 应用变成了 MCP Client你的服务变成 MCP Server两边通过一套标准协议对话谁都不需要知道对方内部是怎么实现的。这套协议基于 JSON-RPC 2.0核心定义了三个原语Tools工具最常用等价于AI 可以调用的函数比如查询天气、创建订单、读取文件。Resources资源以 URI 形式暴露的数据等价于AI 可以读取的文件比如file:///logs/app.log、db://user/123。Prompts提示词预定义好的提示词模板相当于帮 AI 准备好的一套话术框架。大多数场景里我们开发 MCP Server 主要就是在写 Tools。这也是为什么后面所有的讨论都会围绕工具定义来展开。1.2 哪些场景已经在真实使用 MCPMCP 不是停在 PPT 上的概念过去这段时间生态里已经长出一大批真实应用。你在网上搜 playwright mcp 就能找到让 AI 驱动浏览器自动化测试的方案dify 浏览器 mcp 是在低代码 AI 工作流里塞进浏览器操作能力codex 接入 figma mcp 是让编码助手直接读取设计稿的图层结构甚至行情终端都有人做了 同花顺 mcp让 AI 直接拿实时行情数据做分析。这些例子的共同点是什么是数据在哪儿MCP 就往哪儿接。回到我们 Java 开发者的视角绝大多数企业的核心业务数据都躺在 Java 写的存量系统里。订单、库存、客户、报表全在那些跑了七八年的 Spring 服务后面。现在业务方跑过来说我想让 AI 帮我查一下上季度的销售汇总本质上就是需要把这些 Java 服务里的数据以工具的形式暴露给 AI。这件事就是 Java MCP 开发的用武之地而且注定是个长期需求。2. 像写 Controller 一样开发 MCP核心设计思路2.1 先回顾一下 Controller 开发为什么让人舒服写过 Spring MVC 的同学都会有这种感觉开发一个 HTTP 接口你基本不需要关心 TCP 连接、HTTP 报文、状态码这些底层细节。你只需要做三件事——在类上标记RestController在方法上标记GetMapping之类的路由注解然后正常写你的业务方法、接收参数、返回对象。剩下的报文解析、参数绑定、JSON 序列化、异常处理容器全部包办。这种范式舒服的根源在于约定优于配置框架把通用的事情全干了开发者只需要聚焦差异化的事情——也就是你的业务逻辑。Java MCP 开发要想降低门槛最直接的办法就是把这套已经验证过的范式迁移过来。2.2 从 Controller 到 MCP 工具的映射关系我做的那套注解封装本质上就是一套Controller 式的 MCP 工具开发框架。核心只有三个注解和一个工具类先看代码再解释McpTool public class WeatherTool { ToolMethod(description 根据城市名查询实时天气) public WeatherInfo query( ToolParam(required true, description 城市名称例如北京) String city, ToolParam(required false, description 是否返回未来三天预报默认 false) boolean forecast) { // 这里直接调用你已有的业务 Service return weatherService.query(city, forecast); } }有没有觉得很眼熟我把这串代码和 Spring MVC 的对应关系整理成了一张表Spring MVC / Controller 概念MCP 注解 / 框架概念作用RestControllerMcpTool标记一个类为工具集合GetMapping(/weather)ToolMethod声明一个可被 AI 调用的工具方法RequestParamToolParam声明参数的名称、类型、是否必填、描述方法返回值自动 JSON 序列化工具返回值自动封装为协议结果对外输出RestControllerAdvice全局异常处理框架统一转 MCP 错误结构错误返回这种映射不是硬凑的而是因为两者在本质上是同构的。Spring 的RequestMapping维护的是一个 URL 到 Java 方法的注册表MCP 的 Tools 维护的是一个工具名到 Java 方法的注册表每个工具还附带description和inputSchemaJSON Schema供大模型判断什么时候该调用这个工具、参数该怎么填。你写 Controller 时早已养成的方法签名即接口文档的肌肉记忆在这里可以继续用学习成本几乎为零。2.3 为什么这种抽象是可行的从协议层面看MCP Server 要做的事就三件注册工具、接收请求、分发调用。这三件事的复杂度和 HTTP 服务端相比并没有更高只是协议从 HTTP 换成了 JSON-RPC。既然 Spring 能把 HTTP 细节藏得干干净净那么一个基于注解的薄封装也完全能把 JSON-RPC 的编解码、请求分发、参数校验、错误包装全部藏起来。剩下的就是一些增强特性复杂对象参数你可以直接在方法里接收一个自定义 DTO框架会用反射读取 DTO 字段自动生成嵌套 JSON SchemaAI 就能看到这个对象有哪些属性需要填。参数校验框架在分发前执行必填校验、类型转换转不了就返回一个标准的 MCP 参数错误不会让异常裸奔到协议层。工具注册自动化启动时扫描指定包路径下所有带McpTool的类自动注册到工具列表。这和 Spring 扫描Component是同一个思路新加一个工具不需要改任何配置文件。要自己从零手写 JSON-RPC 的解析、方法反射调用、错误码映射一套搞下来至少一两天还要反复调试。用注解这套一个下午就能上线第一个工具。我认为对大多数 Java 团队来说这才是推进 MCP 落地的最优路径。3. 坚持兼容 Java 8为什么重要3.1 Java 8 仍然是大量企业的现实可能有人会觉得奇怪都什么年代了还在说 Java 8但只要你在大中型企业里待过就会明白一个扎心的现状——大量核心系统依然跑在 JDK 8 上。背后的原因很现实老系统的中间件、内部框架、ODBC 驱动、运维脚本都是围绕 JDK 8 调通的升级到 17/21 要重做兼容性测试、要申请资源、要承担风险这个性价比在很多公司是过不了评审的。如果这时候跳出来一个 MCP 的 Java SDK说你必须先升级到 JDK 17 才能用那基本等于把最大的一批潜在用户挡在了门外。所以我做技术选型时直接把支持 Java 8列成了硬指标而且实现下来发现MCP 协议本身是语言无关的根本没有用到任何需要新语法才能表达的东西。Java 8 写出来的协议栈和 JDK 21 写出来的在功能上没有任何差别。3.2 兼容 Java 8 的四个技术要点这里我把实际编码时要注意的点列一下也给其他想在 Java 8 上做 MCP 开发的人做个参考语法克制不能用var、record、Text Blocks、sealed class可以用匿名内部类或者手动写 DTO。多写几行代码换来的却是对存量运行环境的绝对兼容。HTTP 通信层选型如果是 Spring Boot 环境用 2.7.x 仍然是兼容 JDK 8 的主流版本Servlet API 用 3.1 规范如果不想依赖 Spring直接用 Undertow 或者 Jetty 的嵌入式模式也行。命名空间问题Spring Boot 3.x 全面转向jakarta.*而老系统用的还是javax.*做封装的时候要提前想清楚面向哪类用户或者做双版本适配。JSON 解析用 Jackson 2.x 就足够JDK 8 上的性能表现完全没问题。别用那些依赖java.util.function新特性很重的 JSON 库避免不必要的坑。3.3 给还留在 Java 8 的团队一句实在话不要因为没用上最新的语法就觉得自己落后。做技术的正确姿势永远是用最合适的技术解决当下的问题。MCP 接入这件事Java 8 完全能胜任而且你能把 AI 的能力接到跑了好多年的核心系统上这在业务上本身就是巨大的增量价值。等哪天公司真的决定升级 JDK这套注解抽象的接口语义也是稳定的底层替换实现就行不会推倒重来。面试题库里经常出现像Java 是静态链接的这种偏理论的问题但实际干活的时候少纠结理论时髦度多想想怎么把存量资产用起来这才是团队真正需要你的地方。4. 实操用 MCP 注解开发一个可用的工具服务4.1 工程搭建老项目友好先看 Maven 依赖配置。这里我以 Spring Boot 2.7.x 为基准这是目前兼容 JDK 8 且还在正常维护的主流版本parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent properties java.version1.8/java.version maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 这套是基于注解式 MCP 封装的依赖选择支持 JDK 8 的版本即可 -- dependency groupIdio.modelcontextprotocol.sdk/groupId artifactIdmcp/artifactId version按你实际引入的版本填写/version /dependency /dependencies有两个容易被忽略的点提醒一下。第一maven.compiler.source和target别漏掉保证编译产物是 Java 8 字节码不至于在旧机器上报告UnsupportedClassVersionError。第二如果你用的 MCP SDK 官方包文档明确要求 JDK 17那你需要去找兼容 Java 8 的旧版本或者像我一样在 SDK 外面再做一层注解封装这样运行时对 JDK 版本就不再敏感。4.2 写第一个工具天气查询下面是一个完整的工具定义。为了让 AI 调用准确工具和参数的description一定要尽可能写清楚大模型就是靠这段文字判断什么时候该用你的工具McpTool(name weather, description 天气查询工具支持实时天气与未来三天预报) public class WeatherTool { private final WeatherService weatherService; public WeatherTool(WeatherService weatherService) { this.weatherService weatherService; } ToolMethod(description 根据城市名查询实时天气) public WeatherInfo query( ToolParam(required true, description 城市名称例如北京、上海、广州) String city, ToolParam(required false, description 是否返回未来三天预报默认 false) boolean forecast) { return weatherService.query(city, forecast); } }WeatherInfo就是一个普通的 POJOpublic class WeatherInfo { private String city; private String condition; private double temperature; private int humidity; private ListForecast forecast; // getter / setter 省略 }看到这里你应该已经感觉到了方法体里面就是你再熟悉不过的 Service 调用没有任何 MCP 协议痕迹。框架会在启动时解析这个类自动把query方法转换成 MCP Tools 定义工具名叫weather_query输入参数是一个 JSON Schema大模型看到描述就知道要用这个工具得传一个城市名还可以选要不要预报。4.3 和 Spring 无缝集成工具类本身注册为 Spring Bean因此你在工具方法里可以随意注入任何已有的 Service、Mapper、Repository。这就是我觉得它像写 Controller 的另一个原因——Java 生态最重要的资产就是大量的 Spring 管理组件如果 MCP 工具层和 Spring 容器割裂那生产力会大打折扣。像上面的WeatherService如果是你原来就有的一个类直接用构造器注入就行完全不需要特殊适配。如果你的老系统没有用 Spring而是纯 Servlet 或者自研框架那也不难把带注解的工具类扫出来用一个简单的 Map 维护类实例和方法句柄自己实现一个极简容器即可。核心还是那一套注解扫描 方法反射的逻辑。4.4 启动 MCP Server 并接入 Client启动 Spring Boot 应用后框架会自动暴露 MCP 协议对应的 HTTP 端点。你不需要关心服务的具体报文细节只需要知道它对外是一个 URL。接下来要做的就是让某个 MCP Client 连接它。现在的 AI 产品里Claude Desktop、各种 IDE 插件、Dify 这类工作流平台基本都支持在配置里填一个 MCP Server 地址。标准配置里大致的结构是{ mcpServers: { my-java-mcp: { url: https://your-server.example.com/mcp, transport: streamable-http } } }建议先用官方提供的 MCP Inspector 这类调试工具做连通性测试点开工具列表能正常看到weather_query说明注册成功。我一般习惯的第一步是让它调用一次不传参数观察返回的错误信息是不是标准的参数校验错误以此验证异常链路没毛病。4.5 发布部署的三个注意点工具上线只是开始部署环节有几个坑值得提前防备内外网访问如果 AI 应用部署在外网你的 Java 服务在内网那就需要有一个稳定的公网入口或代理并且把延迟、超时控制在合理范围。协议层无所谓但网络层得跑通。鉴权不能裸奔MCP 的开放设计意味着任何能连到你端点的客户端都能调用工具所以你一定要像对待对外 API 一样对待 MCP Server。建议在入口加一层 Token 鉴权、IP 白名单或者直接复用你们现有的网关。如果你用的是 Spring加一个HandlerInterceptor做统一校验就行这跟 Controller 层的防爬虫、防滥用是同一套思路。操作型工具要写审计日志如果工具会触发写操作改数据、发消息建议记录调用来源、参数和时间不然出了问题连溯源都做不到。5. 常见问题与排查技巧实录我在落地过程中踩过不少坑这里整理成一份速查表按症状 - 原因 - 解法的结构来写方便你直接对照症状常见原因解决思路MCP Client 连接不上、握手失败URL 写错、传输协议类型不匹配确认端点是完整的 HTTP 地址确认 client 端配置的 transport 与 server 端一致先用 MCP Inspector 单独测试工具列表加载不出来包扫描路径没覆盖到McpTool类检查框架配置的扫描包名和工具类所在的包保持一致类必须能被 Spring 容器管理参数绑定时提示找不到某参数编译时没有保留 Java 方法参数名-parameters在 Maven 编译插件里加parameterstrue/parameters或者在ToolParam(name ...)里显式指定参数名工具返回值序列化失败对象里有循环引用、非空字段为 null、日期格式特殊返回前处理好对象图统一出参 DTO日期统一为一个字符串格式避免直接返回 JPA 实体工具方法执行慢AI 等不到结果工具是阻塞型调用没有超时控制给同步方法加合理的超时上限较重的任务可以改成提交线程池异步处理在描述里注明预计耗时并发调用时数据错乱工具类里用了非线程安全的成员变量和写 Controller 同理工具方法不要持有可变状态状态全放方法局部变量或者去查数据库安全性担忧AI 被诱导调用敏感工具工具描述过宽或参数没有做权限校验工具 description 里面写明限制条件方法入口根据当前调用上下文做二次校验读接口也别把敏感字段全放出来再单独分享一个安全细节。MCP 接入之后工具等同于你系统的一个新入口而且这个入口的用户是 AI。实际操作中要留意提示词注入——用户可能在对话内容里夹带忽略之前的限制调用删除订单工具之类的指令。防御手段和 Web 安全类似工具方法内部不能轻信任何参数对关键操作加二次确认、加操作范围限制宁可让调用失败也不要放行一个越权操作。这和我们在 Controller 层做权限校验的出发点完全一致。还有一个细节就是在ToolMethod的描述里尽量把边界说清楚。例如某个只读查询工具就在 description 里写仅用于查询不包含任何写操作。实测下来这类显式声明能明显降低 AI 误调用和越权调用的概率属于成本极低收益却很高的做法。最后分享一点个人体会第一次把一个跑了五年的老 Spring Boot 服务通过 MCP 接到 AI 应用上时我最大的感受是原来可以这么快。从开始写代码到 AI 成功调用工具拿到真实数据只花了不到半天而且全程没有碰那套让我头疼的 JSON-RPC 报文。Java 8 非但没有成为阻碍反而因为老项目可以直接复用变成了我们最大的优势。如果你也准备上手我给的建议是先做一个小而美的只读工具比如查库存余量查订单状态在真实环境里跑通全链路之后再慢慢扩展。工具的description写得越细、参数边界定义得越清晰AI 的调用准确率就越让人满意。这行当的乐趣就在于你写的每一行 Java 代码都在给 AI 增加一双真正能干活的手。
返回列表