Spring Boot 后台返回流式内容,常见有两种做法:

  1. Spring MVC + SseEmitter
  2. Spring WebFlux + Flux / ServerSentEvent

它们都可以实现类似 ChatGPT 那种“边生成边返回”的效果,但底层模型、适合场景、复杂度不太一样。


一句话结论

如果你现在是普通 Spring Boot MVC 项目,想快速实现 AI 流式输出:

优先用 SseEmitter。简单、直接、够用。

如果你的项目本身就是 WebFlux 技术栈,或者你要做高并发、全链路非阻塞:

用 WebFlux + Flux / ServerSentEvent 更合适。


1. SseEmitter 是什么?

SseEmitter 是 Spring MVC 里的流式响应工具。

它适合这种项目:

spring-boot-starter-web

也就是传统 Spring MVC 项目,底层一般是:

Tomcat / Jetty / Undertow

用法大概是:

@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chat(String message) {
    SseEmitter emitter = new SseEmitter(0L);

    streamingChatModel.chat(message, new StreamingChatResponseHandler() {
        @Override
        public void onPartialResponse(String partialResponse) {
            try {
                emitter.send(partialResponse);
            } catch (IOException e) {
                emitter.completeWithError(e);
            }
        }

        @Override
        public void onCompleteResponse(ChatResponse completeResponse) {
            emitter.complete();
        }

        @Override
        public void onError(Throwable error) {
            emitter.completeWithError(error);
        }
    });

    return emitter;
}

核心思想是:

后端拿到一段模型输出,就调用 emitter.send(...) 推给前端。


2. WebFlux 是什么?

WebFlux 是 Spring 的响应式编程框架。

它适合这种项目:

spring-boot-starter-webflux

底层通常是:

Netty

它通过 Flux 表示一串连续的数据流。

比如:

@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> chat(String message) {
    return Flux.create(sink -> {
        streamingChatModel.chat(message, new StreamingChatResponseHandler() {
            @Override
            public void onPartialResponse(String partialResponse) {
                sink.next(ServerSentEvent.builder(partialResponse)
                        .event("answer")
                        .build());
            }

            @Override
            public void onCompleteResponse(ChatResponse completeResponse) {
                sink.next(ServerSentEvent.builder("[DONE]")
                        .event("done")
                        .build());
                sink.complete();
            }

            @Override
            public void onError(Throwable error) {
                sink.error(error);
            }
        });
    });
}

核心思想是:

Controller 直接返回一个 Flux,前端不断接收这个 Flux 里面的数据。


3. 它俩都能做 SSE

这一点要先明确:

SseEmitter 和 WebFlux 都可以返回 SSE 流。

SSE 的本质是 HTTP 长连接,服务端不断往客户端推送文本事件。

前端都可以这样接收:

const eventSource = new EventSource("/api/ai/chat");

eventSource.addEventListener("answer", event => {
  console.log(event.data);
});

eventSource.addEventListener("done", event => {
  eventSource.close();
});

区别主要在后端实现方式和底层线程模型。


4. 核心区别对比

对比项SseEmitterWebFlux
所属技术栈Spring MVCSpring WebFlux
常用依赖spring-boot-starter-webspring-boot-starter-webflux
编程模型命令式、回调式响应式、声明式
返回类型SseEmitterFlux<T> / Flux<ServerSentEvent<T>>
底层服务器Tomcat 常见Netty 常见
线程模型Servlet 异步事件循环 + 非阻塞
学习成本低较高
适合项目普通 Spring MVC 项目响应式项目
高并发能力可以,但资源占用相对高更适合大量长连接
背压支持弱更好
和传统业务代码结合很自然需要响应式思维

5. 最大区别:线程模型不同

5.1 SseEmitter:传统 MVC 异步流

SseEmitter 虽然是异步的,但它还是属于 Spring MVC 体系。

你可以理解为:

一个请求进来
   ↓
Spring MVC 创建 SseEmitter
   ↓
请求线程先释放
   ↓
后台有数据时调用 emitter.send()
   ↓
不断写回浏览器

它比普通接口好,因为请求线程不会一直阻塞着等 AI 完整生成。

但是它仍然是传统 Servlet 体系,整体不是完全响应式的。


5.2 WebFlux:响应式非阻塞流

WebFlux 的设计理念是:

数据来了就推
没数据就不占线程
通过事件驱动处理

它更适合大量连接长期挂着的场景。

比如:

  • 大量用户同时和 AI 聊天
  • 每个回答持续几十秒
  • 后端需要维持很多 SSE 长连接
  • 你的数据库、Redis、HTTP Client 也都是响应式的

