package com.xiaopeng.payment.config; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * 修复 multipart 文件上传被 ReplaceRequestFilter 破坏的问题。 *

* ReplaceRequestFilter(来自 request-wrapper jar,logan-starter-core 传递依赖) * 会读取 request 的 InputStream 来包装请求体,导致后续 Spring 解析 multipart 时 * 抛出 "Stream closed" 异常。 *

* 解决方案:注册一个高优先级 Filter,对 multipart 请求提前触发 Servlet 容器的 * multipart 解析(通过调用 request.getParts()),使文件数据被缓存到临时文件中。 * 这样即使后续 ReplaceRequestFilter 消费了 InputStream,multipart 数据仍然可用。 */ @Slf4j @Configuration public class MultipartFilterConfig { private static final String MULTIPART = "multipart/"; @Bean public FilterRegistrationBean earlyMultipartParsingFilter() { FilterRegistrationBean registration = new FilterRegistrationBean<>(); registration.setFilter(new EarlyMultipartParsingFilter()); registration.addUrlPatterns("/*"); // 确保在 ReplaceRequestFilter 之前执行 registration.setOrder(Integer.MIN_VALUE + 10); registration.setName("earlyMultipartParsingFilter"); return registration; } /** * 在 filter 链最前面对 multipart 请求提前解析, * 让 Tomcat 将文件数据缓存到磁盘,避免后续 filter 消费 InputStream 导致 Stream closed。 */ static class EarlyMultipartParsingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request instanceof HttpServletRequest) { HttpServletRequest httpRequest = (HttpServletRequest) request; String contentType = httpRequest.getContentType(); if (contentType != null && contentType.toLowerCase().startsWith(MULTIPART)) { // 提前触发 multipart 解析,Tomcat 会将 parts 缓存 try { httpRequest.getParts(); } catch (Exception e) { log.warn("Early multipart parsing failed, will retry later: {}", e.getMessage()); } } } chain.doFilter(request, response); } } }