解决无法重复读取请求体和响应体的问题

项目场景

C: 这两天实现了一个操作日志功能,需求是要记录指定操作的请求 URL,请求方式、请求头、请求体、响应码、响应头、响应体、请求耗时、操作人、操作IP、操作地址等信息。

考虑了几种方案,结合以前的经验,排掉了 AOP,综合评估后这次采用的是 Spring 拦截器的方式来记录,大体的实现流程是:

  1. 提供一个 @Log 注解
  2. 在需要记录操作日志的接口类及方法上添加 @Log 注解,指定好资源名称和操作类型(具体为什么要在类和方法上都加,是考虑复用操作的资源名称)
  3. 提供一个拦截器,在拦截器中判断当前 Handler 是否存在 @Log 注解
  4. 存在该注解,就在 preHandle() 中开始计时,在 afterCompletion() 中结束计时并获取请求和响应信息
  5. 将请求和响应信息异步存储到数据库中

问题描述

流程很简单,但是在获取 requestBody(请求体)和 responseBody(响应体)时出了些问题。如果我在 preHandle() 中获取了请求体信息,那么对应 Handler 就无法获取了,反之如果我是在 afterCompletion 中获取请求体信息,那么就获取不到了。而对于响应体,在我获取完之后,向前端响应就没内容了。

原因分析

之所以如此,是由于请求体和响应体分别对应的是 InputStream 和 OutputStream,由于流的特性,使用完之后就无法再被使用了。

/**
 * Retrieves the body of the request as binary data using a {@link ServletInputStream}. Either this method or
 * {@link #getReader} may be called to read the body, not both.
 *
 * @return a {@link ServletInputStream} object containing the body of the request
 *
 * @exception IllegalStateException if the {@link #getReader} method has already been called for this request
 *
 * @exception IOException           if an input or output exception occurred
 */
public ServletInputStream getInputStream() throws IOException;
/**
 * Returns a {@link ServletOutputStream} suitable for writing binary data in the response. The servlet container
 * does not encode the binary data.
 *
 * <p>
 * Calling flush() on the ServletOutputStream commits the response.
 *
 * Either this method or {@link #getWriter} may be called to write the body, not both, except when {@link #reset}
 * has been called.
 *
 * @return a {@link ServletOutputStream} for writing binary data
 *
 * @exception IllegalStateException if the <code>getWriter</code> method has been called on this response
 *
 * @exception IOException           if an input or output exception occurred
 *
 * @see #getWriter
 * @see #reset
 */
public ServletOutputStream getOutputStream() throws IOException;

想要解决的话就要想办法把这信息使用完再“塞回去”,直接“塞回去”是不可能的。

解决方案

为了解决这个问题,Servlet 提供了两个类 HttpServletRequestWrapper、HttpServletResponseWrapper,我们可以继承它们来实现请求体和响应体内容的缓存,达到重复读取请求体和响应体的目的。

不过既然我们在使用 Spring 框架,贴心的 Spring 也提供了两个实现类:ContentCachingRequestWrapper、ContentCachingResponseWrapper,这样我们就无需再自行定义相应 Wrapper 直接使用它们就可以解决这个问题了。

下面是在过滤器中对请求对象和响应对象进行包装处理的代码段:

import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;

/**
 * 缓存请求体和响应体过滤器
 *
 * <p>
 * 由于 requestBody 和 responseBody 分别对应的是 InputStream 和 OutputStream,由于流的特性,读取完之后就无法再被使用了。
 * 所以,需要额外缓存一次流信息。
 * </p>
 *
 * @author Charles7c
 * @since 2022/9/22 16:33
 */
@Component
public class ContentCachingWrapperFilter extends OncePerRequestFilter implements Ordered {

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 10;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        // 包装流,可重复读取
        if (!(request instanceof ContentCachingRequestWrapper)) {
            request = new ContentCachingRequestWrapper(request);
        }
        if (!(response instanceof ContentCachingResponseWrapper)) {
            response = new ContentCachingResponseWrapper(response);
        }

        filterChain.doFilter(request, response);
        updateResponse(response);
    }

    /**
     * 更新响应(不操作这一步,会导致接口响应空白)
     *
     * @param response 响应对象
     * @throws IOException /
     */
    private void updateResponse(HttpServletResponse response) throws IOException {
        ContentCachingResponseWrapper responseWrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        Objects.requireNonNull(responseWrapper).copyBodyToResponse();
    }
}

下面是使用缓存对象来获取请求体或响应体的代码段,在你需要的地方使用就可以了:

import org.apache.commons.io.IOUtils;
// --------------------------------------------
/**
 * 获取请求体
 *
 * @param request 请求对象
 * @return 请求体
 */
private String getRequestBody(HttpServletRequest request) {
    String requestBody = "";
    ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
    if (wrapper != null) {
        requestBody = IOUtils.toString(wrapper.getContentAsByteArray(), StandardCharsets.UTF_8.toString());
    }
    return requestBody;
}

/**
 * 获取响应体
 *
 * @param response 响应对象
 * @return 响应体
 */
private String getResponseBody(HttpServletResponse response) {
    String responseBody = "";
    ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
    if (wrapper != null) {
        responseBody = IOUtils.toString(wrapper.getContentAsByteArray(), StandardCharsets.UTF_8.toString());
    }
    return responseBody;
}
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

查尔斯-BUG万象集

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值