C语言 c++ php mysql nginx linux lnmp lamp lanmp memcache redis 面试 笔记 ppt 设计模式 问题 远程连接

随笔 - 305  文章 - 1  评论 - 50  阅读 - 168万

spring inputstream empty

 

1、spring框架记录日志导致

 

在 logging.level.root debug或trace 级别下, org.springframework.web.servlet.DispatcherServlet#logRequest中会调用 request.getParameterMap()

此时会消耗 inputstream ,导致在controller中获取不到 inputstream

 

It will be empty if it's already consumed beforehand. This will be implicitly done whenever you call getParameter()getParameterValues()getParameterMap()getReader(), etc on the HttpServletRequest. Make sure that you don't call any of those kind of methods which by themselves need to gather information from the request body before calling getInputStream(). If your servlet isn't doing that, then start checking the servlet filters which are mapped on the same URL pattern

 

复制代码
org.springframework.core.log.LogFormatUtils#traceDebug
public static void traceDebug(Log logger, Function<Boolean, String> messageFactory) {
    if (logger.isDebugEnabled()) {
        boolean traceEnabled = logger.isTraceEnabled();
        //日志级别是否到trace级别
        String logMessage = messageFactory.apply(traceEnabled);
        if (traceEnabled) {
            logger.trace(logMessage);
        }
        else {
            logger.debug(logMessage);
        }
    }
}

org.springframework.web.servlet.DispatcherServlet#logRequest
private void logRequest(HttpServletRequest request) {
        //debug、trace级别生效,导致inputstream为空
        LogFormatUtils.traceDebug(logger, traceOn -> {
            String params;
            if (isEnableLoggingRequestDetails()) {
                params = request.getParameterMap().entrySet().stream()
                        .map(entry -> entry.getKey() + ":" + Arrays.toString(entry.getValue()))
                        .collect(Collectors.joining(", "));
            }
            else {
                params = (request.getParameterMap().isEmpty() ? "" : "masked");
            }
复制代码

 

如果 content-type为 application/x-www-form-urlencoded 且 logging.level.root 为info级别 则可以获取到

 

如果 content-type为 application/multipart/form-data; 且 logging.level.root 为info级别 仍旧不可以获取到

复制代码
org.springframework.web.servlet.DispatcherServlet#doDispatch
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
    ModelAndView mv = null;
    Exception dispatchException = null;

    try {
        //检测是否为文件上传,如果满足条件则进行解析
        processedRequest = checkMultipart(request);
        multipartRequestParsed = (processedRequest != request);

        // Determine handler for the current request.
        mappedHandler = getHandler(processedRequest);
        if (mappedHandler == null) {
            noHandlerFound(processedRequest, response);
            return;
        }


org.springframework.web.multipart.support.StandardMultipartHttpServletRequest#parseRequest
private void parseRequest(HttpServletRequest request) {
    try {
        Collection<Part> parts = request.getParts(); //消费inputstream
        this.multipartParameterNames = new LinkedHashSet<>(parts.size());
        MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
        for (Part part : parts) {
            String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
            ContentDisposition disposition = ContentDisposition.parse(headerValue);
            String filename = disposition.getFilename();
            if (filename != null) {
                if (filename.startsWith("=?") && filename.endsWith("?=")) {
                    filename = MimeDelegate.decode(filename);
                }
                files.add(part.getName(), new StandardMultipartFile(part, filename));
            }
            else {
                this.multipartParameterNames.add(part.getName());
            }
        }
        setMultipartFiles(files);
    }
    catch (Throwable ex) {
        handleParseFailure(ex);
    }
}
复制代码

设置 spring.servlet.multipart.enabled=false(默认为开),便可以获取到,但是就没法采用 MultipartFile file

其实没有必要, file.getInputStream() 就可以获取到输入流,但是该流是必须上传完毕以后才可以获取,其实读的是本地的缓存问题

 

注意:只要指定的级别不包含 logRequest 所在的包,都不会因为框架记录日志而影响

 

对于  application/x-www-form-urlencoded 用 @RequestBody 总是可以获取到,因为框架会根据 getParameterMap进行重建

复制代码
org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest
/**
     * Invoke the method after resolving its argument values in the context of the given request.
     * <p>Argument values are commonly resolved through
     * {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
     * The {@code providedArgs} parameter however may supply argument values to be used directly,
     * i.e. without argument resolution. Examples of provided argument values include a
     * {@link WebDataBinder}, a {@link SessionStatus}, or a thrown exception instance.
     * Provided argument values are checked before argument resolvers.
     * <p>Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the
     * resolved arguments.
     * @param request the current request
     * @param mavContainer the ModelAndViewContainer for this request
     * @param providedArgs "given" arguments matched by type, not resolved
     * @return the raw value returned by the invoked method
     * @throws Exception raised if no suitable argument resolver can be found,
     * or if the method raised an exception
     * @see #getMethodArgumentValues
     * @see #doInvoke
     */
    @Nullable
    public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
            Object... providedArgs) throws Exception {

        //计算要调用的controller的参数
        Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
        if (logger.isTraceEnabled()) {
            logger.trace("Arguments: " + Arrays.toString(args));
        }
        return doInvoke(args);
    }

//计算 @RequestBody 注解的参数, 调用 getBody方法
org.springframework.http.server.ServletServerHttpRequest#getBody
    @Override
    public InputStream getBody() throws IOException {
        if (isFormPost(this.servletRequest)) {
            //从 request.getParameterMap(); 重新计算body,因为调用 request.getParameterMap body会被消费
            return getBodyFromServletRequestParameters(this.servletRequest);
        }
        else {
            //否则返回真实 InputStream, 注意这个 InputStream, 有可能已经被消费了,所以有可能为可
            return this.servletRequest.getInputStream();
        }
    }


org.springframework.http.server.ServletServerHttpRequest#getBodyFromServletRequestParameters
/**
     * Use {@link javax.servlet.ServletRequest#getParameterMap()} to reconstruct the
     * body of a form 'POST' providing a predictable outcome as opposed to reading
     * from the body, which can fail if any other code has used the ServletRequest
     * to access a parameter, thus causing the input stream to be "consumed".
     */
    private static InputStream getBodyFromServletRequestParameters(HttpServletRequest request) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
        Writer writer = new OutputStreamWriter(bos, FORM_CHARSET);

        //根据参数map重新生成body
        //注意 因为map中包含get参数,所以生成的body中也包含get参数,所以 @RequestBody  并不一定完全等于真实body参数
        Map<String, String[]> form = request.getParameterMap();
        for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
            String name = nameIterator.next();
            List<String> values = Arrays.asList(form.get(name));
            for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext();) {
                String value = valueIterator.next();
                writer.write(URLEncoder.encode(name, FORM_CHARSET.name()));
                if (value != null) {
                    writer.write('=');
                    writer.write(URLEncoder.encode(value, FORM_CHARSET.name()));
                    if (valueIterator.hasNext()) {
                        writer.write('&');
                    }
                }
            }
            if (nameIterator.hasNext()) {
                writer.append('&');
            }
        }
        writer.flush();

        return new ByteArrayInputStream(bos.toByteArray());
    }
复制代码

   

2、过滤器导致

 

参考:

  https://github.com/spring-projects/spring-framework/issues/24176

  

posted on   思齐_  阅读(763)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
历史上的今天:
2015-08-16 动态链接库、静态链接库
< 2025年3月 >
23 24 25 26 27 28 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 1 2 3 4 5

点击右上角即可分享
微信分享提示