2026 进阶篇:Spring Boot响应式编程 + Spring AI 1.1.4 流式实战 + Vue前端完整实现(避坑指南)
作者 :12年OTA公司资深程序员
技术栈 :Spring Boot 3.5.9 + Spring AI 1.1.4 + Reactor + OpenAI API
适用人群:Java开发者、AI应用开发者、对响应式编程感兴趣的技术人员

📖 前言
本文是《2026 新标配:Spring AI 最新版本1.1.4 从零搭建 + 避坑指南》的进阶篇
在AI时代,传统的阻塞式编程已经无法满足高并发、低延迟的AI应用需求。作为一名拥有12年经验的OTA(在线旅游)公司程序员,我深刻感受到:响应式编程 + AI = 未来应用的核心竞争力。
今天,我将基于一个真实的酒店智能助手项目,带你深入理解:
- ✅ Spring Boot响应式编程的核心原理(Reactor模型)
- ✅ Spring AI 1.1.4 流式输出的完整实现
- ✅ Vue前端消费SSE数据的4种方案
- ✅ 实际开发中的采坑经验与解决方案
- ✅ 生产级别的前后端完整代码实践
如果你还没有阅读基础篇,建议先查看:
👉2026 新标配:Spring AI 最新版本1.1.4 从零搭建 + 避坑指南(收藏版)
🎯 一、为什么需要响应式编程?
1.1 传统阻塞式编程的痛点
在传统的Spring MVC应用中,每个请求都会占用一个线程:
java
// 传统阻塞方式
@GetMapping("/chat")
public String chat(String question) {
// 调用AI接口,阻塞等待响应(可能耗时2-5秒)
String response = aiService.call(question);
return response;
}
问题:
- 🔴 线程资源浪费:等待AI响应期间,线程处于空闲状态
- 🔴 并发能力差:100个并发请求需要100个线程
- 🔴 用户体验差:必须等待全部生成完成才能看到结果
1.2 响应式编程的优势
响应式编程基于事件驱动 和非阻塞IO:
java
// 响应式非阻塞方式
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(String message) {
return chatClient.prompt()
.user(message)
.stream()
.content(); // 流式返回,每个token实时推送
}
优势:
- 🟢 资源高效:少量线程即可处理大量并发
- 🟢 实时反馈:用户可以看到逐字生成的效果
- 🟢 背压支持:自动处理生产者与消费者的速度差异
🔧 二、Spring Boot响应式编程核心原理
2.1 Reactor模型详解
Spring的响应式编程基于Project Reactor,核心是两个接口:
Mono:0或1个元素
java
// 适用于单次查询、单个结果
Mono<AiResponse> chat2(String question) {
return chatClient.prompt()
.user(question)
.call()
.entity(AiResponse.class);
}
Flux:0到N个元素
java
// 适用于流式输出、列表数据
Flux<String> streamChat(String message) {
return chatClient.prompt()
.user(message)
.stream()
.content();
}
2.2 响应式执行流程图
客户端请求
↓
WebFlux Dispatcher (非阻塞)
↓
Controller (返回Mono/Flux)
↓
Reactor Scheduler (事件循环)
↓
异步调用AI API (非阻塞HTTP客户端)
↓
数据就绪时触发回调
↓
SSE/WebSocket推送给客户端
关键点:整个链路没有线程阻塞,所有操作都是异步回调!
2.3 线程模型对比
| 特性 | 传统Servlet | WebFlux响应式 |
|---|---|---|
| 线程模型 | 每请求一线程 | 事件循环(少数线程) |
| 并发能力 | 受线程池限制 | 理论上无上限 |
| 内存占用 | 高(线程栈) | 低 |
| 适用场景 | CPU密集型 | IO密集型(AI调用) |
🤖 三、Spring AI响应式搭建实战
3.1 项目依赖配置
xml
<properties>
<java.version>17</java.version>
<spring-ai.version>1.1.4</spring-ai.version>
</properties>
<dependencies>
<!-- Spring Boot Web (支持响应式) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI OpenAI Starter -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</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>
</dependencies>
</dependencyManagement>
3.2 配置文件(application.yml)
yaml
spring:
ai:
openai:
api-key: sk-your-api-key
base-url: https://modelhub.ailemac.com
chat:
options:
model: deepseek-v3.1
temperature: 0.1
max-tokens: 1024
completions-path: /v1/chat/completions
image:
options:
model: gpt-image-2
generations-path: /v1/images/generations
3.3 ChatClient初始化(构造函数注入)
java
@RestController
@RequestMapping("/chat")
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("""
你是酒店智能助手,只能处理:退房、续住、查询状态、打扫、预订。
必须严格按要求返回JSON,禁止多余内容。
""")
.build();
}
}
为什么要用构造函数注入?
- ✅ 保证不可变性(final字段)
- ✅ 便于单元测试
- ✅ 符合Spring最佳实践
💡 四、核心功能实现与代码解析
4.1 流式对话接口(SSE)
这是本项目的核心亮点,实现真正的实时推流:
java
@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public ResponseEntity<Flux<String>> streamChat(
@RequestParam String message,
@RequestParam(required = false, defaultValue = "") String systemPrompt) {
var promptBuilder = chatClient.prompt();
// 支持自定义系统提示词
if (systemPrompt != null && !systemPrompt.isEmpty()) {
promptBuilder.system(systemPrompt);
}
Flux<String> flux = promptBuilder
.user(message)
.stream()
.content()
.map(content -> {
try {
// JSON格式封装,避免中文乱码
Map<String, Object> eventData = new HashMap<>();
eventData.put("content", content);
eventData.put("timestamp", LocalDateTime.now().toString());
String json = objectMapper.writeValueAsString(eventData);
return "data: " + json + "\n\n";
} catch (Exception e) {
return "data: {\"error\": \"序列化失败\"}\n\n";
}
});
HttpHeaders headers = new HttpHeaders();
headers.setContentType(new MediaType("text", "event-stream", StandardCharsets.UTF_8));
headers.setCacheControl("no-cache");
headers.setConnection("keep-alive");
return ResponseEntity.ok().headers(headers).body(flux);
}
关键技术点:
produces = MediaType.TEXT_EVENT_STREAM_VALUE:声明SSE响应类型.stream().content():启用流式模式"data: " + json + "\n\n":SSE协议格式(必须双换行)UTF-8编码:解决中文乱码问题
4.2 意图识别接口(非流式)
使用Mono返回结构化JSON:
java
@RequestMapping(path = "/chat3")
public Mono<AiResponse> chat3(@RequestParam String question) {
String systemText = "用户输入:{question}\n" +
"返回JSON格式:\n" +
"{\n" +
" \"intent\": \"CHECK_OUT/EXTEND_STAY/QUERY/CLEAN/BOOK/UNKNOWN\",\n" +
" \"roomNo\": \"房间号,无则为空\",\n" +
" \"days\": 续住天数(数字,无则为null),\n" +
" \"roomType\": \"房型,无则为空\",\n" +
" \"checkInDate\": \"入住日期,无则为空\"\n" +
"}";
return Mono.fromCallable(() ->
chatClient.prompt()
.system(systemText)
.user(question)
.call()
.entity(IntentResponse.class)
).map(intentResponse -> {
AiResponse aiResponse = new AiResponse();
aiResponse.setCode(200);
aiResponse.setMsg("success");
aiResponse.setIntent(intentResponse.getIntent());
aiResponse.setData(intentResponse.getRoomNo());
return aiResponse;
});
}
设计思路:
- 通过Prompt Engineering让AI返回标准JSON
- 使用
IntentResponse类进行类型安全映射 - 包装成统一的
AiResponse返回给前端
4.3 图片生成功能
java
@PostMapping("/generate")
public Map<String, Object> generateImage(
@RequestParam String prompt,
@RequestParam(required = false, defaultValue = "1024x1024") String size,
@RequestParam(required = false, defaultValue = "1") Integer n) {
// 解析尺寸
String[] dimensions = size.split("x");
int width = Integer.parseInt(dimensions[0]);
int height = dimensions.length > 1 ? Integer.parseInt(dimensions[1]) : width;
// 构建选项
OpenAiImageOptions options = OpenAiImageOptions.builder()
.model("gpt-image-2")
.width(width)
.height(height)
.N(n)
.build();
// 生成图片
ImagePrompt imagePrompt = new ImagePrompt(prompt, options);
ImageResponse response = imageModel.call(imagePrompt);
// 返回URL或Base64
Map<String, Object> result = new HashMap<>();
if (response != null && response.getResults() != null && !response.getResults().isEmpty()) {
var output = response.getResult().getOutput();
if (output.getUrl() != null && !output.getUrl().isEmpty()) {
result.put("imageUrl", output.getUrl());
result.put("imageType", "url");
} else if (output.getB64Json() != null && !output.getB64Json().isEmpty()) {
result.put("imageBase64", output.getB64Json());
result.put("imageType", "base64");
}
}
return result;
}
⚠️ 五、踩坑实录与解决方案
坑1:中文乱码问题
现象 :SSE流式输出时,中文显示为???或乱码
原因:默认编码不是UTF-8
解决方案:
java
// 1. application.yml中配置
spring:
http:
encoding:
charset: UTF-8
enabled: true
force: true
// 2. 响应头明确指定UTF-8
headers.setContentType(new MediaType("text", "event-stream", StandardCharsets.UTF_8));
// 3. 使用JSON包装而非直接返回字符串
Map<String, Object> data = new HashMap<>();
data.put("text", content);
return "data: " + objectMapper.writeValueAsString(data) + "\n\n";
坑2:Flux订阅时机问题
现象:接口立即返回,但AI调用未执行
原因:Reactor是懒执行的,必须有订阅者才会触发
解决方案:
java
// ❌ 错误:不要手动调用subscribe()
flux.subscribe(); // 这会导致异步执行,无法返回给客户端
// ✅ 正确:Spring WebFlux会自动订阅
return ResponseEntity.ok().body(flux); // 框架会处理订阅
坑3:错误处理缺失
现象:AI调用失败时,客户端收到不完整的SSE流
解决方案:
java
Flux<String> flux = chatClient.prompt()
.user(message)
.stream()
.content()
.map(content -> "data: " + content + "\n\n")
.onErrorResume(error -> {
log.error("AI调用失败", error);
return Flux.just("data: {\"error\": \"" + error.getMessage() + "\"}\n\n");
})
.doOnComplete(() -> log.info("流式输出完成"))
.doOnError(error -> log.error("流式输出异常", error));
坑4:图片生成API返回格式不一致
现象:有时返回URL,有时返回Base64
原因:不同模型、不同服务商的返回格式不同
解决方案:
java
// 兼容两种格式
if (output.getUrl() != null && !output.getUrl().isEmpty()) {
result.put("imageUrl", output.getUrl());
result.put("imageType", "url");
} else if (output.getB64Json() != null && !output.getB64Json().isEmpty()) {
result.put("imageBase64", output.getB64Json());
result.put("imageType", "base64");
}
// 添加调试日志
System.out.println("URL: " + output.getUrl());
System.out.println("Base64: " + output.getB64Json());
坑5:Lombok注解处理器配置
现象:编译时报错,找不到getter/setter
原因:Maven编译器插件未正确配置Lombok注解处理器
解决方案:
xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
坑6:Spring AI版本兼容性
现象 :ChatClient.Builder找不到方法
原因:Spring AI 1.0.x和1.1.x API有变化
解决方案:
xml
<!-- 统一使用BOM管理版本 -->
<spring-ai.version>1.1.4</spring-ai.version>
<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>
</dependencies>
</dependencyManagement>
🎨 六、架构设计与最佳实践
6.1 分层架构
┌─────────────────────────────────┐
│ Controller层 │
│ - 接收HTTP请求 │
│ - 参数校验 │
│ - 返回Mono/Flux │
└──────────────┬──────────────────┘
│
┌──────────────▼──────────────────┐
│ Service层(可选) │
│ - 业务逻辑 │
│ - 多个AI调用编排 │
└──────────────┬──────────────────┘
│
┌──────────────▼──────────────────┐
│ AI Client层 │
│ - ChatClient │
│ - ImageModel │
└──────────────┬──────────────────┘
│
┌──────────────▼──────────────────┐
│ OpenAI API │
│ - 聊天模型 │
│ - 图片模型 │
└─────────────────────────────────┘
6.2 响应式编程黄金法则
- 永远不要阻塞 :避免在响应式链中使用
.block() - 错误处理前置 :使用
onErrorResume、onErrorReturn - 资源清理 :使用
doFinally、doOnTerminate - 日志记录 :使用
doOnNext、doOnError打点 - 背压处理 :使用
onBackpressureBuffer、limitRate
6.3 性能优化建议
java
// 1. 连接池复用(Spring AI自动处理)
// 2. 响应缓存(对于相同问题)
@Cacheable(value = "ai-responses", key = "#question")
public Mono<String> cachedChat(String question) { ... }
// 3. 限流保护
.limitRate(10) // 限制每秒最多10个请求
// 4. 超时控制
.timeout(Duration.ofSeconds(30))
🎨 八、Vue前端消费响应式数据完整指南
8.1 方案一:使用EventSource API(推荐)
这是最简单、最原生的SSE消费方式,适合大多数场景。
基础用法
vue
<template>
<div class="chat-container">
<div class="messages">
<div v-for="(msg, index) in messages" :key="index" class="message">
<span class="role">{{ msg.role }}:</span>
<span class="content">{{ msg.content }}</span>
</div>
</div>
<div class="input-area">
<input v-model="userInput" @keyup.enter="sendMessage" placeholder="输入消息..." />
<button @click="sendMessage" :disabled="isStreaming">发送</button>
<button @click="stopStream" v-if="isStreaming">停止</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const userInput = ref('');
const messages = ref([]);
const isStreaming = ref(false);
let eventSource = null;
const sendMessage = () => {
if (!userInput.value.trim() || isStreaming.value) return;
// 添加用户消息
messages.value.push({
role: '用户',
content: userInput.value
});
// 添加AI回复占位
const aiMessageIndex = messages.value.length;
messages.value.push({
role: 'AI',
content: ''
});
startStream(userInput.value, aiMessageIndex);
userInput.value = '';
};
const startStream = (message, messageIndex) => {
isStreaming.value = true;
// 创建EventSource连接
const url = `/chat/stream?message=${encodeURIComponent(message)}`;
eventSource = new EventSource(url);
// 监听消息事件
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// 追加内容到AI消息
if (data.content) {
messages.value[messageIndex].content += data.content;
}
// 滚动到底部
scrollToBottom();
} catch (error) {
console.error('解析SSE数据失败:', error);
}
};
// 监听错误
eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
closeStream();
// 如果内容为空,显示错误提示
if (!messages.value[messageIndex].content) {
messages.value[messageIndex].content = '❌ 连接失败,请重试';
}
};
};
const stopStream = () => {
closeStream();
isStreaming.value = false;
};
const closeStream = () => {
if (eventSource) {
eventSource.close();
eventSource = null;
isStreaming.value = false;
}
};
const scrollToBottom = () => {
setTimeout(() => {
const container = document.querySelector('.messages');
if (container) {
container.scrollTop = container.scrollHeight;
}
}, 0);
};
// 组件卸载时关闭连接
import { onUnmounted } from 'vue';
onUnmounted(() => {
closeStream();
});
</script>
<style scoped>
.chat-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.messages {
height: 500px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 10px;
margin-bottom: 10px;
}
.message {
margin: 10px 0;
padding: 8px;
border-radius: 4px;
}
.message:nth-child(odd) {
background-color: #f0f0f0;
}
.role {
font-weight: bold;
margin-right: 8px;
}
.input-area {
display: flex;
gap: 10px;
}
input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background-color: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
</style>
进阶用法:支持自定义系统提示词
javascript
const startStreamWithSystemPrompt = (message, systemPrompt, messageIndex) => {
isStreaming.value = true;
const params = new URLSearchParams({
message: message,
systemPrompt: systemPrompt || ''
});
const url = `/chat/stream?${params.toString()}`;
eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.content) {
messages.value[messageIndex].content += data.content;
scrollToBottom();
}
};
eventSource.onerror = (error) => {
console.error('SSE错误:', error);
closeStream();
};
};
8.2 方案二:使用Axios + Fetch API
如果需要更精细的控制(如请求头、超时等),可以使用Fetch API。
vue
<script setup>
import { ref } from 'vue';
const streamWithFetch = async (message) => {
isStreaming.value = true;
try {
const response = await fetch(`/chat/stream?message=${encodeURIComponent(message)}`, {
method: 'GET',
headers: {
'Accept': 'text/event-stream'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('流式传输完成');
break;
}
// 解码数据块
buffer += decoder.decode(value, { stream: true });
// 处理SSE格式数据
const lines = buffer.split('\n\n');
buffer = lines.pop(); // 保留不完整的部分
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.substring(6); // 去掉 "data: " 前缀
try {
const data = JSON.parse(jsonStr);
if (data.content) {
currentAIMessage.value += data.content;
scrollToBottom();
}
} catch (e) {
console.error('解析JSON失败:', e);
}
}
}
}
} catch (error) {
console.error('流式请求失败:', error);
currentAIMessage.value = '❌ 请求失败: ' + error.message;
} finally {
isStreaming.value = false;
}
};
</script>
8.3 方案三:使用VueUse库(最优雅)
VueUse 提供了 useEventSource hook,让代码更简洁。
bash
npm install @vueuse/core
vue
<script setup>
import { ref } from 'vue';
import { useEventSource } from '@vueuse/core';
const userInput = ref('');
const messages = ref([]);
const currentAIMessage = ref('');
const { status, data, error } = useEventSource(
(url) => `/chat/stream?message=${encodeURIComponent(url)}`,
[],
{ immediate: false } // 不自动连接
);
const sendMessage = () => {
if (!userInput.value.trim()) return;
messages.value.push({
role: '用户',
content: userInput.value
});
currentAIMessage.value = '';
messages.value.push({
role: 'AI',
content: currentAIMessage
});
// 启动SSE连接
status.value = 'OPEN';
};
// 监听数据变化
watch(data, (newData) => {
if (newData && newData.length > 0) {
const lastEvent = newData[newData.length - 1];
try {
const parsed = JSON.parse(lastEvent.data);
if (parsed.content) {
currentAIMessage.value += parsed.content;
}
} catch (e) {
console.error('解析失败:', e);
}
}
});
// 监听错误
watch(error, (err) => {
if (err) {
console.error('SSE错误:', err);
}
});
</script>
8.4 方案四:非流式接口调用(Mono响应)
对于非流式接口(返回JSON),使用常规的axios调用即可。
vue
<script setup>
import { ref } from 'vue';
import axios from 'axios';
const intentResult = ref(null);
const loading = ref(false);
const analyzeIntent = async (question) => {
loading.value = true;
try {
const response = await axios.get('/chat/chat3', {
params: { question }
});
intentResult.value = response.data;
// 处理意图识别结果
if (intentResult.value.intent === 'CHECK_OUT') {
handleCheckOut(intentResult.value.data); // 房间号
} else if (intentResult.value.intent === 'EXTEND_STAY') {
handleExtendStay();
}
// ... 其他意图处理
} catch (error) {
console.error('意图识别失败:', error);
alert('分析失败,请重试');
} finally {
loading.value = false;
}
};
const handleCheckOut = (roomNo) => {
console.log(`办理退房,房间号: ${roomNo}`);
// 调用后端退房接口
};
const handleExtendStay = () => {
console.log('办理续住');
// 显示续住天数选择器
};
</script>
<template>
<div>
<input v-model="question" placeholder="输入您的问题" />
<button @click="analyzeIntent(question)" :disabled="loading">
{{ loading ? '分析中...' : '分析意图' }}
</button>
<div v-if="intentResult" class="result">
<p>意图: {{ intentResult.intent }}</p>
<p>房间号: {{ intentResult.data }}</p>
<p>消息: {{ intentResult.msg }}</p>
</div>
</div>
</template>
8.5 图片生成功能的前端实现
vue
<template>
<div class="image-generator">
<div class="input-section">
<textarea v-model="prompt" placeholder="描述你想生成的图片..." rows="3"></textarea>
<div class="options">
<select v-model="size">
<option value="1024x1024">1024x1024</option>
<option value="1024x1792">1024x1792(竖版)</option>
<option value="1792x1024">1792x1024(横版)</option>
</select>
<input type="number" v-model.number="n" min="1" max="4" placeholder="数量" />
</div>
<button @click="generateImage" :disabled="loading">
{{ loading ? '生成中...' : '生成图片' }}
</button>
</div>
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>正在生成图片,请稍候...</p>
<p class="tip">图片生成可能需要10-30秒</p>
</div>
<div v-if="imageUrl" class="result">
<img :src="imageUrl" alt="生成的图片" />
<a :href="imageUrl" download target="_blank">下载图片</a>
</div>
<div v-if="imageBase64" class="result">
<img :src="`data:image/png;base64,${imageBase64}`" alt="生成的图片" />
<button @click="downloadBase64">下载图片</button>
</div>
<div v-if="error" class="error">
{{ error }}
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import axios from 'axios';
const prompt = ref('');
const size = ref('1024x1024');
const n = ref(1);
const loading = ref(false);
const imageUrl = ref('');
const imageBase64 = ref('');
const error = ref('');
const generateImage = async () => {
if (!prompt.value.trim()) {
error.value = '请输入图片描述';
return;
}
loading.value = true;
error.value = '';
imageUrl.value = '';
imageBase64.value = '';
try {
const formData = new FormData();
formData.append('prompt', prompt.value);
formData.append('size', size.value);
formData.append('n', n.value);
const response = await axios.post('/image/generate', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
timeout: 60000 // 60秒超时
});
if (response.data.success) {
if (response.data.imageType === 'url') {
imageUrl.value = response.data.imageUrl;
} else if (response.data.imageType === 'base64') {
imageBase64.value = response.data.imageBase64;
}
} else {
error.value = response.data.error || '生成失败';
}
} catch (err) {
console.error('图片生成失败:', err);
error.value = err.response?.data?.error || '网络错误,请重试';
} finally {
loading.value = false;
}
};
const downloadBase64 = () => {
const link = document.createElement('a');
link.href = `data:image/png;base64,${imageBase64.value}`;
link.download = `generated-image-${Date.now()}.png`;
link.click();
};
</script>
<style scoped>
.image-generator {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.input-section {
margin-bottom: 20px;
}
textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
}
.options {
display: flex;
gap: 10px;
margin: 10px 0;
}
select, input[type="number"] {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background-color: #67c23a;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #ccc;
}
.loading {
text-align: center;
padding: 40px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #67c23a;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.tip {
color: #999;
font-size: 12px;
}
.result {
margin-top: 20px;
text-align: center;
}
.result img {
max-width: 100%;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.error {
color: #f56c6c;
padding: 10px;
background-color: #fef0f0;
border-radius: 4px;
margin-top: 10px;
}
</style>
8.6 封装可复用的SSE Hook(最佳实践)
创建一个通用的SSE工具类,方便在多个组件中复用。
javascript
// composables/useSSE.js
import { ref, onUnmounted } from 'vue';
export function useSSE() {
const data = ref('');
const isConnected = ref(false);
const error = ref(null);
let eventSource = null;
const connect = (url, options = {}) => {
const {
onMessage,
onError,
onOpen,
autoReconnect = false,
reconnectInterval = 3000
} = options;
// 关闭已有连接
disconnect();
try {
eventSource = new EventSource(url);
eventSource.onopen = () => {
isConnected.value = true;
error.value = null;
onOpen?.();
};
eventSource.onmessage = (event) => {
try {
const parsed = JSON.parse(event.data);
data.value += parsed.content || '';
onMessage?.(parsed);
} catch (e) {
console.error('解析SSE数据失败:', e);
}
};
eventSource.onerror = (err) => {
console.error('SSE错误:', err);
error.value = err;
onError?.(err);
if (autoReconnect && isConnected.value) {
setTimeout(() => {
connect(url, options);
}, reconnectInterval);
} else {
disconnect();
}
};
} catch (err) {
error.value = err;
onError?.(err);
}
};
const disconnect = () => {
if (eventSource) {
eventSource.close();
eventSource = null;
isConnected.value = false;
}
};
const reset = () => {
data.value = '';
error.value = null;
};
// 组件卸载时自动断开连接
onUnmounted(() => {
disconnect();
});
return {
data,
isConnected,
error,
connect,
disconnect,
reset
};
}
使用示例:
vue
<script setup>
import { useSSE } from '@/composables/useSSE';
const { data, isConnected, error, connect, disconnect, reset } = useSSE();
const sendMessage = (message) => {
reset();
connect(`/chat/stream?message=${encodeURIComponent(message)}`, {
onMessage: (parsed) => {
console.log('收到数据:', parsed);
// 实时更新UI
},
onError: (err) => {
console.error('连接错误:', err);
},
onOpen: () => {
console.log('SSE连接已建立');
},
autoReconnect: true,
reconnectInterval: 5000
});
};
</script>
<template>
<div>
<p>连接状态: {{ isConnected ? '已连接' : '未连接' }}</p>
<p>接收数据: {{ data }}</p>
<p v-if="error" style="color: red">错误: {{ error }}</p>
<button @click="disconnect">断开连接</button>
</div>
</template>
8.7 跨域配置(CORS)
如果前后端分离部署,需要配置CORS。
后端配置(WebConfig.java):
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:5173", "http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
Vite代理配置(vite.config.js):
javascript
export default defineConfig({
server: {
proxy: {
'/chat': {
target: 'http://localhost:8080',
changeOrigin: true
},
'/image': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
});
8.8 性能优化建议
1. 防抖处理(避免频繁请求)
javascript
import { debounce } from 'lodash-es';
const debouncedSendMessage = debounce((message) => {
sendMessage(message);
}, 500);
2. 虚拟滚动(大量消息时)
vue
<template>
<RecycleScroller
class="scroller"
:items="messages"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="message">
{{ item.content }}
</div>
</RecycleScroller>
</template>
<script setup>
import { RecycleScroller } from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
</script>
3. 消息节流显示(提升渲染性能)
javascript
// 使用requestAnimationFrame优化渲染
let rafId = null;
const updateMessage = (content) => {
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
currentMessage.value += content;
rafId = null;
});
};
4. 缓存历史对话
javascript
import { useLocalStorage } from '@vueuse/core';
const chatHistory = useLocalStorage('chat-history', []);
// 保存对话
cwatch(messages, (newMessages) => {
chatHistory.value = newMessages.slice(-50); // 只保留最近50条
}, { deep: true });
🚀 九、生产环境部署建议
9.1 JVM参数调优
bash
java -jar springai.jar \
-Xms512m -Xmx2g \
-XX:+UseG1GC \
-Dreactor.schedulers.defaultBoundedElasticSize=50
9.2 监控指标
xml
<!-- 添加Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
yaml
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
9.3 健康检查
java
@Component
public class AiHealthIndicator implements HealthIndicator {
@Autowired
private ChatClient chatClient;
@Override
public Health health() {
try {
// 快速探测AI服务可用性
chatClient.prompt()
.user("ping")
.call()
.content();
return Health.up().build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
📚 十、扩展阅读与学习资源
推荐书籍
- 《响应式Spring 5实战》
- 《Reactive Programming with Reactor 3》
官方文档
🎯 十一、总结
通过这个项目实践,我们掌握了:
✅ 响应式编程核心概念 :Mono/Flux、非阻塞IO、背压机制
✅ Spring AI完整搭建 :从依赖配置到API调用
✅ 流式输出实现 :SSE协议、实时推送、中文编码
✅ Vue前端消费方案 :EventSource、Fetch API、VueUse Hook
✅ 常见陷阱规避 :乱码、错误处理、版本兼容、跨域配置
✅ 生产级最佳实践 :监控、调优、健康检查、性能优化
✅ 完整的前后端实现:聊天界面、图片生成、意图识别
最后送给大家一句话:
在AI时代,掌握响应式编程不再是可选项,而是必备技能。它不仅能提升应用性能,更能改善用户体验,让你的AI应用脱颖而出!
💬 互动交流
如果你在使用过程中遇到问题,欢迎:
1. 在评论区留言讨论
2.如果觉得有帮助,点赞👍收藏📌关注➕,后续会持续分享SpringAI和AI工程的实战经验!
版权声明 :本文为原创文章,转载请注明出处。
技术栈版本 :Spring Boot 3.5.9 + Spring AI 1.1.4 + Java 17
更新时间:2026-04-26
