如何让Spring Flux逐个发送数据而非批量分块发送?
问题:Spring WebFlux返回NDJSON分块导致前端JSON解析失败
我有一个Flux控制器,会在CompletableFuture完成时返回Name对象:
@GetMapping(value = "generate", produces = MediaType.APPLICATION_NDJSON_VALUE) public Flux<Name> generate(@RequestParam String keywords) { List<CompletableFuture<Name>> completableFuturesList = nameGeneratorService.generateNames(keywords, 1, 20); List<Mono<Name>> mono = completableFuturesList.stream().map(Mono::fromFuture).collect(Collectors.toList()); return Flux.fromIterable(mono).flatMap(Function.identity()); }
我用以下jQuery代码获取数据:
var last_response_len = false; $.ajax({ url: 'http://127.0.0.1:8081/generate', type: 'get', data: { keywords: searchContent }, xhrFields: { onprogress: function (e) { var this_response, response = e.currentTarget.response; if (last_response_len === false) { this_response = response; last_response_len = response.length; } else { this_response = response.substring(last_response_len); last_response_len = response.length; } console.log("NEW RESPONSE!"); console.log(this_response); data = JSON.parse(this_response); } }, success: function (data, status) { } });
遇到的问题:当多个CompletableFuture几乎同时完成时,Spring会把多条NDJSON数据打包成一个块发送,而不是逐行发送。浏览器控制台输出如下:
NEW RESPONSE! {"name":"pizza","available":false} NEW RESPONSE! {"name":"pizzapie","available":true} {"name":"sausagepizza","available":true} {"name":"anchovypizza","available":true} {"name":"pepperonipizza","available":true} {"name":"cheesepizza","available":true} {"name":"sicilianpizza","available":true} Uncaught SyntaxError: Unexpected non-whitespace character after JSON at position 43 at JSON.parse (<anonymous>) at XMLHttpRequest.onprogress (script.js:65:27)
可以看到第二个NEW RESPONSE!后包含多条Name对象的JSON,直接用JSON.parse会报错,因为输入不是单个有效的JSON对象。
解决方案
1. 前端按NDJSON规范处理分块数据
NDJSON的核心是每行一个独立JSON对象,所以不管后端发送的块里有多少行,都需要按换行符拆分,逐个解析每行内容。同时要处理可能的不完整行(块末尾可能是未完成的JSON)。
修改后的前端代码:
var remainingContent = ''; // 存储上一次未完成的行内容 $.ajax({ url: 'http://127.0.0.1:8081/generate', type: 'get', data: { keywords: searchContent }, xhrFields: { onprogress: function (e) { var currentResponse = e.currentTarget.response; // 拼接上次剩余内容与本次响应 var fullContent = remainingContent + currentResponse; // 按换行符分割成多行 var lines = fullContent.split('\n'); // 最后一行可能不完整,留到下次处理 remainingContent = lines.pop(); // 遍历处理每一行有效JSON lines.forEach(line => { line = line.trim(); if (!line) return; // 跳过空行 try { const nameObj = JSON.parse(line); console.log('解析成功:', nameObj); // 这里添加你的业务逻辑,比如渲染到页面 } catch (err) { console.error('JSON解析失败:', err, '出错行:', line); } }); } }, success: function () { // 请求结束后,处理最后剩余的一行(如果存在) remainingContent = remainingContent.trim(); if (remainingContent) { try { const nameObj = JSON.parse(remainingContent); console.log('最后一行解析结果:', nameObj); } catch (err) { console.error('最后一行解析失败:', err); } } } });
2. 后端显式保证NDJSON格式(可选)
虽然Spring WebFlux返回APPLICATION_NDJSON_VALUE时默认会为每个元素添加换行,但如果需要更严格的控制,可以手动序列化并添加换行符:
@Autowired private ObjectMapper objectMapper; @GetMapping(value = "generate", produces = MediaType.APPLICATION_NDJSON_VALUE) public Flux<String> generate(@RequestParam String keywords) { List<CompletableFuture<Name>> completableFuturesList = nameGeneratorService.generateNames(keywords, 1, 20); return Flux.fromIterable(completableFuturesList) .flatMap(Mono::fromFuture) // 序列化Name对象为JSON字符串并添加换行 .map(name -> { try { return objectMapper.writeValueAsString(name) + "\n"; } catch (JsonProcessingException e) { throw new RuntimeException(e); } }); }
问题根源
HTTP分块传输会将多个NDJSON条目打包成一个块发送,尤其是当多个异步任务同时完成时,Spring会批量输出这些数据。原代码假设每次onprogress只收到单个JSON对象,直接解析整个块内容自然会因为包含多个JSON结构而失败。
内容的提问来源于stack exchange,提问作者Ar_
相关产品推荐
相关产品推荐

