【并发技术系列】「Web请求读取系列」如何构建一个可重复读取的Request的流机制

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 【并发技术系列】「Web请求读取系列」如何构建一个可重复读取的Request的流机制

前提背景


项目中需要记录用户的请求参数便于后面查找问题,对于这种需求一般可以通过Spring中的拦截器或者是使Servlet中的过滤器来实现。这里我选择使用过滤器来实现,就是添加一个过滤器,然后在过滤器中获取到Request对象,将Reques中的信息记录到日志中。



问题介绍


在调用request.getReader之后重置HttpRequest:


有时候我们的请求是post,但我们又要对参数签名,这个时候我们需要获取到body的信息,但是当我们使用HttpServletRequest的getReader()和getInputStream()获取参数后,后面不管是框架还是自己想再次获取body已经没办法获取。当然也有一些其他的场景,可能需要多次获取的情况。


可能抛出类似以下的异常

java.lang.IllegalStateException: getReader() has already been called for this request
复制代码


因此,针对这问题,给出一下解决方案:



定义过滤器解决


使用过滤器很快我实现了统一记录请求参数的的功能,整个代码实现如下:

@Slf4j
@Component
public class CheckDataFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Map<String, String[]> parameterMap = request.getParameterMap();
        log.info("请求参数:{}", JSON.toJSONString(parameterMap));
        filterChain.doFilter(request,response);
    }
}
复制代码


上面的实现方式对于GET请求没有问题,可以很好的记录前端提交过来的参数。对于POST请求就没那么简单了。根据POST请求中Content-Type类型我们常用的有下面几种:


  • application/x-www-form-urlencoded:这种方式是最常见的方式,浏览器原生的form表单就是这种方式提交。
  • application/json:这种方式也算是一种常见的方式,当我们在提交一个复杂的对象时往往采用这种方式。
  • multipart/form-data:这种方式通常在使用表单上传文件时会用。


注意:上面三种常见的POST方式我实现的过滤器有一种是无法记录到的,当Content-Type为application/json时,通过调用Request对象中getParameter相关方法是无法获取到请求参数的。



application/json解决方案及问题


想要该形式的请求参数能被打印,我们可以通过读取Request中流的方式来获取请求JSON请求参数,现在修改代码如下:

@Slf4j
@Component
public class CheckDataFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Map<String, String[]> parameterMap = request.getParameterMap();
        log.info("请求参数:{}",JSON.toJSONString(parameterMap));
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        IOUtils.copy(request.getInputStream(),out);
        log.info("请求体:{}", out.toString(request.getCharacterEncoding()));
        filterChain.doFilter(request,response);
    }
}
复制代码

上面的代码中我通过获取Request中的流来获取到请求提交到服务器中的JSON数据,最后在日志中能打印出客户端提交过来的JSON数据。但是最后接口的返回并没有成功,而且在Controller中也无法获取到请求参数,最后程序给出的错误提示关键信息为:Required request body is missing


之所以会出现异常是因为Request中的流只能读取一次,我们在过滤器中读取之后如果后面有再次读取流的操作就会导致服务异常,简单的说就是Request中获取的流不支持重复读取。


所以这种方案Pass



扩展HttpServletRequest


HttpServletRequestWrapper


通过上面的分析我们知道了问题所在,对于Request中流无法重复读取的问题,我们要想办法让其支持重复读取。


难道我们要自己去实现一个Request,且我们的Request中的流还支持重复读取,想想就知道这样做很麻烦了。


幸运的是Servlet中提供了一个HttpServletRequestWrapper类,这个类从名字就能看出它是一个Wrapper类,就是我们可以通过它将原先获取流的方法包装一下,让它支持重复读取即可



创建一个自定义类


继承HttpServletRequestWrapper实现一个CustomHttpServletRequest并且写一个构造函数来缓存body数据,先将RequestBody保存为一个byte数组,然后通过Servlet自带的HttpServletRequestWrapper类覆盖getReader()和getInputStream()方法,使流从保存的byte数组读取。


