You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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_

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 22:42:39