这时候 WebFlux 的资源利用率通常更好。


6. 开发体验区别

6.1 SseEmitter 更符合普通 Spring Boot 开发习惯

如果你平时写的是:

@RestController
@Service
@Mapper

然后 Controller 返回:

String
ResultVO
List<User>

那 SseEmitter 会更自然。

你只需要掌握:

emitter.send(...)
emitter.complete()
emitter.completeWithError(...)

就可以了。

示例:

@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream() {
    SseEmitter emitter = new SseEmitter(0L);

    new Thread(() -> {
        try {
            emitter.send("第一段");
            Thread.sleep(1000);
            emitter.send("第二段");
            Thread.sleep(1000);
            emitter.send("第三段");
            emitter.complete();
        } catch (Exception e) {
            emitter.completeWithError(e);
        }
    }).start();

    return emitter;
}

简单直接。


6.2 WebFlux 需要适应 Flux / Mono

WebFlux 代码更像这样:

@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream() {
    return Flux.just("第一段", "第二段", "第三段")
            .delayElements(Duration.ofSeconds(1));
}

看起来很简洁,但真实业务里你要理解:

  • Mono
  • Flux
  • subscribe
  • sink
  • backpressure
  • Schedulers
  • 非阻塞调用
  • 响应式链路

如果只是为了 AI 流式输出而引入 WebFlux,学习成本会高一些。


7. LangChain4j 结合时有什么区别?

LangChain4j 的低层流式接口本身是回调式的:

new StreamingChatResponseHandler() {
    @Override
    public void onPartialResponse(String partialResponse) {
        
    }

    @Override
    public void onCompleteResponse(ChatResponse completeResponse) {
        
    }

    @Override
    public void onError(Throwable error) {
        
    }
}

所以它天然就很适合和 SseEmitter 搭配:

onPartialResponse(...)
    ↓
emitter.send(...)

非常直观。


如果你用 WebFlux,需要把回调式 API 转换成 Flux:

onPartialResponse(...)
    ↓
sink.next(...)

例如:

@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> chat(@RequestParam String message) {

    return Flux.create(sink -> {

        streamingChatModel.chat(message, new StreamingChatResponseHandler() {

            @Override
            public void onPartialThinking(PartialThinking partialThinking) {
                sink.next(ServerSentEvent.builder(partialThinking.text())
                        .event("thinking")
                        .build());
            }

            @Override
            public void onPartialResponse(String partialResponse) {
                sink.next(ServerSentEvent.builder(partialResponse)
                        .event("answer")
                        .build());
            }

            @Override
            public void onCompleteResponse(ChatResponse completeResponse) {
                sink.next(ServerSentEvent.builder("[DONE]")
                        .event("done")
                        .build());
                sink.complete();
            }

            @Override
            public void onError(Throwable error) {
                sink.error(error);
            }
        });
    });
}

也不难,但比 SseEmitter 多了一层响应式封装。


8. 带思考内容时,两种方式怎么写?

8.1 SseEmitter 写法

@GetMapping(value = "/chat-sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatSse(@RequestParam String message) {
    SseEmitter emitter = new SseEmitter(0L);

    streamingChatModel.chat(message, new StreamingChatResponseHandler() {

        @Override
        public void onPartialThinking(PartialThinking partialThinking) {
            send(emitter, "thinking", partialThinking.text());
        }

        @Override
        public void onPartialResponse(String partialResponse) {
            send(emitter, "answer", partialResponse);
        }

        @Override
        public void onCompleteResponse(ChatResponse completeResponse) {
            send(emitter, "done", "[DONE]");
            emitter.complete();
        }

        @Override
        public void onError(Throwable error) {
            send(emitter, "error", error.getMessage());
            emitter.completeWithError(error);
        }
    });

    return emitter;
}

private void send(SseEmitter emitter, String eventName, String data) {
    try {
        emitter.send(SseEmitter.event()
                .name(eventName)
                .data(data));
    } catch (IOException e) {
        emitter.completeWithError(e);
    }
}

8.2 WebFlux 写法

@GetMapping(value = "/chat-flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> chatFlux(@RequestParam String message) {

    return Flux.create(sink -> {

        streamingChatModel.chat(message, new StreamingChatResponseHandler() {

            @Override
            public void onPartialThinking(PartialThinking partialThinking) {
                sink.next(ServerSentEvent.builder(partialThinking.text())
                        .event("thinking")
                        .build());
            }

            @Override
            public void onPartialResponse(String partialResponse) {
                sink.next(ServerSentEvent.builder(partialResponse)
                        .event("answer")
                        .build());
            }

            @Override
            public void onCompleteResponse(ChatResponse completeResponse) {
                sink.next(ServerSentEvent.builder("[DONE]")
                        .event("done")
                        .build());
                sink.complete();
            }

            @Override
            public void onError(Throwable error) {
                sink.error(error);
            }
        });

        sink.onCancel(() -> {
            System.out.println("前端断开连接");
        });
    });
}

