SpringBoot 2.x + Solon 让 Java 8 项目接入 MCP 协议

MCP(Model Context Protocol)是 Anthropic 提出的大模型工具调用标准协议,越来越多的 AI 助手(Claude、Cursor、Cherry Studio 等)都在支持它。问题是,官方 Java SDK 和 Spring AI 都要求 Java 17+,而我们大量的企业项目还跑在 Java 8 + SpringBoot 2.x 上。

这篇文章介绍一种零侵入的方案:用 Solon-mcp——目前唯一支持 Java 8 的 MCP 服务端实现——以"双容器嵌入"的方式在你的 SpringBoot 2.x 项目里启动一个轻量级 Solon 容器,专门负责 MCP 协议层。原有业务代码一行不动。

项目地址github.com/ov-vo/springboot2-solon-mcp-demo

技术栈:Java 1.8 · Spring Boot 2.3.2.RELEASE · Solon 3.3.1 · Maven 3.6+

一、为什么需要这个方案

现在大模型的"工具调用"越来越强,你可以让 AI 助手直接调用你的业务接口查询数据、执行操作。MCP 就是这个调用过程的标准协议。

但 Java 生态的 MCP 支持现状是这样的:

方案最低 Java 版本适用场景
官方 Java MCP SDKJava 17新项目
Spring AI MCPJava 17(Spring Boot 3.x)新项目
Solon-mcp(本文方案)Java 8存量 SpringBoot 2.x 项目

如果你还在维护 Java 8 的项目,Solon-mcp 是目前唯一的选择。

二、双容器架构原理

整个方案的核心思想是让两个容器在同一个 JVM 进程中共存,各司其职:

┌─────────────────────────────────────────────────────────┐
│                        JVM 进程                          │
│                                                         │
│  ┌─────────────────────┐    ┌───────────────────────┐   │
│  │   SpringBoot 容器    │    │      Solon 容器        │   │
│  │                     │    │                       │   │
│  │  @Component         │    │  MCP 生命周期管理      │   │
│  │  @Service           │    │  Tool/Resource/Prompt │   │
│  │  @Configuration     │    │  注册与路由            │   │
│  │  @Bean              │    │  SSE 长连接通道        │   │
│  │                     │    │                       │   │
│  └──────────┬──────────┘    └──────────┬────────────┘   │
│             │                          │                 │
│             └───────── McpServerConfig ─────────────┘   │
│                          (桥接层)                       │
│                              │                          │
│                   ┌──────────▼──────────┐               │
│                   │  SolonServletFilter │               │
│                   │  拦截 /mcp/* 请求   │               │
│                   └──────────┬──────────┘               │
│                              │                          │
│                   ┌──────────▼──────────┐               │
│                   │  Tomcat(内嵌)      │               │
│                   └─────────────────────┘               │
└─────────────────────────────────────────────────────────┘

关键设计点:

  • SpringBoot 容器:负责所有业务 Bean 的创建和依赖注入(@Component、@Service、@Autowired 等),这部分完全不变。
  • Solon 容器:负责 MCP 协议生命周期,包括 SSE 连接管理、Tool/Resource/Prompt 注册、JSON-RPC 消息分发。
  • McpServerConfig(桥接层):用 Spring 的 @PostConstruct 启动 Solon,用 @Bean 将 Spring 管理的 MCP 端点手动注册到 Solon,用 SolonServletFilter 把 /mcp/* 的 HTTP 请求转发给 Solon 处理。

请求处理流程

当大模型(或 MCP 客户端)发起调用时,流程如下:

  1. MCP 客户端向 /mcp/demo1/sse 发起 HTTP GET 请求建立 SSE 长连接
  2. Tomcat 接收请求,经过 CorsFilter(跨域)、McpFilter(鉴权)
  3. SolonServletFilter 将请求桥接至 Solon 容器处理,建立 SSE 通道
  4. 客户端通过 SSE 通道发送 MCP JSON-RPC 消息(tools/list、tools/call 等)
  5. Solon 路由到对应端点的 @ToolMapping 方法执行
  6. 方法由 Spring 管理的 Bean 实例执行,返回结果经 MCP 协议封装后通过 SSE 流推回

三、5 步迁移手册

以下步骤可以直接套用到你的真实 SpringBoot 2.x 项目中。

第 1 步:添加 Maven 依赖

pom.xml 中添加 Solon BOM 和核心依赖:

<!-- 在 <properties> 中添加 -->
<solon.version>3.3.1</solon.version>

<!-- 在 <dependencyManagement> 中统一管理 Solon 版本 -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.noear</groupId>
      <artifactId>solon-parent</artifactId>
      <version>${solon.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<!-- 在 <dependencies> 中添加以下核心依赖 -->
<dependencies>
  <!-- 你已有的 SpringBoot 依赖保持不变 -->

  <!-- AopUtils 解析代理类需要 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>

  <!-- Solon MCP 核心 -->
  <dependency>
    <groupId>org.noear</groupId>
    <artifactId>solon-ai-mcp</artifactId>
  </dependency>

  <!-- Solon 基础库(3.3.1 必须显式添加,3.4.x+ 不需要) -->
  <dependency>
    <groupId>org.noear</groupId>
    <artifactId>solon-lib</artifactId>
  </dependency>

  <!-- Solon Servlet 桥接(SpringBoot 2.x 用 javax.servlet 版本) -->
  <!-- SpringBoot 3.x 项目改用 solon-web-servlet-jakarta -->
  <dependency>
    <groupId>org.noear</groupId>
    <artifactId>solon-web-servlet</artifactId>
  </dependency>

  <!-- Reactor(Solon 3.3.1 的 SSE 依赖,3.4.x+ 已间接引入) -->
  <dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-core</artifactId>
    <version>3.4.34</version>
  </dependency>
</dependencies>

另外,必须maven-compiler-plugin 中开启 -parameters 编译参数:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <source>1.8</source>
    <target>1.8</target>
    <compilerArgs>
      <arg>-parameters</arg>  <!-- MCP 需要方法参数名,不加则参数名变成 arg0/arg1 -->
    </compilerArgs>
  </configuration>
</plugin>

第 2 步:创建 MCP 端点标识接口

这个空接口的唯一作用是让 Spring 通过 List<IMcpServerEndpoint> 自动收集所有 MCP 端点 Bean:

package com.example.mcp.endpoint;

/**
 * MCP 服务端点标识接口
 * 实现此接口的 Spring Bean 会被 McpServerConfig 自动扫描并注册到 Solon 容器
 */
