• 【SpringBoot】68、SpringBoot解决HttpServletRequest中输入流不能重复读的问题


    Java 中的输入流 InputStream 的 read 方法中有一个标志位 postion,标志当前流读取到的位置,每读取一次,位置就会移动一次,如果读到最后,InputStream.read方法会返回-1,标志已经读取完了,如果想再次读取,可以调用inputstream.reset方法,position就会移动到上次调用mark的位置,mark默认是0,所以就能从头再读了。

    1、原因分析

    HttpServletRequest 中的 getInputStream() 方法返回的是 ServletInputStream 对象,此对象中并没有重写
    InputStream 的 reset() 方法,默认的 InputStream 中的 reset() 方法如下:

    public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }
    
    public boolean markSupported() {
        return false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    所以在 request.getInputStream() 读取一次之后,postion 移动到了末尾,第二次就读取不到数据,由于无法 reset(),所以,request.getInputStream() 只能读取一次。

    而在 SpringBoot 中,我们常用的参数传递的方式有两种,一种是跟在 url 后面,例如:

    http://www.xxxx.com?id=1
    
    • 1

    第二种,我们是将参数传递在 body 中,在 controller 层,我们使用 @RequestBody 获取参数,这种方式实质上就是使用 request.getInputStream() 方式读取的

    2、场景分析

    上述原因分析中,我们知道 HttpServletRequest 中的输入流对象只能读取一次,我们常有的时候需要在请求前拦截器处或者请求后返回时,异常处理时保存操作日志,异常日志等,就需要获取请求的参数,这就导致了重复读取输入流对象会报异常

    3、解决方案

    由于过滤器的执行顺序在最前,如图所示:
    在这里插入图片描述

    我们定义一个过滤器,在最外层将 HttpServletRequest 对象封装一下,代码如下:

    import org.springframework.stereotype.Component;
    import org.springframework.util.StreamUtils;
    
    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    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;
    
    /***
     * HttpServletRequest 过滤器
     * 解决: request.getInputStream()只能读取一次的问题
     * 目标: 流可重复读
     *
     * @author Asurplus
     */
    @Component
    @WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/*")
    public class HttpServletRequestFilter implements Filter {
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            ServletRequest requestWrapper = null;
            // http请求才会被过滤
            if (servletRequest instanceof HttpServletRequest) {
                requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest);
            }
            // 需要过滤
            if (null != requestWrapper) {
                filterChain.doFilter(requestWrapper, servletResponse);
            }
            // 不需要过滤
            else {
                filterChain.doFilter(servletRequest, servletResponse);
            }
        }
    
        @Override
        public void destroy() {
    
        }
    
        /***
         * HttpServletRequest 包装器
         * 解决: request.getInputStream()只能读取一次的问题
         * 目标: 流可重复读
         */
        public static class RequestWrapper extends HttpServletRequestWrapper {
    
            /**
             * 请求体
             */
            private byte[] body;
    
            public RequestWrapper(HttpServletRequest request) throws IOException {
                super(request);
                // 将body数据存储起来
                body = StreamUtils.copyToByteArray(request.getInputStream());
            }
    
            @Override
            public BufferedReader getReader() throws IOException {
                return new BufferedReader(new InputStreamReader(getInputStream()));
            }
    
            @Override
            public ServletInputStream getInputStream() throws IOException {
                // 创建字节数组输入流
                final ByteArrayInputStream bais = new ByteArrayInputStream(body);
    
                return new ServletInputStream() {
                    @Override
                    public boolean isFinished() {
                        return false;
                    }
    
                    @Override
                    public boolean isReady() {
                        return false;
                    }
    
                    @Override
                    public void setReadListener(ReadListener readListener) {
    
                    }
    
                    @Override
                    public int read() throws IOException {
                        return bais.read();
                    }
                };
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102

    我们定义了一个 HttpServletRequestFilter 对象,也就是 HttpServletRequest 的一个包装器,将 body 中的输入流存储起来,使得 request.getInputStream() 可以重复读

    4、文件上传问题

    上述将 HttpServletRequest 进行包装之后,文件上传功能会受影响,查询资料之后,需要加一个判断条件

    4.1 定义文件上传请求类型

    /**
     * 文件上传类型
     */
    private static final String CONTENT_TYPE = "multipart/form-data";
    
    • 1
    • 2
    • 3
    • 4

    4.2 过滤方法优化

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        // http请求才会被过滤
        if (servletRequest instanceof HttpServletRequest) {
            // 转换http请求对象
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            // 文件上传请求
            if (request.getContentType() != null && request.getContentType().contains(CONTENT_TYPE)) {
                requestWrapper = new StandardServletMultipartResolver().resolveMultipart(request);
            }
            // 普通请求
            else {
                requestWrapper = new RequestWrapper(request);
            }
        }
        // 需要过滤
        if (null != requestWrapper) {
            filterChain.doFilter(requestWrapper, servletResponse);
        }
        // 不需要过滤
        else {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    我们判断当前请求是否为文件上传请求,对其做特殊处理

    5、Druid 登录问题

    在进行 HttpServletRequest 包装之后,会出现 druid 登录失败的问题,这个问题困扰了我好久,经查证为该过滤器的问题,我们放开 druid 的路径过滤即可

    5.1 定义排除路径

    /**
     * druid路径
     */
    private static final String DRUID_PATHS = "/druid/**";
    
    • 1
    • 2
    • 3
    • 4

    不过滤 druid 的相关请求

    5.2 过滤方法优化

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        // http请求才会被过滤
        if (servletRequest instanceof HttpServletRequest) {
            // 转换http请求对象
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            // 解决druid登录问题
            if (matches(DRUID_PATHS, request.getRequestURI().replace(request.getContextPath(), ""))) {
                // requestWrapper = servletRequest;
            }
            // 文件上传请求
            else if (request.getContentType() != null && request.getContentType().contains(CONTENT_TYPE)) {
                requestWrapper = new StandardServletMultipartResolver().resolveMultipart(request);
            }
            // 普通请求
            else {
                requestWrapper = new RequestWrapper(request);
            }
        }
        // 需要过滤
        if (null != requestWrapper) {
            filterChain.doFilter(requestWrapper, servletResponse);
        }
        // 不需要过滤
        else {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    5.3 路径匹配方法

    /**
     * 路径匹配器
     *
     * @param url1 模糊路径
     * @param url2 目标路径
     * @return
     */
    public static boolean matches(String url1, String url2) {
        return new PathPatternParser().parse(url1).matches(PathContainer.parsePath(url2));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这样就解决了 druid 的登录失败问题

    如您在阅读中发现不足,欢迎留言!!!

  • 相关阅读:
    Linux 负载均衡介绍之LVS工作模式-DR直接路由模式
    数据服务安全的重要性
    [MySQL]事务ACID详解
    Find My技术|防止你的宠物跑丢,苹果Find My技术可以帮到你
    HTML学生个人网站作业设计——中华美食(HTML+CSS) 美食静态网页制作 WEB前端美食网站设计与实现
    我眼中的IT行业现状与未来趋势
    每日三题 7.23
    java计算机毕业设计交通规则考试系统源码+mysql数据库+系统+lw文档+部署
    pnpm install安装element-plus的版本跟package.json指定的版本不一样
    JAVA毕业设计104—基于Java+Springboot+Vue的医院预约挂号小程序(源码+数据库)
  • 原文地址:https://blog.csdn.net/qq_40065776/article/details/125385314