Java实现/v2请求响应存库及解决/v1端点无响应问题
问题描述
需求
- 调用
/v2端点时,将请求和响应以字符串形式保存至数据库 - 调用
/v1端点时正常返回响应(/v1为XML格式端点,/v2为JSON格式端点)
当前问题
现有实现中/v2的存储功能正常,但调用/v1端点时仅返回200 OK,响应体为空(正常应返回{"id":"123456789"});删除CachingRequestBodyFilter类后,/v1恢复正常,但/v2无法存储数据。
咨询问题
- 当前实现存在什么问题?
- 是否有更优的通过拦截器实现请求响应存库的方案?
拦截器类
@AllArgsConstructor @Component @Data public class RequestInterceptor implements HandlerInterceptor { private final RequestService requestService; @Override public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, Exception ex){ requestService.saveResponse(request, response, ex); } } public void saveResponse(HttpServletRequest request, HttpServletResponse response, Exception ex) { try { String requestBody = getRequestAsString(request); String responseBody = getResponseAsString(response); buildMessages(requestBody, responseBody, ex); } catch (IOException e) { e.printStackTrace(); } } private String getRequestAsString(HttpServletRequest request) throws IOException { ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request; return new String(requestWrapper.getContentAsByteArray(), requestWrapper.getCharacterEncoding()); } private String getResponseAsString(HttpServletResponse response) throws IOException { ContentCachingResponseWrapper responseWrapper = (ContentCachingResponseWrapper) response; byte[] responseArray = responseWrapper.getContentAsByteArray(); String characterEncoding = responseWrapper.getCharacterEncoding(); responseWrapper.copyBodyToResponse(); return new String(responseArray, characterEncoding); }
注:
buildMessages()方法为用于构建待存储对象的构建器方法。
过滤器类
@Component @WebFilter(filterName = "CachingRequestBodyFilter", urlPatterns = {"/v2/*"}) public class CachingRequestBodyFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException { HttpServletRequest request = (HttpServletRequest) req; ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); HttpServletResponse response = (HttpServletResponse) res; ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response); filterChain.doFilter(wrappedRequest, wrappedResponse); } }
问题分析与解决方案
一、当前实现的核心问题
强制类型转换导致的响应清空:拦截器的
saveResponse方法中,直接将HttpServletRequest、HttpServletResponse强转为ContentCachingRequestWrapper和ContentCachingResponseWrapper。但过滤器仅对/v2/*路径做了包装,/v1请求的原始对象并没有被包装。此时强转虽不会抛出异常,但getContentAsByteArray()会返回空数组,后续调用responseWrapper.copyBodyToResponse()时,会把空内容写回原始响应,直接清空了/v1的正常响应体。过滤器未处理响应回写:过滤器中使用
ContentCachingResponseWrapper包装响应后,没有主动调用copyBodyToResponse()将缓存内容写回原始响应。虽然拦截器中调用了该方法,但仅适用于/v2路径,反而给/v1路径带来了副作用。
二、更优实现方案
思路概述
- 让过滤器覆盖所有路径,但仅对
/v2路径执行请求响应包装,避免影响/v1。 - 拦截器仅对
/v2路径触发存储逻辑,同时增加类型判断,避免强制转换的风险。 - 把响应回写的逻辑放在过滤器的finally块中,确保仅在包装过的响应上执行。
优化后代码示例
1. 修改过滤器(全局生效,仅对/v2包装)
@Component @WebFilter(filterName = "CachingRequestBodyFilter", urlPatterns = {"/*"}) public class CachingRequestBodyFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException { boolean isV2Path = req.getRequestURI().startsWith("/v2/"); HttpServletRequest requestToUse = req; HttpServletResponse responseToUse = res; // 仅对/v2路径包装请求响应 if (isV2Path) { requestToUse = new ContentCachingRequestWrapper(req); responseToUse = new ContentCachingResponseWrapper(res); } try { filterChain.doFilter(requestToUse, responseToUse); } finally { // 仅对包装过的响应回写内容 if (isV2Path && responseToUse instanceof ContentCachingResponseWrapper) { ((ContentCachingResponseWrapper) responseToUse).copyBodyToResponse(); } } } }
2. 修改拦截器与RequestService(增加路径判断与类型校验)
@AllArgsConstructor @Component @Data public class RequestInterceptor implements HandlerInterceptor { private final RequestService requestService; @Override public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, Exception ex){ // 仅对/v2路径触发存储逻辑 if (request.getRequestURI().startsWith("/v2/")) { requestService.saveResponse(request, response, ex); } } } // 修改RequestService中的saveResponse方法 public void saveResponse(HttpServletRequest request, HttpServletResponse response, Exception ex) { try { String requestBody = ""; // 先判断是否为包装类,再读取请求体 if (request instanceof ContentCachingRequestWrapper) { ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request; requestBody = new String(requestWrapper.getContentAsByteArray(), requestWrapper.getCharacterEncoding()); } String responseBody = ""; // 先判断是否为包装类,再读取响应体 if (response instanceof ContentCachingResponseWrapper) { ContentCachingResponseWrapper responseWrapper = (ContentCachingResponseWrapper) response; byte[] responseArray = responseWrapper.getContentAsByteArray(); String characterEncoding = responseWrapper.getCharacterEncoding(); responseBody = new String(responseArray, characterEncoding); } buildMessages(requestBody, responseBody, ex); } catch (IOException e) { e.printStackTrace(); } }
方案优势
- 彻底隔离了
/v1和/v2的处理逻辑,保证/v1的响应不受影响。 - 仅对需要存储的
/v2路径执行包装操作,减少不必要的性能开销。 - 增加类型判断,避免了强制转换可能带来的潜在异常。
内容的提问来源于stack exchange,提问作者svz
相关产品推荐
相关产品推荐

