背景
在Spring boot 文件上传案例中可能会存在获取MultipartFile InputStream.available()
方法为0的情况,导致在文件上传到Minio后对象大小为0的情况
问题原因
在介绍问题原因前我们先探究下MultipartFile
是怎么实现的
这里只是剖析InputStream
,所以我们直接断点getInputStream()
就可以了,通过断点可以发现传进来的MultipartFile
实际上是StandardMultipartFile
类
- 通过源码可以看到,它实际上是通过
Part.getInputStream()
获取的一个输入流,关于Part类的介绍可以在网上搜索
package org.springframework.web.multipart.support;
private static class StandardMultipartFile implements MultipartFile, Serializable {
private final Part part;
private final String filename;
public StandardMultipartFile(Part part, String filename) {
this.part = part;
this.filename = filename;
}
public InputStream getInputStream() throws IOException {
return this.part.getInputStream();
}
}
- 通过断点可以看出
StandardMultipartFile
中的part
实际上是ApplicationPart
,而ApplicationPart.getInputStream()
又是通过FileItem.getInputStream()
获取的,下面接着断点
package org.apache.catalina.core;
public class ApplicationPart implements Part {
private final FileItem fileItem;
private final File location;
public ApplicationPart(FileItem fileItem, File location) {
this.fileItem = fileItem;
this.location = location;
}
public InputStream getInputStream() throws IOException {
return this.fileItem.getInputStream();
}
}
FileItem.getInputStream()
这里判断了内存中是否已经存在输入流了,如果内存中没有输入流,
- 通过
this.dfos.getFile().toPath()
去获取一个Path对象,然后再通过Files.newInputStream
去新建一个输入流
package org.apache.tomcat.util.http.fileupload.disk;
public class DiskFileItem implements FileItem {
private byte[] cachedContent;
private transient DeferredFileOutputStream dfos;
public boolean isInMemory() {
return this.cachedContent != null ? true : this.dfos.isInMemory();
}
public InputStream getInputStream() throws IOException {
//判断是否是在内存中,没在内存,这新建输入流,内存中有则直接新建ByteArrayInputStream输入流
if (!this.isInMemory()) {
return Files.newInputStream(this.dfos.getFile().toPath());
} else {
if (this.cachedContent == null) {
this.cachedContent = this.dfos.getData();
}
return new ByteArrayInputStream(this.cachedContent);
}
}
}
Files.newInputStream(this.dfos.getFile().toPath())
方法通过provider(path).newInputStream(path, options)
新建输入流
public final class Files {
private static FileSystemProvider provider(Path path) {
return path.getFileSystem().provider();
}
public static InputStream newInputStream(Path path, OpenOption... options) throws IOException {
return provider(path).newInputStream(path, options);
}
}
- 通过源码可以看出
FileSystemProvider.newInputStream()
最终返回了一个ChannelInputStream
输入流(重点:本文获取到的输入流就是ChannelInputStream
输入流)
public abstract class FileSystemProvider {
public InputStream newInputStream(Path path, OpenOption... options) throws IOException {
if (options.length > 0) {
for (OpenOption opt: options) {
// All OpenOption values except for APPEND and WRITE are allowed
if (opt == StandardOpenOption.APPEND ||
opt == StandardOpenOption.WRITE)
throw new UnsupportedOperationException("'" + opt + "' not allowed");
}
}
ReadableByteChannel rbc = Files.newByteChannel(path, options);
if (rbc instanceof FileChannelImpl) {
((FileChannelImpl) rbc).setUninterruptible();
}
return Channels.newInputStream(rbc);
}
}
public final class Channels {
public static InputStream newInputStream(ReadableByteChannel ch) {
Objects.requireNonNull(ch, "ch");
return new ChannelInputStream(ch);
}
}
ChannelInputStream
通过源码可以看出,他这里继承了InputStream
,并重写了available
方法,而available
方法中是通过position
获取当前的读取位置判断文件大小的,所以如果position
改变了,那么他就有可能获取为0
而类中能改变position
的只有read
方法,所以如果你在调用available
方法之前掉了read
方法,则会出现available
返回值为0的情况,
package sun.nio.ch;
public class ChannelInputStream
extends InputStream
{
public static int read(ReadableByteChannel ch, ByteBuffer bb,
boolean block)
throws IOException
{
if (ch instanceof SelectableChannel) {
SelectableChannel sc = (SelectableChannel)ch;
synchronized (sc.blockingLock()) {
boolean bm = sc.isBlocking();
if (!bm)
throw new IllegalBlockingModeException();
if (bm != block)
sc.configureBlocking(block);
int n = ch.read(bb);
if (bm != block)
sc.configureBlocking(bm);
return n;
}
} else {
return ch.read(bb);
}
}
protected final ReadableByteChannel ch;
private ByteBuffer bb = null;
private byte[] bs = null; // Invoker's previous array
private byte[] b1 = null;
public ChannelInputStream(ReadableByteChannel ch) {
this.ch = ch;
}
public synchronized int read() throws IOException {
if (b1 == null)
b1 = new byte[1];
int n = this.read(b1);
if (n == 1)
return b1[0] & 0xff;
return -1;
}
public synchronized int read(byte[] bs, int off, int len)
throws IOException
{
Objects.checkFromIndexSize(off, len, bs.length);
if (len == 0)
return 0;
ByteBuffer bb = ((this.bs == bs)
? this.bb
: ByteBuffer.wrap(bs));
bb.limit(Math.min(off + len, bb.capacity()));
bb.position(off);
this.bb = bb;
this.bs = bs;
return read(bb);
}
protected int read(ByteBuffer bb)
throws IOException
{
return ChannelInputStream.read(ch, bb, true);
}
public int available() throws IOException {
// special case where the channel is to a file
if (ch instanceof SeekableByteChannel) {
SeekableByteChannel sbc = (SeekableByteChannel)ch;
long rem = Math.max(0, sbc.size() - sbc.position());
return (rem > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)rem;
}
return 0;
}
public synchronized long skip(long n) throws IOException {
// special case where the channel is to a file
if (ch instanceof SeekableByteChannel) {
SeekableByteChannel sbc = (SeekableByteChannel)ch;
long pos = sbc.position();
long newPos;
if (n > 0) {
newPos = pos + n;
long size = sbc.size();
if (newPos < 0 || newPos > size) {
newPos = size;
}
} else {
newPos = Long.max(pos + n, 0);
}
sbc.position(newPos);
return newPos - pos;
}
return super.skip(n);
}
public void close() throws IOException {
ch.close();
}
}
整体流程
解决办法
通过解析源码,可以知道解决解决办就是:
通过MultipartFile.getInputStream
再获取一次输入流就行,因为他会在DiskFileItem
中再次创建一个新的ByteArrayInputStream
输入流