Java文件上传(带进度条)

30

我对Java非常新手,并且大多数时间都是自学,因此我开始构建一个applet。我想制作一个可以从本地磁盘选择文件并将其作为multipart/form-data POST请求上传,但需要一个进度条。显然用户必须授予Java applet访问硬盘的权限。我已经成功完成了第一部分:用户可以使用JFileChooser对象选择文件,该对象方便地返回一个File对象。但我想知道接下来会发生什么。我知道File.length()将给出文件的总字节数,但如何将所选File发送到Web,并如何监视已发送的字节数?提前感谢。


9
这只是一个侧面的评论,我讨厌公司使用小程序来提供更好的上传界面,而不是在HTML中实现。我认为这是一个网络浏览器的问题。你不能盲目地信任请求权限的小程序,但大多数用户仍然会给予它。悲哀但事实。 - Pyrolistical
@Pyrolistical 我同意,但不幸的是,浏览器对于上传文件的支持仍然非常糟糕。如果您需要/希望在这些文件上进行任何客户端处理,那么您除了使用小程序或Flash之外别无选择。 - cdeszaq
10个回答

39

使用HttpClient检查进度,可以将MultipartRequestEntity包装在一个计算发送字节数的对象中。以下是包装器:

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import org.apache.commons.httpclient.methods.RequestEntity;

public class CountingMultipartRequestEntity implements RequestEntity {
    private final RequestEntity delegate;

    private final ProgressListener listener;

    public CountingMultipartRequestEntity(final RequestEntity entity,
            final ProgressListener listener) {
        super();
        this.delegate = entity;
        this.listener = listener;
    }

    public long getContentLength() {
        return this.delegate.getContentLength();
    }

    public String getContentType() {
        return this.delegate.getContentType();
    }

    public boolean isRepeatable() {
        return this.delegate.isRepeatable();
    }

    public void writeRequest(final OutputStream out) throws IOException {
        this.delegate.writeRequest(new CountingOutputStream(out, this.listener));
    }

    public static interface ProgressListener {
        void transferred(long num);
    }

    public static class CountingOutputStream extends FilterOutputStream {

        private final ProgressListener listener;

        private long transferred;

        public CountingOutputStream(final OutputStream out,
                final ProgressListener listener) {
            super(out);
            this.listener = listener;
            this.transferred = 0;
        }

        public void write(byte[] b, int off, int len) throws IOException {
            out.write(b, off, len);
            this.transferred += len;
            this.listener.transferred(this.transferred);
        }

        public void write(int b) throws IOException {
            out.write(b);
            this.transferred++;
            this.listener.transferred(this.transferred);
        }
    }
}

然后实现一个ProgressListener,用于更新进度条。
请记住,进度条的更新不能在事件分派线程上运行。


2
为什么应该将CountingMultipartRequestEntity包装在MultipartRequestEntity周围? - x__dos
2
各位,你们的答案是不正确的。在许多情况下,这将导致所有发送的字节被计算两次。FilterOutputStream#write(byte[],int,int)只是在循环中调用FOS.write(byte),因此您正在重复计算所有字节。此外,FOS文档建议更改此行为,因为它不高效。请参见我的答案,我保存了底层流并调用其write(byte[],int,int)方法。 - Hamy
1
@Harny 我回答了那个问题,而且我也修复了这个答案以做正确的事情。 - ColinD
1
如果有人遇到同样的问题,想要使用这个特定示例的 .jar 文件是 commons-httpclient-3.0.1,它作为 Apache HttpClient 项目页面上的存档副本进行存储。更新版本似乎没有这个示例使用的接口。 - Fiarr
1
非常棒的答案。如果你正在寻找上述代码的调用方,我发现了这个教程,它帮助我理解了双方。http://toolongdidntread.com/android/android-multipart-post-with-progress-bar/ - Kyle Clegg
显示剩余2条评论

12

一个更简单的计数实体不会依赖于特定的实体类型,而是扩展HttpEntityWrapped

