zoukankan      html  css  js  c++  java
  • 云笔记项目- 上传文件报错"java.lang.IllegalStateException: File has been moved

    在做文件上传时,当写入上传的文件到文件时,会报错“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属性配置更大一点。

  • 相关阅读:
    Fedora 12/Debian 以root登录图形界面
    贡献一个简单的日志类
    "没有 pthread_create 的手册页条目"解决办法
    OpenBSD 下架设vsftpd
    NetSnmp初步(一):让我们的程序提供snmp服务
    linux socket接收、发送小工具(支持tcp、udp包(组播)的发送接收)
    NetSnmp初步(二):发送Notification
    netbeans添加现有源文件时自动更新Makefile依赖关系
    Fedora Core12的防火墙会过滤掉部分IP数据包,在开发调试时建议关闭防火墙
    NetSnmp初步(三):接收控制命令:实现SNMP的SET命令
  • 原文地址:https://www.cnblogs.com/youngchaolin/p/10702457.html
Copyright © 2011-2022 走看看