最近在一次处理上传文件的时候遇到需要guess encode问题,在使用jchardet的过程中使用UniversalDetector调用handleData(buf, 0, read)需要传入一个byte buffer,故大致写了如下一个实现试图从MultipartFile获取InputSteam然后读取buffer。

@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile multipartFile) throws IOException {
log.debug("file name=={},size=={}",multipartFile.getOriginalFilename(),multipartFile.getSize());
byte[] buf = new byte[120];
int length=0;
while ((length = multipartFile.getInputStream().read(buf))>0){
log.debug("bytes=={}",buf);
}
return "ok";
}

看上去简单明了,不过部署后发现程序似乎进入了infinite loop没有进行正常响应,调试进入发现程序果然陷入第5行持续循环无法结束,经过watcher发现每次loop都只read到了文件开头的字节,并未如预期read完整个Stream,难道是MultipartFile.getInputStream()实现和预想的不一样?遂step into MultipartFile.getInputStream()发现调用的是StandardMultipartHttpServletRequest的实现,StandardMultipartHttpServletRequest继承自AbstractMultipartHttpServletRequest,是对当前HttpServletRequest的封装。这看上去没什么问题,调用的是partgetInputStream()实现,没错,这里的part是javax.servlet.http.Part的JavaEE接口,具体逻辑由容器的实现。

public InputStream getInputStream() throws IOException {
return this.part.getInputStream();
}

继续跟进,发现part果然进入了org.apache.catalina.core.ApplicationPart的tomcat实现

public InputStream getInputStream() throws IOException {
return this.fileItem.getInputStream();
}

这里的fileItem应该是容器对于上传表单中文件的封装,继续跟进fileItem进入了org.apache.tomcat.util.http.fileupload.disk.DiskFileItem实现,下面是DiskFileItem的一些关键方法,其中dfos是一个DeferredFileOutputStream接口。

private transient DeferredFileOutputStream dfos;
public InputStream getInputStream() throws IOException {
if (!this.isInMemory()) {
return new FileInputStream(this.dfos.getFile());
} else {
if (this.cachedContent == null) {
this.cachedContent = this.dfos.getData();
}
return new ByteArrayInputStream(this.cachedContent);
}
}
public boolean isInMemory() {
return this.cachedContent != null ? true : this.dfos.isInMemory();
}

继续step into进入了org.apache.tomcat.util.http.fileupload.ThresholdingOutputStreamThresholdingOutputStreamDeferredFileOutputStream的一个实现。果然在上传文件大小大于阈值时候tomcat会将内容放置在一个临时目录里,并不会持续将文件保留在内存中。有趣的是默认的threshold配置是0,换言之就是默认情况下对于上传的文件,只要存在内容,tomcat总会将其持久化,并在调用getInputStream()方法的时候创建FileInputStream返回。换言之,就是每次调用getInputStream()返回的都是一个全新的FileInputStream,也解释了为什么在调用getInputStream()总是只能读取前buffer长度的字节,一直无法读取完全的问题。其中ThresholdingOutputStream的关键代码如下。

private final int threshold; //默认为0
public boolean isInMemory() {
return !this.isThresholdExceeded();
}
public boolean isThresholdExceeded() {
return this.written > (long)this.threshold;
}