原创

springboot请求request只读取一次问题

温馨提示:
本文最后更新于 2023年03月08日,已超过 386 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

在springboot项目中,我们会遇到将请求写入日志的操作,但是在实际的项目中,当你在filter或者拦截器中获取request之后进行读取数据,就会发现后续的controller将会miss request body,那到底应该怎么做呢?

1.springboot中request的请求执行顺序

请求 -> filter -> servlet -> interceptor -> aop -> controller
只要在controller前读取过一次请求流,那么后面就再拿不到请求参数了。

2.原因分析

当我们调用getInputStream()方法获取输入流时,得到的是一个InputStream对象,而实际类型是ServletInputStream,它继承InputStream。

查看InputStream的源码可以看到(这里就不贴代码,大家有兴趣可以去找具体的源码部分),读取流的时候会根据position来获取当前位置,每读取一次,该标志就会移动一次,如果读到最后,read()返回-1,表示已经读取完了。如果想要重新读取,可以调用inputstream.reset方法,但是能否reset取决于markSupported方法,返回true可以reset,反之则不行。查看ServletInputStream可知,这个类并没有重写markSupported和reset方法。

综上,InputStream默认不实现reset方法,而ServletInputStream也没有重写reset相关方法,这样就无法重复读取流,这就是我们从request对象中输入流只能读取一次的原因。

3.解决办法

在filter中将流读取出来然后进行存储,方便后续读取使用

BodyReaderHttpServletRequestWrapper

它是一个http请求包装器,其基于装饰者模式实现了HttpServletRequest界面。

我们定义一个容器,将输入流里面的数据存储到这个容器里,然后我们重写getInputStream方法,每次都从这个容器里读数据,这样我们的输入流就可以读取任意次了。


import org.springframework.util.StreamUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * <p>项目名称: zzw-tech </p>
 * <p>包名称: com.zzw.mis.framework.security.filter </p>
 * <p>描述:  </p>
 * <p>  </p>
 * <p>创建时间: 2022/12/26 17 </p>
 *
 * @author coco
 * @version v1.0
 */
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private byte[] requestBody = null;//用于将流保存下来

    public BodyReaderHttpServletRequestWrapper (HttpServletRequest request) throws IOException {
        super(request);
        requestBody = StreamUtils.copyToByteArray(request.getInputStream());
    }


    @Override
    public ServletInputStream getInputStream () throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);

        return new ServletInputStream() {

            @Override
            public int read () throws IOException {
                return bais.read();
            }

            @Override
            public boolean isFinished () {
                return false;
            }

            @Override
            public boolean isReady () {
                return false;
            }

            @Override
            public void setReadListener (ReadListener readListener) {

            }
        };
    }

    @Override
    public BufferedReader getReader () throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}

AuthenticationTokenFilter处理

@Override
    protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
        try {
            String contentType = request.getContentType();
            if (StrUtil.isNotBlank(contentType) && contentType.contains(ContentType.MULTIPART.getValue())) {//multipart/form-data类型
                //spring中使用MultipartResolver处理文件上传,所以这里需要将其封装往后传递
                MultipartResolver resolver = new StandardServletMultipartResolver();
                MultipartHttpServletRequest multipartRequest = resolver.resolveMultipart(request);
                chain.doFilter(multipartRequest, responseWrapper);
            } else {
                //对于其他的情况,我们统一使用包装类,将请求流进行缓存到容器
                BodyReaderHttpServletRequestWrapper cachedBodyHttpServletRequest = new BodyReaderHttpServletRequestWrapper(request);

                // TODO do somethings
                /*
                ……
                */
                chain.doFilter(cachedBodyHttpServletRequest, responseWrapper);

            }
        } finally {
            //读取完 Response body 之后,通过这个设置回去,就可以使得接口调用者可以正常接收响应了,否则会产生空响应的情况
            //注意要在过滤器方法的最后调用
            responseWrapper.copyBodyToResponse();
        }
    }

后续拦截器日志使用请求体参数

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String param = "";
    if (isJson(request)) {
        param = getParamsFromRequestBody(request);
    }
    RequestLog requestLog = new RequestLog();
    Integer userId = RequestUtils.getUserId(request);
    String uri = request.getRequestURI();
    requestLog.setUserId(userId);
    requestLog.setReqIp(IpUtil.getIpAddress(request));
    requestLog.setReqParam(param);
    requestLog.setReqUri(uri);
    requestLog.setReqTime(new Date());
    request.setAttribute(LOGGER_ENTITY, requestLog);
    return true;
}


/**
 * 获取请求体内容
 * @return
 * @throws IOException
 */
private String getParamsFromRequestBody(HttpServletRequest request) throws IOException {
    BufferedReader reader = request.getReader();
    StringBuilder builder = new StringBuilder();
    try {
        String line = null;
        while((line = reader.readLine()) != null) {
            builder.append(line);
        }
        return builder.toString();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return null;
}

/**
* 判断是不是json格式的请求
*/
private boolean isJson(HttpServletRequest request) {
    if (request.getContentType() != null) {
        return request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE);
    }
    return false;
}
正文到此结束
本文目录