9. 用哪个好?

推荐一:普通 Spring Boot 项目,用 SseEmitter

如果你的项目是这种:

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

那建议用:

SseEmitter

原因:

  • 简单
  • 和 MVC 项目兼容好
  • 不用引入 WebFlux 思维
  • 和 LangChain4j 回调模型搭配自然
  • 对大多数 AI 聊天系统已经够用

适合:

  • 公司内部 AI 助手
  • 知识库问答
  • 普通客服机器人
  • 后台管理系统里的 AI 功能
  • 用户量不是特别夸张的应用

推荐二:项目本来就是 WebFlux,用 Flux

如果你的项目本身是:

spring-boot-starter-webflux

那就直接用:

Flux<ServerSentEvent<String>>

不要为了用 SseEmitter 再切回 MVC。

适合:

  • 全链路响应式项目
  • 高并发长连接
  • 大量用户同时流式聊天
  • 网关层、BFF 层
  • 响应式数据库、响应式 Redis、响应式 HTTP Client 都已经在用

推荐三:不要为了“看起来高级”强行上 WebFlux

很多人会觉得:

WebFlux = 高性能

这个理解不完全对。

WebFlux 的优势要发挥出来,需要你的整个调用链路都尽量非阻塞。

如果你 WebFlux 里面还是大量调用阻塞代码,比如:

  • MyBatis 阻塞查询
  • 普通 RedisTemplate
  • 阻塞 HTTP Client
  • 本地文件阻塞读写
  • 同步调用第三方接口

那 WebFlux 的优势会被削弱,甚至代码还更复杂。


10. 性能角度怎么选?

简单理解:

SseEmitter

中小并发,开发简单,够用

比如:

  • 几十个并发流
  • 几百个并发流
  • 公司内部使用
  • 业务系统内嵌 AI 功能

SseEmitter 一般没问题。


WebFlux

大量长连接,更适合

比如:

  • 上千甚至更多并发 SSE
  • 每个连接持续几十秒甚至几分钟
  • 资源利用率要求高
  • 你愿意使用响应式技术栈

可以考虑 WebFlux。


11. 一个重要提醒:不要随便混用 MVC 和 WebFlux

Spring Boot 里如果同时引入:

spring-boot-starter-web
spring-boot-starter-webflux

默认情况下,Spring Boot 通常会优先以 Spring MVC 模式启动。

这会导致有些人以为自己用了 WebFlux,但其实应用还是 MVC 模式。

如果你想真正使用 WebFlux,一般只引入:

spring-boot-starter-webflux

如果你是普通 MVC 项目,一般只引入:

spring-boot-starter-web

不要两个都乱加。


12. 最实际的建议

你现在的情况是:

Spring Boot 后台通过接口返回 AI 流式内容,前端接收并显示,还包括思考内容。

我的建议是:

第一阶段:先用 SseEmitter

先把功能跑通:

LangChain4j StreamingChatModel
+
StreamingChatResponseHandler
+
SseEmitter
+
前端 EventSource

这样最容易理解整个流程。


第二阶段:如果遇到性能瓶颈,再考虑 WebFlux

如果后面发现:

  • 并发连接很多
  • Tomcat 线程压力大
  • SSE 长连接很多
  • 需要更强的非阻塞能力

再升级为:

WebFlux + Flux<ServerSentEvent<String>>

13. 最终选择表

你的情况推荐
普通 Spring Boot MVC 项目SseEmitter
已经用了 spring-boot-starter-webSseEmitter
想快速实现 ChatGPT 式流式输出SseEmitter
刚学习 LangChain4jSseEmitter
项目本来就是 WebFluxFlux<ServerSentEvent<?>>
大量长连接高并发WebFlux
团队熟悉响应式编程WebFlux
项目里大量阻塞调用不建议强行 WebFlux

14. 简单总结

可以这么记:

SseEmitter:
传统 Spring MVC 里的 SSE 工具,简单直接,适合大多数业务系统。

WebFlux:
响应式流式方案,更适合高并发、全链路非阻塞,但学习和维护成本更高。

对于你当前学习 LangChain4j 和 Spring Boot AI 流式输出:

先用 SseEmitter 更合适。等你把流式响应、思考内容、正式回答、前端 EventSource 都跑通之后,再学习 WebFlux。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