public interface IMcpServerEndpoint {
}

第 3 步:创建桥接配置类(核心)

这是整个方案的核心文件,负责双容器的启动和桥接:

@Configuration
public class McpServerConfig {

    /** Spring 初始化完成后启动 Solon 容器 */
    @PostConstruct
    public void start() {
        Solon.start(McpServerConfig.class,
                new String[]{"--cfg=mcpserver.properties"},
                app -> app.enableScanning(false));  // 禁止 Solon 扫描,Bean 由 Spring 管理
    }

    /** Spring 关闭时优雅停止 Solon */
    @PreDestroy
    public void stop() {
        if (Solon.app() != null) {
            Solon.stopBlock(false, Solon.cfg().stopDelay());
        }
    }

    /**
     * 将所有 Spring 管理的 MCP 端点手动注册到 Solon
     * Spring 通过 List<IMcpServerEndpoint> 自动注入所有实现类
     */
    @Bean
    public McpServerConfig init(List<IMcpServerEndpoint> serverEndpoints) {
        for (IMcpServerEndpoint serverEndpoint : serverEndpoints) {
            // 必须用 AopUtils 获取真实类,因为 @Transactional 等注解会产生代理类
            Class<?> clz = AopUtils.getTargetClass(serverEndpoint);
            McpServerEndpoint anno = AnnotationUtils.findAnnotation(clz, McpServerEndpoint.class);
            if (anno == null) continue;

            McpServerEndpointProvider provider = McpServerEndpointProvider.builder()
                    .from(clz, anno).build();

            provider.addTool(new MethodToolProvider(clz, serverEndpoint));
            provider.addResource(new MethodResourceProvider(clz, serverEndpoint));
            provider.addPrompt(new MethodPromptProvider(clz, serverEndpoint));
            provider.postStart();

            // === Solon 3.3.1 特有的兼容性处理 ===
            // enableScanning(false) 模式下 SSE 路由可能未完整注册,需通过反射补救
            try {
                Object transport = provider.getClass()
                        .getDeclaredField("mcpTransportProvider").get(provider);
                if (transport != null) {
                    transport.getClass()
                            .getMethod("toHttpHandler", SolonApp.class)
                            .invoke(transport, Solon.app());
                }
            } catch (Exception e) {
                log.warn("[MCP] Manual route registration skipped: {}", e.getMessage());
            }
        }
        return this;
    }

