基于Spring WebFlux Web Client优化分页REST API迭代方案问询
重构方案:动态流式分页 + 实时限流适配
我来给你提供一个更优雅的重构方案,完美解决你提到的两个问题,同时保持流式处理的核心特性——数据一可用就开始消费,不用等全量加载。
核心思路
- 从第一页直接发起请求,不再做单独的预请求,第一页的数据直接进入处理流程
- 每次请求后从响应头提取最新的限流信息,动态调整后续请求的延迟策略,不再假设限流规则一成不变
- 根据当前页的分页元数据(当前页+总页数)判断是否需要请求下一页,用递归式的Flux生成逻辑串联所有页面的数据流
- 用
concat和defer保证请求的懒加载和顺序执行,既保持流式输出,又不会打乱分页顺序
代码实现
先明确我们需要用到的实体类(和你原定义一致):
// 分页响应包装类 class Paginated { private List<Item> data; private Meta meta; // getter/setter } class Meta { private Pagination pagination; // getter/setter } class Pagination { private int total; private int current; // getter/setter } // 限流信息类 class Limits { private long windowSize; // 窗口大小(毫秒) private long windowRemaining; // 窗口剩余时间(毫秒) private int requestsQuota; // 窗口请求配额 private int requestsLeft; // 当前窗口剩余请求数 // getter/setter } // 业务条目类 class Item { // 你的业务字段 }
接下来是核心的WebClient调用逻辑:
public Flux<Item> fetchAllItems(String baseUri) { // 初始调用:从第1页开始,初始限流信息设为默认值(第一次请求后会自动更新) return fetchPage(baseUri, 1, new Limits()); } private Flux<Item> fetchPage(String baseUri, int currentPage, Limits currentLimits) { return client.get() .uri(uriBuilder -> uriBuilder.path(baseUri).queryParam("page", currentPage).build()) .exchangeToMono(response -> { // 从响应头提取最新限流信息,覆盖当前的限流规则 HttpHeaders headers = response.headers().asHttpHeaders(); currentLimits.setWindowSize(Long.parseLong(headers.getFirst("X-Window-Size"))); currentLimits.setWindowRemaining(Long.parseLong(headers.getFirst("X-Window-Remaining"))); currentLimits.setRequestsQuota(Integer.parseInt(headers.getFirst("X-Requests-Quota"))); currentLimits.setRequestsLeft(Integer.parseInt(headers.getFirst("X-Requests-Remaining"))); // 解析分页响应体 return response.bodyToMono(Paginated.class); }) .flatMapMany(paginated -> { // 把当前页的条目转为Flux,直接推送给下游消费 Flux<Item> currentPageItems = Flux.fromIterable(paginated.getData()); Pagination pagination = paginated.getMeta().getPagination(); // 判断是否还有下一页 if (pagination.getCurrent() >= pagination.getTotal()) { return currentPageItems; } // 计算下一页请求的延迟时间,严格遵守最新的限流规则 Duration delay = calculateRequestDelay(currentLimits); // 递归请求下一页:用defer保证延迟结束后才发起请求,concat保证顺序执行 Flux<Item> nextPageItems = Flux.defer(() -> fetchPage(baseUri, pagination.getCurrent() + 1, currentLimits) ).delaySubscription(delay); // 合并当前页和下一页的数据流 return Flux.concat(currentPageItems, nextPageItems); }) // 可选:添加限流超限的重试逻辑,应对临时的429错误 .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)) .filter(throwable -> throwable instanceof WebClientResponseException.TooManyRequests)); } private Duration calculateRequestDelay(Limits limits) { // 如果当前窗口剩余请求数不足,直接等待窗口重置 if (limits.getRequestsLeft() <= 1) { return Duration.ofMillis(limits.getWindowRemaining()); } // 否则按照窗口配额均匀分配请求间隔,避免突发请求触发限流 long interval = limits.getWindowSize() / limits.getRequestsQuota(); return Duration.ofMillis(interval); }
方案优势
- 彻底消除预请求重复:第一页的请求直接返回数据并进入处理流程,不再额外发起预请求获取总页数
- 动态适配限流变化:每次请求都从响应头拉取最新的限流规则,即使中途规则调整也能自动适配,避免触发超限错误
- 流式特性保持不变:数据一返回就会被下游消费,完全符合你“无需等待所有页面加载”的需求
- 容错性更强:可选的重试逻辑可以处理临时的限流超限情况,提升整体稳定性
关键细节说明
- 用
exchangeToMono替代旧版的exchange,符合WebFlux的最新API规范 - 递归调用时传递更新后的
Limits对象,保证每次请求都用最准确的限流数据计算延迟 Flux.defer确保下一页的请求只有在当前页处理完成且延迟结束后才会发起,避免提前创建无效请求concat保证分页数据的顺序性,和原API的分页顺序完全一致
内容的提问来源于stack exchange,提问作者Andrija Kovačević
相关产品推荐
相关产品推荐

