偶尔发生File has been moved - cannot be read again

使用SPRING上传文件偶尔遇到报错:
java.lang.IllegalStateException: File has been moved - cannot be read again
	at org.springframework.web.multipart.commons.CommonsMultipartFile.getInputStream(CommonsMultipartFile.java:123)


注意该问题是偶尔发生,并非每次都能重现。

第一感这应该是一个多线程问题,因为不是每次能重现很有可能是资源竞争。同时代码中也确实用了多线程.在我的controller中:
@RequestMapping(value = "/uploadFile",method = RequestMethod.POST)
public String uploadExcel(DataSetUploadBean dataSetUploadBean, HttpServletRequest request, Model model) {
...
final MultipartFile file = dataSetUploadBean.getFile();
...
new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(((CommonsMultipartFile)file).getStorageDescription());
                    dataSetUploadImpl.process(dto, file, userId);
                } catch (Exception e) {
                    e.printStackTrace();
                    dto.setIsRun(1);
                    dto.setHasError(1);
                    dto.setErrorMassage("异常原因:"+e.getMessage());
                }
            }
        }).start();
...
}

这里只帖上了关键的代码。上传文件的处理可能是很耗时的,所以新建了一个线程去做处理工作。MultipartFile 上传文件对象由spring mvc绑定在DataSetUploadBean对象中,每次取出上传文件对象,并新建线程调用dataSetUploadImpl service进行处理。

而网上的资料均指向一个问题:
    <beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!--The limitation size of file is 500m。the value -1 means there is no limitation-->
        <beans:property name="maxUploadSize" value="500000000"/>
        <beans:property name="maxInMemorySize" value="500000000"/>
    </beans:bean>

在配置spring MultipartResolver时不仅要配置maxUploadSize,还需要配置maxInMemorySize。但原因都没说的很清楚。只是简单说maxInMemorySize的默认值为1024 bytes(待确认),超出这个大小的文件上传spring会先将上传文件记录到临时文件中。临时文件会被删除。

还有说如果要在系统中读文件两次,而文件不在内存中,就会导致该问题。(已证明其实可以多次读文件,只要是单线程即可)

我将配置maxInMemorySize加上后确实解决了问题。但网上原理明显无法解释我的问题。我的问题是“偶尔”出现,如果确实因为maxInMemorySize的问题,那么问题一定是每次都出现的。所以加大内存将上传文件保存在内存中只是一种临时解决方案,恰好解决了我的问题。特别对于对内存非常在意的系统随意增加内存更是不可取的。

-----------------------------------------------------------------------
多次review代码不存在读文件两次的问题(已证明其实可以多次读文件,只要是单线程即可)
将多线程改为单线程后,该问题不能重现。第一感是正确的还是多线程的原因。但从代码可以看到每次运行均将上传文件获取后进行处理,并没有数据共享,照理应该没有多线程问题。那问题应该是“临时文件会被删除”。

所以估计是spring MultipartResolver有多线程问题,只能放大招跟踪spring源码。

期间有同事估计是spring保存的临时文件重名,导致前一个线程处理完直接删除了后一个线程的文件,虽然觉得spring应该不会犯这种低级错误,但想想也不是没有这个可能。读源码可以看到临时文件是用了uuid来作为文件名的,排除该可能。
源码:
DiskFileItem.java
    protected File getTempFile() {
        if(this.tempFile == null) {
            File tempDir = this.repository;
            if(tempDir == null) {
                tempDir = new File(System.getProperty("java.io.tmpdir"));
            }

            String tempFileName = "upload_" + UID + "_" + getUniqueId() + ".tmp";
            this.tempFile = new File(tempDir, tempFileName);
        }

        return this.tempFile;
    }


------------------------------------------------------------------------
源码链:
CommonsMultipartResolver.java
protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
...
			List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
...	}

spring 使用了 apache fileUpload 包
ServletFileUpload.java
    public List parseRequest(HttpServletRequest request) throws FileUploadException {
        return this.parseRequest(new ServletRequestContext(request));
    }

FileUploadBase.java
    public List /* FileItem */ parseRequest(RequestContext ctx){
...
FileItem fileItem = fac.createItem(item.getFieldName(),
                        item.getContentType(), item.isFormField(),
                        fileName);
...
}

DiskFileItemFactory.java
    public FileItem createItem(String fieldName, String contentType, boolean isFormField, String fileName) {
        DiskFileItem result = new DiskFileItem(fieldName, contentType, isFormField, fileName, this.sizeThreshold, this.repository);
        FileCleaningTracker tracker = this.getFileCleaningTracker();
        if(tracker != null) {
            tracker.track(result.getTempFile(), this);
        }

        return result;
    }

Tracker即用来删除临时文件。理解它的工作机制就会理解临时文件是怎么被删除的,那么就能知道,我们程序为什么读不到文件了。
fileupload的官网地址:
https://commons.apache.org/proper/commons-fileupload/
找到Resource cleanup一段
temporary files are deleted automatically, if they are no longer used (more precisely, if the corresponding instance of java.io.File is garbage collected. This is done silently by the org.apache.commons.io.FileCleaner class, which starts a reaper thread.
相信大家都明白了。
如果文件没有被引用,被GC回收那么文件被清理。

如果读tracker的源码可以发现其实apache使用了PhantomReference虚引用的方式来跟踪文件是否被回收,如果回收则删除文件。但apache创建虚引用的对象并非TempFile本身。在debug模式下也能看到其实TempFile文件的引用是一直存在的。由于apache fileupload的部分代码未下载到源码,无法知晓创建虚引用的对象到底是谁。但能推理出该对象一定存在于tomcat的一个处理线程中的某一个临时变量中。所以当tomcat的处理线程完成后,GC回收了该对象,通过虚引用apache接收到回收的消息删除了临时文件

猜你喜欢

转载自lawrencej.iteye.com/blog/2262675
今日推荐