周一研究了一下spring ai alibaba, 水一篇 (非AI生成, 纯手打)
对应源代码: github.com/uniquejava/…
技术栈
- java 17
- 模型: 包含 openai, ollama 和 阿里的 dashscope.
- spring ai alibaba 1.0.0.2: github.com/alibaba/spr…
- ant design x: x.ant.design/
- webstorm或idea的lingma插件: lingma.aliyun.com/
- 简单的 mcp server
- 简单的 mcp client
- 简单的 frontend (with ant design x)
- 记忆: Chat memory with MySQL 8
TLDR
- 搭建项目, 一个父目录 spring-ai-mcp-demo-2025, 3个子项目
- 编写 mcp server
- 编写 mcp client
- 编写 frontend, 我搭框,然后聊天界面是拷贝别人的代码, 用lingma插件 帮我 修改/适配
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测试.
我试了Apifox, 不支持测试 mcp
3. 编写 mcp client
调用链: RestController -> 自定义 AIService -> Chat Client -> MCP Server
需要的依赖:
- spring-ai-alibaba-starter-dashscope
- spring-ai-starter-mcp-client
- mysql (可选, 如果需要持久化chat message到mysql, 默认是内存)
- spring-ai-alibaba-starter-memory (可选)
- 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
, 给前端提供两个接口
/chat?conversation_id=default
用来和AI对话 (SSE流式输出)/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/
- 测试mcp调用, 观察后台的日志
- 重启服务, 观察记录是否依然存在
- 查看数据库表
6. 后续
- 和deepseek一样, 左侧加入多 conversation
- 使用 spring ai langgraph, 实现 agentic ai
- 加入spring authorization server, 使用 oauth2/oidc 保护 mcp server
- 加入Prometheus三件套, Promethues, Loki 和 Tempo
- 部署到kubernetes集群环境.
- 用 Gitlab CI 做 CICD.