【Srping Boot3】寻找请求里的InputStream

文章目录


前言

我那么大的InputStream呢...需求奇葩,不啰嗦,直接说问题:对请求体进行缓存时,发现ContentType=application/x-www-form-urlencoded时InputSream不见了,越想越不对劲,我都缓存了,还能消失?


一、问题分析

我的SpringBoot版本

xml 复制代码
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

废话不多说,直接说分析过程。。。

Spring配置

yml 复制代码
spring:
  mvc:
    hidden-method: # 解析 urlencoded 类型的参数
      filter:
        enabled: true
java 复制代码
	@Bean
	@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
	@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled")
	public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
		return new OrderedHiddenHttpMethodFilter();
	}

上边代码可以看出来,当spring.mvc.hiddenmethod.filter中存在enabled时会注册org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter

当有application/x-www-form-urlencoded请求时,OrderedHiddenHttpMethodFilter 过滤器会被触发,通过DEFAULT_ORDER 变量可以看出来执行时机是比较靠前的。可能Spring Security有什么对请求参数的分析动作,我没用过,不做分析。

java 复制代码
public class OrderedHiddenHttpMethodFilter extends HiddenHttpMethodFilter implements OrderedFilter {

	/**
	 * The default order is high to ensure the filter is applied before Spring Security.
	 */
	public static final int DEFAULT_ORDER = REQUEST_WRAPPER_FILTER_MAX_ORDER - 10000; 

	private int order = DEFAULT_ORDER;

	@Override
	public int getOrder() {
		return this.order;
	}

	public void setOrder(int order) {
		this.order = order;
	}
}

它的父类org.springframework.web.filter.HiddenHttpMethodFilter中有这么一段代码

java 复制代码
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		HttpServletRequest requestToUse = request;

		if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
			String paramValue = request.getParameter(this.methodParam);
			if (StringUtils.hasLength(paramValue)) {
				String method = paramValue.toUpperCase(Locale.ENGLISH);
				if (ALLOWED_METHODS.contains(method)) {
					requestToUse = new HttpMethodRequestWrapper(request, method);
				}
			}
		}

		filterChain.doFilter(requestToUse, response);
	}

这里String paramValue = request.getParameter(this.methodParam);这句代码会执行request.getParameter("_method");
getParameter代码逻辑在org.apache.catalina.connector.RequestFacade#getParameter

java 复制代码
    @Override
    public String getParameter(String name) {
        checkFacade();

        if (Globals.IS_SECURITY_ENABLED) {
            return AccessController.doPrivileged(new GetParameterPrivilegedAction(name));
        } else {
            return request.getParameter(name); // 这里执行 org.apache.catalina.connector.Request#getParameter
        }
    }

org.apache.catalina.connector.Request#getParameter代码中判断,如果没有解析过参数,开始执行parseParameters函数进行解析

java 复制代码
    /**
     * @return the value of the specified request parameter, if any; otherwise, return <code>null</code>. If there is
     *             more than one value defined, return only the first one.
     *
     * @param name Name of the desired request parameter
     */
    @Override
    public String getParameter(String name) {

        if (!parametersParsed) {
            parseParameters(); 
        }

        return coyoteRequest.getParameters().getParameter(name);

    }

parseParameters代码在org.apache.catalina.connector.Request#parseParameters,代码可以看出来当contentType=application/x-www-form-urlencoded时就把InputStream获取走了

java 复制代码
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
            int maxParameterCount = getConnector().getMaxParameterCount();
            if (parts != null && maxParameterCount > 0) {
                maxParameterCount -= parts.size();
            }
            parameters.setLimit(maxParameterCount);

            // 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 CoyoteAdapter.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 {
                    readPostBodyFully(formData, len);
                } 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);
            }
        }

    }

二、解决方案

这不就好办了吗。。。。既然知道了Spring要解析参数,那我们就多余分析后边的代码(当然~多看一点多一点大局观),如果需求还是要对body进行操作,那就。。。

思路及3个类的代码

写一个过滤器,放到OrderedHiddenHttpMethodFilter之前,application/x-www-form-urlencoded请求时触发getParameter提前缓存数据使InputSream得以保留

这里因为我有对body进行修改的操作,所以我多余封装一层...如果你能看懂,建议你把我多余的代码去掉

创建CachedRequestWrapper.java

java 复制代码
public class CachedRequestWrapper extends HttpServletRequestWrapper {
    private byte[] bytes = null; // 因为我要setBody,所以这里又放了一个缓存
    private BufferedReader reader;

