【FAQ】为啥MultipartFile 的InputStream available会为0

背景

在Spring boot 文件上传案例中可能会存在获取MultipartFile InputStream.available()方法为0的情况,导致在文件上传到Minio后对象大小为0的情况

问题原因

在介绍问题原因前我们先探究下MultipartFile 是怎么实现的

这里只是剖析InputStream,所以我们直接断点getInputStream()就可以了,通过断点可以发现传进来的MultipartFile实际上是StandardMultipartFile

  1. 通过源码可以看到,它实际上是通过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();
    }
}
  1. 通过断点可以看出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();
    }
}
  1. 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输入流

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Space Chars

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值