雲筆記項目- 上傳文件報錯"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屬性配置更大一點。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM