Spring WebClient如何结合缓存处理NOT_MODIFIED(304)响应?
解决方案与实现建议
核心思路
将缓存内容与ETag绑定存储,收到304响应时直接返回本地缓存;仅在远程资源实际变化时更新缓存,同时保留兜底过期时间避免缓存无限期留存。针对缓存过期但远程返回304的场景,需重新发起无ETag的请求获取内容并更新缓存。
1. 调整缓存结构
定义封装类同时存储响应体与ETag,用Caffeine实现缓存兜底:
// 缓存封装类,绑定响应体与ETag public record CachedPage(PageObject page, String etag) {} // 缓存管理类 @Component public class PageCache { private final Cache<String, CachedPage> cache = Caffeine.newBuilder() .expireAfterWrite(24, TimeUnit.HOURS) // 兜底过期时间,避免ETag失效后无缓存可用 .build(); public CachedPage get(String uri) { return cache.getIfPresent(uri); } public void put(String uri, CachedPage cachedPage) { cache.put(uri, cachedPage); } }
2. 重写WebClient请求逻辑
private final PageCache pageCache; private final WebClient webClient; // 构造注入依赖 public YourService(PageCache pageCache, WebClient webClient) { this.pageCache = pageCache; this.webClient = webClient; } public Mono<PageObject> getPageObject(final String callingUri) { // 先尝试从缓存获取内容与ETag CachedPage cachedPage = pageCache.get(callingUri); String etag = cachedPage != null ? cachedPage.etag() : null; return webClient.get() .uri(callingUri) .ifNoneMatch(etag) .exchangeToMono(clientResponse -> { if (clientResponse.statusCode().isSameCodeAs(HttpStatus.NOT_MODIFIED)) { // 处理304响应 if (cachedPage != null) { // 缓存存在,返回缓存内容并重置过期时间 pageCache.put(callingUri, cachedPage); return Mono.just(cachedPage.page()); } else { // 缓存已过期但ETag有效,重新发起无ETag请求获取内容 return webClient.get() .uri(callingUri) .exchangeToMono(newResp -> { if (newResp.statusCode().is2xxSuccessful()) { String newEtag = newResp.headers().header("etag").stream().findFirst().orElse(null); return newResp.bodyToMono(PageObject.class) .doOnNext(page -> { if (newEtag != null) { pageCache.put(callingUri, new CachedPage(page, newEtag)); } }); } return newResp.createException().flatMap(Mono::error); }); } } else if (clientResponse.statusCode().is2xxSuccessful()) { // 处理200响应,更新缓存与ETag String newEtag = clientResponse.headers().header("etag").stream().findFirst().orElse(null); return clientResponse.bodyToMono(PageObject.class) .doOnNext(page -> { if (newEtag != null) { pageCache.put(callingUri, new CachedPage(page, newEtag)); } }); } // 处理其他错误状态 return clientResponse.createException().flatMap(Mono::error); }); }
3. 关键逻辑说明
- 304响应处理:收到304时优先返回本地缓存,通过重新存入缓存重置过期时间,避免缓存因兜底策略到期被清理。
- 缓存过期+304场景:若缓存已被清理但ETag仍有效,需重新发起无ETag的请求获取内容(因为304无响应体),再更新缓存。
- 阻塞模式适配:在MVC Controller中调用时使用
block()获取同步结果:
@GetMapping("/show-page") public String renderPage(Model model) { PageObject page = yourService.getPageObject("https://remote-api/target-page").block(); model.addAttribute("page", page); return "page-template"; }
额外建议
- ETag格式适配:若远程返回弱ETag(带
W/前缀),需在ifNoneMatch中同步添加前缀,避免匹配失败。 - 异常容错:添加
onErrorResume逻辑,在远程请求失败时返回缓存内容(如果存在),提升系统稳定性。 - 缓存键优化:若请求URL包含查询参数,需对参数排序后生成缓存键,避免因参数顺序不同导致缓存冗余。
内容的提问来源于stack exchange,提问作者KaiN
相关产品推荐
相关产品推荐