public class CustomHttpServletRequest extends HttpServletRequestWrapper {
    private byte[] cachedBody;
    public CustomHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        InputStream is = request.getInputStream();
        this.cachedBody = StreamUtils.copyToByteArray(is);
    }
}
复制代码



重写getReader()
@Override
public BufferedReader getReader() throws IOException {
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
    return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
复制代码



重写getInputStream()
@Override
public ServletInputStream getInputStream() throws IOException {
    return new CachedBodyServletInputStream(this.cachedBody);
}
复制代码

然后再Filter中将ServletRequest替换为ServletRequestWrapper。代码如下:



实现ServletInputStream


创建一个继承了ServletInputStream的类

public class CachedBodyServletInputStream extends ServletInputStream {
    private InputStream cachedBodyInputStream;
    public CachedBodyServletInputStream(byte[] cachedBody) {
        this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
    }
    @Override
    public boolean isFinished() {
        try {
            return cachedBodyInputStream.available() == 0;
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return false;
    }
    @Override
    public boolean isReady() {
        return true;
    }
    @Override
    public void setReadListener(ReadListener readListener) {
        throw new UnsupportedOperationException();
    }
    @Override
    public int read() throws IOException {
        return cachedBodyInputStream.read();
    }
}
复制代码



创建一个Filter加入到容器中


既然要加入到容器中,可以创建一个Filter,然后加入配置 我们可以简单的继承OncePerRequestFilter然后实现下面方法即可。


@Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        CustomHttpServletRequest customHttpServletRequest =
                new CustomHttpServletRequest(httpServletRequest);
        filterChain.doFilter(customHttpServletRequest, httpServletResponse);
    }
复制代码


然后,添加该Filter加入即可,在上面的过滤器中先调用了getParameterMap方法获取参数,然后再获取流,如果我先getInputStream然后再调用getParameterMap会导致参数解析失败。


例如,将过滤器中代码调整顺序为如下:

@Slf4j
@Component
public class CheckDataFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //使用包装Request替换原始的Request
        request = new CustomHttpServletRequest(request);
        //读取流中的内容
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        IOUtils.copy(request.getInputStream(),out);
        log.info("请求体:{}", out.toString(request.getCharacterEncoding()));
        Map<String, String[]> parameterMap = request.getParameterMap();
        log.info("请求参数:{}",JSON.toJSONString(parameterMap));
        filterChain.doFilter(request,response);
    }
}
复制代码


调整了getInputStream和getParameterMap这两个方法的调用时机,最后却会产生两种结果,这让我一度以为这个是个BUG。最后我从源码中知道了为啥会有这种结果,如果我们先调用getInputStream,这将会getParameterMap时不会去解析参数,以下代码是SpringBoot中嵌入的tomcat实现。


org.apache.catalina.connector.Request:

