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

SpringBoot中如何同时获取请求头与响应体实现幂等校验缓存

用HandlerInterceptor实现幂等校验并缓存响应体的解决方案

要解决拦截器中无法获取响应体并缓存的问题,核心思路是通过包装HttpServletResponse缓存输出流内容——原生响应流一旦写入就无法重复读取,Spring提供的ContentCachingResponseWrapper可直接实现该需求。具体实现步骤如下:

核心实现逻辑

  1. 在preHandle方法中,将原生HttpServletResponse替换为ContentCachingResponseWrapper,后续控制器的响应会被自动缓存到包装类中。
  2. 放弃在postHandle处理缓存,转而在afterCompletion方法读取缓存内容——该方法在请求完全处理完成后触发,此时响应已完全生成,读取缓存更可靠。
  3. 读取包装类中的响应内容,结合请求头的token作为键,将响应体存入Redis。
  4. 必须调用copyBodyToResponse()将缓存的响应写回原生响应,否则客户端无法收到返回结果。

修改后的完整拦截器代码

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.ContentCachingResponseWrapper;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import com.alibaba.fastjson.JSON;
import java.lang.reflect.Method;

public class IdempotentInterceptor implements HandlerInterceptor {
    @Autowired
    private TokenService tokenService;
    @Autowired
    private RedisCache redisCache;
    @Value("${idempotent.tokenName}")
    private String TOKEN_NAME;
    @Value("${idempotent.respCachePrefix}")
    private String prefix;
    private static final Logger log = LoggerFactory.getLogger(IdempotentInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        Idempotent methodAnnotation = method.getAnnotation(Idempotent.class);
        if (methodAnnotation != null) {
            // 用ContentCachingResponseWrapper包装响应,缓存输出内容
            ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
            request.setAttribute("responseWrapper", responseWrapper);
            
            // 原有幂等校验逻辑
            if (!tokenService.checkIdempotent(request)) {
                String token = request.getHeader(TOKEN_NAME);
                log.info("重复请求,幂等Token: {}", token);
                Object result = redisCache.get(prefix + token);
                response.setCharacterEncoding("UTF-8");
                response.setContentType("application/json");
                if (result != null) {
                    log.info("{}存在缓存结果,返回缓存内容: {}", token, result);
                    response.getWriter().print(JSON.toJSONString(result));
                } else {
                    response.getWriter().print(JSON.toJSONString(new ResultDTO<>(ResultDTO.FAIL_CODE, "请求无效,请检查幂等token。")));
                }
                response.getWriter().flush();
                return false;
            }
            return true;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 此处无需处理缓存逻辑
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        Idempotent methodAnnotation = method.getAnnotation(Idempotent.class);
        if (methodAnnotation != null) {
            ContentCachingResponseWrapper responseWrapper = (ContentCachingResponseWrapper) request.getAttribute("responseWrapper");
            if (responseWrapper != null) {
                try {
                    // 读取缓存的响应体字节数组,转换为字符串
                    byte[] responseBytes = responseWrapper.getContentAsByteArray();
                    if (responseBytes.length > 0) {
                        String responseBody = new String(responseBytes, responseWrapper.getCharacterEncoding());
                        String token = request.getHeader(TOKEN_NAME);
                        if (StringUtils.isNotBlank(token)) {
                            // 将响应体存入Redis,可根据需求选择存字符串或反序列化为ResultDTO对象
                            redisCache.set(prefix + token, responseBody, 600L);
                            log.info("幂等请求响应已缓存,Token: {}", token);
                        }
                    }
                } finally {
                    // 必须将缓存的响应写回原生响应,否则客户端无法接收结果
                    responseWrapper.copyBodyToResponse();
                }
            }
        }
    }
}

补充说明

  • ContentCachingResponseWrapper是Spring MVC自带工具类,无需自定义包装类,会自动拦截并缓存所有写入响应流的内容。
  • 若需缓存ResultDTO对象而非JSON字符串,可通过JSON.parseObject(responseBody, ResultDTO.class)反序列化后存入Redis,重复请求时直接返回对象即可。
  • 必须在finally块中调用copyBodyToResponse(),避免因异常导致客户端无法收到响应。

内容的提问来源于stack exchange,提问作者Weixing Wang

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 22:00:57