在做文件上传通过post方式进行binary上传开发的时候能正常处理,老的设备采用同样的协议进行传输的时候遇到了奇怪的事情,在SpringBoot的Controller里面直接使用HttpServletRequest的getInputStream()方法的时候获得的输入流无数据,通过getContentLength()获得内容长度的时候又是有值的,但是写入文件时通过UltraEdit打开16进制发现全部是0,同时使用DataInputStream解析ServletInputStream流报EOFException异常,接下来开始排查:
1、首先定位EOFException问题导致原因:https://www.cnblogs.com/firstdream/p/9591126.html
分析完后和我们现有问题没有什么关系,继续排查别的方向。
2、通过tcpdump抓包
tcpdump -i eth0 -tnn dst port 80 -w 1234.pcap
然后通过wireshark打开终于发现问题所在,接入如下:
问题出现了:由于之前老的协议采用application/x-www-form-urlencoded,之前定义协议的使用说过要采用oct-stream方式,结果前端开发人员没有注意而采用默认application/x-www-form-urlencoded,在springmvc框架下并没有影响。
总结:定义协议的时候一定确认请求类型和边界。
问题解决:
出现这种情况,首先怀疑输入流已经被使用了,由于请求输入流是不带缓存的,使用一次后流就无效了,通常触发解析输入流就是调用了getParameter()等方法,经过检查代码确认没有做过相关处理,所以怀疑SpringBoot底层做了处理。
查了一下SpringBoot的自动装配配置,在WebMvcAutoConfiguration中初始化了一个OrderedHiddenHttpMethodFilter,默认这个过滤器是生效的,相关代码如下:
@Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = true) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); }
这是老版本的配置:
新版本:
@Bean @ConditionalOnMissingBean({HiddenHttpMethodFilter.class}) @ConditionalOnProperty( prefix = "spring.mvc.hiddenmethod.filter", name = {"enabled"}, matchIfMissing = false ) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); }
默认开启的过滤器部分:
@Bean @ConditionalOnMissingBean({FormContentFilter.class}) @ConditionalOnProperty( prefix = "spring.mvc.formcontent.filter", name = {"enabled"}, matchIfMissing = true ) public OrderedFormContentFilter formContentFilter() { return new OrderedFormContentFilter(); }
OrderedHiddenHttpMethodFilter继承了OrderedHiddenHttpMethodFilter,而OrderedHiddenHttpMethodFilter又继承了HiddenHttpMethodFilter,在该类的doFilterInternal()方法中发现有对参数做处理,相关代码如下:
@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); }
至此就可以定位问题的所在了,找到了问题下面就来看看如何解决。
网上流传比较多的三种解决方案:
方案一:禁用默认的过滤器SpringBoot
在自动装配的时候注入了OrderedHiddenHttpMethodFilter
,如果我们不需要该功能,在配置文件中显示的将其设置为false
。配置如下:
spring.mvc.hiddenmethod.filter.enabled=false
和方案三一样,springboot是为了解决rest开发过程中兼容put、delete等接口使用,新版本钟默认不启用改过滤器,只有标识为true才生效,所以此处置为false无意义。
方案二:使用@RequestBody
注解
在需要获取请求输入流的方法上添加字节数组的参数,并添加@RequestBody
注解,示例代码如下:
@RequestMapping("/**") public 返回值 方法名(@RequestBody byte[] body) { // ... }
此方案也不是很靠谱,针对获取流的应用场景无意义。
方案三:自定义HiddenHttpMethodFilter
过滤器
参考代码如下:
@Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(){ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { filterChain.doFilter(request, response); } }; }
跟进源码其实可以发现,在springboot中不只有此一处使用了,所以这个方案不能很好解决此类问题,和方案一中类似,只有配置true才生效。
后记:
不过遗憾的是上诉三个方案都不生效,最终通过重写OrderedHiddenHttpMethodFilter并添加业务逻辑后完成(暂时解决)。
完美解决方案如下,核心是重写HttpServletRequestWrapper已实现getInputStream多次利用的特点,对于后续的过滤器等处理也不受影响。
代码如下:
1、RequestWrapper.java
import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; /** * @date 2021/5/15 * 类说明: */ public class RequestWrapper extends HttpServletRequestWrapper { //参数字节数组 private byte[] buffer; public RequestWrapper(HttpServletRequest request) throws IOException { super(request); InputStream is = request.getInputStream(); ByteArrayOutputStream os = new ByteArrayOutputStream(); byte[] tmp = new byte[1024]; int read = 0; while ((read = is.read(tmp)) > 0){ os.write(tmp,0,read); } this.buffer = os.toByteArray(); } @Override public ServletInputStream getInputStream() throws IOException { return new BufferServletInputStream(this.buffer); } } class BufferServletInputStream extends ServletInputStream{ private ByteArrayInputStream inputStream; public BufferServletInputStream(byte[] buffer) { this.inputStream = new ByteArrayInputStream(buffer); } @Override public int read() throws IOException { return inputStream.read(); } }
2、重写Filter,RequestWrapperFilter.java
import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * @date 2021/5/15 * 类说明: */ public class RequestWrapperFilter implements Filter { private FilterConfig filterConfig = null; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException, IOException { RequestWrapper request = new RequestWrapper((HttpServletRequest) servletRequest); filterChain.doFilter(request, servletResponse); } @Override public void init(FilterConfig filterConfiguration) throws ServletException { this.filterConfig = filterConfiguration; } @Override public void destroy() { this.filterConfig = null; } }
3、添加@Configyration
import com.alibaba.fastjson.JSONObject; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.HiddenHttpMethodFilter; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.commons.CommonsMultipartResolver; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.lang.reflect.InvocationTargetException; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; /** * @date 2021/5/14 * 类说明:多媒体上传配置 */ @Configuration public class MultipartResolverConfig { private static final Logger logger = LoggerFactory.getLogger(MultipartResolverConfig.class); @Bean public CommonsMultipartResolver multipartResolver() { CommonsMultipartResolver resolver = new CommonsMultipartResolver(); resolver.setMaxUploadSize(10485760); resolver.setDefaultEncoding("UTF-8"); return resolver; } @Bean(name = "uploadFileFilter") public RequestWrapperFilter uploadFileFilter() { return new RequestWrapperFilter(); } @Bean(name = "uploadFileBean") public FilterRegistrationBean uploadFileBean() { FilterRegistrationBean registration = new FilterRegistrationBean(uploadFileFilter()); registration.setOrder(0); registration.addUrlPatterns("/upload/v1/*"); return registration; } }