SpringBoot中如何同时获取请求头与响应体实现幂等校验缓存
用HandlerInterceptor实现幂等校验并缓存响应体的解决方案
要解决拦截器中无法获取响应体并缓存的问题,核心思路是通过包装HttpServletResponse缓存输出流内容——原生响应流一旦写入就无法重复读取,Spring提供的ContentCachingResponseWrapper可直接实现该需求。具体实现步骤如下:
核心实现逻辑
- 在
preHandle方法中,将原生HttpServletResponse替换为ContentCachingResponseWrapper,后续控制器的响应会被自动缓存到包装类中。 - 放弃在
postHandle处理缓存,转而在afterCompletion方法读取缓存内容——该方法在请求完全处理完成后触发,此时响应已完全生成,读取缓存更可靠。 - 读取包装类中的响应内容,结合请求头的token作为键,将响应体存入Redis。
- 必须调用
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
相关产品推荐
相关产品推荐

