云笔记项目- 上传文件报错"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 属性配置更大一点。