protected void parseParameters() {
    parametersParsed = true;
    Parameters parameters = coyoteRequest.getParameters();
    boolean success = false;
    try {
        // Set this every time in case limit has been changed via JMX
        parameters.setLimit(getConnector().getMaxParameterCount());
        // getCharacterEncoding() may have been overridden to search for
        // hidden form field containing request encoding
        Charset charset = getCharset();
        boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
        parameters.setCharset(charset);
        if (useBodyEncodingForURI) {
            parameters.setQueryStringCharset(charset);
        }
        // Note: If !useBodyEncodingForURI, the query string encoding is
        //       that set towards the start of CoyoyeAdapter.service()
        parameters.handleQueryParameters();
        if (usingInputStream || usingReader) {
            success = true;
            return;
        }
        String contentType = getContentType();
        if (contentType == null) {
            contentType = "";
        }
        int semicolon = contentType.indexOf(';');
        if (semicolon >= 0) {
            contentType = contentType.substring(0, semicolon).trim();
        } else {
            contentType = contentType.trim();
        }
        if ("multipart/form-data".equals(contentType)) {
            parseParts(false);
            success = true;
            return;
        }
        if( !getConnector().isParseBodyMethod(getMethod()) ) {
            success = true;
            return;
        }
        if (!("application/x-www-form-urlencoded".equals(contentType))) {
            success = true;
            return;
        }
        int len = getContentLength();
        if (len > 0) {
            int maxPostSize = connector.getMaxPostSize();
            if ((maxPostSize >= 0) && (len > maxPostSize)) {
                Context context = getContext();
                if (context != null && context.getLogger().isDebugEnabled()) {
                    context.getLogger().debug(
                            sm.getString("coyoteRequest.postTooLarge"));
                }
                checkSwallowInput();
                parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
                return;
            }
            byte[] formData = null;
            if (len < CACHED_POST_LEN) {
                if (postData == null) {
                    postData = new byte[CACHED_POST_LEN];
                }
                formData = postData;
            } else {
                formData = new byte[len];
            }
            try {
                if (readPostBody(formData, len) != len) {
                    parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE);
                    return;
                }
            } catch (IOException e) {
                // Client disconnect
                Context context = getContext();
                if (context != null && context.getLogger().isDebugEnabled()) {
                    context.getLogger().debug(
                            sm.getString("coyoteRequest.parseParameters"), e);
                }
                parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
                return;
            }
            parameters.processParameters(formData, 0, len);
        } else if ("chunked".equalsIgnoreCase(
                coyoteRequest.getHeader("transfer-encoding"))) {
            byte[] formData = null;
            try {
                formData = readChunkedPostBody();
            } catch (IllegalStateException ise) {
                // chunkedPostTooLarge error
                parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
                Context context = getContext();
                if (context != null && context.getLogger().isDebugEnabled()) {
                    context.getLogger().debug(
                            sm.getString("coyoteRequest.parseParameters"),
                            ise);
                }
                return;
            } catch (IOException e) {
                // Client disconnect
                parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
                Context context = getContext();
                if (context != null && context.getLogger().isDebugEnabled()) {
                    context.getLogger().debug(
                            sm.getString("coyoteRequest.parseParameters"), e);
                }
                return;
            }
            if (formData != null) {
                parameters.processParameters(formData, 0, formData.length);
            }
        }
        success = true;
    } finally {
        if (!success) {
            parameters.setParseFailedReason(FailReason.UNKNOWN);
        }
    }
}
复制代码


上面代码从方法名字可以看出就是用来解析参数的,其中有一处关键的信息如下:

if (usingInputStream || usingReader) {
            success = true;
            return;
        }
复制代码


这个判断的意思是如果usingInputStream或者usingReader为true,将导致解析中断直接认为已经解析成功了。这个是两个属性默认都为false,而将它们设置为true的地方只有两处,分别为getInputStream和getReader,源码如下:


getInputStream()
public ServletInputStream getInputStream() throws IOException {
    if (usingReader) {
        throw new IllegalStateException(sm.getString("coyoteRequest.getInputStream.ise"));
    }
    //设置usingInputStream 为true
    usingInputStream = true;
    if (inputStream == null) {
        inputStream = new CoyoteInputStream(inputBuffer);
    }
    return inputStream;
}
复制代码


getReader()
public BufferedReader getReader() throws IOException {
    if (usingInputStream) {
        throw new IllegalStateException(sm.getString("coyoteRequest.getReader.ise"));
    }
    if (coyoteRequest.getCharacterEncoding() == null) {
        // Nothing currently set explicitly.
        // Check the content
        Context context = getContext();
        if (context != null) {
            String enc = context.getRequestCharacterEncoding();
            if (enc != null) {
                // Explicitly set the context default so it is visible to
                // InputBuffer when creating the Reader.
                setCharacterEncoding(enc);
            }
        }
    }
    //设置usingReader为true
    usingReader = true;
    inputBuffer.checkConverter();
    if (reader == null) {
        reader = new CoyoteReader(inputBuffer);
    }
    return reader;
}
复制代码


为何在tomcat要如此实现呢?tomcat如此实现可能是有它的道理,作为Servlet容器那必须按照Servlet规范来实现,通过查询相关文档还真就找到了Servlet规范中的内容,下面是Servlet3.1规范中关于参数解析的部分内容:

image.png


总结


为了获取请求中的参数我们要解决的核心问题就是让流可以重复读取即可,同时注意先读取流会导致getParameterMap时参数无法解析这两点关键点即可。




