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

Java实现/v2请求响应存库及解决/v1端点无响应问题

问题描述

需求

  • 调用/v2端点时,将请求和响应以字符串形式保存至数据库
  • 调用/v1端点时正常返回响应(/v1为XML格式端点,/v2为JSON格式端点)

当前问题

现有实现中/v2的存储功能正常,但调用/v1端点时仅返回200 OK,响应体为空(正常应返回{"id":"123456789"});删除CachingRequestBodyFilter类后,/v1恢复正常,但/v2无法存储数据。

咨询问题

  1. 当前实现存在什么问题?
  2. 是否有更优的通过拦截器实现请求响应存库的方案?

拦截器类

@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);
    }
}

问题分析与解决方案

一、当前实现的核心问题

  1. 强制类型转换导致的响应清空:拦截器的saveResponse方法中,直接将HttpServletRequest、HttpServletResponse强转为ContentCachingRequestWrapper和ContentCachingResponseWrapper。但过滤器仅对/v2/*路径做了包装,/v1请求的原始对象并没有被包装。此时强转虽不会抛出异常,但getContentAsByteArray()会返回空数组,后续调用responseWrapper.copyBodyToResponse()时,会把空内容写回原始响应,直接清空了/v1的正常响应体。

  2. 过滤器未处理响应回写:过滤器中使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 06:56:55