    /**
     * 注册 MCP 鉴权 + Solon 桥接 Filter
     * /mcp/** 不经过 Spring MVC,必须在 Filter 层做鉴权
     */
    @Bean
    public FilterRegistrationBean<Filter> mcpServerFilter() {
        SolonServletFilter solonFilter = new SolonServletFilter();
        FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
        bean.setFilter((request, response, chain) -> {
            HttpSession session = ((HttpServletRequest) request).getSession(false);
            if (session == null || session.getAttribute("user") == null) {
                HttpServletResponse resp = (HttpServletResponse) response;
                resp.setStatus(401);
                resp.setContentType("application/json;charset=UTF-8");
                resp.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}");
                return;
            }
            solonFilter.doFilter(request, response, chain);
        });
        bean.addUrlPatterns("/mcp/*");
        bean.setOrder(1);
        return bean;
    }
}

有几个关键设计决策值得解释:

  • 为什么 enableScanning(false) 如果让 Solon 也扫描组件,会与 Spring 重复创建 Bean,导致依赖注入混乱。禁用后,Solon 只加载内置插件(Web/SSE),MCP 端点由我们手动注册。
  • 为什么要 AopUtils.getTargetClass() 如果端点类上有 @Transactional,Spring 会为它创建一个 CGLIB 代理类,代理类上找不到 @McpServerEndpoint 注解,必须拿到原始类。
  • 那段反射代码是什么? 这是 Solon 3.3.1 版本在 enableScanning(false) 模式下的一个 bug——SSE 路由注册不完整,需要通过反射手动补调 toHttpHandler()。Solon 3.4.x+ 已修复,不再需要。
  • 为什么鉴权放在 Filter 而不是 Interceptor? /mcp/** 路径经过 SolonServletFilter 转发,不会走 Spring MVC 的 DispatcherServlet,因此 HandlerInterceptor 无法拦截它。

第 4 步:编写 MCP 端点

MCP 支持三种能力类型:

能力注解大模型如何使用
Tool(工具)@ToolMapping"查一下北京天气" → 调用 getWeather 方法
Resource(资源)@ResourceMapping"应用版本是多少" → 读取 config://app-version
Prompt(提示词)@PromptMapping"生成一个提问" → 使用 askQuestion 模板

Tool 端点示例(天气查询):

@Service  // 用 Spring 的 @Service,不是 Solon 的
@McpServerEndpoint(name = "demo1", sseEndpoint = "/mcp/demo1/sse")
public class WeatherMcpEndpoint implements IMcpServerEndpoint {

    @ToolMapping(description = "查询指定城市的天气预报")
    public String getWeather(
            @Param(name = "location", description = "城市名称") String location) {
        // 这里可以调用任何 Spring Bean,比如 @Autowired 进来的 Service
        return location + ": 晴,14°C,东南风 3 级";
    }
}

Resource + Prompt 端点示例

@Service
@McpServerEndpoint(name = "demo2", sseEndpoint = "/mcp/demo2/sse")
public class AssistantMcpEndpoint implements IMcpServerEndpoint {

    // 静态资源(无参数)
    @ResourceMapping(uri = "config://app-version", description = "获取应用版本号")
    public String getAppVersion() {
        return "v1.2.3";
    }

    // 动态资源(URI 模板参数)
    @ResourceMapping(uri = "db://users/{user_id}/email", description = "根据用户ID查询邮箱")
    public String getEmail(@Param(name = "user_id", description = "用户ID") String user_id) {
        return user_id + "@example.com";
    }

    // 提示词模板
    @PromptMapping(description = "生成关于某个主题的提问")
    public Collection<ChatMessage> askQuestion(
            @Param(name = "topic", description = "主题") String topic) {
        return Arrays.asList(ChatMessage.ofUser("请解释一下'" + topic + "'的概念?"));
    }
}

有一个很好用的特性:你可以在已有的 @RestController 上直接叠加 @McpServerEndpoint,同一个方法同时支持 HTTP 和 MCP 调用,完全不用写重复代码:

@McpServerEndpoint(sseEndpoint = "/mcp/api/sse")
@RestController
@RequestMapping("/api")
public class OrderController {

    @ToolMapping(description = "查询订单详情")
    @GetMapping("/order/{id}")
    public Order getOrder(
            @Param(name = "orderId", description = "订单ID")
            @PathVariable String id) {
        return orderService.findById(id);
    }
}

第 5 步:添加 Solon 配置文件

src/main/resources/ 下创建 mcpserver.properties(可以为空,但必须存在):

# Solon MCP 配置文件
# 当前为空,使用默认配置
# 如需自定义端口等,可在此添加

⚠️ 注意:文件名后缀必须是 .properties,不能是 .yml。Solon 默认的 PropsLoader 不支持 YAML 格式(除非额外添加 solon-config-yaml 依赖)。这个坑我踩过,从 yml 改名后立刻解决。

四、启动验证

启动后,控制台会出现以下日志,确认两个 MCP 端点已成功注册:

[Solon] App: Start loading
[Solon] App: Plugin starting
[Solon] App: End loading elapsed=20ms

Mcp-Server started, name=demo1, channel=sse, sseEndpoint=/mcp/demo1/sse, toolRegistered=1
Mcp-Server started, name=demo2, channel=sse, sseEndpoint=/mcp/demo2/sse,
    toolRegistered=0, resourceRegistered=2, promptRegistered=1

Started McpApplication in 0.883 seconds

curl 快速验证 SSE 连接是否正常:

# 先登录获取 Cookie
curl -c cookies.txt -X POST http://localhost:8080/login \
  -d "username=admin&password=admin123"

# 验证 SSE 端点
curl -b cookies.txt -N http://localhost:8080/mcp/demo1/sse

正常返回:

event:endpoint
data:/mcp/demo1/sse/message?sessionId=xxxx-xxxx-xxxx

五、测试工具

项目内置了一个功能完整的 Python MCP 测试工具(mcp-tester/),零外部依赖,只用 Python 标准库,在完全离线的环境也能跑。

几个常用命令:

cd mcp-tester

# 全量测试(自动执行 SSE 连接 → Initialize → tools/list → tools/call → ...)
python mcp_tester.py http://localhost:8080/mcp/demo1/sse \
  --login-url http://localhost:8080/login \
  --username admin --password admin123

# 健康检查(只验证连接和握手,快速)
python mcp_tester.py --health http://localhost:8080/mcp/demo1/sse

# 单工具调用
python mcp_tester.py --call-tool getWeather \
  --args '{"location": "上海"}' \
  http://localhost:8080/mcp/demo1/sse

# 同时测试多个端点
python mcp_tester.py \
  http://localhost:8080/mcp/demo1/sse \
  http://localhost:8080/mcp/demo2/sse

测试报告输出:

============================================================
  Weather MCP (http://localhost:8080/mcp/demo1/sse)
============================================================
  [PASS] SSE 连接 (125.3ms)
  [PASS] Initialize 握手 (89.7ms)
  [PASS] notifications/initialized (12.1ms)
  [PASS] tools/list (45.2ms)
  [PASS] tools/call: getWeather (234.8ms)
  [WARN] resources/list (56.3ms)   ← WARN 表示该端点未实现此能力,非错误
  [WARN] prompts/list (43.1ms)
------------------------------------------------------------
  [PASS] Total: 7 | Passed: 5 | Failed: 0 | Warned: 2 | Time: 606.5ms
============================================================

六、接入 MCP 客户端

服务启动后,可以直接接入各主流 MCP 客户端:

Cherry Studio:设置 → MCP 服务器 → 添加 → 类型选 SSE → 填入 http://localhost:8080/mcp/demo1/sse

Claude Desktopclaude_desktop_config.json):

{
  "mcpServers": {
    "weather": {
      "url": "http://localhost:8080/mcp/demo1/sse"
    }
  }
}

Cursor:Settings → MCP → Add Server → SSE → 填入端点 URL

七、踩坑记录

这里汇总了开发过程中遇到的所有坑,给后来者省时间:

现象解决方法
配置文件后缀 启动报 profile is not supported: mcpserver.yml 改为 .properties 后缀
缺少 -parameters 编译参数 大模型调用工具时参数传错,参数名变成 arg0/arg1 maven-compiler-plugin 加 <arg>-parameters</arg>
AOP 代理导致注解丢失 @McpServerEndpoint 端点注册不上 AopUtils.getTargetClass() + AnnotationUtils.findAnnotation()
Solon 3.3.1 路由注册不完整 curl 测试 SSE 端点返回 404 反射调用 toHttpHandler() 补注册路由
缺少 solon-lib 依赖 启动时 ClassNotFound 异常 显式添加 solon-lib(3.3.1 必须,3.4.x+ 不需要)
缺少 reactor-core 依赖 SSE 连接时 NoClassDefFoundError: reactor/core/publisher/Sinks 显式添加 reactor-core 3.4.34
鉴权放在 Interceptor /mcp/** 路径鉴权失效,未登录也能访问 改为在 Servlet Filter 层做鉴权
Solon 3.3.1 resources/read Bug 读取 Resource 时 SSE 连接断开 升级到 Solon 3.4.x+(已修复)

Solon 3.3.1 vs 3.4.x+ 对比

如果你新建项目,建议直接用 3.4.x+,省掉几个 workaround:

差异点3.3.13.4.x+
需要显式添加 solon-lib不需要
需要显式添加 reactor-core不需要
需要反射调用 toHttpHandler()不需要
resources/read Jackson Bug存在已修复

八、项目结构速览

springboot2-solon-mcp-demo/
├── pom.xml
├── src/main/java/com/example/mcp/
│   ├── McpApplication.java              # SpringBoot 启动类
│   ├── config/
│   │   ├── McpServerConfig.java         # ⭐ 双容器桥接核心(本文重点)
│   │   └── WebMvcConfig.java            # Spring MVC 拦截器配置
│   ├── controller/
│   │   └── LoginController.java         # 登录/退出接口
│   ├── interceptor/
│   │   └── AuthInterceptor.java         # Spring MVC 路径鉴权
│   └── endpoint/
│       ├── IMcpServerEndpoint.java      # MCP 端点标识接口
│       ├── WeatherMcpEndpoint.java      # Tool 示例
│       └── AssistantMcpEndpoint.java   # Resource + Prompt 示例
├── src/main/resources/
│   ├── application.yml                  # SpringBoot 配置
│   └── mcpserver.properties            # Solon 配置(可空,必须存在)
└── mcp-tester/
    ├── mcp_tester.py                    # 完整测试工具
    ├── sse_client.py                    # SSE 协议客户端
    └── reporter.py                      # 测试报告生成器

九、迁移检查清单

应用到你的真实项目时,逐项核对:

  • pom.xml 添加了 solon-parent BOM(dependencyManagement)
  • ☐ 添加了 solon-ai-mcp、solon-lib、solon-web-servlet、spring-boot-starter-aop、reactor-core
  • ☐ maven-compiler-plugin 配置了 -parameters
  • ☐ SpringBoot 2.x 用 solon-web-servlet(javax.servlet),不是 jakarta 版
  • ☐ 创建了 IMcpServerEndpoint 空接口
  • ☐ 创建了 McpServerConfig(start/stop/init/mcpServerFilter 四个方法)
  • Solon.start() 设置了 enableScanning(false)
  • ☐ 配置文件命名为 mcpserver.properties(不是 .yml)
  • ☐ MCP 端点用 Spring 的 @Service(不是 Solon 的注解)
  • ☐ MCP 端点实现了 IMcpServerEndpoint 接口
  • @Param 注解显式指定了 name 属性
  • ☐ 如需鉴权,在 Filter 层实现(不是 Interceptor)
  • ☐ 启动后 Solon 日志确认端点注册成功

结语

MCP 把大模型和业务系统之间的接口标准化了。对于还跑在 Java 8 上的企业存量系统,Solon-mcp 是目前唯一的入场券。

这套方案的核心价值在于零侵入——你不需要把项目迁移到 Java 17,不需要重构任何业务代码,加几个配置文件和一个配置类,就能让大模型直接调用你的接口。在银行、保险、政务等 Java 8 项目大量存在的行业,这个方案的价值会更明显。

完整代码见 GitHub 仓库,有问题欢迎提 Issue。