    public CachedRequestWrapper(ContentCachingRequestWrapper request) {
        super(request);
        cacheBody();
    }

    /**
     * <p>
     * 只在 contentType 为 application/x-www-form-urlencoded 时,才缓存请求体
     */
    private void cacheBody() {
        if (HttpMethod.GET.matches(getMethod())) {
            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)) {
            return;
        }

        if ("application/x-www-form-urlencoded".equals(contentType)) {
            final ContentCachingRequestWrapper request = (ContentCachingRequestWrapper) getRequest();
            request.getParameter(""); // 调用 ContentCachingRequestWrapper 解析参数并缓存
            bytes = request.getContentAsByteArray(); // 再次缓存数据
            return;
        }

        try {
            bytes = IOUtils.toByteArray(getInputStream()); // 其它类型请求缓存数据
        } catch (IOException e) {
            log.error("缓存请求体出错", e);
        }

    }

    public String getBody() {
        if (null == bytes) {
            return null;
        }
        return IOUtils.toString(bytes, getCharacterEncoding());
    }

    public byte[] getBytes() {
        return bytes;
    }

    public void setBytes(byte[] bytes) throws Exception {
        this.bytes = bytes;
    }

    @Override
    public @NotNull String getParameter(String name) {
        if (name.equals("request")) {
            return requestBody;
        }
        return super.getParameter(name);
    }

    @Override
    public @NotNull ServletInputStream getInputStream() throws IOException {
        if (bytes != null) {
            return new CachedServletInputStream(bytes);
        }
        return super.getInputStream();
    }

    @Override
    public @NotNull BufferedReader getReader() throws IOException {
        if (reader == null) {
            reader = new BufferedReader(new InputStreamReader(this.getInputStream(), getCharacterEncoding()));
        }
        return reader;
    }
}

创建CachedServletInputStream.java

java 复制代码
public class CachedServletInputStream extends ServletInputStream {
    private final ByteArrayInputStream inputStream;

    public CachedServletInputStream(byte[] content) {
        inputStream = new ByteArrayInputStream(content);
    }

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

    @Override
    public boolean isFinished() {
        return inputStream.available() == 0;
    }

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

    @Override
    public void setReadListener(ReadListener readListener) {
        throw new UnsupportedOperationException();
    }
}

创建MyFilter.java,设置过滤器Order

过滤器使用CachedRequestWrapper进行传递

java 复制代码
@Component
public class MyFilter extends OncePerRequestFilter implements OrderedFilter {

	@Override
    protected void doFilterInternal(@NotNull HttpServletRequest httpReq, @NotNull HttpServletResponse httpRes, @NotNull FilterChain chain) throws IOException, ServletException {
        chain.doFilter(
        	new CachedRequestWrapper(new ContentCachingRequestWrapper(request)),
        	new ContentCachingResponseWrapper(response));
    }

    @Override
    public int getOrder() {
        return OrderedHiddenHttpMethodFilter.DEFAULT_ORDER - 1;
    }
}

是不是感觉OrderedFilter 似曾相识...没错就是从OrderedHiddenHttpMethodFilter里边抄的


总结

  • 开启spring.mvc.hiddenmethod.filter.enable=true后,当有application/x-www-form-urlencoded请求时,过滤器会触发参数的解析,方便映射到方法的参数上
  • inputStream只能被读取一次,业务不能再次使用,需要在使用之前就套上缓存

瑕疵很多,欢迎各位大佬提出宝贵意见

相关推荐
梧桐树04299 分钟前
python面向对象高级编程:使用枚举类
java·前端·javascript
测试工程师成长之路11 分钟前
用Maven奏响Spring Boot项目开发乐章
java·spring boot·maven
超爱吃士力架13 分钟前
包装器模式
java·后端·设计模式
NeverSettle10130 分钟前
[Java] 使用 VSCode 来开发 Java
java·maven
A阳俊yi32 分钟前
spring实例化对象的几种方式(使用XML配置文件)
java·后端·spring
岳轩子1 小时前
23种设计模式之状态模式
java·设计模式·状态模式
fyihdg1 小时前
spring boot 接口性能优化实战,类似实现秒杀功能,同时实现spring cloud gateway redis实现限流
spring boot·kafka·gateway·rabbitmq
HHppGo1 小时前
java_多态的应用
android·java·开发语言
液态不合群1 小时前
Java 动态设置 JVM 参数的方法
java·开发语言·jvm
ThetaarSofVenice1 小时前
【Java从入门到放弃 之 通用容器类】
java·开发语言·python