IT俱乐部 Java 解决Java项目中request流只能获取一次的问题

解决Java项目中request流只能获取一次的问题

问题描述

Java项目开发中可能存在以下几种情况:

1、你需要在拦截器中统一拦截请求,拿到请求中的参数,来做统一的判断处理或者其他操作。

那问题就来了,由于request输入流的数据只能读取一次,所以你在拦截器中你读取了输入流的数据,当请求进入后边的Controller时,输入流中已经没有数据了,导致获取不到数据。

2、你项目里可能需要搞一个统一的异常处理器,然后想在异常处理器中把发生异常的接口地址,方法名,以及请求的参数记录到日志里或者直接发送给你配置的告警系统,比如发送给钉钉群通知。这种情况下,因为你前边controller已经获取过一次request输入流了,在后边的异常处理器里你还想再从request输入流中拿到请求参数等信息,所以也会出现request流只能读取一次的错误。

以上两种情况是开发中比较常见的,当然除此之外,别的场景下你可能也会遇到request流只能读取一次的错误,所以今天就来讲一下如果遇到这种情况该怎么解决。

产生原因

1、一个InputStream对象在被读取完成后,将无法被再次读取,始终返回-1;

2、InputStream并没有实现reset方法(可以重置首次读取的位置),无法实现重置操作;

解决方法

一、引入依赖

commons-iocommons-io2.7

二、自定义Wrapper类来包装HttpServletRequest

我们需要写一个自定义包装类,并继承HttpServletRequestWrapper

import org.apache.commons.io.IOUtils;
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.IOException;
import java.io.InputStreamReader;
 
/**
 * @描述 包装HttpServletRequest
 *          MyServletRequestWrapper + RequestReplaceFilter 的作用是:
 *              解决异常处理器中拿post请求的json参数时,报request流只能读一次的错
 *              原因是 request.getReader() 和 request.getInputStream() 都是只能调用一次
 *              所以我这里要使用 HttpServletRequestWrapper 来实现自定义的 MyServletRequestWrapper包装类
 *              把request里的 body 保存在 MyServletRequestWrapper中, 并且重写 getInputStream()方法
 *              然后所有的request都在RequestReplaceFilter中被转换成了我自定义的HttpServletRequestWrapper
 *              然后获取 body时就都是调用 MyServletRequestWrapper中的 getBody()方法了
 * @创建人 caoju
 */
public class MyServletRequestWrapper extends HttpServletRequestWrapper {
 
    private final byte[] body;
 
    public MyServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        body = IOUtils.toByteArray(super.getInputStream());
    }
 
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
 
    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new RequestBodyCachingInputStream(body);
    }
 
    private class RequestBodyCachingInputStream extends ServletInputStream {
        private byte[] body;
        private int lastIndexRetrieved = -1;
        private ReadListener listener;
 
        public RequestBodyCachingInputStream(byte[] body) {
            this.body = body;
        }
 
        @Override
        public int read() throws IOException {
            if (isFinished()) {
                return -1;
            }
            int i = body[lastIndexRetrieved + 1];
            lastIndexRetrieved++;
            if (isFinished() && listener != null) {
                try {
                    listener.onAllDataRead();
                } catch (IOException e) {
                    listener.onError(e);
                    throw e;
                }
            }
            return i;
        }
 
        @Override
        public boolean isFinished() {
            return lastIndexRetrieved == body.length - 1;
        }
 
        @Override
        public boolean isReady() {
            return isFinished();
        }
 
        @Override
        public void setReadListener(ReadListener listener) {
            if (listener == null) {
                throw new IllegalArgumentException("listener cann not be null");
            }
            if (this.listener != null) {
                throw new IllegalArgumentException("listener has been set");
            }
            this.listener = listener;
            if (!isFinished()) {
                try {
                    listener.onAllDataRead();
                } catch (IOException e) {
                    listener.onError(e);
                }
            } else {
                try {
                    listener.onAllDataRead();
                } catch (IOException e) {
                    listener.onError(e);
                }
            }
        }
 
        @Override
        public int available() throws IOException {
            return body.length - lastIndexRetrieved - 1;
        }
 
        @Override
        public void close() throws IOException {
            lastIndexRetrieved = body.length - 1;
            body = null;
        }
    }
}

三、创建过滤器,通过过滤器包装原有的request对象

import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
 
/**
 * @描述
 * @创建人 caoju
 */
@Component
public class RequestReplaceFilter extends OncePerRequestFilter {
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        if (!(request instanceof MyServletRequestWrapper)) {
            request = new MyServletRequestWrapper(request);
        }
        filterChain.doFilter(request, response);
    
        /*//如果有文件上传的业务场景,需要用下面的代码进行处理,不然文件上传的流会有问题
        String contentType = request.getContentType();
        //如果contentType是空
        //或者contentType是多媒体的上传类型则忽略,不进行包装,直接return
        if (contentType == null) {
            filterChain.doFilter(request, response);
            return;
        }else if(request.getContentType().startsWith("multipart/")){
            filterChain.doFilter(request, response);
            return;
        }else if (!(request instanceof MyServletRequestWrapper)) {
            request = new MyServletRequestWrapper(request);
        }
        filterChain.doFilter(request, response);
        */
    }
}

通过以上几步,我们就实现了把request里的 body 保存在 MyServletRequestWrapper中的效果

就可以在整个请求链路中任何地方去重复的获取request流了

四、使用案例

配置好之后,就可以在整个请求链路中任何地方去重复的获取request流了。

比如,你可以在请求刚进来时,在过滤器或者拦截器里拿到request对象,再拿到request对象的流数据,去做一些事情,或者你也可以在请求即将结束时,在统一的异常处理器中拿到request对象,拿到request对象流数据里请求的json参数;等等等等,还有其他很多你想使用的场景,都可以这么做。

下面是在代码中利用RequestContextHolder获取request对象,拿到request对象后就可以获取请求方式、请求url、以及请求参数这些数据了。如果你在某些地方也有需要打印记录请求方式、请求url、请求参数的这些需求,那可以直接复制粘贴我下边的代码就ok了

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        String mode = "";
        String methodUrl = "";
        String param = "";
        if (requestAttributes != null) {
            ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
            HttpServletRequest request = attributes.getRequest();
            //请求方式
            mode = request.getMethod();
            //方法URL
            methodUrl = request.getRequestURI();
            if(mode.equals(HttpMethod.GET.name())){
                param = request.getQueryString();
            }
            if(mode.equals(HttpMethod.POST.name())){
                param = getJsonRequest(request);
            }
        }
    /**
     * 获取Request中的JSON字符串
     * @param request
     * @return
     * @throws IOException
     */
    public static String getJsonRequest(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        try (BufferedReader reader = request.getReader();) {
            char[] buff = new char[1024];
            int len;
            while ((len = reader.read(buff)) != -1) {
                sb.append(buff, 0, len);
            }
        } catch (IOException e) {
            log.error("POST请求参数获取异常", e);
        }
        return sb.toString();
    }

ok,到这里解决request流只能获取一次的问题就搞定了

希望对你有所帮助

以上就是解决Java项目中request流只能获取一次的问题的详细内容,更多关于Java request流获取一次的资料请关注IT俱乐部其它相关文章!

本文收集自网络,不代表IT俱乐部立场,转载请注明出处。https://www.2it.club/code/java/9561.html
上一篇
下一篇
联系我们

联系我们

在线咨询: QQ交谈

邮箱: 1120393934@qq.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部