云笔记项目- 上传文件报错"java.lang.IllegalStateException: File has been moved - cannot be read again"
目录
引言
在做文件上传时,当写入上传的文件到文件时,会报错“java.lang.IllegalStateException: File has been moved - cannot be read again”,网上一般说需要配置 maxInMemorySize,自己测试发现,maxInMemorySize 确实会影响结果,并且项目还跟写入文件到文件夹的 transferTo(File dest) 方法也有关系,以下是报错截图:
问题说明
通过 debug 和浏览器提示,浏览器端代码没有问题,问题跟服务端专门处理上传代码并写出到文件夹的部分有关系。以下为代码,其中分别使用了 transferTo(),getBytes() 和 getInputStream() 来向文件系统写入文件。
1 package Web; 2 3 import java.io.File; 4 import java.io.FileOutputStream; 5 import java.io.IOException; 6 import java.io.InputStream; 7 import java.util.HashMap; 8 import java.util.Map; 9 10 import org.springframework.stereotype.Controller; 11 import org.springframework.web.bind.annotation.RequestMapping; 12 import org.springframework.web.bind.annotation.ResponseBody; 13 import org.springframework.web.multipart.MultipartFile; 14 15 /** 16 * 上传文件的控制器 17 * @author yangchaolin 18 * 19 */ 20 @Controller 21 @RequestMapping("/file") 22 public class UploadController { 23 24 @RequestMapping("/uploadFile.do") 25 @ResponseBody 26 public Object uploadFile(MultipartFile userfile1,MultipartFile userfile2) throws IllegalStateException, IOException{ 27 /** 28 * Spring MVC 中可以使用 MultipartFile 接受上载的文件,文件中的一切数据都可以从此对象中获取 29 * 比如可以获取文件原始名,文件类型等 30 */ 31 32 //比如获取上载文件的原始文件名,就是文件系统中的文件名 33 String filename1=userfile1.getOriginalFilename(); 34 String filename2=userfile2.getOriginalFilename(); 35 System.out.println("文件 1 原始文件名为:"+filename1); 36 System.out.println("文件 2 原始文件名为:"+filename2); 37 38 Map<String,String> map=new HashMap<String,String>(); 39 40 /** 41 * 保存上传的文件有三种方法: 42 * 1 MultipartFile 接口的 transferTo(File dest) 方法 43 * 将文件直接保存到目标文件,适用于大文件 44 * 2 MultipartFile 接口的 getBytes() 方法 45 * 将文件全部读取,返回 byte 数组,保存在内存,适用于小文件,大文件有爆内存风险 46 * 3 MultipartFile 接口的 getInputStream() 方法,将文件读取后返回一个 InputStream 47 * 获取上载文件的流,适用于大文件 48 */ 49 50 //mac 中保存文件地址 /Users/yangchaolin 51 //window 中保存地址 D:/yangchaolin 52 //linux 中保存地址 /home/soft01/yangchaolin 53 54 //1 使用 transferTo(File dest) 55 // 创建目标文件夹 56 File dir=new File("/Users/yangchaolin"); 57 boolean makeDirectoryResult=dir.mkdirs(); 58 System.out.println("文件夹路径是否建立:"+makeDirectoryResult); 59 //往文件夹放第一个文件 60 File file=new File(dir,filename1); 61 userfile1.transferTo(file); 62 63 /** 64 * transferTo 方法如果不注释掉,后面执行第二种方法写入文件到硬盘会报错 65 * 报错内容:java.lang.IllegalStateException: File has been moved - cannot be read again 66 * 原因为 transferTo 方法底层在执行时,会检查需要写入到 OutputStream 的文件字节数是否超过 MultipartResolver 配置的大小, 67 * 默认设置为 10kib,如果超过了,执行完这个方法后会从内存中删除上传的文件,后面再想读取就会报错 68 */ 69 70 //2 使用 getInputStream() 方法 71 File file1=new File(dir,filename1); 72 InputStream isWithoutBuff=userfile1.getInputStream(); 73 //使用 FileoutputStream 写出到文件 74 FileOutputStream fosWithoutbuff=new FileOutputStream(file1); 75 //InputStream 一个字节一个字节的读取,将读取到的结果写入到 FileOutputStream 76 int b;//读取一个 byte 后,以 int 类型显示数值,范围 0~255 77 while((b=isWithoutBuff.read())!=-1) { 78 //read() 方法每次只读取文件的一个 byte 79 fosWithoutbuff.write(b); 80 } 81 isWithoutBuff.close(); 82 fosWithoutbuff.close(); 83 84 //同样使用 InputStream 读取,将读取到的结果写入到 FileOutputStream,但使用了缓冲字节数组 85 File file2=new File(dir,filename2); 86 InputStream isWithBuff=userfile2.getInputStream(); 87 FileOutputStream fosWithBuff=new FileOutputStream(file2); 88 int n;//保存返回读取到的字节数, 一次 8192 个字节,当不够时就是实际读取到的字节数 89 byte[] buff=new byte[8*1024];//8kib 的缓冲字节数组 90 while((n=isWithBuff.read(buff))!=-1) { 91 System.out.println("读取后的字节数:"+n); 92 fosWithBuff.write(buff, 0, n); 93 } 94 isWithBuff.close(); 95 fosWithBuff.close(); 96 97 //3 使用 getBytes() 方法 98 byte[] data=userfile2.getBytes(); 99 //写出 byte 数组到文件 100 File file3=new File(dir,filename2); 101 FileOutputStream fosWithByte=new FileOutputStream(file3); 102 fosWithByte.write(data,0,data.length); 103 fosWithByte.close(); 104 105 map.put("Result", "upload Success"); 106 107 return map;//需要导入 jackson 的三个核心包,否则无法正常转换成 JSON 108 109 } 110 111 }
此外还跟解析器的属性 maxInMemorySize 配置也有关系,以下是解析器配置:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xmlns:context="http://www.springframework.org/schema/context" xmlns:util="http://www.springframework.org/schema/util" 4 xmlns:jee="http://www.springframework.org /schema/jee" xmlns:tx="http://www.springframework.org/schema/tx" 5 xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:mvc="http://www.springframework.org/schema/mvc" 6 xsi:schemaLocation=" 7 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd 8 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd 9 http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd 10 http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd 11 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd 12 http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd 13 http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd"> 14 15 <!-- 配置组件扫描 --> 16 <context:component-scan base-package="Web"></context:component-scan> 17 <!-- 添加注解驱动 --> 18 <mvc:annotation-driven></mvc:annotation-driven> 19 20 <!-- 配置文件上载解析器 MultipartResolver --> 21 <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> 22 <!-- one of the properties available; the maximum file size in bytes --> 23 <!-- 通过 set 方法设置以下两个父类属性,父类为 CommonsFileUploadSupport.class --> 24 <property name="maxUploadSize" value="10000000"/> <!--10M 大小 --> 25 <property name="defaultEncoding" value="UTF-8" /> <!-- 文件名编码,可以适用于中文名 --> 26 27 <!-- <property name="maxInMemorySize" value="10000000" />--> 28 <!-- 上传文件大小默认小于 10kib 时,会将文件写入硬盘前保存在内存中,否则就不会保存在内存中 --> 29 </bean> 30 31 32 </beans>
下面简单测试下代码和配置对上传结果的影响:
(1)保留 transferTo() 代码,并对 maxInMemorySize 配置 10M 大小
(2)保留 transferTo() 代码,对 maxInMemorySize 不进行配置
(3)注释 transferTo() 代码,对 maxInMemorySize 不进行配置
保留 transferTo() 代码,并对 maxInMemorySize 配置 10M 大小
测试结果:可以上传,并且两张图片大小都 25KB 以上
保留 transferTo() 代码,对 maxInMemorySize 不进行配置
测试结果:服务端报错 "File has been moved - cannot be read again" ,页面显示和文件查看表明没有上传成功。
注释 transferTo() 代码,对 maxInMemorySize 不进行配置
测试结果:
可以上传,并且两张图片大小都 25KB 以上
问题分析
从测试结果如下:
(1)当 maxInMemorySize 配置足够大时,就算有 transferTo() 方法执行也能正常上传
(2)当 maxInMemorySize 不配置,当文件比较大时,有 transferTo() 方法执行会报错
(3)当 maxInMemorySize 不配置,没有 transferTo() 方法执行,将正常上传文件
影响报错的主要原因为 transferTo() 方法和 maxInMemorySize 两者,因此需要查看 transferTo 方法的源码。
源码查看
transferTo()方法是 MultipartFile 接口方法,需要查看其实现类方法具体实现,查看发现其实现类为 CommonsMultipartFile,查看其具体实现方法,发现其需要确认 isAvailable() 方法返回的结果,根据其抛出报警内容,发现刚好是项目抛出的异常内容,因此需要继续查看 isAvailable() 方法的执行。
1 @Override 2 public void transferTo(File dest) throws IOException, IllegalStateException { 3 if (!isAvailable()) { 4 throw new IllegalStateException("File has already been moved - cannot be transferred again"); 5 } 6 7 if (dest.exists() && !dest.delete()) { 8 throw new IOException( 9 "Destination file [" + dest.getAbsolutePath() + "] already exists and could not be deleted"); 10 } 11 12 try { 13 this.fileItem.write(dest); 14 if (logger.isDebugEnabled()) { 15 String action = "transferred"; 16 if (!this.fileItem.isInMemory()) { 17 action = isAvailable() ? "copied" : "moved"; 18 } 19 logger.debug("Multipart file'"+ getName() +"'with original filename [" + 20 getOriginalFilename()+ "], stored" + getStorageDescription() + ":" + 21 action + "to [" + dest.getAbsolutePath() + "]"); 22 } 23 } 24 catch (FileUploadException ex) { 25 throw new IllegalStateException(ex.getMessage()); 26 } 27 catch (IOException ex) { 28 throw ex; 29 } 30 catch (Exception ex) { 31 logger.error("Could not transfer to file", ex); 32 throw new IOException("Could not transfer to file:" + ex.getMessage()); 33 } 34 }
isAvailable()方法的代码,发现其需要检查上传的文件是否在内存中,当只有在内存中时,才返回 true,否则返回 false 后抛出异常,因此继续查看 isInMemory() 方法。
1 /** 2 * Determine whether the multipart content is still available. 3 * If a temporary file has been moved, the content is no longer available. 4 */ 5 protected boolean isAvailable() { 6 // If in memory, it's available. 7 if (this.fileItem.isInMemory()) { 8 return true; 9 } 10 // Check actual existence of temporary file. 11 if (this.fileItem instanceof DiskFileItem) { 12 return ((DiskFileItem) this.fileItem).getStoreLocation().exists(); 13 } 14 // Check whether current file size is different than original one. 15 return (this.fileItem.getSize() == this.size); 16 }
查看发现 isInMemory() 方法是 commons-fileupload.jar 包下接口 FileItem 中定义的,因此继续查看接口实现类,发现为 DiskFileItem,并且查看实现类,发现其首先需要检查缓存文件是否存在,如果不存在调用 DeferredFileOutputStream 的 isInMemory 方法继续查询。
1 /** 2 * Provides a hint as to whether or not the file contents will be read 3 * from memory. 4 * 5 * @return <code>true</code> if the file contents will be read 6 * from memory; <code>false</code> otherwise. 7 */ 8 public boolean isInMemory() { 9 if (cachedContent != null) { 10 return true; 11 } 12 return dfos.isInMemory(); 13 }
isInMemory 方法还会继续调用 DeferredFileOutputStream 对象 dfos 的 isInMemory 方法。
1 /** 2 * Determines whether or not the data for this output stream has been 3 * retained in memory. 4 * 5 * @return <code>true</code> if the data is available in memory; 6 * <code>false</code> otherwise. 7 */ 8 public boolean isInMemory() 9 { 10 return !isThresholdExceeded(); 11 }
最后发现调用了 ThresholdingOutputStream 的 isThresholdExceeded()方法,具体代码如下,其会检查准备写出到输出流的文件大小,是否超过设定的阈值,这个阈值通过 debug 发现,就是我们前面配置的参数 maxInMemorySize,其默认是 10Kib。在本项目中,由于上传的图片都在 10Kib 大小以上,其都超过了阈值,方法执行返回为 true,参数传入到 isInMemory 方法后,返回 false,最终传入到最上层会返回 false,从而抛出本次记录的异常。后面将 maxInMemorySize 设置为 10M 后,就算有 transferTo() 方法执行,因上传文件大小分别为 20 多 Kib 均为超过阈值,所以能正常上传。
1 /** 2 * Determines whether or not the configured threshold has been exceeded for 3 * this output stream. 4 * 5 * @return <code>true</code> if the threshold has been reached; 6 * <code>false</code> otherwise. 7 */ 8 public boolean isThresholdExceeded() 9 { 10 return written > threshold; 11 }
再次验证
为了验证上面的结论,准备了两个文件大小在 10Kib 以下的文件进行文件上传测试,并且测试不配置 maxInMemorySize,同时执行 transferTo() 方法。测试结果如下:
总结
(1)如果上传文件准备将文件写入到文件夹,抛出异常 "java.lang.IllegalStateException: File has been moved - cannot be read again",很有可能跟解析器 MultipartResolver 的属性 maxInMemorySize 配置太小有关,由于其默认配置只有 10Kib,当上传文件足够大,并且使用了 MultipartFile 的 transferTo() 方法写入文件到文件夹时,就会抛出异常。
(2)文件上传时,最好将 maxInMemorySize 属性配置更大一点。