【问题标题】:Copy InputStream, abort operation if size exceeds limit复制 InputStream,如果大小超过限制,则中止操作
【发布时间】:2013-03-04 22:34:38
【问题描述】:

我尝试将 InputStream 复制到文件,如果 InputStream 的大小大于 1MB,则中止复制。 在Java7中,我编写了如下代码:

public void copy(InputStream input, Path target) {
    OutputStream out = Files.newOutputStream(target,
            StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
    boolean isExceed = false;
    try {
        long nread = 0L;
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = input.read(buf)) > 0) {
            out.write(buf, 0, n);
            nread += n;
            if (nread > 1024 * 1024) {// Exceed 1 MB
                isExceed = true;
                break;
            }
        }
    } catch (IOException ex) {
        throw ex;
    } finally {
        out.close();
        if (isExceed) {// Abort the copy
            Files.deleteIfExists(target);
            throw new IllegalArgumentException();
        }
    }}
  • 第一个问题:有没有更好的解决方案?
  • 第二个问题:我的另一个解决方案 - 在复制操作之前,我计算了这个 InputStream 的大小。所以我将 InputStream 复制到ByteArrayOutputStream,然后得到size()。但是问题是InputStream可能不是markSupported(),所以不能在复制文件操作中复用InputStream。

【问题讨论】:

  • 我会在写之前做测试,而不是之后。您无法“计算”InputStream 的大小。它可能是无限的。这个概念没有任何意义。与mark() 和reset() 玩诡计只有在您下载整个流的情况下才能奏效,而这正是您要避免的。

标签: java inputstream


【解决方案1】:

检查输入流大小的更方便快捷的解决方案

    FileChannel chanel = (FileChannel) Channels.newChannel(inputStream);
    MappedByteBuffer buffer = chanel.map(FileChannel.MapMode.READ_ONLY, 0, chanel.size());

    System.out.println(buffer.capacity()); // bytes

