2026 进阶篇:Spring Boot响应式编程 + Spring AI 1.1.4 流式实战 + Vue前端完整实现(避坑指南)

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);
}

关键技术点

  1. produces = MediaType.TEXT_EVENT_STREAM_VALUE:声明SSE响应类型
  2. .stream().content():启用流式模式
  3. "data: " + json + "\n\n":SSE协议格式(必须双换行)
  4. 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 响应式编程黄金法则

  1. 永远不要阻塞 :避免在响应式链中使用.block()
  2. 错误处理前置 :使用onErrorResumeonErrorReturn
  3. 资源清理 :使用doFinallydoOnTerminate
  4. 日志记录 :使用doOnNextdoOnError打点
  5. 背压处理 :使用onBackpressureBufferlimitRate

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

相关推荐
明月_清风1 小时前
从 AST 视角看透前端工程化:一条编译管线如何串联起所有工具
前端
Magic-Yuan1 小时前
PySpark Debug 总结
人工智能·python·数据平台
csdn2015_1 小时前
Java List 去重
java·windows·list
MacroZheng1 小时前
面试官:“你连Claude Code都没用过吗?”,我怼回去:“就没用过又怎么了?”
人工智能·后端·claude
白开水都有人用1 小时前
前端 AES 加密 + 后端解密 + MD5 校验登录
前端
IDZSY04301 小时前
【技术视角】从0到1拆解机乎AI:AI社交平台的技术架构与产品设计
人工智能
视觉&物联智能1 小时前
【杂谈】-人工智能于现代网络安全运营的价值持续攀升
人工智能·安全·web安全·ai·chatgpt·agi·deepseek
昨夜见军贴06161 小时前
采购订单校验报告审核效率革命,IACheck与AI报告审核全力赋能无误处理
人工智能
pqq的迷弟1 小时前
多租户实现方案
java·多租户