参考资料





相关文章
|
4天前
|
SQL 安全 前端开发
PHP与现代Web开发:构建高效的网络应用
【10月更文挑战第37天】在数字化时代,PHP作为一门强大的服务器端脚本语言,持续影响着Web开发的面貌。本文将深入探讨PHP在现代Web开发中的角色,包括其核心优势、面临的挑战以及如何利用PHP构建高效、安全的网络应用。通过具体代码示例和最佳实践的分享,旨在为开发者提供实用指南,帮助他们在不断变化的技术环境中保持竞争力。
|
5天前
|
PHP 开发者
深入浅出PHP:构建你的第一个Web应用
【10月更文挑战第35天】在数字时代的浪潮中,掌握编程技能已成为通往未来的钥匙。本文将带你从零开始,一步步走进PHP的世界,解锁创建动态网页的魔法。通过浅显易懂的语言和实际代码示例,我们将共同打造一个简单但功能强大的Web应用。无论你是编程新手还是希望扩展技能的老手,这篇文章都将是你的理想选择。让我们一起探索PHP的魅力,开启你的编程之旅!
|
8天前
|
缓存 前端开发 JavaScript
构建高性能与用户体验并重的现代Web应用
构建高性能与用户体验并重的现代Web应用
25 5
|
6天前
|
开发框架 前端开发 JavaScript
利用Python和Flask构建轻量级Web应用的实战指南
利用Python和Flask构建轻量级Web应用的实战指南
18 2
|
10天前
|
监控 前端开发 JavaScript
探索微前端架构:构建可扩展的现代Web应用
【10月更文挑战第29天】本文探讨了微前端架构的核心概念、优势及实施策略,通过将大型前端应用拆分为多个独立的微应用,提高开发效率、增强可维护性,并支持灵活的技术选型。实际案例包括Spotify和Zalando的成功应用。
|
8天前
|
前端开发 JavaScript jenkins
构建高效、可维护的Web应用
构建高效、可维护的Web应用
27 2
|
11天前
|
SQL 负载均衡 安全
安全至上:Web应用防火墙技术深度剖析与实战
【10月更文挑战第29天】在数字化时代,Web应用防火墙(WAF)成为保护Web应用免受攻击的关键技术。本文深入解析WAF的工作原理和核心组件,如Envoy和Coraza,并提供实战指南,涵盖动态加载规则、集成威胁情报、高可用性配置等内容,帮助开发者和安全专家构建更安全的Web环境。
29 1
|
14天前
|
前端开发 JavaScript API
前端框架新探索:Svelte在构建高性能Web应用中的优势
【10月更文挑战第26天】近年来,前端技术飞速发展,Svelte凭借独特的编译时优化和简洁的API设计,成为构建高性能Web应用的优选。本文介绍Svelte的特点和优势,包括编译而非虚拟DOM、组件化开发、状态管理及响应式更新机制,并通过示例代码展示其使用方法。
32 2
|
14天前
|
人工智能 搜索推荐 PHP
PHP在Web开发中的璀璨星辰:构建动态网站的幕后英雄###
【10月更文挑战第25天】 本文将带您穿越至PHP的宇宙,揭示其作为Web开发常青树的奥秘。通过生动实例与深入解析,展现PHP如何以简便、高效、灵活的姿态,赋能开发者打造动态交互式网站,同时不忘探讨其在新时代技术浪潮中面临的挑战与机遇,激发对技术创新与应用的无限思考。 ###
24 1
|
15天前
|
负载均衡 监控 算法
论负载均衡技术在Web系统中的应用
【11月更文挑战第4天】在当今高并发的互联网环境中,负载均衡技术已经成为提升Web系统性能不可或缺的一环。通过有效地将请求分发到多个服务器上,负载均衡不仅能够提高系统的响应速度和处理能力,还能增强系统的可扩展性和稳定性。本文将结合我参与的一个实际软件项目,从项目概述、负载均衡算法原理以及实际应用三个方面,深入探讨负载均衡技术在Web系统中的应用。
47 2

热门文章

最新文章