package gr.phaistos.android.util;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import org.apache.http.HttpEntity;
import org.apache.http.entity.HttpEntityWrapper;

public class CountingHttpEntity extends HttpEntityWrapper {

    public static interface ProgressListener {
        void transferred(long transferedBytes);
    }


    static class CountingOutputStream extends FilterOutputStream {

        private final ProgressListener listener;
        private long transferred;

        CountingOutputStream(final OutputStream out, final ProgressListener listener) {
            super(out);
            this.listener = listener;
            this.transferred = 0;
        }

        @Override
        public void write(final byte[] b, final int off, final int len) throws IOException {
            //// NO, double-counting, as super.write(byte[], int, int) delegates to write(int).
            //super.write(b, off, len);
            out.write(b, off, len);
            this.transferred += len;
            this.listener.transferred(this.transferred);
        }

        @Override
        public void write(final int b) throws IOException {
            out.write(b);
            this.transferred++;
            this.listener.transferred(this.transferred);
        }

    }


    private final ProgressListener listener;

    public CountingHttpEntity(final HttpEntity entity, final ProgressListener listener) {
        super(entity);
        this.listener = listener;
    }

    @Override
    public void writeTo(final OutputStream out) throws IOException {
        this.wrappedEntity.writeTo(out instanceof CountingOutputStream? out: new CountingOutputStream(out, this.listener));
    }
}

我该如何实现这个?如果你回答了,能否标记一下我 :) - Rakeeb Rajbhandari
ProgressListener listener = new ProgressListener() { @Override public void transferred(long num) { parentTask.publicPublishProgress(num); } }; StringEntity se = new StringEntity(XML); CountingHttpEntity ce = new CountingHttpEntity(se, listener); httppost.setEntity(ce); - Zayin Krige

6
我偶然发现了一个开源的Java上传器小程序,并在其代码中找到了所需的一切信息。以下是描述它的博客文章以及源代码链接:
文章: 链接 源代码: 链接

源代码链接已失效。 - Dalton
毫不奇怪,因为我在这里提出的问题已经有12年了。自那时以来,情况发生了巨大变化,所以如果我是你,2020年肯定不会使用Java小程序来处理文件上传。首先,我认为现在主要浏览器通常会阻止它们,其次,我当时追求的功能现在完全得到了HTML5的支持。根本不需要涉及Java。2008年必需的做法今天已经成为了不良实践。 - soapergem
这不是我想表达的,哈哈。这只是一个关于在S.O.上使用没有内容的链接的评论,因为链接已经失效了,但我在第一条评论中没有表述清楚。但确实我正在研究上传反馈的方法,只是不想用像小程序、swing、javafx甚至HTML这样的GUI形式。祝好! - Dalton

5

监听器返回的字节数与原始文件大小不同。因此,我将 transferred++ 修改为 transferred=len,其中 len 是写入输出流的实际字节数。当我计算传输的总字节数时,它等于由 CountingMultiPartEntity.this.getContentLength(); 返回的实际 ContentLength

public void write(byte[] b, int off, int len) throws IOException {
    wrappedOutputStream_.write(b,off,len);
    transferred=len;
    listener_.transferred(transferred);
}

4

请注意,当网络中的中间组件(例如ISP的HTTP代理或服务器前面的反向HTTP代理)比服务器更快地消耗您的上传时,进度条可能会误导。


2

以下是我的二分之一价值:

这是基于tuler的答案(在写作时有一个错误)。我稍微修改了它,所以这是我对tuler和mmyers答案的版本(我似乎无法编辑他们的答案)。我想尝试让这个更加清晰和快速。除了bug(我在他们的答案中讨论了这个问题),我对他们的版本最大的问题是它每次写入都会创建一个新的CountingOutputStream。这在内存方面可能非常昂贵 - 大量的分配和垃圾回收。更小的问题是,它使用了delegate,而它本来可以扩展MultipartEntity。不确定为什么他们选择了这样做,所以我用更熟悉的方式做了。如果有人知道这两种方法的优缺点,那就太好了。最后,FilterOutputStream#write(byte [],int,int)方法只循环调用FilterOutputStream#write(byte)。FOS文档建议子类重写此行为并使其更有效率。在这里实现最佳方式是让底层的OutputStream处理写入请求。

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntity;

