项目中使用CommonsMultipartResolver作为文件上传解析器,拦截器中进行权限校验以及某些参数校验。在前端调用上传接口的过程中,由于前端参数不正确,拦截器直接返回了错误信息,但却意外抛出了FileUploadException: Stream closed异常。
下面是拦截器直接向前端写回错误信息的代码,content是错误信息的json串,正常情况下这种IO流操作写法是完全没有问题的。
// response返回错误信息并关闭流
public static void returnMessage(HttpServletResponse response, String content) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter pWriter = null;
try {
pWriter = response.getWriter();
pWriter.write(content);
} catch (IOException e) {
logger.error("returnMessage", e);
} finally {
if (pWriter != null) {
pWriter.flush();
pWriter.close();
}
}
}
分析异常信息发现,异常发生在CommonsMultipartResolver 的cleanupMultipart 方法,代码片段如下:
public void cleanupMultipart(MultipartHttpServletRequest request) {
try {
cleanupFileItems(request.getMultiFileMap());
}
catch (Throwable ex) {
logger.warn("Failed to perform multipart cleanup for servlet request", ex);
}
}
当调用request.getMultiFileMap() 时,执行的是AbstractMultipartHttpServletRequest 类的getMultipartFiles() 。这个方法是懒加载方法,只有需要MultipartFile Map 时才会调用,由于请求被拦截器拦截,即清理时第一次使用,这里会初始化;紧接着去调用parseRequest() 解析请求,在使用输入流readBodyData() 的时候,发现流已经关闭了,抛出异常。
粗略探究下发现,虽然关闭的是Response的输出流,但是相应的Request的输入流同时被关闭了。从debug和Tomcat的API可知,Request对象被Tomcat 使用Facade模式进行包装,同时Request对象中包含相对应的Response引用。事实证明关闭Response流会同样关闭Request流,原因有待结合Tomcat原理深入探究。
下面为改造后的代码(或者去掉finally代码块)。
public void returnMessage(HttpServletRequest request, HttpServletResponse response, String content) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter pWriter = null;
try {
pWriter = response.getWriter();
pWriter.write(content);
} catch (IOException e) {
log.error("returnMessage", e);
} finally {
if (!ServletFileUpload.isMultipartContent(request)) {
if (pWriter != null) {
pWriter.flush();
pWriter.close();
}
}
}
}
建议:在Spring框架中不要手动关闭流,交给系统自己关闭。
1、SpringMVC的上传解析器分为两种,分别为CommonsMultipartResolver 和StandardServletMultipartResolver。
2、StandardServletMultipartResolver基于servlet 3.0,是MultipartResolver的标准实现,也是SpringBoot的默认上传组件。对于Web项目,需要在web.xml中对需要影响的servlet配置multipart-config ;对于SpringBoot项目,可以直接在application配置文件中配置属性,或者自定义MultipartConfigElement;对于自定义的servlet,也可以在servlet class上加入MultipartConfig注解。
3、 CommonsMultipartResolver 是web项目中常用的上传组件,需要在容器中定义。
下面以SpringBoot为例说明配置方式。
StandardServletMultipartResolver两种配置方式(二选一):
1、直接在application.yml或者application.properties中配置属性即可,自动配置
spring.http.multipart.maxFileSize=10MB
spring.http.multipart.maxRequestSize=50MB
2、定义MultipartConfigElement
@Bean
public MultipartConfigElement getMultipartElement() {
//生成MultipartConfigElement的工厂
MultipartConfigFactory factory = new MultipartConfigFactory();
//单个文件最大大小,可以使用这种形式,也可以使用字节数
factory.setMaxFileSize("50MB");
//总文件最大大小
factory.setMaxRequestSize("100MB");
MultipartConfigElement element = factory.createMultipartConfig();
return element;
}
CommonsMultipartResolver配置方式
@Bean
public CommonsMultipartResolver getMultipartResolver() {
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
resolver.setMaxUploadSize(439426520l);
resolver.setResolveLazily(true);//懒加载,需要获取文件信息时才会去解析请求数据
resolver.setDefaultEncoding("UTF-8");
return resolver;
}
其XML配置方式为:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="resolveLazily" value="true" />
<!-- 设置上传文件的最大尺寸为10MB -->
<property name="maxUploadSize" value="10485760" />
<property name="defaultEncoding" value="UTF-8" />
</bean>