spring ai alibaba mcp后端 + ant design x前端速通

8 阅读5分钟

周一研究了一下spring ai alibaba, 水一篇 (非AI生成, 纯手打)

对应源代码: github.com/uniquejava/…

frontend.png

技术栈

  1. java 17
  2. 模型: 包含 openai, ollama 和 阿里的 dashscope.
  3. spring ai alibaba 1.0.0.2: github.com/alibaba/spr…
  4. ant design x: x.ant.design/
  5. webstorm或idea的lingma插件: lingma.aliyun.com/
  6. 简单的 mcp server
  7. 简单的 mcp client
  8. 简单的 frontend (with ant design x)
  9. 记忆: Chat memory with MySQL 8

TLDR

  1. 搭建项目, 一个父目录 spring-ai-mcp-demo-2025, 3个子项目
  2. 编写 mcp server
  3. 编写 mcp client
  4. 编写 frontend, 我搭框,然后聊天界面是拷贝别人的代码, 用lingma插件 帮我 修改/适配

project structure.png

1. 搭建项目

1.1 父目录

命令行

cd /Users/cyper/github
mkdir spring-ai-mcp-demo-2025

也可以在IDEA中建一个Empty project: spring-ai-mcp-demo-2025

1.2 前端子项目

使用 vite 搭建 react 19 + ts的项目

  • node 20.15
  • vite 7.0
  • react 19
cd spring-ai-mcp-demo-2025
pnpm create vite frontend --template react-ts
pnpm add -D less prettier
pnpm add -D @types/node
  • 修改 vite.config.ts, 加入alias 和 vite 解析less 文件的 css 配置
  • 修改 tsconfig.json, 加入 baseUrl 和 paths, 以便 IDE 能识别 alias
  • 修改 .editorconfig 和 .prettierrc.cjs, 以便 webstorm 和 vscode 有相同的代码格式化风格

集成 antd 和 antdx, 文档: x.ant.design/docs/react/…

# p是pnpm的alias
p add antd
p add @ant-design/icons
p add @ant-design/x
p add react-router

完整代码见: github.com/uniquejava/…

1.3 后端子项目

使用 spring starter或idea或spring cli创建spring boot项目, 我用3.4.5也可以用最新的3.5.0: start.spring.io/

2. 编写 mcp serer

仅需一个依赖: mcp server! ( 不用选web)

确保你的pom.xml里包含如下依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

修改 application.properties加入

server.port=9090
spring.application.name=sse-mcp-server-demo
spring.ai.mcp.server.enabled=true
spring.ai.mcp.server.stdio=false
spring.ai.mcp.server.name=sse-mcp-server-demo
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.sse-endpoint=/api/v1/sse
spring.ai.mcp.server.sse-message-endpoint=/api/v1/mcp/message
spring.ai.mcp.server.type=sync
logging.level.io.modelcontextprotocol=TRACE
logging.level.org.springframework.ai.mcp=TRACE

编写 MathTools.java 用来告诉 Agent如何做加法和减法运算

package top.billcat.ssemcpserverdemo;

import org.springframework.ai.tool.annotation.Tool;

public class MathTools {
    @Tool(description = "Add two numbers.")
    public int add(int a, int b) {
        return a + b;
    }

    @Tool(description = "Multiply two numbers")
    public int multiply(int a, int b) {
        return a * b;
    }
}

编写 DateTimeTools.java, 告诉 Agent如何获得当前时间

package top.billcat.ssemcpserverdemo;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;

import java.time.LocalDateTime;
import java.time.ZonedDateTime;

public class DateTimeTools {

    @Tool(description = "Get the current date and time in the user's timezone")
    public String getCurrentDateTime() {
       ZonedDateTime localTime = LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId());
       System.out.println("Current time: " + localTime);
       return localTime.toString();
    }
}

将这个两个 Method tool注册到 spring 上下文

@SpringBootApplication
public class SseMcpServerDemoApplication {

    public static void main(String[] args) {
       SpringApplication.run(SseMcpServerDemoApplication.class, args);
    }

    @Bean
    public ToolCallbackProvider mathTools() {
       return  MethodToolCallbackProvider.builder().toolObjects(new MathTools()).build();
    }
    @Bean
    public ToolCallbackProvider dateTimeTools() {
       return  MethodToolCallbackProvider.builder().toolObjects(new DateTimeTools()).build();
    }
}