【讨论】:

    【解决方案2】:

    这是来自 Apache Tomcat 的实现:

    package org.apache.tomcat.util.http.fileupload.util;
    
    import java.io.FilterInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    
    /**
     * An input stream, which limits its data size. This stream is
     * used, if the content length is unknown.
     */
    public abstract class LimitedInputStream extends FilterInputStream implements Closeable {
    
        /**
         * The maximum size of an item, in bytes.
         */
        private final long sizeMax;
    
        /**
         * The current number of bytes.
         */
        private long count;
    
        /**
         * Whether this stream is already closed.
         */
        private boolean closed;
    
        /**
         * Creates a new instance.
         *
         * @param inputStream The input stream, which shall be limited.
         * @param pSizeMax The limit; no more than this number of bytes
         *   shall be returned by the source stream.
         */
        public LimitedInputStream(InputStream inputStream, long pSizeMax) {
            super(inputStream);
            sizeMax = pSizeMax;
        }
    
        /**
         * Called to indicate, that the input streams limit has
         * been exceeded.
         *
         * @param pSizeMax The input streams limit, in bytes.
         * @param pCount The actual number of bytes.
         * @throws IOException The called method is expected
         *   to raise an IOException.
         */
        protected abstract void raiseError(long pSizeMax, long pCount)
                throws IOException;
    
        /**
         * Called to check, whether the input streams
         * limit is reached.
         *
         * @throws IOException The given limit is exceeded.
         */
        private void checkLimit() throws IOException {
            if (count > sizeMax) {
                raiseError(sizeMax, count);
            }
        }
    
        /**
         * Reads the next byte of data from this input stream. The value
         * byte is returned as an <code>int</code> in the range
         * <code>0</code> to <code>255</code>. If no byte is available
         * because the end of the stream has been reached, the value
         * <code>-1</code> is returned. This method blocks until input data
         * is available, the end of the stream is detected, or an exception
         * is thrown.
         * <p>
         * This method
         * simply performs <code>in.read()</code> and returns the result.
         *
         * @return     the next byte of data, or <code>-1</code> if the end of the
         *             stream is reached.
         * @throws  IOException  if an I/O error occurs.
         * @see        java.io.FilterInputStream#in
         */
        @Override
        public int read() throws IOException {
            int res = super.read();
            if (res != -1) {
                count++;
                checkLimit();
            }
            return res;
        }
    
        /**
         * Reads up to <code>len</code> bytes of data from this input stream
         * into an array of bytes. If <code>len</code> is not zero, the method
         * blocks until some input is available; otherwise, no
         * bytes are read and <code>0</code> is returned.
         * <p>
         * This method simply performs <code>in.read(b, off, len)</code>
         * and returns the result.
         *
         * @param      b     the buffer into which the data is read.
         * @param      off   The start offset in the destination array
         *                   <code>b</code>.
         * @param      len   the maximum number of bytes read.
         * @return     the total number of bytes read into the buffer, or
         *             <code>-1</code> if there is no more data because the end of
         *             the stream has been reached.
         * @throws  NullPointerException If <code>b</code> is <code>null</code>.
         * @throws  IndexOutOfBoundsException If <code>off</code> is negative,
         * <code>len</code> is negative, or <code>len</code> is greater than
         * <code>b.length - off</code>
         * @throws  IOException  if an I/O error occurs.
         * @see        java.io.FilterInputStream#in
         */
        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            int res = super.read(b, off, len);
            if (res > 0) {
                count += res;
                checkLimit();
            }
            return res;
        }
    
        /**
         * Returns, whether this stream is already closed.
         *
         * @return True, if the stream is closed, otherwise false.
         * @throws IOException An I/O error occurred.
         */
        @Override
        public boolean isClosed() throws IOException {
            return closed;
        }
    
        /**
         * Closes this input stream and releases any system resources
         * associated with the stream.
         * This
         * method simply performs <code>in.close()</code>.
         *
         * @throws  IOException  if an I/O error occurs.
         * @see        java.io.FilterInputStream#in
         */
        @Override
        public void close() throws IOException {
            closed = true;
            super.close();
        }
    }
    
    

    您应该继承它并覆盖raiseError 方法。

    【讨论】:

      【解决方案3】:

      我个人的选择是一个 InputStream 包装器,它在读取字节时对其进行计数:

      public class LimitedSizeInputStream extends InputStream {
      
          private final InputStream original;
          private final long maxSize;
          private long total;
      
          public LimitedSizeInputStream(InputStream original, long maxSize) {
              this.original = original;
              this.maxSize = maxSize;
          }
      
          @Override
          public int read() throws IOException {
              int i = original.read();
              if (i>=0) incrementCounter(1);
              return i;
          }
      
          @Override
          public int read(byte b[]) throws IOException {
              return read(b, 0, b.length);
          }
      
          @Override
          public int read(byte b[], int off, int len) throws IOException {
              int i = original.read(b, off, len);
              if (i>=0) incrementCounter(i);
              return i;
          }
      
          private void incrementCounter(int size) throws IOException {
              total += size;
              if (total>maxSize) throw new IOException("InputStream exceeded maximum size in bytes.");
          }
      
      }
      

      我喜欢这种方法,因为它是透明的,可以对所有输入流重复使用,并且可以很好地与其他库一起使用。例如使用 Apache Commons 复制最大 4KB 的文件:

      InputStream in = new LimitedSizeInputStream(new FileInputStream("from.txt"), 4096);
      OutputStream out = new FileOutputStream("to.txt");
      IOUtils.copy(in, out);
      

      PS:上面的实现与BoundedInputStream的主要区别在于BoundedInputStream在超过限制时不会抛出异常(它只是关闭流)

      【讨论】:

      • 对于简单的文件上传案例:if (IOUtils.copy(new BoundedInputStream(inputStream, MAX_SIZE + 1), new FileOutputStream(tmpFile)) &gt; MAX_SIZE) { /* error */}
      • 这种方法是有道理的,但为什么在 read() 方法中将 Counter(1) 增加 1 呢?它不应该增加 i 吗?除非 original.read() 总是返回 1?我不确定是否可以假设。此外,您似乎缺少 close() 的覆盖,否则您的包装流可能永远不会被释放。谢谢你的回答。
      • 考虑扩展 FilterInputStream 而不是 InputStream,因为这是实现 InputStream 类型的 de decorator 的推荐方法
      【解决方案4】:

      有以下现成的解决方案:

      【讨论】:

      • uploadFile(ByteStreams.limit(stream, maxSize)); // TODO How would you figure out whether or not you hit the maxSize limit?
      • 太好了,正是我想要的!为什么要再次发明轮子?
      【解决方案5】:

      第一个问题:有没有更好的解决方案?

      不是真的。当然,不会好很多。

      第二个问题:我的另一个解决方案 - 在复制操作之前,我计算了 InputStream 的大小。所以我将 InputStream 复制到 ByteArrayOutputStream 然后获取 size()。但是问题是 InputStream 可能没有 markSupported(),所以 InputStream 不能在复制文件操作中重复使用。

      撇开以上是陈述而不是问题......

      如果您已将字节复制到ByteArrayOutputStream,则可以从baos.toByteArray() 返回的字节数组创建ByteArrayInputStream。所以你不需要标记/重置原始流。

      但是,这是一种非常丑陋的实现方式。尤其是因为您无论如何都在读取和缓冲整个输入流。

      【讨论】:

      • 谢谢!你的意思是你同意我的copy() 解决方案?
      • 正确。如果对该方法的大多数调用导致中止,那么您可能会考虑将最多 1Mb 的内容读取到缓冲区中,并且仅在输入不太大时才创建输出文件。但我怀疑这是真的。
      【解决方案6】:

      我喜欢基于 ByteArrayOutputStream 的解决方案,我不明白为什么它不能工作

      public void copy(InputStream input, Path target) throws IOException {
          ByteArrayOutputStream bos = new ByteArrayOutputStream();
          BufferedInputStream bis = new BufferedInputStream(input);
          for (int b = 0; (b = bis.read()) != -1;) {
              if (bos.size() > BUFFER_SIZE) {
                  throw new IOException();
              }
              bos.write(b);
          }
          Files.write(target, bos.toByteArray());
      }
      

      【讨论】:

      • 当然,它可以工作。我只是质疑像这样在内存中缓冲文件的 utility 。除非我们假设会有很大比例的“太大”情况,否则直接写入输出文件更便宜更简单......并在错误情况下删除它,就像 OP 正在做的那样。
      • 投票。我担心将来限制会更高,因为 ByteArrayOutputStream 会消耗大量内存。
      • 为什么要浪费那么多内存?
      猜你喜欢
      • 2011-09-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-31
      • 2021-06-08
      • 2014-07-24
      • 1970-01-01
      • 2021-07-02
      相关资源
      最近更新 更多