public class CountingMultiPartEntity extends MultipartEntity {

    private UploadProgressListener listener_;
    private CountingOutputStream outputStream_;
    private OutputStream lastOutputStream_;

    // the parameter is the same as the ProgressListener class in tuler's answer
    public CountingMultiPartEntity(UploadProgressListener listener) {
        super(HttpMultipartMode.BROWSER_COMPATIBLE);
        listener_ = listener;
    }

    @Override
    public void writeTo(OutputStream out) throws IOException {
        // If we have yet to create the CountingOutputStream, or the
        // OutputStream being passed in is different from the OutputStream used
        // to create the current CountingOutputStream
        if ((lastOutputStream_ == null) || (lastOutputStream_ != out)) {
            lastOutputStream_ = out;
            outputStream_ = new CountingOutputStream(out);
        }

        super.writeTo(outputStream_);
    }

    private class CountingOutputStream extends FilterOutputStream {

        private long transferred = 0;
            private OutputStream wrappedOutputStream_;

        public CountingOutputStream(final OutputStream out) {
            super(out);
                    wrappedOutputStream_ = out;
        }

        public void write(byte[] b, int off, int len) throws IOException {
                    wrappedOutputStream_.write(b,off,len);
                    ++transferred;
            listener_.transferred(transferred);
        }

        public void write(int b) throws IOException {
            super.write(b);
        }
    }
}

1
这个问题比另一个小问题要复杂得多。 - ColinD

2
正如Vincent发布的文章所指出的,您可以使用Apache commons来完成此操作。
以下是代码片段:

DiskFileUpload upload = new DiskFileUpload();
upload.setHeaderEncoding(ConsoleConstants.UTF8_ENCODING);

upload.setSizeMax(1000000);
upload.setSizeThreshold(1000000);
Iterator it = upload.parseRequest((HttpServletRequest) request).iterator(); FileItem item; while(it.hasNext()){ item = (FileItem) it.next(); if (item.getFieldName("上传字段"){ String fileName = item.getString(ConsoleConstants.UTF8_ENCODING); byte[] fileBytes = item.get(); } }
请注意,此代码使用Apache commons进行文件上传,并可以设置文件大小和编码。在迭代请求中的文件项时,如果找到了名为“上传字段”的文件项,则可以获取文件名和字节数组。

1
从其他答案中,如果您不想创建一个类,可以覆盖AbstractHttpEntity类的子类或实现public void writeTo(OutputStream outstream)方法。
以下是使用FileEntity实例的示例:
FileEntity fileEntity = new FileEntity(new File("img.jpg")){
    @Override
    public void writeTo(OutputStream outstream) throws IOException {
        super.writeTo(new BufferedOutputStream(outstream){
            int writedBytes = 0;

            @Override
            public synchronized void write(byte[] b, int off, int len) throws IOException {
                super.write(b, off, len);

                writedBytes+=len;
                System.out.println("wrote: "+writedBytes+"/"+getContentLength()); //Or anything you want [using other threads]
            }
        });
    }

};

1

参考HTTP客户端上传文件至Web。它应该可以做到这一点。我不确定如何获取进度条,但可能涉及查询该API。


1

Apache Common 是一个非常好的选择。

  1. 可以通过配置(xml 文件)来设置最大文件大小/上传文件大小
  2. 可以设置目标路径(保存上传文件的位置)
  3. 可以设置临时文件夹以交换文件,从而加快文件上传速度。

Apache Commons 组件中的哪一个? - Jaime Hablutzel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接