现在, mcp server 就搭建好了. 正常启动main函数, mcp server会监听9090端口, 可以用最新版的 postman 对mcp server 进行connect/disconnect/tools测试.

postman.png

我试了Apifox, 不支持测试 mcp

3. 编写 mcp client

调用链: RestController -> 自定义 AIService -> Chat Client -> MCP Server

需要的依赖:

  1. spring-ai-alibaba-starter-dashscope
  2. spring-ai-starter-mcp-client
  3. mysql (可选, 如果需要持久化chat message到mysql, 默认是内存)
  4. spring-ai-alibaba-starter-memory (可选)
  5. spring-ai-alibaba-starter-memory-jdbc (可选)

确保你的pom.xml里包含如下依赖:

注意, 我配置了3个模型: openai, ollama和 dashscope, 3选一即可, 不可以同时开启

<dependencies>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    </dependency>

<!--        <dependency>-->
<!--            <groupId>org.springframework.ai</groupId>-->
<!--            <artifactId>spring-ai-starter-model-ollama</artifactId>-->
<!--        </dependency>-->

<!--        <dependency>-->
<!--            <groupId>org.springframework.ai</groupId>-->
<!--            <artifactId>spring-ai-starter-model-openai</artifactId>-->
<!--        </dependency>-->

    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-client</artifactId>
    </dependency>

    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-memory</artifactId>
    </dependency>

    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-memory-jdbc</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
        <version>${lombok.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-bom</artifactId>
            <version>1.0.0.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

修改 application.properties加入

注意, 我配置了3个模型, 3选一即可, pom.xml里只能有一个依赖,但是配置文件里可以多写几个备用

spring:
  application:
    name: mcp-client-demo
  ai:
    dashscope:
      api-key: ${AI_DASHSCOPE_API_KEY}
      chat:
        options:
          model: qwen-plus
    ollama:
      base-url: http://localhost:11434
      chat:
          options:
              model: llama3.2:1b
    openai:
      api-key: ${AI_OPENAI_API_KEY}
      base-url: ${AI_OPENAI_BASE_URL}
      chat:
        options:
          model: o4-mini-2025-04-16
    mcp:
      client:
        enabled: true
        name: ${spring.application.name}
        version: 1.0.0
        initialized: true
        request-timeout: 20s
        type: sync
        root-change-notification: true
        toolcallback:
          enabled: true
        sse:
          connections:
            # a list of McpSyncClient
            server1:
              url: http://localhost:9090
              sse-endpoint: /api/v1/sse
    memory:
      mysql.enabled: true
  logging.level:
    io.modelcontextprotocol: TRACE
    org.springframework.ai.mcp: TRACE

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring_ai_alibaba_mysql?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
    username: root
    password: root

编写 Rest Controller: ChatController.java, 给前端提供两个接口

  1. /chat?conversation_id=default 用来和AI对话 (SSE流式输出)
  2. /messages 用来获取聊天历史记录
@RestController
@CrossOrigin(origins = "*")
public class ChatController {
    private final AiService aiService;

    public ChatController(AiService aiService) {
        this.aiService = aiService;
    }

    @PostMapping(value = "/chat")
    public Flux<String> advisorChat(HttpServletResponse response, @RequestBody MessageDTO messageDTO) {
        response.setCharacterEncoding("UTF-8");
        return aiService.complete(messageDTO.getQuery());
    }

    @GetMapping(value = "/messages")
    public List<Message> advisorChat(@RequestParam(value = "conversation_id", defaultValue = ChatMemory.DEFAULT_CONVERSATION_ID) String conversationId) {
        return aiService.messages(conversationId);
    }
}

编写 Service 接口: AiService.java

package top.billcat.mcpclientdemo.service;

import org.springframework.ai.chat.messages.Message;
import reactor.core.publisher.Flux;

import java.util.List;

public interface AiService {
    Flux<String> complete(String message);
    List<Message> messages(String conversationId);
}

编写 Service Impl: AiServiceImpl.java


@Service
public class AiServiceImpl implements AiService {
    private final ChatClient chatClient;
    private final MessageWindowChatMemory messageWindowChatMemory;

    public AiServiceImpl(ChatClient.Builder builder,
                         JdbcChatMemoryRepository jdbcChatMemoryRepository,
                         ToolCallbackProvider toolCallbackProvider) {
        this.messageWindowChatMemory = MessageWindowChatMemory.builder()
                .chatMemoryRepository(jdbcChatMemoryRepository)
                .maxMessages(100).build();
        this.chatClient = builder
                .defaultToolCallbacks(toolCallbackProvider)
                .defaultAdvisors(
                        MessageChatMemoryAdvisor.builder(messageWindowChatMemory).build(),
                        new SimpleLoggerAdvisor()
                )
                .build();
    }

    @Override
    public Flux<String> complete(String message) {
        return this.chatClient.prompt()
                .user(message)
                .stream()
                .content();
    }

    @Override
    public List<Message> messages(String conversationId) {
        return messageWindowChatMemory.get(conversationId);
    }
}

4. 编写 frontend (使用lingma插件辅助编写)

参考 前面的步骤搭建 vite 7 + react 19 + antd 5的框架代码, 然后

从这里拷贝聊天界面: github.com/yossi-lee/s…

扔到: frontend/src/pages/chat/Chat.tsx

注意这个项目是umi + react 18, 和我的技术栈不同, 我喜欢从0开始搭建: 用的 vite7 + react 19

把 ChatController.java 扔给 lingma插件, 让他根据我的 ChatController 修改前端代码, 经过lingma插件的不懈努力, 最终它给我生成了如下代码 (还贴心的给我加了一点中文注释):

import { UserOutlined } from '@ant-design/icons';
import { Flex, Layout } from 'antd';
import { Bubble, Sender } from '@ant-design/x';
import { useState } from 'react';
import './Chat.less';
import { getMessages, sendMessage } from '@/api/apiService.ts';
import { useEffectAsync } from '@/utils/utils.ts';

// 定义允许匿名对象扩展的Message类型
type ChatMessage = {
  content?: string;
  sender?: string;
};

export default function HomePage() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [value, setValue] = useState<string>('');

  const chatId = 'default';

  useEffectAsync(async () => {
    const backendMessages = await getMessages(chatId);
    // 将后端消息格式映射为前端所需的格式
    const frontendMessages = backendMessages.map((msg) => ({
      content: msg.text,
      sender: msg.messageType === 'USER' ? 'user' : 'assistant',
    }) satisfies ChatMessage);
    setMessages((prevMessages: ChatMessage[]) => [
      ...prevMessages,
      ...frontendMessages
    ]);
  }, []);

  const handleSendMessage = async () => {
    if (!value.trim()) return;

    // 添加用户消息到聊天记录
    setMessages((prevMessages) => [
      ...prevMessages,
      { content: value, sender: 'user' },
    ]);

    setValue('');

    try {
      let accumulatedResponse = '';
      await sendMessage(chatId, value, (chunk) => {
        accumulatedResponse += chunk;
        console.log(chunk);

        setMessages((prevMessages) => {
          if (prevMessages[prevMessages.length - 1]?.sender === 'assistant') {
            return [
              ...prevMessages.slice(0, -1),
              { content: accumulatedResponse, sender: 'assistant' },
            ];
          }
          return [...prevMessages, { content: accumulatedResponse, sender: 'assistant' }];
        });
      });
    } catch (error) {
      console.error('Failed to send message:', error);
    }
  };

  return (
    <Layout style={{ height: '90vh' }}>
      <Flex gap="middle" vertical>
        {messages.map((msg, index) => (
          <Bubble
            key={index}
            placement={msg.sender === 'user' ? 'end' : 'start'}
            content={msg.content}
            avatar={{ icon: <UserOutlined /> }}
          />
        ))}
      </Flex>
      <div className="sender-container">
        <Sender
          value={value}
          onChange={(v) => {
            setValue(v);
          }}
          onSubmit={handleSendMessage}
          autoSize={{ minRows: 2, maxRows: 6 }}
        />
      </div>
    </Layout>
  );
}

5. 测试

启动mcp server (9090) 端口

启动 mcp client (8080) 端口

启动前端: pnpm dev, 访问: http://localhost:5173/

  1. 测试mcp调用, 观察后台的日志
  2. 重启服务, 观察记录是否依然存在
  3. 查看数据库表

datagrip.png

6. 后续

image.png

  1. 和deepseek一样, 左侧加入多 conversation
  2. 使用 spring ai langgraph, 实现 agentic ai
  3. 加入spring authorization server, 使用 oauth2/oidc 保护 mcp server
  4. 加入Prometheus三件套, Promethues, Loki 和 Tempo
  5. 部署到kubernetes集群环境.
  6. 用 Gitlab CI 做 CICD.